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] | ||||
| rand = "0.9.0" | ||||
| rodio = "0.20.1" | ||||
| 
 | ||||
| [dependencies.sdl2] | ||||
| version = "0.37.0" | ||||
| features = ["ttf"] | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| use std::collections::HashMap; | ||||
| 
 | ||||
| #[derive(Hash, PartialEq, Eq)] | ||||
| pub enum Controls { | ||||
| pub enum Action { | ||||
|     Left, | ||||
|     Right, | ||||
|     SoftDrop, | ||||
| @ -11,17 +11,20 @@ pub enum Controls { | ||||
|     RotateCcw, | ||||
| } | ||||
| 
 | ||||
| pub struct ControlsHeld(HashMap<Controls, usize>); | ||||
| pub struct ActionsHeld(HashMap<Action, usize>); | ||||
| 
 | ||||
| impl ControlsHeld { | ||||
|     pub fn just_pressed(&self, ticks: usize, control: &Controls) -> bool { | ||||
| impl ActionsHeld { | ||||
|     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) | ||||
|     } | ||||
| 
 | ||||
|     pub fn held_for<F: Fn(usize) -> bool>( | ||||
|         &self, | ||||
|         ticks: usize, | ||||
|         control: &Controls, | ||||
|         control: &Action, | ||||
|         functor: F, | ||||
|     ) -> bool { | ||||
|         self.get(control) | ||||
| @ -30,15 +33,15 @@ impl ControlsHeld { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::ops::Deref for ControlsHeld { | ||||
|     type Target = HashMap<Controls, usize>; | ||||
| impl std::ops::Deref for ActionsHeld { | ||||
|     type Target = HashMap<Action, usize>; | ||||
| 
 | ||||
|     fn deref(&self) -> &Self::Target { | ||||
|         &self.0 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl std::ops::DerefMut for ControlsHeld { | ||||
| impl std::ops::DerefMut for ActionsHeld { | ||||
|     fn deref_mut(&mut self) -> &mut Self::Target { | ||||
|         &mut self.0 | ||||
|     } | ||||
|  | ||||
							
								
								
									
										40
									
								
								src/board.rs
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								src/board.rs
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | ||||
| use std::ops::{Deref, DerefMut}; | ||||
| 
 | ||||
| use crate::{CurrentTetromino, Tetromino}; | ||||
| use crate::{game::CurrentTetromino, Tetromino}; | ||||
| 
 | ||||
| #[derive(PartialEq)] | ||||
| pub struct Board([[Option<Tetromino>; Self::WIDTH]; Self::HEIGHT]); | ||||
| @ -28,26 +28,35 @@ impl Board { | ||||
|         Board(board) | ||||
|     } | ||||
| 
 | ||||
|     pub fn colliding( | ||||
|     pub fn lowest_y( | ||||
|         &self, | ||||
|         CurrentTetromino { | ||||
|             tetromino, | ||||
|             direction, | ||||
|             x: cur_x, | ||||
|             y: cur_y, | ||||
|             x, | ||||
|             y, | ||||
|         }: &CurrentTetromino, | ||||
|     ) -> bool { | ||||
|     ) -> i8 { | ||||
|         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() { | ||||
|             for x in row | ||||
|     fn pattern_and_position_colliding(&self, pattern: [[bool; 4]; 4], x: i8, y: i8) -> bool { | ||||
|         for (y_offset, row) in pattern.iter().enumerate() { | ||||
|             for x_offset in row | ||||
|                 .iter() | ||||
|                 .enumerate() | ||||
|                 .filter(|(_, exists)| **exists) | ||||
|                 .map(|(x, _)| x) | ||||
|             { | ||||
|                 let x = x as i8 + cur_x; | ||||
|                 let y = y as i8 + cur_y; | ||||
|                 let x = x_offset as i8 + x; | ||||
|                 let y = y_offset as i8 + y; | ||||
| 
 | ||||
|                 if y < 0 { | ||||
|                     continue; | ||||
| @ -70,6 +79,19 @@ impl Board { | ||||
|         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 { | ||||
|         let line_clears: Vec<_> = self | ||||
|             .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 board::Board; | ||||
| use tetromino::{Direction, DirectionDiff, Tetromino}; | ||||
| use tetromino::Tetromino; | ||||
| 
 | ||||
| mod actions; | ||||
| mod board; | ||||
| mod game; | ||||
| mod gui; | ||||
| mod tetromino; | ||||
| 
 | ||||
| struct Rgb(u8, u8, u8); | ||||
| 
 | ||||
| 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] | ||||
|         ); | ||||
|     } | ||||
| fn main() { | ||||
|     gui::start_game().unwrap(); | ||||
| } | ||||
|  | ||||
							
								
								
									
										196
									
								
								src/tetromino.rs
									
									
									
									
									
								
							
							
						
						
									
										196
									
								
								src/tetromino.rs
									
									
									
									
									
								
							| @ -1,5 +1,3 @@ | ||||
| use crate::Rgb; | ||||
| 
 | ||||
| #[derive(Clone, Debug, PartialEq)] | ||||
| pub enum Tetromino { | ||||
|     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] { | ||||
|         let dir = match self { | ||||
|             Self::I => match direction { | ||||
|                 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::Up => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['#', '#', '#', '#'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                 ], | ||||
|                 Direction::Right => [ | ||||
|                     ['-', '-', '#', '-'], | ||||
|                     ['-', '-', '#', '-'], | ||||
|                     ['-', '-', '#', '-'], | ||||
|                     ['-', '-', '#', '-'], | ||||
|                 ], | ||||
|                 Direction::Down => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['#', '#', '#', '#'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                 ], | ||||
|                 Direction::Left => [ | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                 ], | ||||
|             }, | ||||
|             Self::J => match direction { | ||||
|                 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::Up => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['#', '-', '-', '-'], | ||||
|                     ['#', '#', '#', '-'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                 ], | ||||
|                 Direction::Right => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '#', '#', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                 ], | ||||
|                 Direction::Down => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['#', '#', '#', '-'], | ||||
|                     ['-', '-', '#', '-'], | ||||
|                 ], | ||||
|                 Direction::Left => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['#', '#', '-', '-'], | ||||
|                 ], | ||||
|             }, | ||||
|             Self::L => match direction { | ||||
|                 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::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::Up => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '-', '#', '-'], | ||||
|                     ['#', '#', '#', '-'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                 ], | ||||
|                 Direction::Right => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['-', '#', '#', '-'], | ||||
|                 ], | ||||
|                 Direction::Down => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['#', '#', '#', '-'], | ||||
|                     ['#', '-', '-', '-'], | ||||
|                 ], | ||||
|                 Direction::Left => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['#', '#', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                 ], | ||||
|             }, | ||||
|             Self::S => match direction { | ||||
|                 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::Up => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '#', '#', '-'], | ||||
|                     ['#', '#', '-', '-'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                 ], | ||||
|                 Direction::Right => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['-', '#', '#', '-'], | ||||
|                     ['-', '-', '#', '-'], | ||||
|                 ], | ||||
|                 Direction::Down => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '#', '#', '-'], | ||||
|                     ['#', '#', '-', '-'], | ||||
|                 ], | ||||
|                 Direction::Left => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['#', '-', '-', '-'], | ||||
|                     ['#', '#', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                 ], | ||||
|             }, | ||||
|             Self::T => match direction { | ||||
|                 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::Up => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['#', '#', '#', '-'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                 ], | ||||
|                 Direction::Right => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['-', '#', '#', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                 ], | ||||
|                 Direction::Down => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['#', '#', '#', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                 ], | ||||
|                 Direction::Left => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                     ['#', '#', '-', '-'], | ||||
|                     ['-', '#', '-', '-'], | ||||
|                 ], | ||||
|             }, | ||||
|             Self::Z => match direction { | ||||
|                 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::Up => [ | ||||
|                     ['-', '-', '-', '-'], | ||||
|                     ['#', '#', '-', '-'], | ||||
|                     ['-', '#', '#', '-'], | ||||
|                     ['-', '-', '-', '-'], | ||||
|                 ], | ||||
|                 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] { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user