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
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 |
|
}
|
|
|