Compare commits
	
		
			No commits in common. "1830aec847ff95d41a66b96fb1786b062e13ae7e" and "eddf9d81c0f7090e074cc241f4b8b7185e18c3b7" have entirely different histories.
		
	
	
		
			1830aec847
			...
			eddf9d81c0
		
	
		
							
								
								
									
										911
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										911
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -5,8 +5,3 @@ edition = "2021" | |||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| rand = "0.9.0" | rand = "0.9.0" | ||||||
| rodio = "0.20.1" |  | ||||||
| 
 |  | ||||||
| [dependencies.sdl2] |  | ||||||
| version = "0.37.0" |  | ||||||
| features = ["ttf"] |  | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
| 
 | 
 | ||||||
| #[derive(Hash, PartialEq, Eq)] | #[derive(Hash, PartialEq, Eq)] | ||||||
| pub enum Action { | pub enum Controls { | ||||||
|     Left, |     Left, | ||||||
|     Right, |     Right, | ||||||
|     SoftDrop, |     SoftDrop, | ||||||
| @ -11,20 +11,17 @@ pub enum Action { | |||||||
|     RotateCcw, |     RotateCcw, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub struct ActionsHeld(HashMap<Action, usize>); | pub struct ControlsHeld(HashMap<Controls, usize>); | ||||||
| 
 | 
 | ||||||
| impl ActionsHeld { | impl ControlsHeld { | ||||||
|     pub fn new() -> Self { |     pub fn just_pressed(&self, ticks: usize, control: &Controls) -> bool { | ||||||
|         Self(HashMap::new()) |  | ||||||
|     } |  | ||||||
|     pub fn just_pressed(&self, ticks: usize, control: &Action) -> bool { |  | ||||||
|         self.held_for(ticks, control, |held_for| held_for == 0) |         self.held_for(ticks, control, |held_for| held_for == 0) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn held_for<F: Fn(usize) -> bool>( |     pub fn held_for<F: Fn(usize) -> bool>( | ||||||
|         &self, |         &self, | ||||||
|         ticks: usize, |         ticks: usize, | ||||||
|         control: &Action, |         control: &Controls, | ||||||
|         functor: F, |         functor: F, | ||||||
|     ) -> bool { |     ) -> bool { | ||||||
|         self.get(control) |         self.get(control) | ||||||
| @ -33,15 +30,15 @@ impl ActionsHeld { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl std::ops::Deref for ActionsHeld { | impl std::ops::Deref for ControlsHeld { | ||||||
|     type Target = HashMap<Action, usize>; |     type Target = HashMap<Controls, usize>; | ||||||
| 
 | 
 | ||||||
|     fn deref(&self) -> &Self::Target { |     fn deref(&self) -> &Self::Target { | ||||||
|         &self.0 |         &self.0 | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl std::ops::DerefMut for ActionsHeld { | impl std::ops::DerefMut for ControlsHeld { | ||||||
|     fn deref_mut(&mut self) -> &mut Self::Target { |     fn deref_mut(&mut self) -> &mut Self::Target { | ||||||
|         &mut self.0 |         &mut self.0 | ||||||
|     } |     } | ||||||
|  | |||||||
							
								
								
									
										40
									
								
								src/board.rs
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								src/board.rs
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | |||||||
| use std::ops::{Deref, DerefMut}; | use std::ops::{Deref, DerefMut}; | ||||||
| 
 | 
 | ||||||
| use crate::{game::CurrentTetromino, Tetromino}; | use crate::{CurrentTetromino, Tetromino}; | ||||||
| 
 | 
 | ||||||
| #[derive(PartialEq)] | #[derive(PartialEq)] | ||||||
| pub struct Board([[Option<Tetromino>; Self::WIDTH]; Self::HEIGHT]); | pub struct Board([[Option<Tetromino>; Self::WIDTH]; Self::HEIGHT]); | ||||||
| @ -28,35 +28,26 @@ impl Board { | |||||||
|         Board(board) |         Board(board) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn lowest_y( |     pub fn colliding( | ||||||
|         &self, |         &self, | ||||||
|         CurrentTetromino { |         CurrentTetromino { | ||||||
|             tetromino, |             tetromino, | ||||||
|             direction, |             direction, | ||||||
|             x, |             x: cur_x, | ||||||
|             y, |             y: cur_y, | ||||||
|         }: &CurrentTetromino, |         }: &CurrentTetromino, | ||||||
|     ) -> i8 { |     ) -> bool { | ||||||
|         let pattern = tetromino.direction_pattern(direction); |         let pattern = tetromino.direction_pattern(direction); | ||||||
|         let mut y = *y; |  | ||||||
|         loop { |  | ||||||
|             if self.pattern_and_position_colliding(pattern, *x, y) { |  | ||||||
|                 break y - 1; |  | ||||||
|             } |  | ||||||
|             y += 1; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     fn pattern_and_position_colliding(&self, pattern: [[bool; 4]; 4], x: i8, y: i8) -> bool { |         for (y, row) in pattern.iter().enumerate() { | ||||||
|         for (y_offset, row) in pattern.iter().enumerate() { |             for x in row | ||||||
|             for x_offset in row |  | ||||||
|                 .iter() |                 .iter() | ||||||
|                 .enumerate() |                 .enumerate() | ||||||
|                 .filter(|(_, exists)| **exists) |                 .filter(|(_, exists)| **exists) | ||||||
|                 .map(|(x, _)| x) |                 .map(|(x, _)| x) | ||||||
|             { |             { | ||||||
|                 let x = x_offset as i8 + x; |                 let x = x as i8 + cur_x; | ||||||
|                 let y = y_offset as i8 + y; |                 let y = y as i8 + cur_y; | ||||||
| 
 | 
 | ||||||
|                 if y < 0 { |                 if y < 0 { | ||||||
|                     continue; |                     continue; | ||||||
| @ -79,19 +70,6 @@ impl Board { | |||||||
|         false |         false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn colliding( |  | ||||||
|         &self, |  | ||||||
|         CurrentTetromino { |  | ||||||
|             tetromino, |  | ||||||
|             direction, |  | ||||||
|             x, |  | ||||||
|             y, |  | ||||||
|         }: &CurrentTetromino, |  | ||||||
|     ) -> bool { |  | ||||||
|         let pattern = tetromino.direction_pattern(direction); |  | ||||||
|         self.pattern_and_position_colliding(pattern, *x, *y) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn lines_cleared(&mut self) -> usize { |     pub fn lines_cleared(&mut self) -> usize { | ||||||
|         let line_clears: Vec<_> = self |         let line_clears: Vec<_> = self | ||||||
|             .iter() |             .iter() | ||||||
|  | |||||||
							
								
								
									
										317
									
								
								src/game.rs
									
									
									
									
									
								
							
							
						
						
									
										317
									
								
								src/game.rs
									
									
									
									
									
								
							| @ -1,317 +0,0 @@ | |||||||
| use crate::actions::{Action, ActionsHeld}; |  | ||||||
| use crate::board::Board; |  | ||||||
| use crate::tetromino::{Direction, DirectionDiff, Tetromino}; |  | ||||||
| 
 |  | ||||||
| pub struct CurrentTetromino { |  | ||||||
|     pub tetromino: Tetromino, |  | ||||||
|     pub direction: Direction, |  | ||||||
|     pub x: i8, |  | ||||||
|     pub y: i8, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl CurrentTetromino { |  | ||||||
|     fn new(tetromino: Tetromino) -> Self { |  | ||||||
|         const PIECE_WIDTH: i8 = 2; |  | ||||||
|         Self { |  | ||||||
|             tetromino, |  | ||||||
|             direction: Direction::Up, |  | ||||||
|             x: (Board::WIDTH as i8 - PIECE_WIDTH) / 2, |  | ||||||
|             y: -1, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub struct Game { |  | ||||||
|     pub game_over: bool, |  | ||||||
|     pub board: Board, |  | ||||||
|     pub next_tetrominos: [Tetromino; 3], |  | ||||||
|     pub current_tetromino: CurrentTetromino, |  | ||||||
|     pub held_tetromino: Option<Tetromino>, |  | ||||||
|     has_swapped_held: bool, |  | ||||||
|     pub score: Score, |  | ||||||
|     pub ticks: usize, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub enum SoundEffect { |  | ||||||
|     HardDrop, |  | ||||||
|     LineClear(usize), |  | ||||||
|     Move, |  | ||||||
|     Rotation, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub struct Score { |  | ||||||
|     pub level: usize, |  | ||||||
|     pub points: usize, |  | ||||||
|     pub lines: usize, |  | ||||||
|     pub combo: usize, |  | ||||||
|     back_to_back: bool, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Score { |  | ||||||
|     const fn new() -> Self { |  | ||||||
|         Self { |  | ||||||
|             level: 0, |  | ||||||
|             points: 0, |  | ||||||
|             lines: 0, |  | ||||||
|             combo: 0, |  | ||||||
|             back_to_back: false, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn level_up(&mut self, lines_cleared: usize) { |  | ||||||
|         self.lines += lines_cleared; |  | ||||||
|         if self.lines > self.level * 5 { |  | ||||||
|             self.level += 1; |  | ||||||
|             self.lines = 0; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn point_multiplier_from_lines_cleared(lines_cleared: usize) -> f32 { |  | ||||||
|         match lines_cleared { |  | ||||||
|             0 => 0.0, |  | ||||||
|             1 => 100.0, |  | ||||||
|             2 => 300.0, |  | ||||||
|             3 => 500.0, |  | ||||||
|             4 => 800.0, |  | ||||||
|             _ => unreachable!("we cannot clear more than 4 lines"), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn combos(&self, lines_cleared: usize) -> usize { |  | ||||||
|         if lines_cleared > 0 { |  | ||||||
|             self.combo * 50 * self.level |  | ||||||
|         } else { |  | ||||||
|             0 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Game { |  | ||||||
|     pub fn new() -> Self { |  | ||||||
|         Self { |  | ||||||
|             game_over: false, |  | ||||||
|             board: Board::new(), |  | ||||||
|             next_tetrominos: std::array::from_fn(|_| Tetromino::random()), |  | ||||||
|             current_tetromino: CurrentTetromino::new(Tetromino::random()), |  | ||||||
|             held_tetromino: None, |  | ||||||
|             has_swapped_held: false, |  | ||||||
|             score: Score::new(), |  | ||||||
|             ticks: 0, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     fn take_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_hard_drop(&mut self, actions: &ActionsHeld, effects: &mut Vec<SoundEffect>) { |  | ||||||
|         if !actions.just_pressed(self.ticks, &Action::HardDrop) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         let start_y = self.current_tetromino.y; |  | ||||||
|         loop { |  | ||||||
|             self.current_tetromino.y += 1; |  | ||||||
|             if !self.board.colliding(&self.current_tetromino) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             self.current_tetromino.y -= 1; |  | ||||||
|             self.score.points += (self.current_tetromino.y - start_y) as usize * 2; |  | ||||||
|             self.place_current_tetromino(); |  | ||||||
|             self.check_line_clears(effects); |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn soft_drop(&mut self, actions: &ActionsHeld, effects: &mut Vec<SoundEffect>) { |  | ||||||
|         let mut delay = 32 - self.score.level * 2; |  | ||||||
|         if actions.contains_key(&Action::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(effects); |  | ||||||
|         } else if actions.contains_key(&Action::SoftDrop) { |  | ||||||
|             self.score.points += 1; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn try_move_horizontally(&mut self, actions: &ActionsHeld, effects: &mut Vec<SoundEffect>) { |  | ||||||
|         for key in [Action::Left, Action::Right] { |  | ||||||
|             let just_pressed = actions.just_pressed(self.ticks, &key); |  | ||||||
|             let long_press = actions.held_for(self.ticks, &key, |held_for| held_for > 15); |  | ||||||
|             if !just_pressed && !long_press { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             let offset = match key { |  | ||||||
|                 Action::Left => -1, |  | ||||||
|                 Action::Right => 1, |  | ||||||
|                 _ => unreachable!(), |  | ||||||
|             }; |  | ||||||
|             self.current_tetromino.x += offset; |  | ||||||
|             if self.board.colliding(&self.current_tetromino) { |  | ||||||
|                 self.current_tetromino.x -= offset; |  | ||||||
|             } else { |  | ||||||
|                 effects.push(SoundEffect::Move); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn check_line_clears(&mut self, effects: &mut Vec<SoundEffect>) { |  | ||||||
|         let lines_cleared = self.board.lines_cleared(); |  | ||||||
| 
 |  | ||||||
|         self.score.level_up(lines_cleared); |  | ||||||
| 
 |  | ||||||
|         let mut points = |  | ||||||
|             self.score.level as f32 * Score::point_multiplier_from_lines_cleared(lines_cleared); |  | ||||||
| 
 |  | ||||||
|         if self.score.back_to_back && lines_cleared == 4 { |  | ||||||
|             points *= 1.5; |  | ||||||
|         } |  | ||||||
|         points += self.score.combos(lines_cleared) as f32; |  | ||||||
| 
 |  | ||||||
|         self.score.points += points as usize; |  | ||||||
| 
 |  | ||||||
|         if lines_cleared == 4 { |  | ||||||
|             self.score.back_to_back = true; |  | ||||||
|         } else if lines_cleared > 0 { |  | ||||||
|             self.score.back_to_back = false; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if lines_cleared > 0 { |  | ||||||
|             self.score.combo += 1; |  | ||||||
|             effects.push(SoundEffect::LineClear(lines_cleared)); |  | ||||||
|         } else { |  | ||||||
|             self.score.combo = 0; |  | ||||||
|             effects.push(SoundEffect::HardDrop); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn step(&mut self, actions: &ActionsHeld) -> Vec<SoundEffect> { |  | ||||||
|         if self.game_over { |  | ||||||
|             panic!("should check if game is over before stepping"); |  | ||||||
|         } |  | ||||||
|         let mut effects = Vec::new(); |  | ||||||
|         self.try_hard_drop(actions, &mut effects); |  | ||||||
|         self.soft_drop(actions, &mut effects); |  | ||||||
|         self.try_move_horizontally(actions, &mut effects); |  | ||||||
| 
 |  | ||||||
|         if actions.just_pressed(self.ticks, &Action::Swap) { |  | ||||||
|             self.try_swap_tetromino(&mut effects); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (control, direction) in [ |  | ||||||
|             (Action::RotateCw, DirectionDiff::Cw), |  | ||||||
|             (Action::RotateCcw, DirectionDiff::Ccw), |  | ||||||
|         ] { |  | ||||||
|             if !actions.just_pressed(self.ticks, &control) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             self.try_rotate(direction, &mut effects); |  | ||||||
|         } |  | ||||||
|         self.ticks += 1; |  | ||||||
|         effects |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn try_rotate(&mut self, diff: DirectionDiff, effects: &mut Vec<SoundEffect>) { |  | ||||||
|         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) { |  | ||||||
|             effects.push(SoundEffect::Rotation); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         let wall_kicks = self |  | ||||||
|             .current_tetromino |  | ||||||
|             .tetromino |  | ||||||
|             .wall_kicks(&old_direction, &diff); |  | ||||||
| 
 |  | ||||||
|         for (x, y) in wall_kicks { |  | ||||||
|             self.current_tetromino.x += x; |  | ||||||
|             self.current_tetromino.y += y; |  | ||||||
|             if !(self.board.colliding(&self.current_tetromino)) { |  | ||||||
|                 effects.push(SoundEffect::Rotation); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|             self.current_tetromino.x -= x; |  | ||||||
|             self.current_tetromino.y -= y; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         self.current_tetromino.direction = old_direction; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn place_current_tetromino(&mut self) { |  | ||||||
|         let next = CurrentTetromino::new(self.take_next_in_bag(Tetromino::random())); |  | ||||||
|         let current = std::mem::replace(&mut self.current_tetromino, next); |  | ||||||
|         let pattern = current.tetromino.direction_pattern(¤t.direction); |  | ||||||
| 
 |  | ||||||
|         if current.y <= 0 { |  | ||||||
|             self.game_over = true; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (y, row) in pattern.iter().enumerate() { |  | ||||||
|             for x in row |  | ||||||
|                 .iter() |  | ||||||
|                 .enumerate() |  | ||||||
|                 .filter(|(_, exists)| **exists) |  | ||||||
|                 .map(|(x, _)| x) |  | ||||||
|             { |  | ||||||
|                 let y = current.y + y as i8; |  | ||||||
|                 if y < 0 { |  | ||||||
|                     continue; |  | ||||||
|                 } |  | ||||||
|                 let y = y as usize; |  | ||||||
|                 let x = (current.x + x as i8) as usize; |  | ||||||
|                 self.board[y][x] = Some(current.tetromino.clone()); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         self.has_swapped_held = false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn try_swap_tetromino(&mut self, effects: &mut Vec<SoundEffect>) { |  | ||||||
|         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.take_next_in_bag(Tetromino::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); |  | ||||||
|         effects.push(SoundEffect::Rotation); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[cfg(test)] |  | ||||||
| mod test { |  | ||||||
|     use super::{Board, CurrentTetromino, Game, Score, Tetromino}; |  | ||||||
| 
 |  | ||||||
|     #[test] |  | ||||||
|     fn advance_bag() { |  | ||||||
|         let mut game = Game { |  | ||||||
|             game_over: false, |  | ||||||
|             board: Board::new(), |  | ||||||
|             score: Score::new(), |  | ||||||
|             next_tetrominos: [Tetromino::I, Tetromino::J, Tetromino::O], |  | ||||||
|             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!( |  | ||||||
|             game.next_tetrominos, |  | ||||||
|             [Tetromino::J, Tetromino::O, Tetromino::S] |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,5 +0,0 @@ | |||||||
| mod audio; |  | ||||||
| mod sdl; |  | ||||||
| mod ui; |  | ||||||
| 
 |  | ||||||
| pub use sdl::start_game; |  | ||||||
| @ -1,99 +0,0 @@ | |||||||
| use std::{fs::File, io::BufReader, sync::mpsc}; |  | ||||||
| 
 |  | ||||||
| use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink, Source}; |  | ||||||
| 
 |  | ||||||
| use crate::game::SoundEffect; |  | ||||||
| 
 |  | ||||||
| fn source_from_path<P: AsRef<std::path::Path>>(path: P) -> Decoder<BufReader<File>> { |  | ||||||
|     let file = BufReader::new(File::open(path).unwrap()); |  | ||||||
|     let source = Decoder::new(file).unwrap(); |  | ||||||
|     source |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| fn play_audio<P: AsRef<std::path::Path>>( |  | ||||||
|     stream_handle: &OutputStreamHandle, |  | ||||||
|     sink: &mut Option<Sink>, |  | ||||||
|     path: P, |  | ||||||
|     volume: f32, |  | ||||||
| ) { |  | ||||||
|     let source = source_from_path(path); |  | ||||||
|     *sink = Sink::try_new(&stream_handle).ok(); |  | ||||||
|     if let Some(sink) = sink { |  | ||||||
|         sink.set_volume(volume); |  | ||||||
|         sink.append(source); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub enum Command { |  | ||||||
|     ToggleMuted, |  | ||||||
|     PlayEffect(SoundEffect), |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub fn audio_thread() -> mpsc::Sender<Command> { |  | ||||||
|     let (sender, receiver) = mpsc::channel::<Command>(); |  | ||||||
| 
 |  | ||||||
|     let _ = std::thread::spawn(move || { |  | ||||||
|         let (_stream, stream_handle) = OutputStream::try_default().unwrap(); |  | ||||||
|         let music_sink = Sink::try_new(&stream_handle).unwrap(); |  | ||||||
|         let mut hard_drop_sink = None; |  | ||||||
|         let mut line_clear_sink = None; |  | ||||||
|         let mut move_sink = None; |  | ||||||
|         let mut rotation_sink = None; |  | ||||||
|         let mut muted = false; |  | ||||||
| 
 |  | ||||||
|         music_sink.append(source_from_path("resources/music.ogg").repeat_infinite()); |  | ||||||
| 
 |  | ||||||
|         loop { |  | ||||||
|             let Ok(cmd) = receiver.recv() else { |  | ||||||
|                 break; |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             let effect = match cmd { |  | ||||||
|                 Command::ToggleMuted => { |  | ||||||
|                     muted = !muted; |  | ||||||
|                     if muted { |  | ||||||
|                         music_sink.pause(); |  | ||||||
|                     } else { |  | ||||||
|                         music_sink.play(); |  | ||||||
|                     } |  | ||||||
|                     continue; |  | ||||||
|                 } |  | ||||||
|                 Command::PlayEffect(effect) => effect, |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             if muted { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             let base_volume = 0.5; |  | ||||||
|             match effect { |  | ||||||
|                 SoundEffect::HardDrop => play_audio( |  | ||||||
|                     &stream_handle, |  | ||||||
|                     &mut hard_drop_sink, |  | ||||||
|                     "resources/hard_drop.ogg", |  | ||||||
|                     base_volume, |  | ||||||
|                 ), |  | ||||||
|                 SoundEffect::LineClear(lines_cleared) => play_audio( |  | ||||||
|                     &stream_handle, |  | ||||||
|                     &mut line_clear_sink, |  | ||||||
|                     "resources/line_clear.ogg", |  | ||||||
|                     base_volume + (lines_cleared as f32 - 1.0) * 0.5, |  | ||||||
|                 ), |  | ||||||
|                 SoundEffect::Move => play_audio( |  | ||||||
|                     &stream_handle, |  | ||||||
|                     &mut move_sink, |  | ||||||
|                     "resources/move.ogg", |  | ||||||
|                     base_volume, |  | ||||||
|                 ), |  | ||||||
|                 SoundEffect::Rotation => play_audio( |  | ||||||
|                     &stream_handle, |  | ||||||
|                     &mut rotation_sink, |  | ||||||
|                     "resources/rotation.ogg", |  | ||||||
|                     base_volume, |  | ||||||
|                 ), |  | ||||||
|             }; |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     sender |  | ||||||
| } |  | ||||||
							
								
								
									
										223
									
								
								src/gui/sdl.rs
									
									
									
									
									
								
							
							
						
						
									
										223
									
								
								src/gui/sdl.rs
									
									
									
									
									
								
							| @ -1,223 +0,0 @@ | |||||||
| use crate::actions::{Action, ActionsHeld}; |  | ||||||
| use crate::game::Game; |  | ||||||
| use sdl2::event::Event; |  | ||||||
| use sdl2::keyboard::Keycode; |  | ||||||
| use sdl2::pixels::Color; |  | ||||||
| use sdl2::rect::Rect; |  | ||||||
| use sdl2::render::{Texture, TextureCreator, WindowCanvas}; |  | ||||||
| use sdl2::ttf::Sdl2TtfContext; |  | ||||||
| use std::time::Duration; |  | ||||||
| 
 |  | ||||||
| use super::audio::{self}; |  | ||||||
| use super::ui::{GameUiCtx, Rgb, UiCtx}; |  | ||||||
| 
 |  | ||||||
| const WIDTH: i32 = 1000; |  | ||||||
| const HEIGHT: i32 = 800; |  | ||||||
| 
 |  | ||||||
| fn font_texture<'font, 'a, P: AsRef<std::path::Path>, Text: AsRef<str>, C>( |  | ||||||
|     font: P, |  | ||||||
|     text: Text, |  | ||||||
|     ttf_context: &'a Sdl2TtfContext, |  | ||||||
|     texture_creator: &'font TextureCreator<C>, |  | ||||||
| ) -> Result<Texture<'font>, String> { |  | ||||||
|     let font = ttf_context.load_font(font, 24)?; |  | ||||||
|     let game_over_text = font |  | ||||||
|         .render(text.as_ref()) |  | ||||||
|         .solid(Color::RGB(255, 255, 255)) |  | ||||||
|         .map_err(|err| err.to_string())?; |  | ||||||
|     let texture = texture_creator |  | ||||||
|         .create_texture_from_surface(game_over_text) |  | ||||||
|         .map_err(|err| err.to_string())?; |  | ||||||
| 
 |  | ||||||
|     Ok(texture) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| struct SdlUiCtx { |  | ||||||
|     canvas: WindowCanvas, |  | ||||||
|     ttf: Sdl2TtfContext, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl SdlUiCtx { |  | ||||||
|     fn present(&mut self) { |  | ||||||
|         self.canvas.present(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl UiCtx<String> for SdlUiCtx { |  | ||||||
|     fn window_size(&self) -> Result<(i32, i32), String> { |  | ||||||
|         let (width, height) = self.canvas.window().size(); |  | ||||||
|         Ok((width as i32, height as i32)) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn fill_rect( |  | ||||||
|         &mut self, |  | ||||||
|         x: i32, |  | ||||||
|         y: i32, |  | ||||||
|         width: i32, |  | ||||||
|         height: i32, |  | ||||||
|         rgb: &super::ui::Rgb, |  | ||||||
|     ) -> Result<(), String> { |  | ||||||
|         self.canvas.set_draw_color(Color::RGB(rgb.0, rgb.1, rgb.2)); |  | ||||||
|         self.canvas |  | ||||||
|             .fill_rect(Rect::new(x, y, width as u32, height as u32))?; |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn outline_rect( |  | ||||||
|         &mut self, |  | ||||||
|         x: i32, |  | ||||||
|         y: i32, |  | ||||||
|         width: i32, |  | ||||||
|         height: i32, |  | ||||||
|         rgb: &super::ui::Rgb, |  | ||||||
|     ) -> Result<(), String> { |  | ||||||
|         self.canvas.set_draw_color(Color::RGB(rgb.0, rgb.1, rgb.2)); |  | ||||||
|         self.canvas |  | ||||||
|             .draw_rect(Rect::new(x, y, width as u32, height as u32))?; |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn text_size<P: AsRef<std::path::Path>, Text: AsRef<str>>( |  | ||||||
|         &mut self, |  | ||||||
|         font: P, |  | ||||||
|         text: Text, |  | ||||||
|     ) -> Result<(i32, i32), String> { |  | ||||||
|         let texture_creator = self.canvas.texture_creator(); |  | ||||||
|         let texture = font_texture(font, text, &self.ttf, &texture_creator)?; |  | ||||||
|         let query = texture.query(); |  | ||||||
|         Ok((query.width as i32, query.height as i32)) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn fill_text<P: AsRef<std::path::Path>, Text: AsRef<str>>( |  | ||||||
|         &mut self, |  | ||||||
|         font: P, |  | ||||||
|         text: Text, |  | ||||||
|         x: i32, |  | ||||||
|         y: i32, |  | ||||||
|         width: i32, |  | ||||||
|         height: i32, |  | ||||||
|     ) -> Result<(), String> { |  | ||||||
|         let texture_creator = self.canvas.texture_creator(); |  | ||||||
|         let texture = font_texture(font, text, &self.ttf, &texture_creator)?; |  | ||||||
|         self.canvas.copy( |  | ||||||
|             &texture, |  | ||||||
|             None, |  | ||||||
|             Some(Rect::new(x, y, width as u32, height as u32)), |  | ||||||
|         )?; |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn clear(&mut self, rgb: &Rgb) -> Result<(), String> { |  | ||||||
|         self.canvas.set_draw_color(Color::RGB(rgb.0, rgb.1, rgb.2)); |  | ||||||
|         self.canvas.clear(); |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub fn start_game() -> Result<(), String> { |  | ||||||
|     let mut game = Game::new(); |  | ||||||
|     let mut actions = ActionsHeld::new(); |  | ||||||
|     let mut paused = false; |  | ||||||
| 
 |  | ||||||
|     let audio_thread = audio::audio_thread(); |  | ||||||
| 
 |  | ||||||
|     let sdl_context = sdl2::init()?; |  | ||||||
|     let ttf_context = sdl2::ttf::init().unwrap(); |  | ||||||
|     let video_subsystem = sdl_context.video()?; |  | ||||||
| 
 |  | ||||||
|     let window = video_subsystem |  | ||||||
|         .window("reimtris2", WIDTH as u32, HEIGHT as u32) |  | ||||||
|         .resizable() |  | ||||||
|         .position_centered() |  | ||||||
|         .build() |  | ||||||
|         .unwrap(); |  | ||||||
| 
 |  | ||||||
|     let canvas = window.into_canvas().build().unwrap(); |  | ||||||
|     let mut ctx = SdlUiCtx { |  | ||||||
|         canvas, |  | ||||||
|         ttf: ttf_context, |  | ||||||
|     }; |  | ||||||
|     let mut event_pump = sdl_context.event_pump()?; |  | ||||||
|     'running: loop { |  | ||||||
|         ctx.clear(&Rgb(16, 16, 16))?; |  | ||||||
|         for event in event_pump.poll_iter() { |  | ||||||
|             match event { |  | ||||||
|                 Event::Quit { .. } |  | ||||||
|                 | Event::KeyDown { |  | ||||||
|                     keycode: Some(Keycode::Escape), |  | ||||||
|                     .. |  | ||||||
|                 } => break 'running Ok(()), |  | ||||||
|                 Event::KeyDown { |  | ||||||
|                     keycode: Some(keycode), |  | ||||||
|                     .. |  | ||||||
|                 } => { |  | ||||||
|                     let keycode = match keycode { |  | ||||||
|                         Keycode::Return if !paused && game.game_over => { |  | ||||||
|                             game = Game::new(); |  | ||||||
|                             continue; |  | ||||||
|                         } |  | ||||||
|                         Keycode::M => { |  | ||||||
|                             audio_thread.send(audio::Command::ToggleMuted).unwrap(); |  | ||||||
|                             continue; |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         Keycode::P => { |  | ||||||
|                             paused = !paused; |  | ||||||
|                             continue; |  | ||||||
|                         } |  | ||||||
|                         Keycode::Left | Keycode::A => Action::Left, |  | ||||||
|                         Keycode::Right | Keycode::D => Action::Right, |  | ||||||
|                         Keycode::Down | Keycode::S => Action::SoftDrop, |  | ||||||
|                         Keycode::Space => Action::HardDrop, |  | ||||||
|                         Keycode::Z => Action::RotateCcw, |  | ||||||
|                         Keycode::X => Action::RotateCw, |  | ||||||
|                         Keycode::C => Action::Swap, |  | ||||||
|                         _ => continue, |  | ||||||
|                     }; |  | ||||||
|                     actions.insert(keycode, game.ticks); |  | ||||||
|                 } |  | ||||||
|                 Event::KeyUp { |  | ||||||
|                     keycode: Some(keycode), |  | ||||||
|                     .. |  | ||||||
|                 } => { |  | ||||||
|                     let keycode = match keycode { |  | ||||||
|                         Keycode::Left | Keycode::A => Action::Left, |  | ||||||
|                         Keycode::Right | Keycode::D => Action::Right, |  | ||||||
|                         Keycode::Down | Keycode::S => Action::SoftDrop, |  | ||||||
|                         Keycode::Space => Action::HardDrop, |  | ||||||
|                         Keycode::Z => Action::RotateCcw, |  | ||||||
|                         Keycode::X => Action::RotateCw, |  | ||||||
|                         Keycode::C => Action::Swap, |  | ||||||
|                         _ => continue, |  | ||||||
|                     }; |  | ||||||
|                     actions.remove(&keycode); |  | ||||||
|                 } |  | ||||||
|                 _ => {} |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         ctx.draw_board(&game.board, &game.current_tetromino)?; |  | ||||||
| 
 |  | ||||||
|         if paused { |  | ||||||
|             ctx.draw_important_text( |  | ||||||
|                 "resources/josenfin_sans_regular.ttf", |  | ||||||
|                 "game paused o_o... press [p] to unpause !!", |  | ||||||
|             )?; |  | ||||||
|         } else if game.game_over { |  | ||||||
|             ctx.draw_important_text( |  | ||||||
|                 "resources/josenfin_sans_regular.ttf", |  | ||||||
|                 "game over T_T... press [enter] 2 restart :D", |  | ||||||
|             )?; |  | ||||||
|         } else { |  | ||||||
|             let effects = game.step(&actions); |  | ||||||
|             effects.into_iter().for_each(|effect| { |  | ||||||
|                 audio_thread |  | ||||||
|                     .send(audio::Command::PlayEffect(effect)) |  | ||||||
|                     .unwrap() |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         ctx.present(); |  | ||||||
|         ::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60)); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										156
									
								
								src/gui/ui.rs
									
									
									
									
									
								
							
							
						
						
									
										156
									
								
								src/gui/ui.rs
									
									
									
									
									
								
							| @ -1,156 +0,0 @@ | |||||||
| use crate::{board::Board, game::CurrentTetromino, tetromino::Tetromino}; |  | ||||||
| 
 |  | ||||||
| pub trait UiCtx<Err> { |  | ||||||
|     fn window_size(&self) -> Result<(i32, i32), Err>; |  | ||||||
|     fn fill_rect(&mut self, x: i32, y: i32, width: i32, height: i32, rgb: &Rgb) -> Result<(), Err>; |  | ||||||
|     fn outline_rect( |  | ||||||
|         &mut self, |  | ||||||
|         x: i32, |  | ||||||
|         y: i32, |  | ||||||
|         width: i32, |  | ||||||
|         height: i32, |  | ||||||
|         rgb: &Rgb, |  | ||||||
|     ) -> Result<(), Err>; |  | ||||||
|     fn text_size<P: AsRef<std::path::Path>, Text: AsRef<str>>( |  | ||||||
|         &mut self, |  | ||||||
|         font: P, |  | ||||||
|         text: Text, |  | ||||||
|     ) -> Result<(i32, i32), Err>; |  | ||||||
|     fn fill_text<P: AsRef<std::path::Path>, Text: AsRef<str>>( |  | ||||||
|         &mut self, |  | ||||||
|         font: P, |  | ||||||
|         text: Text, |  | ||||||
|         x: i32, |  | ||||||
|         y: i32, |  | ||||||
|         width: i32, |  | ||||||
|         height: i32, |  | ||||||
|     ) -> Result<(), Err>; |  | ||||||
|     fn clear(&mut self, rgb: &Rgb) -> Result<(), Err>; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub trait GameUiCtx<Err>: UiCtx<Err> { |  | ||||||
|     fn draw_tetromino_from_parts( |  | ||||||
|         &mut self, |  | ||||||
|         x: i8, |  | ||||||
|         y: i8, |  | ||||||
|         color: Rgb, |  | ||||||
|         pattern: [[bool; 4]; 4], |  | ||||||
|         filled: bool, |  | ||||||
|     ) -> Result<(), Err> { |  | ||||||
|         for (y_offset, row) in pattern.iter().enumerate() { |  | ||||||
|             for x_offset in row |  | ||||||
|                 .iter() |  | ||||||
|                 .enumerate() |  | ||||||
|                 .filter(|(_, exists)| **exists) |  | ||||||
|                 .map(|(x, _)| x) |  | ||||||
|             { |  | ||||||
|                 let x = x_offset as i8 + x; |  | ||||||
|                 let y = y_offset as i8 + y; |  | ||||||
| 
 |  | ||||||
|                 if y < 0 { |  | ||||||
|                     continue; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 self.draw_board_tile(x as i32, y as i32, &color, filled)? |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn draw_board_tile(&mut self, x: i32, y: i32, color: &Rgb, filled: bool) -> Result<(), Err> { |  | ||||||
|         let tile_size = 24; |  | ||||||
|         let (win_width, win_height) = self.window_size()?; |  | ||||||
|         let x = center(tile_size * Board::WIDTH as i32, win_width) + x * tile_size; |  | ||||||
|         let y = center(tile_size * Board::HEIGHT as i32, win_height) + y * tile_size; |  | ||||||
|         if filled { |  | ||||||
|             self.fill_rect(x, y, 24, 24, color)?; |  | ||||||
|         } else { |  | ||||||
|             self.outline_rect(x, y, 24, 24, color)?; |  | ||||||
|         } |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn draw_board(&mut self, board: &Board, current: &CurrentTetromino) -> Result<(), Err> { |  | ||||||
|         let (win_width, win_height) = self.window_size()?; |  | ||||||
|         self.outline_rect( |  | ||||||
|             center(24 * Board::WIDTH as i32, win_width) - 1, |  | ||||||
|             center(24 * Board::HEIGHT as i32, win_height) - 1, |  | ||||||
|             24 * Board::WIDTH as i32 + 2, |  | ||||||
|             24 * Board::HEIGHT as i32 + 2, |  | ||||||
|             &Rgb(255, 255, 255), |  | ||||||
|         )?; |  | ||||||
| 
 |  | ||||||
|         for (y, row) in board.iter().enumerate() { |  | ||||||
|             for (x, piece) in row.iter().enumerate() { |  | ||||||
|                 let color = match piece { |  | ||||||
|                     Some(t) => Rgb::from_tetromino(t), |  | ||||||
|                     None => Rgb(0, 0, 0), |  | ||||||
|                 }; |  | ||||||
|                 self.draw_board_tile(x as i32, y as i32, &color, true)? |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let pattern = current.tetromino.direction_pattern(¤t.direction); |  | ||||||
| 
 |  | ||||||
|         self.draw_tetromino_from_parts( |  | ||||||
|             current.x, |  | ||||||
|             board.lowest_y(¤t), |  | ||||||
|             Rgb(255, 255, 255), |  | ||||||
|             pattern, |  | ||||||
|             false, |  | ||||||
|         )?; |  | ||||||
| 
 |  | ||||||
|         self.draw_tetromino_from_parts( |  | ||||||
|             current.x, |  | ||||||
|             current.y, |  | ||||||
|             Rgb::from_tetromino(¤t.tetromino), |  | ||||||
|             pattern, |  | ||||||
|             true, |  | ||||||
|         )?; |  | ||||||
| 
 |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn draw_important_text<P: AsRef<std::path::Path>, Text: AsRef<str>>( |  | ||||||
|         &mut self, |  | ||||||
|         font: P, |  | ||||||
|         text: Text, |  | ||||||
|     ) -> Result<(), Err> { |  | ||||||
|         let (win_width, win_height) = self.window_size()?; |  | ||||||
|         let size = self.text_size(font.as_ref(), text.as_ref())?; |  | ||||||
|         let width = size.0; |  | ||||||
|         let height = size.1; |  | ||||||
| 
 |  | ||||||
|         let x = center(width, win_width); |  | ||||||
|         let y = center(height, win_height); |  | ||||||
| 
 |  | ||||||
|         self.outline_rect(x - 9, y - 9, width + 18, height + 18, &Rgb(255, 255, 255))?; |  | ||||||
| 
 |  | ||||||
|         self.fill_rect(x - 8, y - 8, width + 16, height + 16, &Rgb(16, 16, 16))?; |  | ||||||
|         self.fill_text(font, text, x, y, width, height)?; |  | ||||||
| 
 |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl<T, Err> GameUiCtx<Err> for T where T: UiCtx<Err> {} |  | ||||||
| 
 |  | ||||||
| pub struct Rgb(pub u8, pub u8, pub u8); |  | ||||||
| 
 |  | ||||||
| impl Rgb { |  | ||||||
|     pub fn from_tetromino(tetromino: &Tetromino) -> Self { |  | ||||||
|         match tetromino { |  | ||||||
|             Tetromino::I => Self(0, 255, 255), |  | ||||||
|             Tetromino::J => Self(0, 0, 255), |  | ||||||
|             Tetromino::L => Self(255, 128, 0), |  | ||||||
|             Tetromino::O => Self(255, 255, 0), |  | ||||||
|             Tetromino::S => Self(0, 255, 0), |  | ||||||
|             Tetromino::T => Self(255, 0, 255), |  | ||||||
|             Tetromino::Z => Self(255, 0, 0), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| fn center(length: i32, max: i32) -> i32 { |  | ||||||
|     (max - length) / 2 |  | ||||||
| } |  | ||||||
							
								
								
									
										287
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										287
									
								
								src/main.rs
									
									
									
									
									
								
							| @ -1,11 +1,288 @@ | |||||||
| use tetromino::Tetromino; | use actions::{Controls, ControlsHeld}; | ||||||
|  | use board::Board; | ||||||
|  | use tetromino::{Direction, DirectionDiff, Tetromino}; | ||||||
| 
 | 
 | ||||||
| mod actions; | mod actions; | ||||||
| mod board; | mod board; | ||||||
| mod game; |  | ||||||
| mod gui; |  | ||||||
| mod tetromino; | mod tetromino; | ||||||
| 
 | 
 | ||||||
| fn main() { | struct Rgb(u8, u8, u8); | ||||||
|     gui::start_game().unwrap(); | 
 | ||||||
|  | struct CurrentTetromino { | ||||||
|  |     tetromino: Tetromino, | ||||||
|  |     direction: Direction, | ||||||
|  |     x: i8, | ||||||
|  |     y: i8, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl CurrentTetromino { | ||||||
|  |     fn new(tetromino: Tetromino) -> Self { | ||||||
|  |         const PIECE_WIDTH: i8 = 2; | ||||||
|  |         Self { | ||||||
|  |             tetromino, | ||||||
|  |             direction: Direction::Up, | ||||||
|  |             x: (Board::WIDTH as i8 - PIECE_WIDTH) / 2, | ||||||
|  |             y: -1, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | struct Game { | ||||||
|  |     board: Board, | ||||||
|  |     next_tetrominos: [Tetromino; 3], | ||||||
|  |     current_tetromino: CurrentTetromino, | ||||||
|  |     held_tetromino: Option<Tetromino>, | ||||||
|  |     has_swapped_held: bool, | ||||||
|  |     score: Score, | ||||||
|  |     ticks: usize, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | struct Score { | ||||||
|  |     level: usize, | ||||||
|  |     points: usize, | ||||||
|  |     lines: usize, | ||||||
|  |     combo: usize, | ||||||
|  |     back_to_back: bool, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Score { | ||||||
|  |     const fn new() -> Self { | ||||||
|  |         Self { | ||||||
|  |             level: 0, | ||||||
|  |             points: 0, | ||||||
|  |             lines: 0, | ||||||
|  |             combo: 0, | ||||||
|  |             back_to_back: false, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn level_up(&mut self, lines_cleared: usize) { | ||||||
|  |         self.lines += lines_cleared; | ||||||
|  |         if self.lines > self.level * 5 { | ||||||
|  |             self.level += 1; | ||||||
|  |             self.lines = 0; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn point_multiplier_from_lines_cleared(lines_cleared: usize) -> f32 { | ||||||
|  |         match lines_cleared { | ||||||
|  |             0 => 0.0, | ||||||
|  |             1 => 100.0, | ||||||
|  |             2 => 300.0, | ||||||
|  |             3 => 500.0, | ||||||
|  |             4 => 800.0, | ||||||
|  |             _ => unreachable!("we cannot clear more than 4 lines"), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn combos(&self, lines_cleared: usize) -> usize { | ||||||
|  |         if lines_cleared > 0 { | ||||||
|  |             self.combo * 50 * self.level | ||||||
|  |         } else { | ||||||
|  |             0 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Game { | ||||||
|  |     fn take_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 hard_drop(&mut self, controls: &ControlsHeld) { | ||||||
|  |         if !controls.just_pressed(self.ticks, &Controls::HardDrop) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         let start_y = self.current_tetromino.y; | ||||||
|  |         loop { | ||||||
|  |             self.current_tetromino.y += 1; | ||||||
|  |             if !self.board.colliding(&self.current_tetromino) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             self.current_tetromino.y -= 1; | ||||||
|  |             self.score.points += (self.current_tetromino.y - start_y) as usize * 2; | ||||||
|  |             self.place_current_tetromino(); | ||||||
|  |             self.check_line_clears(); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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(); | ||||||
|  |         } 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 just_pressed = controls.just_pressed(self.ticks, &key); | ||||||
|  |             let long_press = controls.held_for(self.ticks, &key, |held_for| held_for > 15); | ||||||
|  |             if !just_pressed && !long_press { | ||||||
|  |                 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(&mut self) { | ||||||
|  |         let lines_cleared = self.board.lines_cleared(); | ||||||
|  | 
 | ||||||
|  |         self.score.level_up(lines_cleared); | ||||||
|  | 
 | ||||||
|  |         let mut points = | ||||||
|  |             self.score.level as f32 * Score::point_multiplier_from_lines_cleared(lines_cleared); | ||||||
|  | 
 | ||||||
|  |         if self.score.back_to_back && lines_cleared == 4 { | ||||||
|  |             points *= 1.5; | ||||||
|  |         } | ||||||
|  |         points += self.score.combos(lines_cleared) as f32; | ||||||
|  | 
 | ||||||
|  |         self.score.points += points as usize; | ||||||
|  | 
 | ||||||
|  |         if lines_cleared == 4 { | ||||||
|  |             self.score.back_to_back = true; | ||||||
|  |         } else if lines_cleared > 0 { | ||||||
|  |             self.score.back_to_back = false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if lines_cleared > 0 { | ||||||
|  |             self.score.combo += 1; | ||||||
|  |             // play_line_clears_sound();
 | ||||||
|  |         } else { | ||||||
|  |             self.score.combo = 0; | ||||||
|  |             // play_hard_drop_sound();
 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn step(&mut self, controls: &ControlsHeld) { | ||||||
|  |         // TODO: ensure game is running at 60 ticks per second? (`if !check_update_time(context, 60) { return; }`)
 | ||||||
|  |         self.hard_drop(controls); | ||||||
|  |         self.soft_drop(controls); | ||||||
|  |         self.move_horizontally(controls); | ||||||
|  | 
 | ||||||
|  |         if controls.just_pressed(self.ticks, &Controls::Swap) { | ||||||
|  |             self.try_swap_tetromino(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (control, direction) in [ | ||||||
|  |             (Controls::RotateCw, DirectionDiff::Cw), | ||||||
|  |             (Controls::RotateCcw, DirectionDiff::Ccw), | ||||||
|  |         ] { | ||||||
|  |             if !controls.just_pressed(self.ticks, &control) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             self.try_rotate(direction); | ||||||
|  |         } | ||||||
|  |         self.ticks += 1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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 true; | ||||||
|  |         } | ||||||
|  |         let wall_kicks = self | ||||||
|  |             .current_tetromino | ||||||
|  |             .tetromino | ||||||
|  |             .wall_kicks(&old_direction, &diff); | ||||||
|  | 
 | ||||||
|  |         for (x, y) in wall_kicks { | ||||||
|  |             self.current_tetromino.x += x; | ||||||
|  |             self.current_tetromino.y += y; | ||||||
|  |             if !(self.board.colliding(&self.current_tetromino)) { | ||||||
|  |                 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) { | ||||||
|  |         let next = CurrentTetromino::new(self.take_next_in_bag(Tetromino::random())); | ||||||
|  |         let current = std::mem::replace(&mut self.current_tetromino, next); | ||||||
|  |         let pattern = current.tetromino.direction_pattern(¤t.direction); | ||||||
|  | 
 | ||||||
|  |         for (y, row) in pattern.iter().enumerate() { | ||||||
|  |             for x in row | ||||||
|  |                 .iter() | ||||||
|  |                 .enumerate() | ||||||
|  |                 .filter(|(_, exists)| **exists) | ||||||
|  |                 .map(|(x, _)| x) | ||||||
|  |             { | ||||||
|  |                 let y = (current.y + y as i8) as usize; | ||||||
|  |                 let x = (current.x + x as i8) as usize; | ||||||
|  |                 self.board[y][x] = Some(current.tetromino.clone()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         self.has_swapped_held = false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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.take_next_in_bag(Tetromino::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, Score, Tetromino}; | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn advance_bag() { | ||||||
|  |         let mut game = Game { | ||||||
|  |             board: Board::new(), | ||||||
|  |             score: Score::new(), | ||||||
|  |             next_tetrominos: [Tetromino::I, Tetromino::J, Tetromino::O], | ||||||
|  |             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!( | ||||||
|  |             game.next_tetrominos, | ||||||
|  |             [Tetromino::J, Tetromino::O, Tetromino::S] | ||||||
|  |         ); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										196
									
								
								src/tetromino.rs
									
									
									
									
									
								
							
							
						
						
									
										196
									
								
								src/tetromino.rs
									
									
									
									
									
								
							| @ -1,3 +1,5 @@ | |||||||
|  | use crate::Rgb; | ||||||
|  | 
 | ||||||
| #[derive(Clone, Debug, PartialEq)] | #[derive(Clone, Debug, PartialEq)] | ||||||
| pub enum Tetromino { | pub enum Tetromino { | ||||||
|     I, |     I, | ||||||
| @ -51,173 +53,65 @@ impl Tetromino { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     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] { |     pub fn direction_pattern(&self, direction: &Direction) -> [[bool; 4]; 4] { | ||||||
|         let dir = match self { |         let dir = match self { | ||||||
|             Self::I => match direction { |             Self::I => match direction { | ||||||
|                 Direction::Up => [ |                 Direction::Up => [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |                 Direction::Right => [[0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0]], | ||||||
|                     ['#', '#', '#', '#'], |                 Direction::Down => [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |                 Direction::Left => [[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Right => [ |  | ||||||
|                     ['-', '-', '#', '-'], |  | ||||||
|                     ['-', '-', '#', '-'], |  | ||||||
|                     ['-', '-', '#', '-'], |  | ||||||
|                     ['-', '-', '#', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Down => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['#', '#', '#', '#'], |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Left => [ |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|             }, |             }, | ||||||
|             Self::J => match direction { |             Self::J => match direction { | ||||||
|                 Direction::Up => [ |                 Direction::Up => [[0, 0, 0, 0], [1, 0, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |                 Direction::Right => [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 0, 0]], | ||||||
|                     ['#', '-', '-', '-'], |                 Direction::Down => [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 0], [0, 0, 1, 0]], | ||||||
|                     ['#', '#', '#', '-'], |                 Direction::Left => [[0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [1, 1, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Right => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '#', '#', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Down => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['#', '#', '#', '-'], |  | ||||||
|                     ['-', '-', '#', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Left => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                     ['#', '#', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|             }, |             }, | ||||||
|             Self::L => match direction { |             Self::L => match direction { | ||||||
|                 Direction::Up => [ |                 Direction::Up => [[0, 0, 0, 0], [0, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |                 Direction::Right => [[0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0]], | ||||||
|                     ['-', '-', '#', '-'], |                 Direction::Down => [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 0], [1, 0, 0, 0]], | ||||||
|                     ['#', '#', '#', '-'], |                 Direction::Left => [[0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |             }, | ||||||
|                 ], |             Self::O => match direction { | ||||||
|                 Direction::Right => [ |                 Direction::Up => [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |                 Direction::Right => [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], | ||||||
|                     ['-', '#', '-', '-'], |                 Direction::Down => [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], | ||||||
|                     ['-', '#', '-', '-'], |                 Direction::Left => [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], | ||||||
|                     ['-', '#', '#', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Down => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['#', '#', '#', '-'], |  | ||||||
|                     ['#', '-', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Left => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['#', '#', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|             }, |             }, | ||||||
|             Self::S => match direction { |             Self::S => match direction { | ||||||
|                 Direction::Up => [ |                 Direction::Up => [[0, 0, 0, 0], [0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |                 Direction::Right => [[0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 0]], | ||||||
|                     ['-', '#', '#', '-'], |                 Direction::Down => [[0, 0, 0, 0], [0, 0, 0, 0], [0, 1, 1, 0], [1, 1, 0, 0]], | ||||||
|                     ['#', '#', '-', '-'], |                 Direction::Left => [[0, 0, 0, 0], [1, 0, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Right => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                     ['-', '#', '#', '-'], |  | ||||||
|                     ['-', '-', '#', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Down => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '#', '#', '-'], |  | ||||||
|                     ['#', '#', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Left => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['#', '-', '-', '-'], |  | ||||||
|                     ['#', '#', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|             }, |             }, | ||||||
|             Self::T => match direction { |             Self::T => match direction { | ||||||
|                 Direction::Up => [ |                 Direction::Up => [[0, 0, 0, 0], [0, 1, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |                 Direction::Right => [[0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0]], | ||||||
|                     ['-', '#', '-', '-'], |                 Direction::Down => [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 0], [0, 1, 0, 0]], | ||||||
|                     ['#', '#', '#', '-'], |                 Direction::Left => [[0, 0, 0, 0], [0, 1, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Right => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                     ['-', '#', '#', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Down => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['#', '#', '#', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Left => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                     ['#', '#', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|             }, |             }, | ||||||
|             Self::Z => match direction { |             Self::Z => match direction { | ||||||
|                 Direction::Up => [ |                 Direction::Up => [[0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |                 Direction::Right => [[0, 0, 0, 0], [0, 0, 1, 0], [0, 1, 1, 0], [0, 1, 0, 0]], | ||||||
|                     ['#', '#', '-', '-'], |                 Direction::Down => [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 1, 0]], | ||||||
|                     ['-', '#', '#', '-'], |                 Direction::Left => [[0, 0, 0, 0], [0, 1, 0, 0], [1, 1, 0, 0], [1, 0, 0, 0]], | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Right => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '-', '#', '-'], |  | ||||||
|                     ['-', '#', '#', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Down => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['#', '#', '-', '-'], |  | ||||||
|                     ['-', '#', '#', '-'], |  | ||||||
|                 ], |  | ||||||
|                 Direction::Left => [ |  | ||||||
|                     ['-', '-', '-', '-'], |  | ||||||
|                     ['-', '#', '-', '-'], |  | ||||||
|                     ['#', '#', '-', '-'], |  | ||||||
|                     ['#', '-', '-', '-'], |  | ||||||
|                 ], |  | ||||||
|             }, |             }, | ||||||
|             Self::O => [ |  | ||||||
|                 ['-', '-', '-', '-'], |  | ||||||
|                 ['-', '#', '#', '-'], |  | ||||||
|                 ['-', '#', '#', '-'], |  | ||||||
|                 ['-', '-', '-', '-'], |  | ||||||
|             ], |  | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         dir.map(|row| row.map(|v| v != '-')) |         dir.map(|row| row.map(|v| v != 0)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub const fn wall_kicks(&self, direction: &Direction, diff: &DirectionDiff) -> [(i8, i8); 5] { |     pub const fn wall_kicks(&self, direction: &Direction, diff: &DirectionDiff) -> [(i8, i8); 5] { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user