Rotate tetrominoes around their center
5 files changed, 150 insertions(+), 45 deletions(-) M board.ha M main.ha M notes.txt M tetrominoes.ha M update.ha
M board.ha => board.ha +32 -5
@@ 1,5 1,6 @@ use math::random; use sdl2; use types; def BOARD_WIDTH: int = 10; @@ def BOARD_HEIGHT: int = 20; 72,6 73,27 @@ fn draw_board(state: *state, board: *board) (void | sdl2::error) = { }; draw_active(state, board.ghost_x, board.ghost_y, 128)?; const now = sdl2::get_ticks(); switch (state.state) { case gamestate::INTRO => // XXX: This is hacky as fuck. // https://todo.sr.ht/~sircmpwn/hare/54 let alpha = 255u8; const duration = (state.fadein_finish - state.fadein_start): int; const diff = (now - state.fadein_start): int; let workaround = 1.0 - (diff: f64 / duration: f64); if (diff > duration) { workaround = 0.0; }; if (now >= state.fadein_start) { alpha = (workaround * 255.0): int: u8; }; sdl2::set_render_draw_color(state.render, 0, 0, 0, alpha)?; sdl2::render_fill_rect(state.render, null)?; case => void; }; draw_active(state, board.active_x, board.active_y, 255)?; }; @@ 90,7 112,7 @@ fn draw_active( continue; }; const x = x + active.0, y = y + active.1; if (y < 0) { if (y < 0 && state.state != gamestate::INTRO) { continue; }; @@ let color = &color_map[cell]; 151,8 173,6 @@ fn update_ghost(state: *state) void = { const board = &state.board; const cells = &board.cells; const active = &board.active[board.active_i]; let width = 0, height = 0; activesize(state, &width, &height); let y = 1; @@ for (canmove(state, active, 0, y); y += 1) void; 225,6 245,7 @@ fn activesize( width: nullable *int, height: nullable *int, ) void = { let min_x = types::INT_MAX, min_y = types::INT_MAX; let max_x = 0, max_y = 0; const active = &state.board.active[state.board.active_i]; @@ for (let y = 0; y < TETROMINO_HEIGHT; y += 1) 236,15 257,21 @@ fn activesize( if (active && y > max_y) { max_y = y; }; if (active && x < min_x) { min_x = x; }; if (active && y < min_y) { min_y = y; }; }; match (width) { case let ptr: *int => *ptr = max_x + 1; *ptr = max_x - min_x + 1; case null => void; }; match (height) { case let ptr: *int => *ptr = max_y + 1; *ptr = max_y - min_y + 1; case null => void; }; };
M main.ha => main.ha +15 -2
@@ 15,6 15,7 @@ type texture = struct { }; type gamestate = enum { INTRO, SPAWN, FALL, @@ CLEAR, 43,6 44,10 @@ type state = struct { lastcolor: color, clearframe: int, clearlines: []int, // Intro fadein_start: u32, fadein_finish: u32, }; @@ export fn main() void = { 95,7 100,7 @@ fn run() (void | fs::error | sdl2::error) = { ... }, rand = random::init(seed), speed = 250, speed = 60 * 1000 / 84, // TODO: Set this to BPM properly ... }; @@ defer sdl2::destroy_texture(state.piece.tex); 106,6 111,7 @@ fn run() (void | fs::error | sdl2::error) = { mixer::play_channel(0, state.music, 0)?; sdl2::set_texture_blend_mode(state.piece.tex, sdl2::blend_mode::BLEND)?; sdl2::set_render_draw_blend_mode(state.render, sdl2::blend_mode::BLEND)?; let controller: nullable *sdl2::gamecontroller = null; @@ for (let i = 0; i < sdl2::numjoysticks()?; i += 1) { 125,8 131,15 @@ fn run() (void | fs::error | sdl2::error) = { sdl2::game_controller_close(c); }; const now = sdl2::get_ticks(); shuffle_tetrominoes(&state); do_spawn(&state, sdl2::get_ticks()); do_spawn(&state, now); // Set up intro state.state = gamestate::INTRO; state.board.active_y -= 5; state.fadein_start = now + 4000; state.fadein_finish = now + 8000; for (state.run) { update(&state)?;
M notes.txt => notes.txt +21 -3
@@ 1,8 1,6 @@ - tetromino swapping - rotate pieces around center origin - music - sound effects - level files & timings - level files & music timings - fix thumbsticks - scorekeeping/display @@ - keyboard input 11,6 9,7 @@ later: - menu system - options (music/sfx volume) - key/controller re-bindings - custom piece designs @@ - local multiplayer 25,3 24,22 @@ later: https://tetris.fandom.com/wiki/Tetris_Guideline https://tetris.fandom.com/wiki/SRS https://tetris.fandom.com/wiki/TGM_Rotation music sources: loops: https://opengameart.org/content/a-journey-awaits https://opengameart.org/content/snowfall https://opengameart.org/content/funny-chase-8-bit-chiptune https://opengameart.org/content/raining-beat-loop-8-bit-cyber-style-0 https://opengameart.org/content/menu-music themes: https://opengameart.org/content/rise-of-spirit https://opengameart.org/content/menu-music https://opengameart.org/content/erdreich-des-h%C3%BCnchenvolk https://opengameart.org/content/acoustic-jazz animation for concert opening: - dark screen, one falling piece as the clarinet solo comes in - when the rest of the band comes in, the rest of the level fades in
M tetrominoes.ha => tetrominoes.ha +58 -22
@@ 9,22 9,22 @@ const tetrominoes: [_][][TETROMINO_LEN]color = [ 0, 0, 0, 0, ], [ 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], [ 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], [ 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, ], ], @@ [ 36,21 36,21 @@ const tetrominoes: [_][][TETROMINO_LEN]color = [ 0, 0, 0, 0, ], [ 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, ], [ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], [ 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, ], @@ ], 72,6 72,18 @@ const tetrominoes: [_][][TETROMINO_LEN]color = [ 0, 0, 0, 0, ], [ 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, ], [ 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, ], [ 1, 0, 0, 0, 1, 1, 0, 0, @@ 0, 1, 0, 0, 87,6 99,18 @@ const tetrominoes: [_][][TETROMINO_LEN]color = [ 0, 0, 0, 0, ], [ 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, ], [ 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, ], [ 0, 1, 0, 0, 1, 1, 0, 0, @@ 1, 0, 0, 0, 102,16 126,16 @@ const tetrominoes: [_][][TETROMINO_LEN]color = [ 0, 0, 0, 0, ], [ 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, ], [ 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], [ @@ 0, 1, 0, 0, 123,16 147,28 @@ const tetrominoes: [_][][TETROMINO_LEN]color = [ [ // | [ 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, ], [ 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, ], [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, ], [ 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, ], ], ];
M update.ha => update.ha +24 -13
@@ 44,8 44,11 @@ fn update(state: *state) (void | sdl2::error) = { controller_button::X, controller_button::Y => rotate(state); case controller_button::DPAD_UP => state.board.active_y = state.board.ghost_y; commit(state); if (state.state != gamestate::INTRO) { state.board.active_y = state.board.ghost_y; commit(state); state.next = now; }; case controller_button::DPAD_DOWN => state.faster = true; @@ case => void; 61,6 64,8 @@ fn update(state: *state) (void | sdl2::error) = { if (state.next <= now) { switch (state.state) { case gamestate::INTRO => do_intro(state, now); case gamestate::SPAWN => do_spawn(state, now); @@ case gamestate::FALL => 74,6 79,14 @@ fn update(state: *state) (void | sdl2::error) = { }; }; fn do_intro(state: *state, now: u32) void = { state.board.active_y += 1; if (state.fadein_finish <= now) { state.state = gamestate::FALL; }; schedule_tick(state, now); }; fn do_spawn(state: *state, now: u32) void = { let next = state.board.tetrominoes[state.board.next]; @@ state.board.next += 1; 107,7 120,7 @@ fn do_spawn(state: *state, now: u32) void = { let width = 0, height = 0; activesize(state, &width, &height); state.board.active_y = -height; state.board.active_y = -(height - 1); state.board.active_x = BOARD_WIDTH / 2 - width / 2; state.state = gamestate::FALL; @@ update_ghost(state); 115,51 128,49 @@ fn do_spawn(state: *state, now: u32) void = { }; fn do_fall(state: *state, now: u32) void = { let height = 0; activesize(state, null, &height); const active = &state.board.active[state.board.active_i]; if (canmove(state, active, 0, 1)) { state.board.active_y += 1; schedule_tick(state, now); } else { commit(state); }; schedule_tick(state, now); }; fn do_clear(state: *state, now: u32) void = { state.clearframe += 1; state.clearframe %= 12; state.next = now + (state.speed / 6); state.next = now + 25; // XXX: Based on tempo? if (state.clearframe == 4) { clear_lines(state); }; if (state.clearframe == 9) { shiftrows(state); state.state = gamestate::SPAWN; state.board.active_y = -4; shiftrows(state); }; }; fn schedule_tick(state: *state, now: u32) void = { if (state.faster) { state.next = now + state.speed / 4; state.next = now + 100; // TODO: Based on BPM multiplier } else { state.next = now + state.speed; }; }; fn move_left(state: *state) void = { if (state.board.active_x > 0) { const active = &state.board.active[state.board.active_i]; if (canmove(state, active, -1, 0)) { state.board.active_x -= 1; update_ghost(state); }; }; fn move_right(state: *state) void = { let width = 0; activesize(state, &width, null); if (state.board.active_x < BOARD_WIDTH - width) { const active = &state.board.active[state.board.active_i]; if (canmove(state, active, 1, 0)) { state.board.active_x += 1; update_ghost(state); };