// 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 gg2 as gg import glfw import sokol import sokol.sapp import math import freetype const ( block_size = 20 // pixels field_height = 20 // # of blocks field_width = 10 tetro_size = 4 win_width = block_size * field_width win_height = block_size * field_height timer_period = 250 // ms text_size = 12 limit_thickness = 3 ) const ( text_cfg = gx.TextCfg{ align:gx.align_left size:text_size color:gx.rgb(0, 0, 0) } over_cfg = gx.TextCfg{ align:gx.align_left size:text_size 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 ? ] background_color = gx.white ui_color = 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 frame(game &Game) { game.gg.begin() game.draw_scene() game.gg.end() } fn main() { mut game := &Game{} game.gg = gg.new_context( bg_color: gx.white width: win_width height: win_height use_ortho: true // This is needed for 2D drawing create_window: true window_title: 'V tetris' frame_fn: frame user_data: game //on_key_down: key_down event_cb: on_event ) //font_path: os.resource_abs_path('assets/fonts/RobotoMono-Regular.ttf') /* gconfig := gg.Config{ width: win_width height: win_height use_ortho: true // This is needed for 2D drawing create_window: true window_title: 'V Tetris' //window_user_ptr: game } fconfig := gg.Config{ width: win_width height: win_height 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 game.gg.run() // Run the render loop in the main thread /* game.font_loaded = game.ft != 0 for { game.draw_scene() game.gg.render() if game.gg.window.should_close() { game.gg.window.destroy() return } } */ } fn (mut g 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 _ 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.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() } glfw.post_empty_event() // force window redraw time.sleep_ms(timer_period) } } fn (mut g 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 (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 := 1; x <= field_width; 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 <= field_width; x++ { 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 = rand.next(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] } // 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) * block_size, (i - 1) * block_size, block_size - 1, block_size - 1, color) } fn (g &Game) draw_field() { for i := 1; i < field_height + 1; i++ { for j := 1; j < field_width + 1; j++ { f := g.field[i] if f[j] > 0 { g.draw_block(i, j, f[j]) } } } } fn (mut g 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, win_height / 2 - text_size, win_width, 5 * text_size, ui_color) g.ft.draw_text(1, win_height / 2 + 0 * text_size, 'Game Over', over_cfg) g.ft.draw_text(1, win_height / 2 + 2 * text_size, 'Space to restart', over_cfg) } else if g.state == .paused { g.gg.draw_rect(0, win_height / 2 - text_size, win_width, 5 * text_size, ui_color) g.ft.draw_text(1, win_height / 2 + 0 * text_size, 'Game Paused', text_cfg) g.ft.draw_text(1, win_height / 2 + 2 * text_size, 'SPACE to resume', text_cfg) } } //g.gg.draw_rect(0, block_size, win_width, limit_thickness, ui_color) } fn (mut g 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 } fn on_event(e &sapp.Event, game mut Game) { if e.typ == .key_down { game.key_down(e.key_code) } } fn (game mut Game) key_down(key sapp.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 } else { } } }