Last active
January 27, 2021 18:22
-
-
Save spaceface777/9dc8d771c5bd5ec72494dea88c16974a to your computer and use it in GitHub Desktop.
terminal tetris made with the term.ui V module
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import rand | |
import time | |
import term.ui as tui | |
const ( | |
block_size = 1 // pixels | |
field_height = 20 // # of blocks | |
field_width = 10 | |
tetro_size = 4 | |
timer_period = 250 // ms | |
text_size = 24 | |
limit_thickness = 3 | |
) | |
const ( | |
// Tetros' 4 possible states are encoded in binaries | |
// 0000 0 0000 0 0000 0 0000 0 0000 0 0000 0 | |
// 0000 0 0000 0 0000 0 0000 0 0011 3 0011 3 | |
// 0110 6 0010 2 0011 3 0110 6 0001 1 0010 2 | |
// 0110 6 0111 7 0110 6 0011 3 0001 1 0010 2 | |
// There is a special case 1111, since 15 can't be used. | |
b_tetros = [ | |
[66, 66, 66, 66], | |
[27, 131, 72, 232], | |
[36, 231, 36, 231], | |
[63, 132, 63, 132], | |
[311, 17, 223, 74], | |
[322, 71, 113, 47], | |
[1111, 9, 1111, 9], | |
] | |
// Each tetro has its unique color | |
colors = [ | |
tui.Color{0, 0, 0}, // unused ? | |
tui.Color{255, 242, 0}, // yellow quad | |
tui.Color{174, 0, 255}, // purple triple | |
tui.Color{60, 255, 0}, // green short topright | |
tui.Color{255, 0, 0}, // red short topleft | |
tui.Color{255, 180, 31}, // orange long topleft | |
tui.Color{33, 66, 255}, // blue long topright | |
tui.Color{74, 198, 255}, // lightblue longest | |
tui.Color{0, 170, 170}, // unused ? | |
] | |
background_color = tui.Color{255, 255, 255} | |
ui_color = tui.Color{210, 0, 0} | |
) | |
struct Block { | |
mut: | |
x int | |
y int | |
} | |
enum GameState { | |
paused | |
running | |
gameover | |
} | |
struct Game { | |
mut: | |
// Score of the current game | |
score int | |
// Lines of the current game | |
lines int | |
// State of the current game | |
state GameState | |
// Position of the current tetro | |
pos_x int | |
pos_y int | |
// field[y][x] contains the color of the block with (x,y) coordinates | |
// "-1" border is to avoid bounds checking. | |
// -1 -1 -1 -1 | |
// -1 0 0 -1 | |
// -1 0 0 -1 | |
// -1 -1 -1 -1 | |
field [][]int | |
// TODO: tetro Tetro | |
tetro []Block | |
// TODO: tetros_cache []Tetro | |
tetros_cache []Block | |
// Index of the current tetro. Refers to its color. | |
tetro_idx int | |
// Idem for the next tetro | |
next_tetro_idx int | |
// Index of the rotation (0-3) | |
rotation_idx int | |
tui &tui.Context = 0 | |
// frame/time counters: | |
frame int | |
frame_old int | |
} | |
fn frame(mut game Game) { | |
game.tui.clear() | |
game.draw_scene() | |
game.tui.flush() | |
} | |
fn main() { | |
mut game := &Game{} | |
game.tui = tui.init( | |
window_title: 'V Terminal Tetris' | |
user_data: game | |
frame_fn: frame | |
event_fn: on_event | |
hide_cursor: true | |
) | |
game.init_game() | |
go game.run() // Run the game loop in a new thread | |
game.tui.run() ? // Run the render loop in the main thread | |
} | |
fn (mut g Game) init_game() { | |
g.parse_tetros() | |
g.next_tetro_idx = rand.intn(b_tetros.len) // generate initial "next" | |
g.generate_tetro() | |
g.field = [] | |
// Generate the field, fill it with 0's, add -1's on each edge | |
for _ in 0 .. field_height + 2 { | |
mut row := [0].repeat(field_width + 2) | |
row[0] = -1 | |
row[field_width + 1] = -1 | |
g.field << row | |
} | |
mut first_row := g.field[0] | |
mut last_row := g.field[field_height + 1] | |
for j in 0 .. field_width + 2 { | |
first_row[j] = -1 | |
last_row[j] = -1 | |
} | |
g.score = 0 | |
g.lines = 0 | |
g.state = .running | |
} | |
fn (mut g Game) parse_tetros() { | |
for b_tetros0 in b_tetros { | |
for b_tetro in b_tetros0 { | |
for t in parse_binary_tetro(b_tetro) { | |
g.tetros_cache << t | |
} | |
} | |
} | |
} | |
fn (mut g Game) run() { | |
for { | |
if g.state == .running { | |
g.move_tetro() | |
g.delete_completed_lines() | |
} | |
time.sleep_ms(timer_period) | |
} | |
} | |
fn (mut g Game) move_tetro() bool { | |
// Check each block in current tetro | |
for block in g.tetro { | |
y := block.y + g.pos_y + 1 | |
x := block.x + g.pos_x | |
// Reached the bottom of the screen or another block? | |
if g.field[y][x] != 0 { | |
// The new tetro has no space to drop => end of the game | |
if g.pos_y < 2 { | |
g.state = .gameover | |
return false | |
} | |
// Drop it and generate a new one | |
g.drop_tetro() | |
g.generate_tetro() | |
return false | |
} | |
} | |
g.pos_y++ | |
return true | |
} | |
fn (mut g Game) move_right(dx int) bool { | |
// Reached left/right edge or another tetro? | |
for i in 0 .. tetro_size { | |
tetro := g.tetro[i] | |
y := tetro.y + g.pos_y | |
x := tetro.x + g.pos_x + dx | |
row := g.field[y] | |
if row[x] != 0 { | |
// Do not move | |
return false | |
} | |
} | |
g.pos_x += dx | |
return true | |
} | |
fn (mut g Game) delete_completed_lines() { | |
for y := field_height; y >= 1; y-- { | |
g.delete_completed_line(y) | |
} | |
} | |
fn (mut g Game) delete_completed_line(y int) { | |
for x in 1 .. field_width { | |
f := g.field[y] | |
if f[x] == 0 { | |
return | |
} | |
} | |
g.score += 10 | |
g.lines++ | |
// Move everything down by 1 position | |
for yy := y - 1; yy >= 1; yy-- { | |
for x in 1 .. field_width { | |
mut a := g.field[yy + 1] | |
b := g.field[yy] | |
a[x] = b[x] | |
} | |
} | |
} | |
// Place a new tetro on top | |
fn (mut g Game) generate_tetro() { | |
g.pos_y = 0 | |
g.pos_x = field_width / 2 - tetro_size / 2 | |
g.tetro_idx = g.next_tetro_idx | |
g.next_tetro_idx = rand.intn(b_tetros.len) | |
g.rotation_idx = 0 | |
g.get_tetro() | |
} | |
// Get the right tetro from cache | |
fn (mut g Game) get_tetro() { | |
idx := g.tetro_idx * tetro_size * tetro_size + g.rotation_idx * tetro_size | |
g.tetro = g.tetros_cache[idx..idx + tetro_size] | |
} | |
fn (mut g Game) drop_tetro() { | |
for i in 0 .. tetro_size { | |
tetro := g.tetro[i] | |
x := tetro.x + g.pos_x | |
y := tetro.y + g.pos_y | |
// Remember the color of each block | |
g.field[y][x] = g.tetro_idx + 1 | |
} | |
} | |
fn (mut g Game) draw_tetro() { | |
for i in 0 .. tetro_size { | |
tetro := g.tetro[i] | |
g.draw_block(g.pos_y + tetro.y, g.pos_x + tetro.x, g.tetro_idx + 1) | |
} | |
} | |
fn (mut g Game) draw_next_tetro() { | |
if g.state != .gameover { | |
idx := g.next_tetro_idx * tetro_size * tetro_size | |
next_tetro := g.tetros_cache[idx..idx + tetro_size] | |
pos_y := 0 | |
pos_x := field_width / 2 - tetro_size / 2 | |
for i in 0 .. tetro_size { | |
block := next_tetro[i] | |
g.draw_block_color(pos_y + block.y, pos_x + block.x, { r: 220, g: 220, b: 220 }) | |
} | |
} | |
} | |
fn (mut g Game) draw_block_color(i int, j int, color tui.Color) { | |
xoffset, yoffset := (g.tui.window_width - 20) / 2, (g.tui.window_height - 20) / 2 + 1 | |
g.tui.set_bg_color(color) | |
g.tui.draw_rect(xoffset + j * 2 - 2, yoffset + i, xoffset + j * 2 - 1, yoffset + i) | |
g.tui.reset() | |
} | |
fn (mut g Game) draw_block(i int, j int, color_idx int) { | |
color := if g.state == .gameover { tui.Color{ 127, 127, 127 } } else { colors[color_idx] } | |
g.draw_block_color(i, j, color) | |
} | |
fn (mut g Game) draw_field() { | |
for i in 1 .. field_height + 1 { | |
for j in 1 .. field_width + 1 { | |
f := g.field[i] | |
if f[j] > 0 { | |
g.draw_block(i, j, f[j]) | |
} | |
} | |
} | |
} | |
fn (mut g Game) draw_ui() { | |
ww, wh := g.tui.window_width, g.tui.window_height | |
xstart, ystart := (ww - 20) / 2, (wh - 20) / 2 | |
g.tui.draw_text(xstart - 1, ystart - 1, 'Score: $g.score') | |
lines := 'Lines: $g.lines' | |
g.tui.draw_text(xstart + 21 - lines.len, ystart - 1, lines) | |
if g.state !in [.gameover, .paused] { return } | |
g.tui.set_bg_color(r: 220, g: 0, b: 0) | |
g.tui.set_color(r: 255, g: 255, b: 255) | |
g.tui.write('\x1b[1m') // bold | |
g.tui.draw_rect(xstart, wh / 2 - 1, xstart + 19, wh / 2 + 3) | |
if g.state == .gameover { | |
g.tui.draw_text(xstart + 5, wh / 2, 'Game Over') | |
g.tui.draw_text(xstart + 2, wh / 2 + 2, 'Space to restart') | |
} else { | |
g.tui.draw_text(xstart + 4, wh / 2, 'Game Paused') | |
g.tui.draw_text(xstart + 2, wh / 2 + 2, 'Space to resume') | |
} | |
g.tui.reset() | |
} | |
fn (mut g Game) draw_border() { | |
ww, wh := g.tui.window_width, g.tui.window_height | |
xstart, ystart := (ww - 22) / 2, (wh - 22) / 2 + 1 | |
g.tui.set_cursor_position(xstart, ystart) | |
g.tui.write('╔') | |
g.tui.write('═'.repeat(20)) | |
g.tui.write('╗') | |
for i in 1 .. 22 { | |
g.tui.set_cursor_position(xstart, ystart + i) | |
g.tui.write('║') | |
g.tui.set_cursor_position(xstart + 21, ystart + i) | |
g.tui.write('║') | |
} | |
g.tui.set_cursor_position(xstart, ystart + 22) | |
g.tui.write('╚') | |
g.tui.write('═'.repeat(20)) | |
g.tui.write('╝') | |
} | |
fn (mut g Game) draw_scene() { | |
g.draw_border() | |
g.draw_next_tetro() | |
g.draw_tetro() | |
g.draw_field() | |
g.draw_ui() | |
} | |
fn parse_binary_tetro(t_ int) []Block { | |
mut t := t_ | |
mut res := []Block{ len: 4 } | |
mut cnt := 0 | |
horizontal := t == 9 // special case for the horizontal line | |
ten_powers := [1000, 100, 10, 1] | |
for i := 0; i <= 3; i++ { | |
// Get ith digit of t | |
p := ten_powers[i] | |
mut digit := t / p | |
t %= p | |
// Convert the digit to binary | |
for j := 3; j >= 0; j-- { | |
bin := digit % 2 | |
digit /= 2 | |
if bin == 1 || (horizontal && i == tetro_size - 1) { | |
res[cnt].x = j | |
res[cnt].y = i | |
cnt++ | |
} | |
} | |
} | |
return res | |
} | |
fn on_event(e &tui.Event, mut game Game) { | |
// println('code=$e.char_code') | |
if e.typ == .key_down { | |
game.key_down(e.code) | |
} | |
} | |
fn (mut game Game) key_down(key tui.KeyCode) { | |
// global keys | |
match key { | |
.escape { | |
exit(0) | |
} | |
.space { | |
if game.state == .running { | |
game.state = .paused | |
} else if game.state == .paused { | |
game.state = .running | |
} else if game.state == .gameover { | |
game.init_game() | |
game.state = .running | |
} | |
} | |
else {} | |
} | |
if game.state != .running { | |
return | |
} | |
// keys while game is running | |
match key { | |
.up { | |
// Rotate the tetro | |
old_rotation_idx := game.rotation_idx | |
game.rotation_idx++ | |
if game.rotation_idx == tetro_size { | |
game.rotation_idx = 0 | |
} | |
game.get_tetro() | |
if !game.move_right(0) { | |
game.rotation_idx = old_rotation_idx | |
game.get_tetro() | |
} | |
if game.pos_x < 0 { | |
// game.pos_x = 1 | |
} | |
} | |
.left { | |
game.move_right(-1) | |
} | |
.right { | |
game.move_right(1) | |
} | |
.down { | |
game.move_tetro() // drop faster when the player presses <down> | |
} | |
.d { | |
for game.move_tetro() {} | |
} | |
else {} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment