diff --git a/examples/tetris/README.md b/examples/tetris/README.md new file mode 100644 index 0000000000..a5db860ebc --- /dev/null +++ b/examples/tetris/README.md @@ -0,0 +1 @@ + diff --git a/examples/tetris/screenshot.png b/examples/tetris/screenshot.png new file mode 100644 index 0000000000..67013c1011 Binary files /dev/null and b/examples/tetris/screenshot.png differ diff --git a/examples/tetris/tetris.v b/examples/tetris/tetris.v new file mode 100644 index 0000000000..003cee8ef5 --- /dev/null +++ b/examples/tetris/tetris.v @@ -0,0 +1,322 @@ +import rand +import time +import gx +import gl +import gg +import glfw + +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 +) + +// TODO: type Tetro [TETRO_SIZE]struct{ x, y int } +struct Block { + x int + y int +} + +const ( + // Tetros and their 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), + gx.rgb(253, 32, 47), + gx.rgb(0, 110, 194), + gx.rgb(34, 169, 16), + gx.rgb(170, 0, 170), + gx.rgb(0, 0, 170), + gx.rgb(0, 170, 0), + gx.rgb(170, 85, 0), + gx.rgb(0, 170, 170), + ] +) + +struct Game { + // Position of the dropping tetromino + posX int + posY 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 + // TODO: field [][]int + field array_array_int + // TODO: tetro Tetro + tetro []Block + // Index of the dropping tetromino. Refers to its color. + tetroIdx int + // Index of the rotation (0-3) + rotationIdx int + // gg context for drawing + gg *gg.GG +} + +fn main() { + mut game := &Game{} + game.init_game() + glfw.init() + mut window := glfw.create_window(glfw.WinCfg { + width: WIN_WIDTH + height: WIN_HEIGHT + title: 'V Tetris' + ptr: game // glfw user pointer + }) + window.make_context_current() + window.onkeydown(key_down) + gg.init() + game.gg = gg.new_context(gg.Cfg { + width: WIN_WIDTH + height: WIN_HEIGHT + use_ortho: true // This is needed for 2D drawing + }) + go game.run() // Run the game loop in a new thread + gl.clear() // For some reason this is necessary to avoid an intial flickering + gl.clear_color(255, 255, 255, 255) + for { + gl.clear() + gl.clear_color(255, 255, 255, 255) + game.draw_scene() + window.swap_buffers() + glfw.wait_events() + } +} + +fn (g mut Game) init_game() { + rand.seed() + g.generate_tetro() + g.field = []array_int // TODO: g.field = [][]int + // Generate the field, fill it with 0's, add -1's on each edge + for i := 0; i < FIELD_HEIGHT + 2; i++ { + mut row := [0; 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 := 0; j < FIELD_WIDTH + 2; j++ { + first_row[j] = - 1 + last_row[j] = - 1 + } +} + +fn (g mut Game) run() { + for { + g.move_tetro() + g.delete_completed_lines() + glfw.post_empty_event() // force window redraw + time.sleep_ms(TIMER_PERIOD) + } +} + +fn (g mut Game) move_tetro() { + // Check each block in the dropping tetro + for i := 0; i < TETRO_SIZE; i++ { + tetro := g.tetro[i] + y := tetro.y + g.posY + 1 + x := tetro.x + g.posX + // Reached the bottom of the screen or another block? + // TODO: 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.posY < 2 { + g.init_game() + return + } + // Drop it and generate a new one + g.drop_tetro() + g.generate_tetro() + return + } + } + g.posY++ +} + +fn (g mut Game) move_right(dx int) { + for i := 0; i < TETRO_SIZE; i++ { + // Reached left/right edges? + tetro := g.tetro[i] + y := tetro.y + g.posY + x := tetro.x + g.posX + dx + row := g.field[y] + if row[x] != 0 { + // Do not move + return + } + } + g.posX += dx +} + +fn (g mut Game) delete_completed_lines() { + for y := FIELD_HEIGHT; y >= 1; y-- { + g.delete_completed_line(y) + } +} + +fn (g mut Game) delete_completed_line(y int) { + for x := 1; x <= FIELD_WIDTH; x++ { + f := g.field[y] + if f[x] == 0 { + return + } + } + // 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] + mut b := g.field[yy] + a[x] = b[x] + } + } +} + +// Place a new tetro on top +fn (g mut Game) generate_tetro() { + g.posY = 0 + g.posX = FIELD_WIDTH / 2 - TETRO_SIZE / 2 + g.tetroIdx = rand.next(B_TETROS.len) + g.rotationIdx = 0 + b := B_TETROS[g.tetroIdx] + g.tetro = parse_binary_tetro(b[0]) +} + +fn (g mut Game) drop_tetro() { + for i := 0; i < TETRO_SIZE; i++ { + tetro := g.tetro[i] + x := tetro.x + g.posX + y := tetro.y + g.posY + // Remember the color of each block + // TODO: g.field[y][x] = g.tetroIdx + 1 + mut row := g.field[y] + row[x] = g.tetroIdx + 1 + } +} + +fn (g mut Game) draw_tetro() { + for i := 0; i < TETRO_SIZE; i++ { + tetro := g.tetro[i] + g.draw_block(g.posY + tetro.y, g.posX + tetro.x, g.tetroIdx + 1) + } +} + +fn (g mut Game) draw_block(i, j int, color_idx int) { + g.gg.draw_rect((j - 1) * BLOCK_SIZE, (i - 1) * BLOCK_SIZE, + BLOCK_SIZE - 1, BLOCK_SIZE - 1, COLORS[color_idx]) +} + +fn (g mut 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 (g mut Game) draw_scene() { + g.draw_tetro() + g.draw_field() +} + +fn parse_binary_tetro(t int) []Block { + res := [Block{} + ; 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 := int(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 *glfw.Window, key int, code int, 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)) + switch key { + case GLFW_KEY_UP: + // Rotate the tetro + game.rotationIdx++ + if game.rotationIdx == TETRO_SIZE { + game.rotationIdx = 0 + } + t := B_TETROS[game.tetroIdx] + // game.tetro = parse_binary_tetro(B_TETROS[game.tetroIdx][game.rotationIdx]) + game.tetro = parse_binary_tetro(t[game.rotationIdx]) + if game.posX < 0 { + game.posX = 1 + } + case GLFW_KEY_LEFT: + game.move_right(-1) + case GLFW_KEY_RIGHT: + game.move_right(1) + case GLFW_KEY_DOWN: + game.move_tetro() // drop faster when the player preses + } +} +