Skip to content

Instantly share code, notes, and snippets.

@spaceface777
Last active January 27, 2021 18:22
Show Gist options
  • Save spaceface777/9dc8d771c5bd5ec72494dea88c16974a to your computer and use it in GitHub Desktop.
Save spaceface777/9dc8d771c5bd5ec72494dea88c16974a to your computer and use it in GitHub Desktop.
terminal tetris made with the term.ui V module
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