This commit is contained in:
Theis Pieter Hollebeek 2025-03-02 22:14:31 +01:00
parent 4b5124f1c3
commit 8e588a8a70
4 changed files with 342 additions and 226 deletions

25
src/actions.rs Normal file
View File

@ -0,0 +1,25 @@
use std::collections::HashMap;
#[derive(Hash, PartialEq, Eq)]
pub enum Controls {
Left,
Right,
SoftDrop,
HardDrop,
}
pub struct ControlsHeld(HashMap<Controls, usize>);
impl std::ops::Deref for ControlsHeld {
type Target = HashMap<Controls, usize>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for ControlsHeld {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

71
src/board.rs Normal file
View File

@ -0,0 +1,71 @@
use std::ops::{Deref, DerefMut};
use crate::{CurrentTetromino, Tetromino};
pub struct Board([[Option<Tetromino>; Self::WIDTH]; Self::HEIGHT]);
impl Deref for Board {
type Target = [[Option<Tetromino>; 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
}
}

View File

@ -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<Rgb>; 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<Tetromino>,
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!(

166
src/tetromino.rs Normal file
View File

@ -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],
}
}
}