commit e392a4132ac5d74f92b3233fb78a7fb8eca177e1 Author: Theis Pieter Hollebeek Date: Sun Mar 2 16:59:44 2025 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ec71789 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,243 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi", + "windows-targets", +] + +[[package]] +name = "libc" +version = "0.2.170" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy 0.7.35", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha", + "rand_core", + "zerocopy 0.8.21", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "reimtris2" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf01143b2dd5d134f11f545cf9f1431b13b749695cb33bcce051e7568f99478" +dependencies = [ + "zerocopy-derive 0.8.21", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c803713 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "reimtris2" +version = "0.1.0" +edition = "2021" + +[dependencies] +rand = "0.9.0" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..93a023f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,226 @@ +#[derive(Debug, PartialEq)] +enum Tetromino { + I, + J, + L, + O, + S, + T, + Z, +} + +#[derive(Copy, Clone)] +struct Rgb(u8, u8, u8); + +enum Direction { + Up, + Right, + Down, + Left, +} + +enum DirectionDiff { + CW, + CCW, +} + +impl Tetromino { + fn new_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), + } + } + + const fn directions(&self) -> [[[i8; 4]; 4]; 4] { + 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]], + ], + } + } + + const fn wallkicks(&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, + x: i8, + y: i8, +} + +impl CurrentTetromino { + fn new(tetromino: Tetromino) -> Self { + const BOARD_WIDTH: i8 = 10; + const PIECE_WIDTH: i8 = 2; + Self { + tetromino, + direction: Direction::Up, + x: (Board::WIDTH as i8 - PIECE_WIDTH) / 2, + y: -1, + } + } +} + +struct Board([[Rgb; Self::WIDTH]; Self::HEIGHT]); + +impl Board { + const WIDTH: usize = 10; + const HEIGHT: usize = 20; +} + +impl Board { + pub fn new() -> Self { + Board([[Rgb(0, 0, 0); Self::WIDTH]; Self::HEIGHT]) + } +} + +struct Game { + board: Board, + next_tetrominos: [Tetromino; 3], + current_tetromino: CurrentTetromino, + held_tetromino: Option, + has_swapped_held: bool, +} + +impl Game { + fn next_in_bag(&mut self, mut last: Tetromino) -> Tetromino { + for value in self.next_tetrominos.iter_mut().rev() { + std::mem::swap(value, &mut last) + } + last + } + + fn try_swap_tetromino(&mut self) { + if self.has_swapped_held { + return; + } + self.has_swapped_held = true; + let held_or_first_in_bag_tetromino = self + .held_tetromino + .take() + .unwrap_or_else(|| self.next_in_bag(Tetromino::new_random())); + let current_tetromino = CurrentTetromino::new(held_or_first_in_bag_tetromino); + let old_tetromino = std::mem::replace(&mut self.current_tetromino, current_tetromino); + self.held_tetromino.replace(old_tetromino.tetromino); + } +} + +fn main() {} + +#[cfg(test)] +mod test { + use crate::{Board, CurrentTetromino, Game, Tetromino}; + + #[test] + fn advance_bag() { + let mut game = Game { + board: Board::new(), + next_tetrominos: [Tetromino::I, Tetromino::J, Tetromino::O], + current_tetromino: CurrentTetromino::new(Tetromino::J), + held_tetromino: None, + has_swapped_held: false, + }; + assert_eq!(game.next_in_bag(Tetromino::S), Tetromino::I); + assert_eq!( + game.next_tetrominos, + [Tetromino::J, Tetromino::O, Tetromino::S] + ); + } +} diff --git a/src/oldmain.rs b/src/oldmain.rs new file mode 100755 index 0000000..b85e8ec --- /dev/null +++ b/src/oldmain.rs @@ -0,0 +1,1222 @@ +// Window size in pixels +const WINDOW_WIDTH: f32 = 480.0; +const WINDOW_HEIGHT: f32 = 460.0; + +// Offset of the game board in pixels +const GAME_OFFSET_X: f32 = 140.0; +const GAME_OFFSET_Y: f32 = 30.0; + +// Game size in blocks +const GAME_WIDTH: i32 = 10; +const GAME_HEIGHT: i32 = 20; + +// Block size in pixels +const SIZE: f32 = 20.0; + +const FPS: u32 = 60; + +// Resources +const BACKGROUND_MUSIC: &'static [u8] = include_bytes!("res/music.ogg"); +const HARD_DROP_SOUND: &'static [u8] = include_bytes!("res/hard_drop.ogg"); +const LINE_CLEAR_SOUND: &'static [u8] = include_bytes!("res/line_clear.ogg"); +const MOVE_SOUND: &'static [u8] = include_bytes!("res/move.ogg"); +const ROTATION_SOUND: &'static [u8] = include_bytes!("res/rotation.ogg"); +const DEFAULT_CONFIG: &'static [u8] = include_bytes!("res/default_config.toml"); +const FONT: &'static [u8] = include_bytes!("res/josenfin_sans_regular.ttf"); + +#[derive(Clone)] +struct Tetromino { + name: char, + color: Color, + rotations: [[[i8; 4]; 4]; 4], +} + +impl Debug for Tetromino { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "{}", self.name) + } +} + +lazy_static! { + static ref TETROMINOS: [Tetromino; 7] = [ + Tetromino { + name: 'I', + color: Color::from_rgb(0, 255, 255), + rotations: [ + [[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]] + ] + }, + Tetromino { + name: 'J', + color: Color::from_rgb(0, 0, 255), + rotations: [ + [[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]] + ] + }, + Tetromino { + name: 'L', + color: Color::from_rgb(255, 128, 0), + rotations: [ + [[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]] + ] + }, + Tetromino { + name: 'O', + color: Color::from_rgb(255, 255, 0), + rotations: [ + [[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]] + ] + }, + Tetromino { + name: 'S', + color: Color::from_rgb(0, 255, 0), + rotations: [ + [[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]] + ] + }, + Tetromino { + name: 'T', + color: Color::from_rgb(255, 0, 255), + rotations: [ + [[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]] + ] + }, + Tetromino { + name: 'Z', + color: Color::from_rgb(255, 0, 0), + rotations: [ + [[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]] + ] + } + ]; +} + +struct Coords { + x: i8, + y: i8, +} + +impl Coords { + pub fn new(x: i8, y: i8) -> Coords { + Coords { x, y } + } +} + +// Get wall kick data for this rotation +fn wall_kicks(tetromino: &Tetromino, old_rotation: i8, new_rotation: i8) -> [Coords; 5] { + const DEFAULT: i8 = 0; + const RIGHT: i8 = 1; + const UPSIDE_DOWN: i8 = 2; + const LEFT: i8 = 3; + + match tetromino.name { + 'J' | 'L' | 'S' | 'T' | 'Z' => match old_rotation { + DEFAULT => match new_rotation { + RIGHT => [ + Coords::new(0, 0), + Coords::new(-1, 0), + Coords::new(-1, 1), + Coords::new(0, -2), + Coords::new(-1, -2), + ], + LEFT => [ + Coords::new(0, 0), + Coords::new(1, 0), + Coords::new(1, 1), + Coords::new(0, -2), + Coords::new(1, -2), + ], + _ => panic!("Invalid rotation"), + }, + RIGHT => match new_rotation { + DEFAULT => [ + Coords::new(0, 0), + Coords::new(1, 0), + Coords::new(1, -1), + Coords::new(0, 2), + Coords::new(1, 2), + ], + UPSIDE_DOWN => [ + Coords::new(0, 0), + Coords::new(1, 0), + Coords::new(1, -1), + Coords::new(0, 2), + Coords::new(1, 2), + ], + _ => panic!("Invalid rotation"), + }, + UPSIDE_DOWN => match new_rotation { + RIGHT => [ + Coords::new(0, 0), + Coords::new(-1, 0), + Coords::new(-1, 1), + Coords::new(0, -2), + Coords::new(-1, -2), + ], + LEFT => [ + Coords::new(0, 0), + Coords::new(1, 0), + Coords::new(1, 1), + Coords::new(0, -2), + Coords::new(1, -2), + ], + _ => panic!("Invalid rotation"), + }, + LEFT => match new_rotation { + DEFAULT => [ + Coords::new(0, 0), + Coords::new(-1, 0), + Coords::new(-1, -1), + Coords::new(0, 2), + Coords::new(-1, 2), + ], + UPSIDE_DOWN => [ + Coords::new(0, 0), + Coords::new(1, 0), + Coords::new(1, 1), + Coords::new(0, -2), + Coords::new(1, -2), + ], + _ => panic!("Invalid rotation"), + }, + _ => panic!("Invalid rotation state"), + }, + 'I' => match old_rotation { + DEFAULT => match new_rotation { + RIGHT => [ + Coords::new(0, 0), + Coords::new(-2, 0), + Coords::new(1, 0), + Coords::new(-2, -1), + Coords::new(1, 2), + ], + LEFT => [ + Coords::new(0, 0), + Coords::new(-1, 0), + Coords::new(2, 0), + Coords::new(-1, 2), + Coords::new(2, -1), + ], + _ => panic!("Invalid rotation"), + }, + RIGHT => match new_rotation { + DEFAULT => [ + Coords::new(0, 0), + Coords::new(2, 0), + Coords::new(-1, 0), + Coords::new(2, 1), + Coords::new(-1, -2), + ], + UPSIDE_DOWN => [ + Coords::new(0, 0), + Coords::new(-1, 0), + Coords::new(2, 0), + Coords::new(1, 2), + Coords::new(2, -1), + ], + _ => panic!("Invalid rotation"), + }, + UPSIDE_DOWN => match new_rotation { + RIGHT => [ + Coords::new(0, 0), + Coords::new(1, 0), + Coords::new(-2, 0), + Coords::new(1, -2), + Coords::new(-2, 1), + ], + LEFT => [ + Coords::new(0, 0), + Coords::new(2, 0), + Coords::new(-1, 0), + Coords::new(2, 1), + Coords::new(-1, -2), + ], + _ => panic!("Invalid rotation"), + }, + LEFT => match new_rotation { + DEFAULT => [ + Coords::new(0, 0), + Coords::new(1, 0), + Coords::new(-2, 0), + Coords::new(1, -2), + Coords::new(-2, 1), + ], + UPSIDE_DOWN => [ + Coords::new(0, 0), + Coords::new(-2, 0), + Coords::new(1, 0), + Coords::new(-2, -1), + Coords::new(1, 2), + ], + _ => panic!("Invalid rotation"), + }, + _ => panic!("Invalid rotation state"), + }, + 'O' => [ + Coords::new(0, 0), + Coords::new(0, 0), + Coords::new(0, 0), + Coords::new(0, 0), + Coords::new(0, 0), + ], + _ => panic!("Invalid tetromino"), + } +} + +#[derive(Clone)] +struct CurrentTetromino { + tetromino: Tetromino, + rotation: i8, + x: i8, + y: i8, +} + +impl CurrentTetromino { + fn new(tetromino: Tetromino) -> CurrentTetromino { + CurrentTetromino { + tetromino, + rotation: 0, + x: (GAME_WIDTH / 2) as i8 - 2, + y: -1, + } + } +} + +#[derive(Serialize, Deserialize)] +struct Config { + muted: bool, + controls: Controls, +} + +#[derive(Serialize, Deserialize)] +struct Controls { + move_left: String, + move_right: String, + rotate_cw: String, + rotate_ccw: String, + soft_drop: String, + hard_drop: String, + hold: String, + pause: String, + mute: String, + restart: String, +} + +impl Config { + fn update(&mut self, context: &mut Context) { + let toml = toml::to_string_pretty(&self).unwrap(); + let mut config_path = user_config_dir(context).to_owned(); + config_path.push("config.toml"); + let mut config_file = File::create(config_path).unwrap(); + config_file.write_all(toml.as_bytes()).unwrap(); + } +} + +struct GameState { + board: [[Color; GAME_WIDTH as usize]; GAME_HEIGHT as usize], // The tetris play field with all the colors + bag: Vec, // The tetromino bag with up to 7 tetrominos + current_tetromino: Option, + next_tetromino: Option, + held_tetromino: Option, + has_swapped: bool, // Whether or not the player has swapped the held tetromino + level: i32, + score: i32, + lines: i32, // Number of lines cleared since last levelup + combo: i32, + draw: bool, // Whether or not to draw the next frame + game_over: bool, // Whether or not the game has ended + paused: bool, + focused: bool, + bgm: Source, + back_to_back: bool, + fps: u8, // The FPS value to draw + ticks: usize, + held_keys: HashMap, // Which keys are currently held down and at which tick the player started holding them down + config: Config, +} + +impl GameState { + pub fn new(context: &mut Context) -> Self { + // Get config file + let mut config_str = String::new(); + let mut config_path = user_config_dir(context).to_owned(); + config_path.push("config.toml"); + + // If the file exists, read it + if config_path.exists() { + let mut config_file = + File::open(config_path.clone()).expect("Could not open config file"); + config_file + .read_to_string(&mut config_str) + .expect("Could not read config file"); + + // If not, create it with default values + } else { + let mut config_file = + File::create(config_path.clone()).expect("Could not create config file"); + config_file + .write_all(DEFAULT_CONFIG) + .expect("Could not write to config file"); + config_str = String::from_utf8(DEFAULT_CONFIG.to_vec()).unwrap(); + } + + let config: Config = match toml::from_str(&*config_str) { + Ok(result) => result, + Err(err) => { + remove_file(config_path).expect(&*format!("Could not parse config file. {}", err)); + panic!("Could not parse config file. It has been reset now, try running the program again."); + } + }; + + let mut result = GameState { + board: [[BLACK; GAME_WIDTH as usize]; GAME_HEIGHT as usize], + bag: Vec::with_capacity(7), + current_tetromino: None, + next_tetromino: None, + held_tetromino: None, + has_swapped: false, + level: 1, + score: 0, + lines: 0, + combo: 0, + draw: true, + game_over: false, + paused: false, + focused: true, + bgm: play_sound(context, BACKGROUND_MUSIC, 1.0, true, false), + back_to_back: false, + fps: 0, + ticks: 0, + held_keys: HashMap::new(), + config, + }; + if result.config.muted { + result.bgm.pause(); + } + new_current_tetromino(&mut result); + result + } +} + +impl EventHandler for GameState { + fn update(&mut self, context: &mut Context) -> GameResult { + if check_update_time(context, FPS) { + if self.game_over || self.paused || !self.focused { + return Ok(()); + } + + self.ticks += 1; + + // Falling delay, faster when soft drop key is held down + let mut delay = 32 - self.level as usize * 2; + if is_key_pressed(context, KeyCode::Down) { + delay /= 10; + } + + // Make tetrominos fall down + if self.ticks % delay == 0 { + let mut current_tetromino = self.current_tetromino.clone().unwrap(); + current_tetromino.y += 1; + + if collides(¤t_tetromino, self.board) { + current_tetromino.y -= 1; + get_new_tetromino(self); + check_line_clears(self, context); + } else { + self.current_tetromino = Some(current_tetromino); + if is_key_pressed(context, KeyCode::Down) { + self.score += 1; + } + } + + self.draw = true; + } + + // Holding down left/right after delay + for key in [KeyCode::Left, KeyCode::Right].iter() { + if self.held_keys.contains_key(key) && self.held_keys[key] < self.ticks - 15 { + let mut tetromino = self.current_tetromino.clone().unwrap(); + tetromino.x += match key { + KeyCode::Left => -1, + KeyCode::Right => 1, + _ => 0, + }; + if !collides(&tetromino, self.board) { + self.current_tetromino = Some(tetromino); + if !self.config.muted { + play_sound(context, MOVE_SOUND, 1.0, false, true); + } + } + self.draw = true; + } + } + + // Update FPS counter + if self.ticks % 60 == 0 { + self.fps = fps(context).floor() as u8; + self.draw = true; + } + } + + Ok(()) + } + + fn draw(&mut self, context: &mut Context) -> GameResult { + if !self.draw { + return Ok(()); + } + + self.draw = false; + + clear(context, BLACK); + + let font = Font::new_glyph_font_bytes(context, FONT).unwrap_or(Font::default()); + + let mut mesh = MeshBuilder::new(); + + // Draw grid lines + for i in 0..self.board.len() { + mesh.line( + &[ + Point2::from([GAME_OFFSET_X, GAME_OFFSET_Y + i as f32 * SIZE]), + Point2::from([ + GAME_OFFSET_X + GAME_WIDTH as f32 * SIZE, + GAME_OFFSET_Y + i as f32 * SIZE, + ]), + ], + 1.0, + Color::from_rgb(50, 50, 50), + ) + .unwrap(); + } + + for i in 0..self.board[0].len() { + mesh.line( + &[ + Point2::from([GAME_OFFSET_X + i as f32 * SIZE, GAME_OFFSET_Y]), + Point2::from([ + GAME_OFFSET_X + i as f32 * SIZE, + GAME_OFFSET_Y + GAME_HEIGHT as f32 * SIZE, + ]), + ], + 1.0, + Color::from_rgb(50, 50, 50), + ) + .unwrap(); + } + + // Draw board frame + mesh.rectangle( + DrawMode::Stroke(StrokeOptions::default()), + Rect::new( + GAME_OFFSET_X, + GAME_OFFSET_Y, + 1.0 + SIZE * GAME_WIDTH as f32, + 1.0 + SIZE * GAME_HEIGHT as f32, + ), + WHITE, + ); + + // Draw board content + for i in 0..self.board.len() { + for j in 0..self.board[i].len() { + // Don't draw invisible blocks + if i >= GAME_HEIGHT as usize || self.board[i][j] == BLACK { + continue; + } + + let offset = if self.board[i][j] == BLACK { 1.0 } else { 0.0 }; + let color = if self.game_over { + Color::from_rgb(200, 200, 200) + } else { + self.board[i][j] + }; + mesh.rectangle( + DrawMode::fill(), + Rect::new( + GAME_OFFSET_X + j as f32 * SIZE + offset, + GAME_OFFSET_Y + i as f32 * SIZE + offset, + SIZE - offset, + SIZE - offset, + ), + color, + ); + } + } + + // Draw ghost tetromino + let mut ghost_tetromino = self.current_tetromino.clone().unwrap(); + loop { + ghost_tetromino.y += 1; + if collides(&ghost_tetromino, self.board) { + ghost_tetromino.y -= 1; + draw_tetromino(&ghost_tetromino, Color::from_rgb(100, 100, 100), &mut mesh); + break; + } + } + + // Draw current tetromino + let current_tetromino = self.current_tetromino.clone().unwrap(); + let color = if self.game_over { + Color::from_rgb(200, 200, 200) + } else { + current_tetromino.tetromino.color + }; + draw_tetromino(¤t_tetromino, color, &mut mesh); + + if self.paused || self.game_over { + // Draw box behind pause/game over text + let height = 60.0; + mesh.rectangle( + DrawMode::fill(), + Rect::new( + GAME_OFFSET_X, + GAME_OFFSET_Y + (GAME_HEIGHT as f32 * SIZE / 2.0) - height / 2.0, + GAME_WIDTH as f32 * SIZE, + height, + ), + Color::from_rgba(0, 0, 0, 90), + ); + + // Draw text + let mut text: Text; + if self.paused { + text = Text::new("PAUSED"); + } else { + text = Text::new("GAME OVER"); + } + text.set_font(font, Scale::uniform(28.0)); + let width = text.width(context); + let height = text.height(context); + queue_text( + context, + &text, + Point2 { + x: WINDOW_WIDTH / 2.0 - width as f32 / 2.0, + y: WINDOW_HEIGHT / 2.0 - height as f32 / 2.0, + }, + Some(WHITE), + ); + } + + // Draw 'next' text + let mut text = Text::new("NEXT"); + text.set_font(font, Scale::uniform(20.0)); + queue_text( + context, + &text, + Point2 { + x: GAME_OFFSET_X + (GAME_WIDTH as f32 * SIZE) + 20.0, + y: GAME_OFFSET_Y, + }, + Some(WHITE), + ); + + // Draw next piece + let text_height = text.height(context); + let next_tetromino = self.next_tetromino.as_ref().unwrap(); + let tetromino = next_tetromino.rotations[0]; + for i in 1..tetromino.len() { + for j in 0..tetromino[i].len() { + if tetromino[i][j] != 1 { + continue; + } + + mesh.rectangle( + DrawMode::fill(), + Rect::new( + GAME_OFFSET_X + (GAME_WIDTH as f32 * SIZE) + 20.0 + SIZE * j as f32, + GAME_OFFSET_Y + text_height as f32 + 2.0 + SIZE * i as f32, + SIZE, + SIZE, + ), + next_tetromino.color, + ); + } + } + + // Draw 'hold' text + let mut text = Text::new("HOLD"); + text.set_font(font, Scale::uniform(20.0)); + let text_width = text.width(context); + let text_height = text.height(context); + queue_text( + context, + &text, + Point2 { + x: GAME_OFFSET_X - 20.0 - text_width as f32, + y: GAME_OFFSET_Y, + }, + Some(WHITE), + ); + + // Draw held piece + if self.held_tetromino.is_some() { + let held_tetromino = self.held_tetromino.as_ref().unwrap(); + let tetromino = held_tetromino.rotations[0]; + for i in 1..tetromino.len() { + for j in 0..tetromino[i].len() { + if tetromino[i][j] != 1 { + continue; + } + + let mut offset = 3.0; + if held_tetromino.name == 'I' { + offset = 4.0; + } + + mesh.rectangle( + DrawMode::fill(), + Rect::new( + GAME_OFFSET_X - 20.0 - SIZE * offset + SIZE * j as f32, + GAME_OFFSET_Y + text_height as f32 + 2.0 + SIZE * i as f32, + SIZE, + SIZE, + ), + held_tetromino.color, + ); + } + } + } + + let mesh = mesh.build(context).unwrap(); + draw(context, &mesh, DrawParam::default()).unwrap(); + + // Draw 'level' text + let mut text = Text::new("LEVEL"); + text.set_font(font, Scale::uniform(20.0)); + let text_width = text.width(context); + let text_height = text.height(context); + queue_text( + context, + &text, + Point2 { + x: GAME_OFFSET_X - 20.0 - text_width as f32, + y: GAME_OFFSET_Y + 200.0, + }, + Some(WHITE), + ); + + // Draw level + let mut text = Text::new(self.level.to_string()); + text.set_font(font, Scale::uniform(20.0)); + let level_text_width = text.width(context); + queue_text( + context, + &text, + Point2 { + x: GAME_OFFSET_X - 20.0 - text_width as f32 / 2.0 - level_text_width as f32 / 2.0, + y: GAME_OFFSET_Y + 210.0 + text_height as f32, + }, + Some(WHITE), + ); + + // Draw 'score' text + let mut text = Text::new("SCORE"); + text.set_font(font, Scale::uniform(20.0)); + let text_height = text.height(context); + queue_text( + context, + &text, + Point2 { + x: GAME_OFFSET_X + (SIZE * GAME_WIDTH as f32) + 20.0, + y: GAME_OFFSET_Y + 200.0, + }, + Some(WHITE), + ); + + // Draw score + let mut text = Text::new(self.score.to_string()); + text.set_font(font, Scale::uniform(20.0)); + let score_text_width = text.width(context); + queue_text( + context, + &text, + Point2 { + x: GAME_OFFSET_X + (SIZE * GAME_WIDTH as f32) + 20.0 + text_width as f32 / 2.0 + - score_text_width as f32 / 2.0, + y: GAME_OFFSET_Y + 210.0 + text_height as f32, + }, + Some(WHITE), + ); + + // Draw FPS counter + let mut text = Text::new("FPS: ".to_string() + &self.fps.to_string()); + text.set_font(font, Scale::uniform(12.0)); + draw( + context, + &text, + DrawParam::default().dest(Point2 { x: 2.0, y: 2.0 }), + ) + .unwrap(); + + sleep(Duration::from_millis(1)); + + draw_queued_text(context, DrawParam::default(), None, FilterMode::Linear).unwrap(); + present(context).unwrap(); + + Ok(()) + } + + fn key_down_event( + &mut self, + context: &mut Context, + key_code: KeyCode, + _key_mods: KeyMods, + _repeat: bool, + ) { + let key = serde_json::to_string(&key_code) + .expect(&*format!("Invalid key: {:?}", key_code)) + .replace("\"", ""); + + self.held_keys.insert(key_code, self.ticks); + + if is_key_repeated(context) { + return (); + } + + let controls = &self.config.controls; + + // Enter will restart if the game has ended + if self.game_over { + if key == controls.restart { + self.board = [[BLACK; GAME_WIDTH as usize]; GAME_HEIGHT as usize]; + self.bag = Vec::with_capacity(7); + self.current_tetromino = None; + self.next_tetromino = None; + self.held_tetromino = None; + self.draw = true; + self.game_over = false; + self.back_to_back = false; + self.score = 0; + self.combo = 0; + self.level = 1; + new_current_tetromino(self); + if !self.config.muted { + self.bgm.play().unwrap(); + } + } + return (); + } + + let mut current_tetromino = self.current_tetromino.clone().unwrap(); + let mut draw = true; + + if key == controls.mute { + // Mute/unmute + if self.config.muted { + self.bgm.resume(); + } else { + self.bgm.pause(); + } + self.config.muted = !self.config.muted; + self.config.update(context); + } else if key == controls.pause { + // Pause / unpause + self.paused = !self.paused; + if self.paused { + self.bgm.pause(); + } else if !self.config.muted { + self.bgm.resume(); + } + } else if self.paused { + // The controls below should not work if the game is paused + return (); + } else if key == controls.move_left { + // Move left + current_tetromino.x -= 1; + if !collides(¤t_tetromino, self.board) { + self.current_tetromino = Some(current_tetromino); + if !self.config.muted { + play_sound(context, MOVE_SOUND, 1.0, false, true); + } + } + } else if key == controls.move_right { + // Move right + current_tetromino.x += 1; + if !collides(¤t_tetromino, self.board) { + self.current_tetromino = Some(current_tetromino); + if !self.config.muted { + play_sound(context, MOVE_SOUND, 1.0, false, true); + } + } + } else if key == controls.rotate_cw { + // Rotate CW + current_tetromino.rotation = (1 + current_tetromino.rotation) % 4; + attempt_rotation(self, current_tetromino, context); + } else if key == controls.rotate_ccw { + // Rotate CCW + current_tetromino.rotation -= 1; + if current_tetromino.rotation < 0 { + current_tetromino.rotation = 3; + } + attempt_rotation(self, current_tetromino, context); + } else if key == controls.hard_drop { + // Hard drop + loop { + current_tetromino.y += 1; + if collides(¤t_tetromino, self.board) { + current_tetromino.y -= 1; + + self.score += (current_tetromino.y - self.current_tetromino.as_ref().unwrap().y) + as i32 + * 2; + self.current_tetromino = Some(current_tetromino); + get_new_tetromino(self); + check_line_clears(self, context); + break; + } + } + } else if key == controls.hold { + // Hold piece + if !self.has_swapped { + if self.held_tetromino.is_some() { + // Swap current and held tetromino + let temp = self.current_tetromino.clone().unwrap(); + self.current_tetromino = + Some(CurrentTetromino::new(self.held_tetromino.clone().unwrap())); + self.held_tetromino = Some(temp.tetromino); + } else { + self.held_tetromino = Some(self.current_tetromino.clone().unwrap().tetromino); + new_current_tetromino(self); + } + + self.has_swapped = true; + if !self.config.muted { + play_sound(context, ROTATION_SOUND, 1.0, false, true); + } + } + } else { + draw = false; // Don't draw to the screen if nothing was changed + } + + self.draw = draw; + } + + fn key_up_event(&mut self, _context: &mut Context, key_code: KeyCode, _key_mods: KeyMods) { + self.held_keys.remove(&key_code); + } + + fn focus_event(&mut self, _context: &mut Context, gained: bool) { + self.focused = gained; + self.draw = true; + } +} + +fn play_sound( + context: &mut Context, + sound: &[u8], + volume: f32, + repeat: bool, + detached: bool, +) -> Source { + let mut sound = Source::from_data(context, SoundData::from_bytes(sound)).unwrap(); + sound.set_repeat(repeat); + if detached { + sound.play_detached().unwrap(); + } else { + sound.play().unwrap(); + } + sound.set_volume(volume); + return sound; +} + +fn attempt_rotation( + game_state: &mut GameState, + mut new_tetromino: CurrentTetromino, + context: &mut Context, +) { + let mut can_rotate = true; + if collides(&new_tetromino, game_state.board) { + can_rotate = false; + let wall_kicks = wall_kicks( + &new_tetromino.tetromino, + game_state.current_tetromino.clone().unwrap().rotation, + new_tetromino.rotation, + ); + for wall_kick in wall_kicks.iter() { + let mut tetromino = new_tetromino.clone(); + tetromino.x += wall_kick.x; + tetromino.y += wall_kick.y; + if !collides(&tetromino, game_state.board) { + new_tetromino = tetromino; + can_rotate = true; + break; + } + } + } + if can_rotate { + game_state.current_tetromino = Some(new_tetromino); + check_collision(game_state, context); + if !game_state.config.muted { + play_sound(context, ROTATION_SOUND, 1.0, false, true); + } + } +} + +fn new_current_tetromino(game_state: &mut GameState) { + if game_state.bag.is_empty() { + game_state.bag = TETROMINOS + .choose_multiple(&mut thread_rng(), 7) + .cloned() + .collect() + } + + if game_state.next_tetromino.is_some() { + game_state.current_tetromino = Some(CurrentTetromino::new( + game_state.next_tetromino.clone().unwrap(), + )); + game_state.next_tetromino = Some(game_state.bag.remove(0)); + } else { + game_state.current_tetromino = Some(CurrentTetromino::new(game_state.bag.remove(0))); + game_state.next_tetromino = Some(game_state.bag.remove(0)); + } +} + +// Returns whether or not the current tetromino is colliding +fn collides( + current_tetromino: &CurrentTetromino, + board: [[Color; GAME_WIDTH as usize]; GAME_HEIGHT as usize], +) -> bool { + let tetromino = ¤t_tetromino.tetromino.rotations[current_tetromino.rotation as usize]; + for i in 0..tetromino.len() { + for j in 0..tetromino[i].len() { + if tetromino[i][j] == 0 { + continue; + } + + let x = j as i8 + current_tetromino.x; + let y = i as i8 + current_tetromino.y; + + if y < 0 { + continue; + } + + // Collides with floor Collides with walls Collides with other pieces + if y as i32 >= GAME_HEIGHT + || x >= GAME_WIDTH as i8 + || x < 0 + || board[y as usize][x as usize] != BLACK + { + return true; + } + } + } + false +} + +fn check_collision(game_state: &mut GameState, context: &mut Context) { + let mut current_tetromino = game_state.current_tetromino.clone().unwrap(); + current_tetromino.y += 1; + + if collides(¤t_tetromino, game_state.board) { + get_new_tetromino(game_state); + check_line_clears(game_state, context); + } +} + +fn check_line_clears(game_state: &mut GameState, context: &mut Context) { + let mut line_clears: Vec = Vec::with_capacity(GAME_HEIGHT as usize); + + // Add the indexes of the cleared lines to array + for i in 0..game_state.board.len() { + if !game_state.board[i].contains(&BLACK) { + line_clears.push(i); + } + } + + let mut line_clears_num = 0; + for i in (0..game_state.board.len()).rev() { + // Move lines above cleared lines down + if i + line_clears_num < GAME_HEIGHT as usize { + game_state.board[i + line_clears_num] = game_state.board[i]; + } + + if line_clears.contains(&i) { + line_clears_num += 1; + } + } + + // Level up + game_state.lines += line_clears_num as i32; + if game_state.lines > game_state.level * 5 { + game_state.level += 1; + game_state.lines = 0; + } + + // Award score (stored as float so it can be multiplied by 1.5) + let mut score: f32 = game_state.level as f32 + * match line_clears_num { + 0 => 0.0, + 1 => 100.0, + 2 => 300.0, + 3 => 500.0, + 4 => 800.0, + _ => panic!("You somehow cleared a number of lines that is below 0 or above 4"), + }; + // Back to back tetris + if game_state.back_to_back && line_clears_num == 4 { + score *= 1.5; + } + // Combos + if line_clears_num > 0 { + score += (game_state.combo * 50 * game_state.level) as f32; + } + game_state.score += score as i32; + + if line_clears_num == 4 { + game_state.back_to_back = true; + } else if line_clears_num > 0 { + game_state.back_to_back = false; + } + + if line_clears_num > 0 { + game_state.combo += 1; + if !game_state.config.muted { + play_sound( + context, + LINE_CLEAR_SOUND, + 1.0 + ((line_clears_num - 1) as f32 * 2.0), + false, + true, + ); + } + } else { + game_state.combo = 0; + if !game_state.config.muted { + play_sound(context, HARD_DROP_SOUND, 1.0, false, true); + } + } +} + +fn get_new_tetromino(game_state: &mut GameState) { + let current_tetromino = game_state.current_tetromino.clone().unwrap(); + let tetromino = ¤t_tetromino.tetromino.rotations[current_tetromino.rotation as usize]; + + // Add tetromino to board and get new current tetromino + for i in 0..tetromino.len() { + for j in 0..tetromino[i].len() { + if current_tetromino.tetromino.rotations[current_tetromino.rotation as usize][i][j] == 1 + { + game_state.board[(current_tetromino.y + i as i8) as usize] + [(current_tetromino.x + j as i8) as usize] = current_tetromino.tetromino.color; + } + } + } + + game_state.has_swapped = false; + + new_current_tetromino(game_state); + + if current_tetromino.y < 1 { + game_state.bgm.pause(); + game_state.game_over = true; + game_state.draw = true; + } +} + +fn draw_tetromino(current_tetromino: &CurrentTetromino, color: Color, mesh: &mut MeshBuilder) { + let tetromino = ¤t_tetromino.tetromino.rotations[current_tetromino.rotation as usize]; + + for i in 0..tetromino.len() { + for j in 0..tetromino[i].len() { + if tetromino[i][j] == 1 { + mesh.rectangle( + DrawMode::fill(), + Rect::new( + GAME_OFFSET_X + SIZE * (current_tetromino.x + j as i8) as f32, + GAME_OFFSET_Y + SIZE * (current_tetromino.y + i as i8) as f32, + SIZE, + SIZE, + ), + color, + ); + } + } + } +} + +fn main() { + let (context, event_loop) = &mut ContextBuilder::new("Reimtris", "Reimar") + .window_setup(WindowSetup { + title: "Reimtris".to_string(), + samples: Zero, + vsync: false, + icon: "".to_string(), + srgb: false, + }) + .window_mode(WindowMode::default().dimensions(WINDOW_WIDTH, WINDOW_HEIGHT)) + .build() + .unwrap(); + + // Crash handler + let user_data_path = user_data_dir(context).to_owned(); + let panic_hook = panic::take_hook(); + panic::set_hook(Box::new(move |info: &panic::PanicInfo| { + let mut path = user_data_path.clone(); + path.push("crash.txt"); + let mut file = File::create(path.clone()).unwrap(); + file.write_all(info.to_string().as_bytes()).unwrap(); + + // Create popup window if using windows + unsafe { + create_popup_message(info, path); + } + + panic_hook(info); + })); + + let game_state = &mut GameState::new(context); + event::run(context, event_loop, game_state).unwrap(); +} + +#[cfg(windows)] +unsafe fn create_popup_message(info: &panic::PanicInfo, path: PathBuf) { + let location = info.location().unwrap(); + let title: Vec = "Reimtris crashed :(\0".encode_utf16().collect(); + let message: Vec = (format!( + "{}\n\n{}:{}:{}\n\nFull error at: {}\0", + info.payload() + .downcast_ref::<&str>() + .unwrap_or(&"Unknown error"), + location.file(), + location.line(), + location.column(), + path.to_str().unwrap_or("Unknown path") + )) + .encode_utf16() + .collect(); + winapi::um::winuser::MessageBoxW( + null_mut(), + message.as_ptr(), + title.as_ptr(), + winapi::um::winuser::MB_OK | winapi::um::winuser::MB_ICONERROR, + ); +} + +#[cfg(not(windows))] +fn create_popup_message(info: &panic::PanicInfo) { + // Do nothing +} + diff --git a/src/res/default_config.toml b/src/res/default_config.toml new file mode 100755 index 0000000..9a94696 --- /dev/null +++ b/src/res/default_config.toml @@ -0,0 +1,14 @@ +muted = false + +# List of keys: https://docs.rs/winit/0.23.0/winit/event/enum.VirtualKeyCode.html#variants +[controls] +move_left = "Left" +move_right = "Right" +rotate_cw = "X" +rotate_ccw = "Z" +soft_drop = "Down" +hard_drop = "Space" +hold = "Up" +pause = "Escape" +mute = "M" +restart = "Return" \ No newline at end of file diff --git a/src/res/desktop.ini b/src/res/desktop.ini new file mode 100755 index 0000000..370d107 --- /dev/null +++ b/src/res/desktop.ini @@ -0,0 +1,2 @@ +[LocalizedFileNames] +media1.m4a=@media1.m4a,0 diff --git a/src/res/hard_drop.ogg b/src/res/hard_drop.ogg new file mode 100755 index 0000000..1ac2fdf Binary files /dev/null and b/src/res/hard_drop.ogg differ diff --git a/src/res/josenfin_sans_regular.ttf b/src/res/josenfin_sans_regular.ttf new file mode 100755 index 0000000..89d36f8 Binary files /dev/null and b/src/res/josenfin_sans_regular.ttf differ diff --git a/src/res/line_clear.ogg b/src/res/line_clear.ogg new file mode 100755 index 0000000..549962d Binary files /dev/null and b/src/res/line_clear.ogg differ diff --git a/src/res/move.ogg b/src/res/move.ogg new file mode 100755 index 0000000..0a86a4b Binary files /dev/null and b/src/res/move.ogg differ diff --git a/src/res/music.ogg b/src/res/music.ogg new file mode 100755 index 0000000..7cf9705 Binary files /dev/null and b/src/res/music.ogg differ diff --git a/src/res/rotation.ogg b/src/res/rotation.ogg new file mode 100755 index 0000000..b50d2e0 Binary files /dev/null and b/src/res/rotation.ogg differ