From 8e588a8a702d16c53d1d2e41796f4c184014b314 Mon Sep 17 00:00:00 2001 From: Theis Pieter Hollebeek Date: Sun, 2 Mar 2025 22:14:31 +0100 Subject: [PATCH] stepping --- src/actions.rs | 25 ++++ src/board.rs | 71 +++++++++++ src/main.rs | 306 +++++++++++++---------------------------------- src/tetromino.rs | 166 +++++++++++++++++++++++++ 4 files changed, 342 insertions(+), 226 deletions(-) create mode 100644 src/actions.rs create mode 100644 src/board.rs create mode 100644 src/tetromino.rs diff --git a/src/actions.rs b/src/actions.rs new file mode 100644 index 0000000..5791e4a --- /dev/null +++ b/src/actions.rs @@ -0,0 +1,25 @@ +use std::collections::HashMap; + +#[derive(Hash, PartialEq, Eq)] +pub enum Controls { + Left, + Right, + SoftDrop, + HardDrop, +} + +pub struct ControlsHeld(HashMap); + +impl std::ops::Deref for ControlsHeld { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for ControlsHeld { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/src/board.rs b/src/board.rs new file mode 100644 index 0000000..cc002c9 --- /dev/null +++ b/src/board.rs @@ -0,0 +1,71 @@ +use std::ops::{Deref, DerefMut}; + +use crate::{CurrentTetromino, Tetromino}; + +pub struct Board([[Option; Self::WIDTH]; Self::HEIGHT]); + +impl Deref for Board { + type Target = [[Option; Self::WIDTH]; Self::HEIGHT]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Board { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Board { + pub const WIDTH: usize = 10; + pub const HEIGHT: usize = 20; + + pub fn new() -> Self { + let board = std::array::from_fn(|_| std::array::from_fn(|_| None)); + Board(board) + } + + pub fn colliding( + &self, + CurrentTetromino { + tetromino, + direction, + x: cur_x, + y: cur_y, + }: &CurrentTetromino, + ) -> bool { + let pattern = tetromino.direction_pattern(direction); + + for (y, row) in pattern.iter().enumerate() { + for x in row + .iter() + .enumerate() + .filter(|(_, exists)| **exists) + .map(|(x, _)| x) + { + let x = x as i8 + cur_x; + let y = y as i8 + cur_y; + + if y < 0 { + continue; + } + + if y >= Board::HEIGHT as i8 { + return true; + } + + if x < 0 || x >= Board::WIDTH as i8 { + return true; + } + + if self.0[y as usize][x as usize].is_some() { + return true; + } + } + } + + false + } +} diff --git a/src/main.rs b/src/main.rs index d00300a..7d40745 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,171 +1,15 @@ -#[derive(Debug, PartialEq)] -enum Tetromino { - I, - J, - L, - O, - S, - T, - Z, -} +#![allow(dead_code)] + +use actions::{Controls, ControlsHeld}; +use board::Board; +use tetromino::{Direction, DirectionDiff, Tetromino}; + +mod actions; +mod board; +mod tetromino; -#[derive(Copy, Clone)] struct Rgb(u8, u8, u8); -enum Direction { - Up, - Right, - Down, - Left, -} - -impl Direction { - fn rotate(&self, diff: &DirectionDiff) -> Self { - match (self, diff) { - (Direction::Up, DirectionDiff::CW) => Self::Right, - (Direction::Up, DirectionDiff::CCW) => Self::Left, - (Direction::Right, DirectionDiff::CW) => Self::Down, - (Direction::Right, DirectionDiff::CCW) => Self::Up, - (Direction::Down, DirectionDiff::CW) => Self::Left, - (Direction::Down, DirectionDiff::CCW) => Self::Right, - (Direction::Left, DirectionDiff::CW) => Self::Up, - (Direction::Left, DirectionDiff::CCW) => Self::Down, - } - } -} - -enum DirectionDiff { - CW, - CCW, -} - -impl Tetromino { - fn random() -> Self { - let v: u8 = rand::random(); - match v % 7 { - 0 => Self::I, - 1 => Self::J, - 2 => Self::L, - 3 => Self::O, - 4 => Self::S, - 5 => Self::T, - 6 => Self::Z, - _ => unreachable!("v%7 is always in range 0..=6"), - } - } - - const fn color(&self) -> Rgb { - match self { - Self::I => Rgb(0, 255, 255), - Self::J => Rgb(0, 0, 255), - Self::L => Rgb(255, 128, 0), - Self::O => Rgb(255, 255, 0), - Self::S => Rgb(0, 255, 0), - Self::T => Rgb(255, 0, 255), - Self::Z => Rgb(255, 0, 0), - } - } - - fn direction_pattern(&self, direction: &Direction) -> [[bool; 4]; 4] { - let idx = match direction { - Direction::Up => 0, - Direction::Right => 1, - Direction::Down => 2, - Direction::Left => 3, - }; - - self.directions()[idx] - } - - fn directions(&self) -> [[[bool; 4]; 4]; 4] { - let dir = match self { - Self::I => [ - [[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]], - [[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]], - ], - Self::J => [ - [[0, 0, 0, 0], [1, 0, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0]], - [[0, 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, 1, 0, 0], [0, 1, 0, 0], [1, 1, 0, 0]], - ], - Self::L => [ - [[0, 0, 0, 0], [0, 0, 1, 0], [1, 1, 1, 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, 0, 0], [1, 1, 1, 0], [1, 0, 0, 0]], - [[0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]], - ], - Self::O => [ - [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], - [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], - [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], - [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], - ], - Self::S => [ - [[0, 0, 0, 0], [0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0]], - [[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]], - ], - Self::T => [ - [[0, 0, 0, 0], [0, 1, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0]], - [[0, 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, 1, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0]], - ], - Self::Z => [ - [[0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0]], - [[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]], - ], - }; - - dir.map(|dir| dir.map(|row| row.map(|v| v != 0))) - } - - const fn wall_kicks(&self, direction: &Direction, diff: &DirectionDiff) -> [(i8, i8); 5] { - match self { - Self::J | Self::L | Self::S | Self::T | Self::Z => match (direction, diff) { - (Direction::Up, DirectionDiff::CW) => [(0, 0), (-1, 0), (-1, 1), (0, -2), (-1, -2)], - (Direction::Up, DirectionDiff::CCW) => [(0, 0), (1, 0), (1, 1), (0, -2), (1, -2)], - - (Direction::Right, DirectionDiff::CW) => [(0, 0), (1, 0), (1, -1), (0, 2), (1, 2)], - (Direction::Right, DirectionDiff::CCW) => [(0, 0), (1, 0), (1, -1), (0, 2), (1, 2)], - - (Direction::Down, DirectionDiff::CW) => [(0, 0), (1, 0), (1, 1), (0, -2), (1, -2)], - (Direction::Down, DirectionDiff::CCW) => { - [(0, 0), (-1, 0), (-1, 1), (0, -2), (-1, -2)] - } - - (Direction::Left, DirectionDiff::CW) => { - [(0, 0), (-1, 0), (-1, -1), (0, 2), (-1, 2)] - } - (Direction::Left, DirectionDiff::CCW) => [(0, 0), (1, 0), (1, 1), (0, -2), (1, -2)], - }, - Self::I => match (direction, diff) { - (Direction::Up, DirectionDiff::CW) => [(0, 0), (-2, 0), (1, 0), (-2, -1), (1, 2)], - (Direction::Up, DirectionDiff::CCW) => [(0, 0), (-1, 0), (2, 0), (-1, 2), (2, -1)], - (Direction::Right, DirectionDiff::CW) => [(0, 0), (-1, 0), (2, 0), (1, 2), (2, -1)], - (Direction::Right, DirectionDiff::CCW) => { - [(0, 0), (2, 0), (-1, 0), (2, 1), (-1, -2)] - } - (Direction::Down, DirectionDiff::CW) => [(0, 0), (2, 0), (-1, 0), (2, 1), (-1, -2)], - (Direction::Down, DirectionDiff::CCW) => { - [(0, 0), (1, 0), (-2, 0), (1, -2), (-2, 1)] - } - (Direction::Left, DirectionDiff::CW) => [(0, 0), (1, 0), (-2, 0), (1, -2), (-2, 1)], - (Direction::Left, DirectionDiff::CCW) => { - [(0, 0), (-2, 0), (1, 0), (-2, -1), (1, 2)] - } - }, - Self::O => [(0, 0); 5], - } - } -} - struct CurrentTetromino { tetromino: Tetromino, direction: Direction, @@ -185,60 +29,6 @@ impl CurrentTetromino { } } -struct Board(pub [[Option; Self::WIDTH]; Self::HEIGHT]); - -impl Board { - const WIDTH: usize = 10; - const HEIGHT: usize = 20; -} - -impl Board { - pub fn new() -> Self { - Board([[None; Self::WIDTH]; Self::HEIGHT]) - } - - pub fn colliding( - &self, - CurrentTetromino { - tetromino, - direction, - x: cur_x, - y: cur_y, - }: &CurrentTetromino, - ) -> bool { - let pattern = tetromino.direction_pattern(direction); - - for y in 0..pattern.len() { - for x in 0..pattern[y].len() { - if !pattern[y][x] { - continue; - } - - let x = x as i8 + cur_x; - let y = y as i8 + cur_y; - - if y < 0 { - continue; - } - - if y >= Board::HEIGHT as i8 { - return true; - } - - if x < 0 || x >= Board::WIDTH as i8 { - return true; - } - - if self.0[y as usize][x as usize].is_some() { - return true; - } - } - } - - false - } -} - struct Game { board: Board, next_tetrominos: [Tetromino; 3], @@ -246,11 +36,12 @@ struct Game { held_tetromino: Option, has_swapped_held: bool, score: Score, + ticks: usize, } struct Score { level: usize, - score: usize, + points: usize, lines: usize, combo: usize, } @@ -259,7 +50,7 @@ impl Score { const fn new() -> Self { Self { level: 0, - score: 0, + points: 0, lines: 0, combo: 0, } @@ -274,11 +65,72 @@ impl Game { last } - fn try_rotate(&mut self, diff: DirectionDiff) { + fn hard_drop(&mut self, controls: &ControlsHeld) { + if controls.contains_key(&Controls::HardDrop) { + loop { + todo!() + } + } + } + + fn soft_drop(&mut self, controls: &ControlsHeld) { + let mut delay = 32 - self.score.level * 2; + if controls.contains_key(&Controls::SoftDrop) { + delay /= 10; + } + + if self.ticks % delay != 0 { + return; + } + + self.current_tetromino.y += 1; + if self.board.colliding(&self.current_tetromino) { + self.current_tetromino.y -= 1; + self.place_current_tetromino(); + self.check_line_clears(); + self.has_swapped_held = false; + } else if controls.contains_key(&Controls::SoftDrop) { + self.score.points += 1; + } + } + + fn move_horizontally(&mut self, controls: &ControlsHeld) { + for key in [Controls::Left, Controls::Right] { + let Some(held_since) = controls.get(&key) else { + continue; + }; + let held_for = self.ticks - held_since; + if held_for < 15 { + continue; + } + let offset = match key { + Controls::Left => -1, + Controls::Right => 1, + _ => unreachable!(), + }; + self.current_tetromino.x += offset; + if self.board.colliding(&self.current_tetromino) { + self.current_tetromino.x -= offset; + } + } + } + + fn check_line_clears(&self) { + todo!() + } + + fn step(&mut self, controls: &ControlsHeld) { + // TODO: ensure game is running at 60fps (`if !check_update_time(context, 60) { return; }`) + self.ticks += 1; + self.soft_drop(controls); + self.move_horizontally(controls); + } + + fn try_rotate(&mut self, diff: DirectionDiff) -> bool { let rotated = self.current_tetromino.direction.rotate(&diff); let old_direction = std::mem::replace(&mut self.current_tetromino.direction, rotated); if !self.board.colliding(&self.current_tetromino) { - return; + return true; } let wall_kicks = self .current_tetromino @@ -289,13 +141,14 @@ impl Game { self.current_tetromino.x += x; self.current_tetromino.y += y; if !(self.board.colliding(&self.current_tetromino)) { - return; + return true; } self.current_tetromino.x -= x; self.current_tetromino.y -= y; } self.current_tetromino.direction = old_direction; + false } fn place_current_tetromino(&mut self) { @@ -308,11 +161,11 @@ impl Game { .iter() .enumerate() .filter(|(_, exists)| **exists) - .map(|(idx, _)| idx) + .map(|(x, _)| x) { let y = (current.y + y as i8) as usize; let x = (current.x + x as i8) as usize; - self.board.0[y][x] = Some(current.tetromino.color()); + self.board[y][x] = Some(current.tetromino.clone()); } } } @@ -347,6 +200,7 @@ mod test { current_tetromino: CurrentTetromino::new(Tetromino::J), held_tetromino: None, has_swapped_held: false, + ticks: 0, }; assert_eq!(game.take_next_in_bag(Tetromino::S), Tetromino::I); assert_eq!( diff --git a/src/tetromino.rs b/src/tetromino.rs new file mode 100644 index 0000000..ff827c8 --- /dev/null +++ b/src/tetromino.rs @@ -0,0 +1,166 @@ +use crate::Rgb; + +#[derive(Clone, Debug, PartialEq)] +pub enum Tetromino { + I, + J, + L, + O, + S, + T, + Z, +} + +pub enum Direction { + Up, + Right, + Down, + Left, +} + +impl Direction { + pub fn rotate(&self, diff: &DirectionDiff) -> Self { + match (self, diff) { + (Direction::Up, DirectionDiff::Cw) => Self::Right, + (Direction::Up, DirectionDiff::Ccw) => Self::Left, + (Direction::Right, DirectionDiff::Cw) => Self::Down, + (Direction::Right, DirectionDiff::Ccw) => Self::Up, + (Direction::Down, DirectionDiff::Cw) => Self::Left, + (Direction::Down, DirectionDiff::Ccw) => Self::Right, + (Direction::Left, DirectionDiff::Cw) => Self::Up, + (Direction::Left, DirectionDiff::Ccw) => Self::Down, + } + } +} + +pub enum DirectionDiff { + Cw, + Ccw, +} + +impl Tetromino { + pub fn random() -> Self { + let v: u8 = rand::random(); + match v % 7 { + 0 => Self::I, + 1 => Self::J, + 2 => Self::L, + 3 => Self::O, + 4 => Self::S, + 5 => Self::T, + 6 => Self::Z, + _ => unreachable!("v%7 is always in range 0..=6"), + } + } + + const fn color(&self) -> Rgb { + match self { + Self::I => Rgb(0, 255, 255), + Self::J => Rgb(0, 0, 255), + Self::L => Rgb(255, 128, 0), + Self::O => Rgb(255, 255, 0), + Self::S => Rgb(0, 255, 0), + Self::T => Rgb(255, 0, 255), + Self::Z => Rgb(255, 0, 0), + } + } + + pub fn direction_pattern(&self, direction: &Direction) -> [[bool; 4]; 4] { + let idx = match direction { + Direction::Up => 0, + Direction::Right => 1, + Direction::Down => 2, + Direction::Left => 3, + }; + + self.directions()[idx] + } + + fn directions(&self) -> [[[bool; 4]; 4]; 4] { + let dir = match self { + Self::I => [ + [[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]], + [[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]], + ], + Self::J => [ + [[0, 0, 0, 0], [1, 0, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0]], + [[0, 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, 1, 0, 0], [0, 1, 0, 0], [1, 1, 0, 0]], + ], + Self::L => [ + [[0, 0, 0, 0], [0, 0, 1, 0], [1, 1, 1, 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, 0, 0], [1, 1, 1, 0], [1, 0, 0, 0]], + [[0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]], + ], + Self::O => [ + [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], + [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], + [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], + [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], + ], + Self::S => [ + [[0, 0, 0, 0], [0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0]], + [[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]], + ], + Self::T => [ + [[0, 0, 0, 0], [0, 1, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0]], + [[0, 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, 1, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0]], + ], + Self::Z => [ + [[0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0]], + [[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]], + ], + }; + + dir.map(|dir| dir.map(|row| row.map(|v| v != 0))) + } + + pub const fn wall_kicks(&self, direction: &Direction, diff: &DirectionDiff) -> [(i8, i8); 5] { + match self { + Self::J | Self::L | Self::S | Self::T | Self::Z => match (direction, diff) { + (Direction::Up, DirectionDiff::Cw) => [(0, 0), (-1, 0), (-1, 1), (0, -2), (-1, -2)], + (Direction::Up, DirectionDiff::Ccw) => [(0, 0), (1, 0), (1, 1), (0, -2), (1, -2)], + + (Direction::Right, DirectionDiff::Cw) => [(0, 0), (1, 0), (1, -1), (0, 2), (1, 2)], + (Direction::Right, DirectionDiff::Ccw) => [(0, 0), (1, 0), (1, -1), (0, 2), (1, 2)], + + (Direction::Down, DirectionDiff::Cw) => [(0, 0), (1, 0), (1, 1), (0, -2), (1, -2)], + (Direction::Down, DirectionDiff::Ccw) => { + [(0, 0), (-1, 0), (-1, 1), (0, -2), (-1, -2)] + } + + (Direction::Left, DirectionDiff::Cw) => { + [(0, 0), (-1, 0), (-1, -1), (0, 2), (-1, 2)] + } + (Direction::Left, DirectionDiff::Ccw) => [(0, 0), (1, 0), (1, 1), (0, -2), (1, -2)], + }, + Self::I => match (direction, diff) { + (Direction::Up, DirectionDiff::Cw) => [(0, 0), (-2, 0), (1, 0), (-2, -1), (1, 2)], + (Direction::Up, DirectionDiff::Ccw) => [(0, 0), (-1, 0), (2, 0), (-1, 2), (2, -1)], + (Direction::Right, DirectionDiff::Cw) => [(0, 0), (-1, 0), (2, 0), (1, 2), (2, -1)], + (Direction::Right, DirectionDiff::Ccw) => { + [(0, 0), (2, 0), (-1, 0), (2, 1), (-1, -2)] + } + (Direction::Down, DirectionDiff::Cw) => [(0, 0), (2, 0), (-1, 0), (2, 1), (-1, -2)], + (Direction::Down, DirectionDiff::Ccw) => { + [(0, 0), (1, 0), (-2, 0), (1, -2), (-2, 1)] + } + (Direction::Left, DirectionDiff::Cw) => [(0, 0), (1, 0), (-2, 0), (1, -2), (-2, 1)], + (Direction::Left, DirectionDiff::Ccw) => { + [(0, 0), (-2, 0), (1, 0), (-2, -1), (1, 2)] + } + }, + Self::O => [(0, 0); 5], + } + } +}