package main import ( "fmt" "git.kiefte.eu/lapingvino/clitris/tris" "github.com/pkg/term" "math/rand" "time" "os" "strconv" ) func pos(l, c int) { fmt.Printf("\033[%d;%dH", l, c) } func ppos(l, c int, s string) { pos(l, c) fmt.Print(s) } func fpos(l, c int, f tris.Field) { pos(l, c) for i, r := range f { ppos(l+i, c, render(r, "\u2588\u2589", "\u2591\u2591")+"|") } } func npos(l, c int, f tris.Field) { pos(l, c) for i, r := range f { ppos(l+i, c, render(r, "\u2588\u2589", " ")) } } func render(r []int, block, empty string) string { var s string for _, c := range r { if c > 0 { s += fmt.Sprintf("\033[%dm%s\033[0m", c, block) } else { s += empty } } return s } // GetKey() returns the key currently pressed. It always returns a 3 byte slice. Check first element for Escape for handling arrow keys // Because a defer would trigger too late and the Restore and Close are essential, separated in a function. func GetKey(t *term.Term) []byte { key := make([]byte, 3) t.Read(key) return key } func main() { var startlevel int if len(os.Args) > 1 { startlevel, _ = strconv.Atoi(os.Args[1]) } t, _ := term.Open("/dev/tty") defer t.Close() term.RawMode(t) defer t.Restore() defer pos(23, 0) // Set the cursor to just below the playing field t.SetReadTimeout(time.Second / 1000) rand.Seed(time.Now().UnixNano()) // to start with truly random pieces f := tris.NewField(20, 10) // the playing field (10x20) var floor, topout, harddrop bool // state machine variables to check special situations // define outside of game loop to avoid accidental resets of position // x, y and rot are the values calculated to feed to Move // which then checks collisions and does the wall kicks var x, y int var rot tris.Rotation var level, linescleared, score, dropfrom int // init variable for pressed key - MUST be initialized 3 long to avoid crash, GetKey() does this key := GetKey(t) fmt.Print("\033[2J") // Clear screen b := tris.NewBag() b, p := b.Pick() lev := time.NewTicker(time.Second) level = 1 for { x, y, rot = p.X, p.Y, p.Rot if level > startlevel || linescleared > startlevel * 10 { level = linescleared/10 + 1 } else { level = startlevel } slevel := fmt.Sprintf("level %d", level) sscore := fmt.Sprintf("score %d", score) slines := fmt.Sprintf("lines %d", linescleared) if !harddrop { ppos(0, 0, "Hold (c)") npos(3, 0, tris.HoldBox) fpos(0, 10, f.Add(p)) var next tris.Field b, next = b.Next(5) npos(0, 34, next) ppos(1, 42, sscore) ppos(3, 42, slevel) ppos(5, 42, slines) key = GetKey(t) } switch key[0] { case 27: // Escape, read the arrow key pressed switch key[2] { case 65: // Up rot = (p.Rot + 1) % 4 case 66: // Down y = p.Y + 1 score += 1 case 67: // Right x = p.X + 1 case 68: // Left x = p.X - 1 default: ppos(22, 0, "...escape, escape!") return } case 'w', 'i': // Up rot = (p.Rot + 1) % 4 case 's', 'k': // Down y = p.Y + 1 score += 1 case 'd', 'l': // Right x = p.X + 1 case 'a', 'j': // Left x = p.X - 1 case 'x', '.': rot = (p.Rot + 1) % 4 case 'z', ',': rot = (p.Rot + 3) % 4 case 'c': b, p = b.Swap(p) continue case ' ': if !harddrop { dropfrom = p.Y } harddrop = true case 'q': ppos(22, 0, "...that was exciting!") return case 'Q': ppos(22, 0, "...never let an engineer pick the name of your software?") return } select { case <-lev.C: y = p.Y + 1 lev.Reset(time.Second * 100 / (100 * time.Duration(level))) default: if harddrop { y = p.Y + 1 } } p, floor, topout = p.Move(f, rot, x, y) // Check if the piece can move, then do it and communicate back for housekeeping // Give some time before actually locking in to enable tucks // This code runs when the time is over if floor && p.Lock.Add(tris.LockDelay).Before(time.Now()) { if harddrop { score += 2 * (p.Y - dropfrom) harddrop = false } var l int f = f.Add(p) l, f = f.Lines() // count and remove full lines if l > 0 { linescleared += l score += 40 * level * l * l } fmt.Print("\033[2J") // Clear screen b, p = b.Pick() // ... and pick a new piece from the bag } // when not touching a piece or floor below yet, reset lock delay if !floor { p.Lock = time.Now() } if topout { ppos(4, 15, " GAME OVER ") return } } }