diff --git a/iced-tetris/Cargo.lock b/iced-tetris/Cargo.lock index 9d42bba..eb56105 100644 --- a/iced-tetris/Cargo.lock +++ b/iced-tetris/Cargo.lock @@ -998,6 +998,7 @@ dependencies = [ "iced", "iced_native", "rand", + "tetris-logic", ] [[package]] @@ -2099,6 +2100,13 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "tetris-logic" +version = "0.1.0" +dependencies = [ + "rand", +] + [[package]] name = "thiserror" version = "1.0.29" diff --git a/iced-tetris/Cargo.toml b/iced-tetris/Cargo.toml index e8c702e..1c0aa52 100644 --- a/iced-tetris/Cargo.toml +++ b/iced-tetris/Cargo.toml @@ -11,3 +11,7 @@ iced = { git = "https://github.com/hecrj/iced", rev = "099981cfc2f61a1f37e841005 iced_native = { git = "https://github.com/hecrj/iced", rev = "099981cfc2f61a1f37e84100592d65babb94fb82"} chrono = "0.4.19" rand = "0.8.4" +tetris-logic = { path = "tetris-logic" } + +[workspace] +members = ["tetris-logic"] diff --git a/iced-tetris/src/main.rs b/iced-tetris/src/main.rs index 15b64ac..dd0bedf 100644 --- a/iced-tetris/src/main.rs +++ b/iced-tetris/src/main.rs @@ -4,10 +4,7 @@ use iced::{ Settings, Size, Subscription, }; use iced_native::{event, subscription, Event}; -use rand::distributions::{Distribution, Standard}; - -const START_LOCATION: (u8, u8) = (5, 1); -type PieceBlocks = [(i8, i8); 4]; +use tetris_logic::{BlockGrid, MoveDirection, Tetromino}; fn main() -> iced::Result { Tetris::run(Settings::default()) @@ -68,17 +65,14 @@ impl Application for Tetris { } }; - match self.blocks.active_piece { - None => { - let piece: Tetromino = rand::random(); - self.blocks.drop_piece(piece); + if self.blocks.piece_currently_active() { + if self.ticks % 10 == 0 { + self.blocks.move_active_piece(MoveDirection::SoftDrop); } - Some(_) => { - if self.ticks % 10 == 0 { - self.blocks.move_active_piece(MoveDirection::SoftDrop); - } - } - }; + } else { + let piece: Tetromino = rand::random(); + self.blocks.drop_piece(piece); + } let lines_removed = self.blocks.clear_pieces(); self.lines_removed += lines_removed; @@ -135,6 +129,7 @@ impl<'a> canvas::Program for Tetris { ); let block = Path::rectangle(point, block_size); let color = tetronimo.color(); + let color = Color::from_rgb8(color.0, color.1, color.2); frame.fill(&block, color); } @@ -164,346 +159,3 @@ enum Message { Pause, Tick(chrono::DateTime), } - -struct BlockGrid { - state: [[Option; 20]; 10], - active_piece: Option, -} - -#[derive(Debug, Copy, Clone)] -struct ActivePiece { - location: (u8, u8), - tetromino: Tetromino, - orientation: Orientation, -} - -impl ActivePiece { - fn move_piece(&self, direction: &MoveDirection) -> ActivePiece { - use MoveDirection::*; - let (cur_x, cur_y) = self.location; - ActivePiece { - tetromino: self.tetromino, - orientation: self.orientation, - location: match direction { - Left => (cur_x.checked_sub(1).unwrap_or(0), cur_y), - Right => (cur_x + 1, cur_y), - SoftDrop => (cur_x, cur_y + 1), - HardDrop => (cur_x, cur_y), - }, - } - } - - fn rotate_piece(&self) -> ActivePiece { - ActivePiece { - tetromino: self.tetromino, - location: self.location, - orientation: match self.orientation { - Orientation::A => Orientation::B, - Orientation::B => Orientation::C, - Orientation::C => Orientation::D, - Orientation::D => Orientation::A, - }, - } - } -} - -impl BlockGrid { - fn new() -> BlockGrid { - let mut state = [[None; 20]; 10]; - BlockGrid { - state, - active_piece: None, - } - } - - /// If it's impossible to drop a piece, return false - fn drop_piece(&mut self, tetromino: Tetromino) -> bool { - if let None = self.active_piece { - let piece = ActivePiece { - location: START_LOCATION, - tetromino, - orientation: Orientation::A, - }; - let piece_blocks = Self::piece_blocks(&piece); - if self.piece_blocks_in_bounds(piece_blocks) { - self.active_piece = Some(piece); - true - } else { - false - } - } else { - false - } - } - - fn piece_blocks_in_bounds(&self, piece_blocks: PieceBlocks) -> bool { - piece_blocks.iter().all(|(x, y)| { - let x = *x; - let y = *y; - x >= 0 && y >= 0 && x < 10 && y < 20 && self.state[x as usize][y as usize].is_none() - }) - } - - /// If there is an active piece, and the move is legal, move it. Return true if it was possible - /// to move an active piece, false otherwise. - fn move_active_piece(&mut self, direction: MoveDirection) -> bool { - let active = self.active_piece; - match (active, direction) { - (None, _) => false, - (Some(piece), MoveDirection::Left | MoveDirection::Right) => { - let new_piece = piece.move_piece(&direction); - let new_blocks = Self::piece_blocks(&new_piece); - if self.piece_blocks_in_bounds(new_blocks) { - self.active_piece = Some(new_piece); - true - } else { - false - } - } - (Some(ref piece), MoveDirection::HardDrop) => { - let mut new_piece = *piece; - loop { - let p = new_piece.move_piece(&MoveDirection::SoftDrop); - let new_blocks = Self::piece_blocks(&p); - if self.piece_blocks_in_bounds(new_blocks) { - new_piece = p; - } else { - break; - } - } - - self.active_piece = Some(new_piece); - self.place_active_piece(); - true - } - (Some(ref piece), MoveDirection::SoftDrop) => { - let new_piece = piece.move_piece(&MoveDirection::SoftDrop); - let new_blocks = Self::piece_blocks(&new_piece); - if self.piece_blocks_in_bounds(new_blocks) { - self.active_piece = Some(new_piece); - true - } else { - self.place_active_piece(); - false - } - } - } - } - - fn rotate_active_piece(&mut self) -> bool { - let active = self.active_piece; - if let Some(piece) = active { - let new_piece = piece.rotate_piece(); - let new_blocks = Self::piece_blocks(&new_piece); - if self.piece_blocks_in_bounds(new_blocks) { - self.active_piece = Some(new_piece); - true - } else { - false - } - } else { - false - } - } - - /// Remove the currently active piece and place its blocks onto the board. - fn place_active_piece(&mut self) { - if let Some(piece) = self.active_piece { - let cur_blocks = Self::piece_blocks(&piece); - for (x, y) in cur_blocks.iter() { - self.state[*x as usize][*y as usize] = Some(Block { - source: piece.tetromino, - }); - } - self.active_piece = None; - } - } - - fn piece_blocks(piece: &ActivePiece) -> PieceBlocks { - use Orientation::*; - use Tetromino::*; - let (x, y) = piece.location; - let (x, y) = (x as i8, y as i8); - - // These use the "Original Rotation System" cf. Tetris wiki - match (piece.tetromino, piece.orientation) { - (I, A | C) => [(x - 2, y), (x - 1, y), (x, y), (x + 1, y)], - (I, B | D) => [(x, y - 1), (x, y), (x, y + 1), (x, y + 2)], - (J, A) => [(x - 1, y), (x, y), (x + 1, y), (x + 1, y + 1)], - (J, B) => [(x, y), (x + 1, y), (x, y + 1), (x, y + 2)], - (J, C) => [(x - 1, y), (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)], - (J, D) => [(x, y), (x, y + 1), (x, y + 2), (x - 1, y + 2)], - (L, A) => [(x - 1, y + 1), (x - 1, y), (x, y), (x + 1, y)], - (L, B) => [(x, y), (x, y + 1), (x, y + 2), (x + 1, y + 2)], - (L, C) => [(x - 1, y + 1), (x, y + 1), (x + 1, y + 1), (x + 1, y)], - (L, D) => [(x - 1, y), (x, y), (x, y + 1), (x, y + 2)], - (O, _) => [(x, y), (x, y + 1), (x - 1, y), (x - 1, y + 1)], - (S, A | C) => [(x - 1, y + 1), (x, y + 1), (x, y), (x + 1, y)], - (S, B | D) => [(x, y), (x, y + 1), (x + 1, y + 1), (x + 1, y + 2)], - (T, A) => [(x - 1, y), (x, y), (x + 1, y), (x, y + 1)], - (T, B) => [(x, y), (x, y + 1), (x + 1, y + 1), (x, y + 2)], - (T, C) => [(x, y), (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)], - (T, D) => [(x - 1, y + 1), (x, y), (x, y + 1), (x, y + 2)], - (Z, A | C) => [(x - 1, y + 1), (x, y + 1), (x, y + 2), (x + 1, y + 2)], - (Z, B | D) => [(x, y), (x, y + 1), (x + 1, y), (x + 1, y - 1)], - } - } - - /// Returns number of lines removed - fn clear_pieces(&mut self) -> u32 { - let mut new_state = [[None; 20]; 10]; - let mut offset = 0; - for row in (0..=19).rev() { - let mut all_filled = true; - let mut all_empty = true; - for col in 0..10 { - if self.state[col][row].is_none() { - all_filled = false; - } - if self.state[col][row].is_some() { - all_empty = false; - } - } - - if all_filled && !all_empty { - offset += 1; - } else { - if offset != 0 { - println!("Offset {} row {}", offset, row); - } - for col in 0..10 { - new_state[col][row+offset] = self.state[col][row]; - } - } - } - - self.state = new_state; - println!("OFFSET: {}", offset); - offset as u32 - } - - fn iter<'a>(&'a self) -> BlockGridIter<'a> { - let active_piece_blocks = self.active_piece.as_ref().map(|piece| { - let b = Self::piece_blocks(piece); - let blocks = [ - (b[0].0 as u8, b[0].1 as u8), - (b[1].0 as u8, b[1].1 as u8), - (b[2].0 as u8, b[2].1 as u8), - (b[3].0 as u8, b[3].1 as u8), - ]; - - (piece.tetromino, blocks) - }); - BlockGridIter::new(&self.state, active_piece_blocks) - } -} - -struct BlockGridIter<'a> { - outer_state: &'a [[Option; 20]; 10], - outer_active: Option<(Tetromino, [(u8, u8); 4])>, - idx: usize, - active_pice_idx: usize, -} - -impl<'a> BlockGridIter<'a> { - fn new( - state: &'a [[Option; 20]; 10], - outer_active: Option<(Tetromino, [(u8, u8); 4])>, - ) -> Self { - BlockGridIter { - outer_state: state, - idx: 0, - active_pice_idx: 0, - outer_active, - } - } -} - -impl<'a> std::iter::Iterator for BlockGridIter<'a> { - type Item = (u8, u8, Tetromino); - fn next(&mut self) -> Option { - loop { - if self.idx >= 200 { - return self.outer_active.and_then(|(tetromino, block_array)| { - let i = self.active_pice_idx; - self.active_pice_idx += 1; - block_array.get(i).map(|(x, y)| (*x, *y, tetromino)) - }); - } - let i = self.idx % 10; - let j = self.idx / 10; - match self.outer_state[i][j] { - Some(block) => { - self.idx += 1; - return Some((i as u8, j as u8, block.source)); - } - None => { - self.idx += 1; - } - } - } - } -} - -#[derive(Debug, Clone, Copy)] -struct Block { - source: Tetromino, -} - -#[derive(Debug, Copy, Clone)] -enum MoveDirection { - Left, - Right, - HardDrop, - SoftDrop, -} - -#[derive(Debug, Copy, Clone)] -enum Orientation { - A, - B, - C, - D, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -enum Tetromino { - I, - J, - L, - O, - S, - T, - Z, -} - -impl Tetromino { - fn color(self) -> Color { - use Tetromino::*; - match self { - I => Color::from_rgb8(0, 255, 255), - J => Color::from_rgb8(0, 0, 255), - L => Color::from_rgb8(255, 165, 0), - O => Color::from_rgb8(255, 255, 0), - S => Color::from_rgb8(0, 255, 0), - T => Color::from_rgb8(255, 255, 0), - Z => Color::from_rgb8(128, 0, 128), - } - } -} - -impl Distribution for Standard { - fn sample(&self, rng: &mut R) -> Tetromino { - let index: u8 = rng.gen_range(0..=6); - match index { - 0 => Tetromino::I, - 1 => Tetromino::J, - 2 => Tetromino::L, - 3 => Tetromino::O, - 4 => Tetromino::S, - 5 => Tetromino::T, - 6 => Tetromino::Z, - _ => unreachable!(), - } - } -} diff --git a/iced-tetris/tetris-logic/Cargo.toml b/iced-tetris/tetris-logic/Cargo.toml new file mode 100644 index 0000000..0c4e7f2 --- /dev/null +++ b/iced-tetris/tetris-logic/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "tetris-logic" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.8.4" diff --git a/iced-tetris/tetris-logic/src/lib.rs b/iced-tetris/tetris-logic/src/lib.rs new file mode 100644 index 0000000..b5638ca --- /dev/null +++ b/iced-tetris/tetris-logic/src/lib.rs @@ -0,0 +1,347 @@ +use rand::distributions::{Distribution, Standard}; + +const START_LOCATION: (u8, u8) = (5, 1); +type PieceBlocks = [(i8, i8); 4]; + +pub struct BlockGrid { + state: [[Option; 20]; 10], + active_piece: Option, +} + +impl BlockGrid { + pub fn new() -> BlockGrid { + let mut state = [[None; 20]; 10]; + BlockGrid { + state, + active_piece: None, + } + } + + pub fn piece_currently_active(&self) -> bool { + self.active_piece.is_some() + } + + /// If it's impossible to drop a piece, return false + pub fn drop_piece(&mut self, tetromino: Tetromino) -> bool { + if let None = self.active_piece { + let piece = ActivePiece { + location: START_LOCATION, + tetromino, + orientation: Orientation::A, + }; + let piece_blocks = Self::piece_blocks(&piece); + if self.piece_blocks_in_bounds(piece_blocks) { + self.active_piece = Some(piece); + true + } else { + false + } + } else { + false + } + } + + fn piece_blocks_in_bounds(&self, piece_blocks: PieceBlocks) -> bool { + piece_blocks.iter().all(|(x, y)| { + let x = *x; + let y = *y; + x >= 0 && y >= 0 && x < 10 && y < 20 && self.state[x as usize][y as usize].is_none() + }) + } + + /// If there is an active piece, and the move is legal, move it. Return true if it was possible + /// to move an active piece, false otherwise. + pub fn move_active_piece(&mut self, direction: MoveDirection) -> bool { + let active = self.active_piece; + match (active, direction) { + (None, _) => false, + (Some(piece), MoveDirection::Left | MoveDirection::Right) => { + let new_piece = piece.move_piece(&direction); + let new_blocks = Self::piece_blocks(&new_piece); + if self.piece_blocks_in_bounds(new_blocks) { + self.active_piece = Some(new_piece); + true + } else { + false + } + } + (Some(ref piece), MoveDirection::HardDrop) => { + let mut new_piece = *piece; + loop { + let p = new_piece.move_piece(&MoveDirection::SoftDrop); + let new_blocks = Self::piece_blocks(&p); + if self.piece_blocks_in_bounds(new_blocks) { + new_piece = p; + } else { + break; + } + } + + self.active_piece = Some(new_piece); + self.place_active_piece(); + true + } + (Some(ref piece), MoveDirection::SoftDrop) => { + let new_piece = piece.move_piece(&MoveDirection::SoftDrop); + let new_blocks = Self::piece_blocks(&new_piece); + if self.piece_blocks_in_bounds(new_blocks) { + self.active_piece = Some(new_piece); + true + } else { + self.place_active_piece(); + false + } + } + } + } + + pub fn rotate_active_piece(&mut self) -> bool { + let active = self.active_piece; + if let Some(piece) = active { + let new_piece = piece.rotate_piece(); + let new_blocks = Self::piece_blocks(&new_piece); + if self.piece_blocks_in_bounds(new_blocks) { + self.active_piece = Some(new_piece); + true + } else { + false + } + } else { + false + } + } + + /// Remove the currently active piece and place its blocks onto the board. + fn place_active_piece(&mut self) { + if let Some(piece) = self.active_piece { + let cur_blocks = Self::piece_blocks(&piece); + for (x, y) in cur_blocks.iter() { + self.state[*x as usize][*y as usize] = Some(Block { + source: piece.tetromino, + }); + } + self.active_piece = None; + } + } + + fn piece_blocks(piece: &ActivePiece) -> PieceBlocks { + use Orientation::*; + use Tetromino::*; + let (x, y) = piece.location; + let (x, y) = (x as i8, y as i8); + + // These use the "Original Rotation System" cf. Tetris wiki + match (piece.tetromino, piece.orientation) { + (I, A | C) => [(x - 2, y), (x - 1, y), (x, y), (x + 1, y)], + (I, B | D) => [(x, y - 1), (x, y), (x, y + 1), (x, y + 2)], + (J, A) => [(x - 1, y), (x, y), (x + 1, y), (x + 1, y + 1)], + (J, B) => [(x, y), (x + 1, y), (x, y + 1), (x, y + 2)], + (J, C) => [(x - 1, y), (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)], + (J, D) => [(x, y), (x, y + 1), (x, y + 2), (x - 1, y + 2)], + (L, A) => [(x - 1, y + 1), (x - 1, y), (x, y), (x + 1, y)], + (L, B) => [(x, y), (x, y + 1), (x, y + 2), (x + 1, y + 2)], + (L, C) => [(x - 1, y + 1), (x, y + 1), (x + 1, y + 1), (x + 1, y)], + (L, D) => [(x - 1, y), (x, y), (x, y + 1), (x, y + 2)], + (O, _) => [(x, y), (x, y + 1), (x - 1, y), (x - 1, y + 1)], + (S, A | C) => [(x - 1, y + 1), (x, y + 1), (x, y), (x + 1, y)], + (S, B | D) => [(x, y), (x, y + 1), (x + 1, y + 1), (x + 1, y + 2)], + (T, A) => [(x - 1, y), (x, y), (x + 1, y), (x, y + 1)], + (T, B) => [(x, y), (x, y + 1), (x + 1, y + 1), (x, y + 2)], + (T, C) => [(x, y), (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)], + (T, D) => [(x - 1, y + 1), (x, y), (x, y + 1), (x, y + 2)], + (Z, A | C) => [(x - 1, y + 1), (x, y + 1), (x, y + 2), (x + 1, y + 2)], + (Z, B | D) => [(x, y), (x, y + 1), (x + 1, y), (x + 1, y - 1)], + } + } + + /// Returns number of lines removed + pub fn clear_pieces(&mut self) -> u32 { + let mut new_state = [[None; 20]; 10]; + let mut offset = 0; + for row in (0..=19).rev() { + let mut all_filled = true; + let mut all_empty = true; + for col in 0..10 { + if self.state[col][row].is_none() { + all_filled = false; + } + if self.state[col][row].is_some() { + all_empty = false; + } + } + + if all_filled && !all_empty { + offset += 1; + } else { + for col in 0..10 { + new_state[col][row + offset] = self.state[col][row]; + } + } + } + + self.state = new_state; + offset as u32 + } + + pub fn iter<'a>(&'a self) -> BlockGridIter<'a> { + let active_piece_blocks = self.active_piece.as_ref().map(|piece| { + let b = Self::piece_blocks(piece); + let blocks = [ + (b[0].0 as u8, b[0].1 as u8), + (b[1].0 as u8, b[1].1 as u8), + (b[2].0 as u8, b[2].1 as u8), + (b[3].0 as u8, b[3].1 as u8), + ]; + + (piece.tetromino, blocks) + }); + BlockGridIter::new(&self.state, active_piece_blocks) + } +} + +#[derive(Debug, Copy, Clone)] +struct ActivePiece { + location: (u8, u8), + tetromino: Tetromino, + orientation: Orientation, +} + +impl ActivePiece { + fn move_piece(&self, direction: &MoveDirection) -> ActivePiece { + use MoveDirection::*; + let (cur_x, cur_y) = self.location; + ActivePiece { + tetromino: self.tetromino, + orientation: self.orientation, + location: match direction { + Left => (cur_x.checked_sub(1).unwrap_or(0), cur_y), + Right => (cur_x + 1, cur_y), + SoftDrop => (cur_x, cur_y + 1), + HardDrop => (cur_x, cur_y), + }, + } + } + + fn rotate_piece(&self) -> ActivePiece { + ActivePiece { + tetromino: self.tetromino, + location: self.location, + orientation: match self.orientation { + Orientation::A => Orientation::B, + Orientation::B => Orientation::C, + Orientation::C => Orientation::D, + Orientation::D => Orientation::A, + }, + } + } +} + +pub struct BlockGridIter<'a> { + outer_state: &'a [[Option; 20]; 10], + outer_active: Option<(Tetromino, [(u8, u8); 4])>, + idx: usize, + active_pice_idx: usize, +} + +impl<'a> BlockGridIter<'a> { + fn new( + state: &'a [[Option; 20]; 10], + outer_active: Option<(Tetromino, [(u8, u8); 4])>, + ) -> Self { + BlockGridIter { + outer_state: state, + idx: 0, + active_pice_idx: 0, + outer_active, + } + } +} + +impl<'a> std::iter::Iterator for BlockGridIter<'a> { + type Item = (u8, u8, Tetromino); + fn next(&mut self) -> Option { + loop { + if self.idx >= 200 { + return self.outer_active.and_then(|(tetromino, block_array)| { + let i = self.active_pice_idx; + self.active_pice_idx += 1; + block_array.get(i).map(|(x, y)| (*x, *y, tetromino)) + }); + } + let i = self.idx % 10; + let j = self.idx / 10; + match self.outer_state[i][j] { + Some(block) => { + self.idx += 1; + return Some((i as u8, j as u8, block.source)); + } + None => { + self.idx += 1; + } + } + } + } +} + +#[derive(Debug, Clone, Copy)] +struct Block { + source: Tetromino, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Tetromino { + I, + J, + L, + O, + S, + T, + Z, +} + +#[derive(Debug, Copy, Clone)] +pub enum Orientation { + A, + B, + C, + D, +} + +#[derive(Debug, Copy, Clone)] +pub enum MoveDirection { + Left, + Right, + HardDrop, + SoftDrop, +} + +impl Tetromino { + pub fn color(self) -> (u8, u8, u8) { + use Tetromino::*; + match self { + I => (0, 255, 255), + J => (0, 0, 255), + L => (255, 165, 0), + O => (255, 255, 0), + S => (0, 255, 0), + T => (255, 255, 0), + Z => (128, 0, 128), + } + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Tetromino { + let index: u8 = rng.gen_range(0..=6); + match index { + 0 => Tetromino::I, + 1 => Tetromino::J, + 2 => Tetromino::L, + 3 => Tetromino::O, + 4 => Tetromino::S, + 5 => Tetromino::T, + 6 => Tetromino::Z, + _ => unreachable!(), + } + } +}