You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
244 lines
5.3 KiB
Go
244 lines
5.3 KiB
Go
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
|
|
}
|