455 lines
9.4 KiB
V
455 lines
9.4 KiB
V
// Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved.
|
|
// Use of this source code is governed by an MIT license
|
|
// that can be found in the LICENSE file.
|
|
|
|
module main
|
|
|
|
import rand
|
|
import time
|
|
import gx
|
|
import gg
|
|
import glfw
|
|
import math
|
|
import freetype
|
|
|
|
const (
|
|
k_up = glfw.key_up
|
|
k_left = glfw.key_left
|
|
k_right = glfw.key_right
|
|
k_down = glfw.key_down
|
|
k_escape = glfw.key_escape
|
|
k_space = glfw.key_space
|
|
)
|
|
|
|
const (
|
|
BlockSize = 20 // pixels
|
|
FieldHeight = 20 // # of blocks
|
|
FieldWidth = 10
|
|
tetro_size = 4
|
|
WinWidth = BlockSize * FieldWidth
|
|
WinHeight = BlockSize * FieldHeight
|
|
TimerPeriod = 250 // ms
|
|
TextSize = 12
|
|
LimitThickness = 3
|
|
)
|
|
|
|
const (
|
|
text_cfg = gx.TextCfg{
|
|
align:gx.align_left
|
|
size:TextSize
|
|
color:gx.rgb(0, 0, 0)
|
|
}
|
|
over_cfg = gx.TextCfg{
|
|
align:gx.align_left
|
|
size:TextSize
|
|
color:gx.White
|
|
}
|
|
)
|
|
|
|
const (
|
|
// Tetros' 4 possible states are encoded in binaries
|
|
b_tetros = [
|
|
// 0000 0
|
|
// 0000 0
|
|
// 0110 6
|
|
// 0110 6
|
|
[66, 66, 66, 66],
|
|
// 0000 0
|
|
// 0000 0
|
|
// 0010 2
|
|
// 0111 7
|
|
[27, 131, 72, 232],
|
|
// 0000 0
|
|
// 0000 0
|
|
// 0011 3
|
|
// 0110 6
|
|
[36, 231, 36, 231],
|
|
// 0000 0
|
|
// 0000 0
|
|
// 0110 6
|
|
// 0011 3
|
|
[63, 132, 63, 132],
|
|
// 0000 0
|
|
// 0011 3
|
|
// 0001 1
|
|
// 0001 1
|
|
[311, 17, 223, 74],
|
|
// 0000 0
|
|
// 0011 3
|
|
// 0010 2
|
|
// 0010 2
|
|
[322, 71, 113, 47],
|
|
// Special case since 15 can't be used
|
|
// 1111
|
|
[1111, 9, 1111, 9],
|
|
]
|
|
// Each tetro has its unique color
|
|
Colors = [
|
|
gx.rgb(0, 0, 0), // unused ?
|
|
gx.rgb(255, 242, 0), // yellow quad
|
|
gx.rgb(174, 0, 255), // purple triple
|
|
gx.rgb(60, 255, 0), // green short topright
|
|
gx.rgb(255, 0, 0), // red short topleft
|
|
gx.rgb(255, 180, 31), // orange long topleft
|
|
gx.rgb(33, 66, 255), // blue long topright
|
|
gx.rgb(74, 198, 255), // lightblue longest
|
|
gx.rgb(0, 170, 170), // unused ?
|
|
]
|
|
|
|
BackgroundColor = gx.White
|
|
UIColor = gx.Red
|
|
)
|
|
|
|
// TODO: type Tetro [tetro_size]struct{ x, y int }
|
|
struct Block {
|
|
mut:
|
|
x int
|
|
y int
|
|
}
|
|
|
|
enum GameState {
|
|
paused running gameover
|
|
}
|
|
struct Game {
|
|
mut:
|
|
// Score of the current game
|
|
score 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
|
|
// Index of the rotation (0-3)
|
|
rotation_idx int
|
|
// gg context for drawing
|
|
gg &gg.GG
|
|
// ft context for font drawing
|
|
ft &freetype.FreeType
|
|
font_loaded bool
|
|
}
|
|
|
|
fn main() {
|
|
glfw.init_glfw()
|
|
|
|
gconfig := gg.Cfg {
|
|
width: WinWidth
|
|
height: WinHeight
|
|
use_ortho: true // This is needed for 2D drawing
|
|
create_window: true
|
|
window_title: 'V Tetris'
|
|
//window_user_ptr: game
|
|
}
|
|
|
|
fconfig := gg.Cfg{
|
|
width: WinWidth
|
|
height: WinHeight
|
|
use_ortho: true
|
|
font_path: '../assets/fonts/RobotoMono-Regular.ttf'
|
|
font_size: 18
|
|
scale: 2
|
|
window_user_ptr: 0
|
|
}
|
|
mut game := &Game{
|
|
gg: gg.new_context(gconfig)
|
|
ft: freetype.new_context(fconfig)
|
|
}
|
|
game.gg.window.set_user_ptr(game) // TODO remove this when `window_user_ptr:` works
|
|
game.init_game()
|
|
game.gg.window.onkeydown(key_down)
|
|
go game.run() // Run the game loop in a new thread
|
|
gg.clear(BackgroundColor)
|
|
game.font_loaded = game.ft != 0
|
|
for {
|
|
gg.clear(BackgroundColor)
|
|
game.draw_scene()
|
|
game.gg.render()
|
|
if game.gg.window.should_close() {
|
|
game.gg.window.destroy()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
fn (g mut Game) init_game() {
|
|
g.parse_tetros()
|
|
rand.seed(time.now().unix)
|
|
g.generate_tetro()
|
|
g.field = [] // TODO: g.field = [][]int
|
|
// Generate the field, fill it with 0's, add -1's on each edge
|
|
for i in 0..FieldHeight + 2 {
|
|
mut row := [0].repeat(FieldWidth + 2)
|
|
row[0] = - 1
|
|
row[FieldWidth + 1] = - 1
|
|
g.field << row
|
|
}
|
|
mut first_row := g.field[0]
|
|
mut last_row := g.field[FieldHeight + 1]
|
|
for j in 0..FieldWidth + 2 {
|
|
first_row[j] = - 1
|
|
last_row[j] = - 1
|
|
}
|
|
g.score = 0
|
|
g.state = .running
|
|
}
|
|
|
|
fn (g mut 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 (g mut Game) run() {
|
|
for {
|
|
if g.state == .running {
|
|
g.move_tetro()
|
|
g.delete_completed_lines()
|
|
}
|
|
glfw.post_empty_event() // force window redraw
|
|
time.sleep_ms(TimerPeriod)
|
|
}
|
|
}
|
|
|
|
fn (g mut Game) move_tetro() {
|
|
// 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?
|
|
// TODO: if g.field[y][x] != 0
|
|
//if g.field[y][x] != 0 {
|
|
row := g.field[y]
|
|
if row[x] != 0 {
|
|
// The new tetro has no space to drop => end of the game
|
|
if g.pos_y < 2 {
|
|
g.state = .gameover
|
|
return
|
|
}
|
|
// Drop it and generate a new one
|
|
g.drop_tetro()
|
|
g.generate_tetro()
|
|
return
|
|
}
|
|
}
|
|
g.pos_y++
|
|
}
|
|
|
|
fn (g mut 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 (g mut Game) delete_completed_lines() {
|
|
for y := FieldHeight; y >= 1; y-- {
|
|
g.delete_completed_line(y)
|
|
}
|
|
}
|
|
|
|
fn (g mut Game) delete_completed_line(y int) {
|
|
for x := 1; x <= FieldWidth; x++ {
|
|
f := g.field[y]
|
|
if f[x] == 0 {
|
|
return
|
|
}
|
|
}
|
|
g.score += 10
|
|
// Move everything down by 1 position
|
|
for yy := y - 1; yy >= 1; yy-- {
|
|
for x := 1; x <= FieldWidth; x++ {
|
|
mut a := g.field[yy + 1]
|
|
b := g.field[yy]
|
|
a[x] = b[x]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Place a new tetro on top
|
|
fn (g mut Game) generate_tetro() {
|
|
g.pos_y = 0
|
|
g.pos_x = FieldWidth / 2 - tetro_size / 2
|
|
g.tetro_idx = rand.next(b_tetros.len)
|
|
g.rotation_idx = 0
|
|
g.get_tetro()
|
|
}
|
|
|
|
// Get the right tetro from cache
|
|
fn (g mut 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]
|
|
}
|
|
|
|
// TODO mut
|
|
fn (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
|
|
// TODO: g.field[y][x] = g.tetro_idx + 1
|
|
mut row := g.field[y]
|
|
row[x] = g.tetro_idx + 1
|
|
}
|
|
}
|
|
|
|
fn (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 (g &Game) draw_block(i, j, color_idx int) {
|
|
color := if g.state == .gameover { gx.Gray } else { Colors[color_idx] }
|
|
g.gg.draw_rect((j - 1) * BlockSize, (i - 1) * BlockSize,
|
|
BlockSize - 1, BlockSize - 1, color)
|
|
}
|
|
|
|
fn (g &Game) draw_field() {
|
|
for i := 1; i < FieldHeight + 1; i++ {
|
|
for j := 1; j < FieldWidth + 1; j++ {
|
|
f := g.field[i]
|
|
if f[j] > 0 {
|
|
g.draw_block(i, j, f[j])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn (g mut Game) draw_ui() {
|
|
if g.font_loaded {
|
|
g.ft.draw_text(1, 3, g.score.str(), text_cfg)
|
|
if g.state == .gameover {
|
|
g.gg.draw_rect(0, WinHeight / 2 - TextSize, WinWidth,
|
|
5 * TextSize, UIColor)
|
|
g.ft.draw_text(1, WinHeight / 2 + 0 * TextSize, 'Game Over', over_cfg)
|
|
g.ft.draw_text(1, WinHeight / 2 + 2 * TextSize, 'Space to restart', over_cfg)
|
|
} else if g.state == .paused {
|
|
g.gg.draw_rect(0, WinHeight / 2 - TextSize, WinWidth,
|
|
5 * TextSize, UIColor)
|
|
g.ft.draw_text(1, WinHeight / 2 + 0 * TextSize, 'Game Paused', text_cfg)
|
|
g.ft.draw_text(1, WinHeight / 2 + 2 * TextSize, 'SPACE to resume', text_cfg)
|
|
}
|
|
}
|
|
//g.gg.draw_rect(0, BlockSize, WinWidth, LimitThickness, UIColor)
|
|
}
|
|
|
|
fn (g mut Game) draw_scene() {
|
|
g.draw_tetro()
|
|
g.draw_field()
|
|
g.draw_ui()
|
|
}
|
|
|
|
fn parse_binary_tetro(t_ int) []Block {
|
|
mut t := t_
|
|
res := [Block{}].repeat(4)
|
|
mut cnt := 0
|
|
horizontal := t == 9// special case for the horizontal line
|
|
for i := 0; i <= 3; i++ {
|
|
// Get ith digit of t
|
|
p := int(math.pow(10, 3 - 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) {
|
|
// TODO: res[cnt].x = j
|
|
// res[cnt].y = i
|
|
mut point := &res[cnt]
|
|
point.x = j
|
|
point.y = i
|
|
cnt++
|
|
}
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
// TODO: this exposes the unsafe C interface, clean up
|
|
fn key_down(wnd voidptr, key, code, action, mods int) {
|
|
if action != 2 && action != 1 {
|
|
return
|
|
}
|
|
// Fetch the game object stored in the user pointer
|
|
mut game := &Game(glfw.get_window_user_pointer(wnd))
|
|
// global keys
|
|
match key {
|
|
k_escape {
|
|
eprintln('should close')
|
|
glfw.set_should_close(wnd, true)
|
|
}
|
|
k_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 {
|
|
k_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
|
|
}
|
|
}
|
|
k_left {
|
|
game.move_right(-1)
|
|
}
|
|
k_right {
|
|
game.move_right(1)
|
|
}
|
|
k_down {
|
|
game.move_tetro() // drop faster when the player presses <down>
|
|
}
|
|
else { }
|
|
}
|
|
}
|