Compare commits
	
		
			9 Commits
		
	
	
		
			eddf9d81c0
			...
			1830aec847
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1830aec847 | |||
| 489c2814e2 | |||
| ecbb7c6756 | |||
| a19a9e9cc3 | |||
| 7d6822cd34 | |||
| fc6ebcdbc2 | |||
| 63a8c0bd78 | |||
| db57450452 | |||
| f1877a1e8c | 
							
								
								
									
										911
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										911
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -5,3 +5,8 @@ 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 Controls { | pub enum Action { | ||||||
|     Left, |     Left, | ||||||
|     Right, |     Right, | ||||||
|     SoftDrop, |     SoftDrop, | ||||||
| @ -11,17 +11,20 @@ pub enum Controls { | |||||||
|     RotateCcw, |     RotateCcw, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub struct ControlsHeld(HashMap<Controls, usize>); | pub struct ActionsHeld(HashMap<Action, usize>); | ||||||
| 
 | 
 | ||||||
| impl ControlsHeld { | impl ActionsHeld { | ||||||
|     pub fn just_pressed(&self, ticks: usize, control: &Controls) -> bool { |     pub fn new() -> Self { | ||||||
|  |         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: &Controls, |         control: &Action, | ||||||
|         functor: F, |         functor: F, | ||||||
|     ) -> bool { |     ) -> bool { | ||||||
|         self.get(control) |         self.get(control) | ||||||
| @ -30,15 +33,15 @@ impl ControlsHeld { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl std::ops::Deref for ControlsHeld { | impl std::ops::Deref for ActionsHeld { | ||||||
|     type Target = HashMap<Controls, usize>; |     type Target = HashMap<Action, usize>; | ||||||
| 
 | 
 | ||||||
|     fn deref(&self) -> &Self::Target { |     fn deref(&self) -> &Self::Target { | ||||||
|         &self.0 |         &self.0 | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl std::ops::DerefMut for ControlsHeld { | impl std::ops::DerefMut for ActionsHeld { | ||||||
|     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::{CurrentTetromino, Tetromino}; | use crate::{game::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,26 +28,35 @@ impl Board { | |||||||
|         Board(board) |         Board(board) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn colliding( |     pub fn lowest_y( | ||||||
|         &self, |         &self, | ||||||
|         CurrentTetromino { |         CurrentTetromino { | ||||||
|             tetromino, |             tetromino, | ||||||
|             direction, |             direction, | ||||||
|             x: cur_x, |             x, | ||||||
|             y: cur_y, |             y, | ||||||
|         }: &CurrentTetromino, |         }: &CurrentTetromino, | ||||||
|     ) -> bool { |     ) -> i8 { | ||||||
|         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; | ||||||
|  |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|         for (y, row) in pattern.iter().enumerate() { |     fn pattern_and_position_colliding(&self, pattern: [[bool; 4]; 4], x: i8, y: i8) -> bool { | ||||||
|             for x in row |         for (y_offset, row) in pattern.iter().enumerate() { | ||||||
|  |             for x_offset in row | ||||||
|                 .iter() |                 .iter() | ||||||
|                 .enumerate() |                 .enumerate() | ||||||
|                 .filter(|(_, exists)| **exists) |                 .filter(|(_, exists)| **exists) | ||||||
|                 .map(|(x, _)| x) |                 .map(|(x, _)| x) | ||||||
|             { |             { | ||||||
|                 let x = x as i8 + cur_x; |                 let x = x_offset as i8 + x; | ||||||
|                 let y = y as i8 + cur_y; |                 let y = y_offset as i8 + y; | ||||||
| 
 | 
 | ||||||
|                 if y < 0 { |                 if y < 0 { | ||||||
|                     continue; |                     continue; | ||||||
| @ -70,6 +79,19 @@ 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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								src/game.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,317 @@ | |||||||
|  | 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] | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								src/gui.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/gui.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | mod audio; | ||||||
|  | mod sdl; | ||||||
|  | mod ui; | ||||||
|  | 
 | ||||||
|  | pub use sdl::start_game; | ||||||
							
								
								
									
										99
									
								
								src/gui/audio.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/gui/audio.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | |||||||
|  | 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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								src/gui/sdl.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,223 @@ | |||||||
|  | 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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								src/gui/ui.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,156 @@ | |||||||
|  | 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,288 +1,11 @@ | |||||||
| use actions::{Controls, ControlsHeld}; | use tetromino::Tetromino; | ||||||
| use board::Board; |  | ||||||
| use tetromino::{Direction, DirectionDiff, Tetromino}; |  | ||||||
| 
 | 
 | ||||||
| mod actions; | mod actions; | ||||||
| mod board; | mod board; | ||||||
|  | mod game; | ||||||
|  | mod gui; | ||||||
| mod tetromino; | mod tetromino; | ||||||
| 
 | 
 | ||||||
| struct Rgb(u8, u8, u8); | fn main() { | ||||||
| 
 |     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,5 +1,3 @@ | |||||||
| use crate::Rgb; |  | ||||||
| 
 |  | ||||||
| #[derive(Clone, Debug, PartialEq)] | #[derive(Clone, Debug, PartialEq)] | ||||||
| pub enum Tetromino { | pub enum Tetromino { | ||||||
|     I, |     I, | ||||||
| @ -53,65 +51,173 @@ 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 => [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], |                 Direction::Up => [ | ||||||
|                 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 => [[0, 0, 0, 0], [1, 0, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0]], |                 Direction::Up => [ | ||||||
|                 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 => [[0, 0, 0, 0], [0, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0]], |                 Direction::Up => [ | ||||||
|                 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::Up => [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]], |                 Direction::Right => [ | ||||||
|                 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 => [[0, 0, 0, 0], [0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0]], |                 Direction::Up => [ | ||||||
|                 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 => [[0, 0, 0, 0], [0, 1, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0]], |                 Direction::Up => [ | ||||||
|                 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 => [[0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0]], |                 Direction::Up => [ | ||||||
|                 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 != 0)) |         dir.map(|row| row.map(|v| v != '-')) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     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