From ba4a970b4c7d5ff003223b9eb7713c82f66f4797 Mon Sep 17 00:00:00 2001 From: PimWagemans09 Date: Mon, 9 Mar 2026 10:45:51 +0100 Subject: [PATCH 1/3] implement tetris --- fl16-inputmodules/src/control.rs | 6 +- fl16-inputmodules/src/games/mod.rs | 2 + fl16-inputmodules/src/games/tetris.rs | 412 ++++++++++++++++++++++++++ fl16-inputmodules/src/matrix.rs | 2 + ledmatrix/src/main.rs | 6 +- 5 files changed, 425 insertions(+), 3 deletions(-) create mode 100644 fl16-inputmodules/src/games/tetris.rs diff --git a/fl16-inputmodules/src/control.rs b/fl16-inputmodules/src/control.rs index 42961486..73eb8955 100644 --- a/fl16-inputmodules/src/control.rs +++ b/fl16-inputmodules/src/control.rs @@ -40,6 +40,7 @@ use is31fl3741::PwmFreq; #[cfg(feature = "c1minimal")] use smart_leds::{SmartLedsWrite, RGB8}; +use crate::games::tetris; #[repr(u8)] #[derive(num_derive::FromPrimitive)] @@ -351,7 +352,7 @@ pub fn parse_module_command(count: usize, buf: &[u8]) -> Option { Some(CommandVals::StartGame) => match arg.and_then(FromPrimitive::from_u8) { Some(GameVal::Snake) => Some(Command::StartGame(Game::Snake)), Some(GameVal::Pong) => Some(Command::StartGame(Game::Pong)), - Some(GameVal::Tetris) => None, + Some(GameVal::Tetris) => Some(Command::StartGame(Game::Tetris)), Some(GameVal::GameOfLife) => { if count >= 5 { FromPrimitive::from_u8(buf[4]) @@ -590,7 +591,7 @@ pub fn handle_command( match game { Game::Snake => snake::start_game(state, random), Game::Pong => pong::start_game(state, random), - Game::Tetris => {} + Game::Tetris => {tetris::start_game(state, random)} Game::GameOfLife(param) => game_of_life::start_game(state, random, *param), } None @@ -599,6 +600,7 @@ pub fn handle_command( match state.game { Some(GameState::Snake(_)) => snake::handle_control(state, arg), Some(GameState::Pong(_)) => pong::handle_control(state, arg), + Some(GameState::Tetris(_)) => tetris::handle_control(state, arg), Some(GameState::GameOfLife(_)) => game_of_life::handle_control(state, arg), _ => {} } diff --git a/fl16-inputmodules/src/games/mod.rs b/fl16-inputmodules/src/games/mod.rs index 6263c98d..3c2744a7 100644 --- a/fl16-inputmodules/src/games/mod.rs +++ b/fl16-inputmodules/src/games/mod.rs @@ -3,3 +3,5 @@ pub mod pong; pub mod pong_animation; pub mod snake; pub mod snake_animation; + +pub mod tetris; diff --git a/fl16-inputmodules/src/games/tetris.rs b/fl16-inputmodules/src/games/tetris.rs new file mode 100644 index 00000000..cb8bed8b --- /dev/null +++ b/fl16-inputmodules/src/games/tetris.rs @@ -0,0 +1,412 @@ +use crate::control::GameControlArg; +use crate::matrix::{GameState, Grid, LedmatrixState, HEIGHT, WIDTH}; + +use heapless::Vec; +use crate::animations::{Animation, StartupPercentageIterator}; + +type Position = (i8, i8); + +/* +Square: +██ ██ +██ ██ + +LLeft: +██ ██ + ██ + ██ + +LRight: +██ ██ +██ +██ + +T: +██ ██ ██ + ██ + +I: +██ +██ +██ +██ + +ZLeft: +██ ██ + ██ ██ + +ZRight: + ██ ██ +██ ██ + +*/ + +#[derive(Clone, Copy, Eq, PartialEq)] +enum Tetromino { + None = 0, + Square, + LLeft, + LRight, + T, + I, + ZLeft, + ZRight, +} + +fn get_tetromino_from_num(num: u8) -> Tetromino { + match num { + 0 => { + return Tetromino::None; + } + 1 => { + return Tetromino::Square; + } + 2 => { + return Tetromino::LLeft; + } + 3 => { + return Tetromino::LRight; + } + 4 => { + return Tetromino::T; + } + 5 => { + return Tetromino::I; + } + 6 => { + return Tetromino::ZLeft; + } + 7 => { + return Tetromino::ZRight; + } + _ => { Tetromino::None } + } +} + +#[derive(Clone)] +pub struct TetrisState { + placed_tetrominos: [[u8; HEIGHT]; WIDTH], + game_over: bool, + current_tetromino: Tetromino, + rotation: u8, + position: Position, + moved_this_tick: bool, +} + +fn get_tetromino(tetromino: Tetromino, rotation: u8) -> Vec { + let mut result: Vec = Vec::new(); + result.push((0, 0)).unwrap(); + result.push((0, 0)).unwrap(); + result.push((0, 0)).unwrap(); + result.push((0, 0)).unwrap(); + match tetromino { + Tetromino::Square => { + result[0] = (0, 0); + result[1] = (0, 1); + result[2] = (1, 0); + result[3] = (1, 1); + } + Tetromino::LLeft => match rotation { + 0 => { + result[0] = (0, 0); + result[1] = (0, 1); + result[2] = (0, -1); + result[3] = (-1, -1); + } + 1 => { + result[0] = (0, 0); + result[1] = (-1, 0); + result[2] = (1, 0); + result[3] = (1, -1); + } + 2 => { + result[0] = (0, 0); + result[1] = (0, 1); + result[2] = (0, -1); + result[3] = (1, 1); + } + 3 => { + result[0] = (0, 0); + result[1] = (-1, 0); + result[2] = (1, 0); + result[3] = (-1, 1); + } + _ => {} + }, + Tetromino::LRight => match rotation { + 0 => { + result[0] = (0, 0); + result[1] = (0, 1); + result[2] = (0, -1); + result[3] = (1, -1); + } + 1 => { + result[0] = (0, 0); + result[1] = (-1, 0); + result[2] = (1, 0); + result[3] = (1, 1); + } + 2 => { + result[0] = (0, 0); + result[1] = (0, 1); + result[2] = (0, -1); + result[3] = (-1, 1); + } + 3 => { + result[0] = (0, 0); + result[1] = (-1, 0); + result[2] = (1, 0); + result[3] = (-1, -1); + } + _ => {} + }, + Tetromino::T => match rotation { + 0 => { + result[0] = (0, 0); + result[1] = (0, 1); + result[2] = (-1, 0); + result[3] = (1, 0); + } + 1 => { + result[0] = (0, 0); + result[1] = (0, 1); + result[2] = (-1, 0); + result[3] = (0, -1); + } + 2 => { + result[0] = (0, 0); + result[1] = (0, -1); + result[2] = (-1, 0); + result[3] = (1, 0); + } + 3 => { + result[0] = (0, 0); + result[1] = (0, 1); + result[2] = (1, 0); + result[3] = (0, -1); + } + _ => {} + }, + Tetromino::I => match rotation { + 0 | 2 => { + result[0] = (0, 0); + result[1] = (0, -1); + result[2] = (0, 1); + result[3] = (0, 2); + } + 1 | 3 => { + result[0] = (0, 0); + result[1] = (-1, 0); + result[2] = (1, 0); + result[3] = (2, 0); + } + _ => {} + }, + Tetromino::ZLeft => match rotation { + 0 | 2 => { + result[0] = (0, 0); + result[1] = (-1, 0); + result[2] = (0, 1); + result[3] = (1, 1); + } + 1 | 3 => { + result[0] = (0, 0); + result[1] = (0, -1); + result[2] = (-1, 0); + result[3] = (-1, 1); + } + _ => {} + }, + Tetromino::ZRight => match rotation { + 0 | 2 => { + result[0] = (0, 0); + result[1] = (1, 0); + result[2] = (0, 1); + result[3] = (-1, 1); + } + 1 | 3 => { + result[0] = (0, 0); + result[1] = (0, -1); + result[2] = (1, 0); + result[3] = (1, 1); + } + _ => {} + }, + _ => {} + } + result +} + +impl TetrisState { + pub fn new(random: u8) -> Self { + TetrisState { + placed_tetrominos: [[0; 34]; 9], + game_over: false, + current_tetromino: get_tetromino_from_num((random % 7) + 1), + rotation: random % 4, + position: (4, 1), + moved_this_tick: false, + } + } + + pub fn tick(&mut self, random: u8) { + if self.game_over { + return; + } + self.moved_this_tick = false; + let tetronimo = get_tetromino(self.current_tetromino, self.rotation); + let mut can_fall: bool = true; + for i in 0..4 { + let x = self.position.0 + tetronimo[i].0; + let y = self.position.1 + tetronimo[i].1 + 1; + if y >= 34 { + can_fall = false; + break; + } + if self.placed_tetrominos[x as usize][y as usize] != 0 { + can_fall = false; + break; + } + } + if can_fall { + self.position.1 += 1; + } else { + for i in 0..4 { + let x = self.position.0 + tetronimo[i].0; + let y = self.position.1 + tetronimo[i].1; + if self.placed_tetrominos[x as usize][y as usize] == 1{ + self.game_over = true; + } + self.placed_tetrominos[x as usize][y as usize] = 1; + } + if self.game_over{ + return; + } + self.position = (4, 1); + self.rotation = random % 4; + self.current_tetromino = get_tetromino_from_num((random % 7) + 1); + + let mut cleared_rows: u8 = 0; + for y in (0..34).rev() { + let mut filled = true; + for x in 0..9 { + if self.placed_tetrominos[x][y] == 0 { + filled = false; + break; + } + } + + if filled { + cleared_rows += 1; + } else { + if cleared_rows == 0 { + continue; + } + for x in 0..9 { + self.placed_tetrominos[x][y + cleared_rows as usize] = self.placed_tetrominos[x][y]; + self.placed_tetrominos[x][y] = 0; + } + } + } + } + } + + pub fn draw_matrix(&self) -> Grid { + let mut grid: Grid = Grid::default(); + for x in 0..WIDTH { + for y in 0..HEIGHT { + grid.0[x][y] = (self.placed_tetrominos[x][y]) * 0xFF; + } + } + let tetronimo = get_tetromino(self.current_tetromino, self.rotation); + for i in 0..4 { + let x = self.position.0 + tetronimo[i].0; + let y = self.position.1 + tetronimo[i].1; + grid.0[x as usize][y as usize] = 0xff; + } + grid + } + + pub fn handle_control(&mut self, arg: &GameControlArg) { + let tetromino = get_tetromino(self.current_tetromino, self.rotation); + match arg { + GameControlArg::Left => { + if self.moved_this_tick { + return; + } + let mut can_move: bool = true; + for i in 0..4 { + if self.position.0 + tetromino[i].0 == 0 { + can_move = false; + break; + } + } + if can_move { + self.position.0 += 1; + self.moved_this_tick = true; + } + } + GameControlArg::Right => { + if self.moved_this_tick { + return; + } + let mut can_move: bool = true; + for i in 0..4 { + if self.position.0 + tetromino[i].0 == 8 { + can_move = false; + break; + } + } + if can_move { + self.position.0 -= 1; + self.moved_this_tick = true; + } + }, + GameControlArg::Up => { + let tetromino = get_tetromino(self.current_tetromino, (self.rotation+1)%4); + let mut can_rotate = true; + for i in 0..4{ + let x = self.position.0 + tetromino[i].0; + let y = self.position.1 + tetromino[i].1; + if x < 0 || x >= 9 || y >= 34{ + can_rotate = false; + break; + } + if self.placed_tetrominos[x as usize][y as usize] == 1{ + can_rotate = false; + break; + } + } + if can_rotate{ + self.rotation = (self.rotation + 1) % 4; + } + } + _ => {} + } + } +} + +pub fn start_game(state: &mut LedmatrixState, random: u8) { + state.game = Some(GameState::Tetris(TetrisState::new(random))); +} + +pub fn handle_control(state: &mut LedmatrixState, arg: &GameControlArg) { + if let Some(GameState::Tetris(ref mut tetris_state)) = state.game { + match arg { + GameControlArg::Exit => state.game = None, + _ => tetris_state.handle_control(arg), + } + } +} + +pub fn game_step(state: &mut LedmatrixState, random: u8) { + if let Some(GameState::Tetris(ref mut tetris_state)) = state.game { + if !tetris_state.game_over { + tetris_state.tick(random); + if tetris_state.game_over{ + state.upcoming_frames = Some(Animation::Percentage(StartupPercentageIterator::default())); + } + } + state.grid = tetris_state.draw_matrix(); + } +} \ No newline at end of file diff --git a/fl16-inputmodules/src/matrix.rs b/fl16-inputmodules/src/matrix.rs index 71a625fd..027b6c3d 100644 --- a/fl16-inputmodules/src/matrix.rs +++ b/fl16-inputmodules/src/matrix.rs @@ -3,6 +3,7 @@ use crate::control::PwmFreqArg; use crate::games::game_of_life::GameOfLifeState; use crate::games::pong::PongState; use crate::games::snake::SnakeState; +use crate::games::tetris::TetrisState; pub const WIDTH: usize = 9; pub const HEIGHT: usize = 34; @@ -73,5 +74,6 @@ pub enum SleepReason { pub enum GameState { Snake(SnakeState), Pong(PongState), + Tetris(TetrisState), GameOfLife(GameOfLifeState), } diff --git a/ledmatrix/src/main.rs b/ledmatrix/src/main.rs index 9337f053..eebca98f 100644 --- a/ledmatrix/src/main.rs +++ b/ledmatrix/src/main.rs @@ -144,7 +144,7 @@ use core::fmt::Write; use heapless::String; use fl16_inputmodules::control::*; -use fl16_inputmodules::games::{pong, snake}; +use fl16_inputmodules::games::{pong, snake, tetris}; use fl16_inputmodules::matrix::*; use fl16_inputmodules::patterns::*; use fl16_inputmodules::serialnum::{device_release, get_serialnum}; @@ -542,6 +542,10 @@ fn main() -> ! { let _ = serial.write(b"Pong Game step\r\n"); pong::game_step(&mut state, random); } + Some(GameState::Tetris(_)) => { + let _ = serial.write(b"Tetris Game step\r\n"); + tetris::game_step(&mut state, random); + } Some(GameState::Snake(_)) => { let _ = serial.write(b"Snake Game step\r\n"); let (direction, game_over, points, (x, y)) = From 5533dca88a17cc9c0c3ccbbcfb6d8fd74d9ff877 Mon Sep 17 00:00:00 2001 From: PimWagemans09 Date: Mon, 9 Mar 2026 11:16:35 +0100 Subject: [PATCH 2/3] fix tetris losing animation --- ledmatrix/src/main.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ledmatrix/src/main.rs b/ledmatrix/src/main.rs index eebca98f..fa428489 100644 --- a/ledmatrix/src/main.rs +++ b/ledmatrix/src/main.rs @@ -399,6 +399,15 @@ fn main() -> ! { } else { // Animation is over. Clear screen state.grid = Grid::default(); + + match state.game { + Some(GameState::Tetris(_)) => { + // after tetris plays the losing animation make sure to render the game again + tetris::game_step(&mut state, 0); + } + None => {} + _ => {} + } } } From 51bf5ddf9d0c02bd7fb3d68ad7ad58d356154a7d Mon Sep 17 00:00:00 2001 From: PimWagemans09 Date: Mon, 9 Mar 2026 12:41:03 +0100 Subject: [PATCH 3/3] add tetris startup animation --- fl16-inputmodules/src/animations.rs | 3 + fl16-inputmodules/src/games/mod.rs | 2 +- fl16-inputmodules/src/games/tetris.rs | 10 +- .../src/games/tetris_animation.rs | 108 ++++++++++++++++++ ledmatrix/src/main.rs | 6 +- 5 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 fl16-inputmodules/src/games/tetris_animation.rs diff --git a/fl16-inputmodules/src/animations.rs b/fl16-inputmodules/src/animations.rs index cad4071a..ff9d983c 100644 --- a/fl16-inputmodules/src/animations.rs +++ b/fl16-inputmodules/src/animations.rs @@ -2,6 +2,7 @@ use crate::control::*; use crate::games::game_of_life::*; use crate::games::pong_animation::*; use crate::games::snake_animation::*; +use crate::games::tetris_animation::*; use crate::matrix::Grid; use crate::matrix::*; use crate::patterns::*; @@ -19,6 +20,7 @@ pub enum Animation { Breathing(BreathingIterator), Snake(SnakeIterator), Pong(PongIterator), + Tetris(TetrisIterator) } impl Iterator for Animation { type Item = Grid; @@ -31,6 +33,7 @@ impl Iterator for Animation { Animation::Breathing(x) => x.next(), Animation::Snake(x) => x.next(), Animation::Pong(x) => x.next(), + Animation::Tetris(x) => x.next() } } } diff --git a/fl16-inputmodules/src/games/mod.rs b/fl16-inputmodules/src/games/mod.rs index 3c2744a7..226c7e17 100644 --- a/fl16-inputmodules/src/games/mod.rs +++ b/fl16-inputmodules/src/games/mod.rs @@ -3,5 +3,5 @@ pub mod pong; pub mod pong_animation; pub mod snake; pub mod snake_animation; - pub mod tetris; +pub mod tetris_animation; diff --git a/fl16-inputmodules/src/games/tetris.rs b/fl16-inputmodules/src/games/tetris.rs index cb8bed8b..cf28d6a1 100644 --- a/fl16-inputmodules/src/games/tetris.rs +++ b/fl16-inputmodules/src/games/tetris.rs @@ -315,14 +315,16 @@ impl TetrisState { let mut grid: Grid = Grid::default(); for x in 0..WIDTH { for y in 0..HEIGHT { - grid.0[x][y] = (self.placed_tetrominos[x][y]) * 0xFF; + // [8 - x] because the rest of the firmware expects (0,0) to be the top-right corner + // but I programmed the game assuming (0,0) is the top-left corner + grid.0[8-x][y] = (self.placed_tetrominos[x][y]) * 0xFF; } } let tetronimo = get_tetromino(self.current_tetromino, self.rotation); for i in 0..4 { let x = self.position.0 + tetronimo[i].0; let y = self.position.1 + tetronimo[i].1; - grid.0[x as usize][y as usize] = 0xff; + grid.0[8-x as usize][y as usize] = 0xff; } grid } @@ -342,7 +344,7 @@ impl TetrisState { } } if can_move { - self.position.0 += 1; + self.position.0 -= 1; self.moved_this_tick = true; } } @@ -358,7 +360,7 @@ impl TetrisState { } } if can_move { - self.position.0 -= 1; + self.position.0 += 1; self.moved_this_tick = true; } }, diff --git a/fl16-inputmodules/src/games/tetris_animation.rs b/fl16-inputmodules/src/games/tetris_animation.rs new file mode 100644 index 00000000..81ff86c6 --- /dev/null +++ b/fl16-inputmodules/src/games/tetris_animation.rs @@ -0,0 +1,108 @@ +use crate::control::GameControlArg; +use crate::games::tetris::TetrisState; +use crate::matrix::Grid; + +pub struct TetrisIterator { + state: TetrisState, + commands: [(Option, u8); 64], + current_tick: usize, +} + +impl Default for TetrisIterator { + fn default() -> Self{ Self::new(27)} +} + +impl TetrisIterator { + pub fn new(random: u8) -> Self { + Self { + state: TetrisState::new(random), + commands: SAMPLE_GAME, + current_tick: 0, + } + } +} + +impl Iterator for TetrisIterator { + type Item = Grid; + + fn next(&mut self) -> Option { + if self.current_tick / 4 >= self.commands.len() { + return None; + } + + let (maybe_cmd, random) = self.commands[self.current_tick/4]; + if let Some(command) = maybe_cmd { + self.state.handle_control(&command); + } + self.state.tick(random); + self.current_tick += 1; + Some(self.state.draw_matrix()) + } +} + +const SAMPLE_GAME: [(Option, u8); 64] = [ + (Some(GameControlArg::Up), 31), + (None, 2), + (None, 255), + (None, 10), + (None, 6), + (Some(GameControlArg::Left), 50), + (Some(GameControlArg::Left), 200), + (None, 27), + (None, 0), + (Some(GameControlArg::Up), 83), + (None, 240), + (None, 50), + (None, 7), + (None, 56), + (None, 4), + (None, 3), + (Some(GameControlArg::Right), 2), + (None, 1), + (None, 7), + (None, 6), + (None, 78), + (None, 123), + (None, 45), + (Some(GameControlArg::Up), 0), + (None, 67), + (None, 89), + (None, 10), + (None, 11), + (Some(GameControlArg::Right), 20), + (Some(GameControlArg::Right), 09), + (Some(GameControlArg::Right), 12), + (None, 83), // + (None, 101), + (None, 99), + (None, 114), + (None, 101), + (None, 116), + (None, 32), + (None, 77), + (None, 101), + (None, 115), + (None, 115), + (None, 97), + (None, 104), + (None, 101), + (Some(GameControlArg::Left), 33), + (Some(GameControlArg::Right), 75), + (None, 43), + (None, 9), + (None, 87), + (None, 36), + (None, 99), + (None, 100), + (None, 200), + (None, 45), + (None, 54), + (None, 130), + (None, 145), + (None, 150), + (None, 89), + (None, 56), + (None, 0), + (None, 1), + (None, 2), +]; diff --git a/ledmatrix/src/main.rs b/ledmatrix/src/main.rs index fa428489..8b49ffb6 100644 --- a/ledmatrix/src/main.rs +++ b/ledmatrix/src/main.rs @@ -144,12 +144,13 @@ use core::fmt::Write; use heapless::String; use fl16_inputmodules::control::*; +use fl16_inputmodules::games::tetris_animation::TetrisIterator; use fl16_inputmodules::games::{pong, snake, tetris}; use fl16_inputmodules::matrix::*; use fl16_inputmodules::patterns::*; use fl16_inputmodules::serialnum::{device_release, get_serialnum}; -// FRA - Framwork +// FRA - Framework // KDE - C1 LED Matrix // BZ - BizLink // 01 - SKU, Default Configuration @@ -252,7 +253,7 @@ fn main() -> ! { }; state.debug_mode = dip1.is_low().unwrap(); if show_startup_animation(&state) { - state.upcoming_frames = Some(match get_random_byte(&rosc) % 8 { + state.upcoming_frames = Some(match get_random_byte(&rosc) % 9 { 0 => Animation::Percentage(StartupPercentageIterator::default()), 1 => Animation::ZigZag(ZigZagIterator::default()), 2 => Animation::Gof(GameOfLifeIterator::new(GameOfLifeStartParam::Pattern1, 200)), @@ -264,6 +265,7 @@ fn main() -> ! { 5 => Animation::Breathing(BreathingIterator::default()), 6 => Animation::Pong(PongIterator::default()), 7 => Animation::Snake(SnakeIterator::default()), + 8 => Animation::Tetris(TetrisIterator::default()), _ => unreachable!(), }); } else {