You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

243 lines
5.3 KiB

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 lpos(l, c int, f tris.Field) {
pos(l, c)
ppos(l, c, render(where(f), "\u2591\u2591", " "))
}
func where(f tris.Field) []int {
var line []int
for _, r := range f {
if len(line) == 0 {
line = r
}
for i := range line {
if r[i] > 0 {
line[i] = r[i]
}
}
}
return line
}
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() {
restart:
var startlevel int
var pause bool
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)
emptyf := tris.NewField(20, 10) // empty 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 {
if pause {
ppos(0, 0, " PAUSED ")
} else {
ppos(0, 0, "Hold (c)")
}
npos(3, 0, tris.HoldBox)
fpos(0, 10, f.Add(p))
lpos(20, 10, emptyf.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(0, 0, "PAUSE")
pause = !pause
}
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
}
if pause {
continue
}
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
}
go func() {
for i := 0; i < l; i++ {
fmt.Print("\007")
time.Sleep(time.Second)
}
}()
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 ")
break
}
}
ppos(6, 15, " replay? [Y/n] ")
t.Restore() // return from raw mode
var yes string
fmt.Scanln(&yes)
if len(yes) < 1 {
goto restart
}
if yes[0] == 'n' || yes[0] == 'N' {
return
}
goto restart
}