From e5d3bbb628b3eb54626f8b5efe7a781646dabc6e Mon Sep 17 00:00:00 2001 From: Edward Shen Date: Sat, 28 Mar 2020 20:57:39 -0400 Subject: [PATCH] fixed early game over bug --- src/game.rs | 109 +++++++++++++++++++++++++++++++++++++++-------- src/main.rs | 61 ++++++++++++++++++++++---- src/playfield.rs | 45 +++++++++---------- src/srs.rs | 5 ++- 4 files changed, 168 insertions(+), 52 deletions(-) diff --git a/src/game.rs b/src/game.rs index 458b061..a1a25bc 100644 --- a/src/game.rs +++ b/src/game.rs @@ -7,13 +7,19 @@ use crate::srs::SRS; use crate::tetromino::Position; use crate::Renderable; use crate::TICKS_PER_SECOND; -use log::trace; +use log::{error, info, trace}; use sdl2::{render::Canvas, video::Window}; use std::fmt; // I think this was correct, can't find source const LINE_CLEAR_DELAY: u64 = TICKS_PER_SECOND as u64 * 41 / 60; +#[derive(Debug, Clone, Copy)] +pub enum LossReason { + TopOut, + LockOut, + BlockOut(Position), +} // Logic is based on 60 ticks / second pub struct Game { playfield: PlayField, @@ -24,7 +30,7 @@ pub struct Game { next_gravity_tick: u64, next_lock_tick: u64, next_spawn_tick: u64, - is_game_over: bool, + is_game_over: Option, /// The last clear action performed, used for determining if a back-to-back /// bonus is needed. last_clear_action: ClearAction, @@ -49,7 +55,7 @@ impl Default for Game { next_gravity_tick: 60, next_lock_tick: 0, next_spawn_tick: 0, - is_game_over: false, + is_game_over: None, last_clear_action: ClearAction::Single, // Doesn't matter what it's initialized to } } @@ -61,20 +67,32 @@ pub trait Tickable { impl Tickable for Game { fn tick(&mut self) { - if self.is_game_over() { + if self.is_game_over().is_some() { return; } self.tick += 1; match self.tick { - t if t == self.next_spawn_tick => self.spawn_tetromino(), + t if t == self.next_spawn_tick => { + trace!("Spawn tick was met, spawning new Tetromino!"); + self.spawn_tetromino(); + } t if t == self.next_lock_tick => { - self.try_lock_tetromino(); + trace!("Lock tick was met, trying to locking tetromino"); + if self.try_lock_tetromino() { + trace!("Successfully locked Tetromino"); + } else { + trace!("Failed to lock Tetromino."); + } } t if t == self.next_gravity_tick => { - self.playfield.tick_gravity(); - if !self.playfield.can_active_piece_move_down() { - self.update_lock_tick(); + if self.playfield.active_piece.is_some() { + self.playfield.tick_gravity(); + trace!("Ticking gravity"); + if !self.playfield.can_active_piece_move_down() { + self.update_lock_tick(); + } } + self.update_gravity_tick(); } _ => (), @@ -95,8 +113,12 @@ enum ClearAction { } impl Game { - pub fn is_game_over(&self) -> bool { - self.is_game_over || !self.playfield.is_active_piece_in_valid_position() + pub fn is_game_over(&self) -> Option { + self.is_game_over.or_else(|| { + self.playfield + .is_active_piece_in_valid_position() + .map(|p| LossReason::BlockOut(p)) + }) } fn update_gravity_tick(&mut self) { @@ -119,7 +141,11 @@ impl Game { .playfield .active_piece .map(|t| t.get_cur_occupied_spaces()) - .map(|i| i.iter().map(|p| p.y).collect::>()) + .map(|i| { + let mut a = i.iter().map(|p| p.y).collect::>(); + a.sort(); + a + }) .unwrap_or_default(); let mut rows_cleared = 0; @@ -136,9 +162,17 @@ impl Game { // It's possible that the player moved the piece in the meantime. if !self.playfield.can_active_piece_move_down() { let positions = self.playfield.lock_active_piece(); - self.is_game_over = self.is_game_over || positions.iter().map(|p| p.y).all(|y| y < 20); + self.is_game_over = self.is_game_over.or_else(|| { + if positions.iter().map(|p| p.y).all(|y| y < 20) { + println!("{:?}", positions); + Some(LossReason::LockOut) + } else { + None + } + }); if self.clear_lines() > 0 { + println!("Lines were cleared."); self.playfield.active_piece = None; self.next_spawn_tick = self.tick + LINE_CLEAR_DELAY; } else { @@ -170,18 +204,30 @@ pub trait Controllable { impl Controllable for Game { fn move_left(&mut self) { + if self.playfield.active_piece.is_none() { + return; + } + self.playfield.move_offset(-1, 0); if !self.playfield.can_active_piece_move_down() { self.update_lock_tick(); } } fn move_right(&mut self) { + if self.playfield.active_piece.is_none() { + return; + } + self.playfield.move_offset(1, 0); if !self.playfield.can_active_piece_move_down() { self.update_lock_tick(); } } fn move_down(&mut self) { + if self.playfield.active_piece.is_none() { + return; + } + if self.playfield.move_offset(0, 1) { self.score += 1; self.update_gravity_tick(); @@ -189,6 +235,10 @@ impl Controllable for Game { } } fn rotate_left(&mut self) { + if self.playfield.active_piece.is_none() { + return; + } + match self.rotation_system.rotate_left(&self.playfield) { Ok(Position { x, y }) => { let mut active_piece = self.playfield.active_piece.unwrap().clone(); @@ -201,6 +251,10 @@ impl Controllable for Game { } } fn rotate_right(&mut self) { + if self.playfield.active_piece.is_none() { + return; + } + match self.rotation_system.rotate_right(&self.playfield) { Ok(Position { x, y }) => { let mut active_piece = self.playfield.active_piece.unwrap().clone(); @@ -213,19 +267,40 @@ impl Controllable for Game { } } fn hard_drop(&mut self) { + if self.playfield.active_piece.is_none() { + return; + } + + let mut lines_fallen = 0; + trace!("Score before hard drop: {}", self.score); while self.playfield.can_active_piece_move_down() { self.score += 2; + lines_fallen += 1; self.playfield.move_offset(0, 1); } + trace!( + "Score after hard dropping {} lines: {}", + lines_fallen, + self.score + ); - if !self.try_lock_tetromino() { - println!("couldn't lock tetromino despite hard dropping!"); + trace!( + "Found active piece {:?}, trying to lock it", + self.playfield.active_piece + ); + if self.try_lock_tetromino() { + trace!("Successfully locked piece, disabling lock tick."); + self.next_lock_tick = std::u64::MAX; + } else { + error!("couldn't lock tetromino despite hard dropping!"); } - - self.next_lock_tick = std::u64::MAX; } fn hold(&mut self) { + if self.playfield.active_piece.is_none() { + return; + } + let _ = self.playfield.try_swap_hold(); } } diff --git a/src/main.rs b/src/main.rs index 230261d..44e199b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ use game::{Controllable, Game, Tickable}; use graphics::COLOR_BACKGROUND; +use log::{debug, info, trace}; use sdl2::event::Event; use sdl2::keyboard::Keycode; use sdl2::{render::Canvas, video::Window}; +use simple_logger; use std::time::Duration; use tokio::time::interval; @@ -21,6 +23,7 @@ pub trait Renderable { #[tokio::main] async fn main() -> Result<(), Box> { + simple_logger::init()?; let sdl_context = sdl2::init()?; let video_subsystem = sdl_context.video()?; let window = video_subsystem @@ -32,36 +35,60 @@ async fn main() -> Result<(), Box> { let mut game = Game::default(); let mut interval = interval(Duration::from_millis(1000 / TICKS_PER_SECOND as u64)); - 'running: while !game.is_game_over() { + 'running: loop { + match game.is_game_over() { + Some(e) => { + println!("Lost due to: {:?}", e); + break; + } + None => (), + } + for event in event_pump.poll_iter() { match event { Event::Quit { .. } | Event::KeyDown { keycode: Some(Keycode::Escape), .. - } => break 'running, + } => { + debug!("Escape registered"); + break 'running; + } Event::KeyDown { keycode: Some(Keycode::Left), .. } => { + debug!("Move left registered"); game.move_left(); } Event::KeyDown { keycode: Some(Keycode::Right), .. - } => game.move_right(), + } => { + debug!("Move right registered"); + game.move_right(); + } Event::KeyDown { keycode: Some(Keycode::Down), .. - } => game.move_down(), + } => { + debug!("Soft drop registered"); + game.move_down(); + } Event::KeyDown { keycode: Some(Keycode::Z), .. - } => game.rotate_left(), + } => { + debug!("Rotate left registered"); + game.rotate_left(); + } Event::KeyDown { keycode: Some(Keycode::X), .. - } => game.rotate_right(), + } => { + debug!("Rotate right registered"); + game.rotate_right(); + } Event::KeyDown { keycode: Some(Keycode::Space), .. @@ -69,12 +96,28 @@ async fn main() -> Result<(), Box> { | Event::KeyDown { keycode: Some(Keycode::Up), .. - } => game.hard_drop(), + } => { + debug!("Hard drop registered"); + game.hard_drop(); + } Event::KeyDown { keycode: Some(Keycode::LShift), .. - } => game.hold(), - _ => {} + } => { + debug!("Hold registered"); + game.hold(); + } + Event::KeyDown { + keycode: Some(Keycode::R), + .. + } => { + info!("Restarting game"); + game = Game::default(); + } + Event::KeyDown { + keycode: Some(e), .. + } => trace!("Ignoring keycode {}", e), + _ => (), } } game.tick(); diff --git a/src/playfield.rs b/src/playfield.rs index 6b69628..88af181 100644 --- a/src/playfield.rs +++ b/src/playfield.rs @@ -149,17 +149,13 @@ impl PlayField { pub fn can_active_piece_move_down(&self) -> bool { self.active_piece - .and_then(|p| { - Some( - p.get_falling_occupied_spaces() - .iter() - .fold(true, |acc, pos| { - acc && (pos.y as usize) < self.field.len() - && self.field[pos.y as usize][pos.x as usize].is_none() - }), - ) + .expect("Tried to request the status of a non-existing active piece!") + .get_falling_occupied_spaces() + .iter() + .fold(true, |acc, pos| { + acc && (pos.y as usize) < self.field.len() + && self.field[pos.y as usize][pos.x as usize].is_none() }) - .unwrap_or_else(|| false) } pub fn lock_active_piece(&mut self) -> Vec { @@ -173,26 +169,25 @@ impl PlayField { new_pieces } - None => vec![], + None => panic!("Tried to lock a piece that wasn't active"), } } - pub fn is_active_piece_in_valid_position(&self) -> bool { - match self.active_piece { - Some(active_piece) => self.can_piece_be_at_position(&active_piece), - None => true, - } + pub fn is_active_piece_in_valid_position(&self) -> Option { + self.active_piece + .and_then(|t| self.can_piece_be_at_position(&t)) } - pub fn can_piece_be_at_position(&self, tetromino: &Tetromino) -> bool { - tetromino - .get_cur_occupied_spaces() - .iter() - .all(|Position { x, y }| { - (*y as usize) < self.field.len() - && (*x as usize) < PLAYFIELD_WIDTH - && self.field[*y as usize][*x as usize].is_none() - }) + pub fn can_piece_be_at_position(&self, tetromino: &Tetromino) -> Option { + for Position { x, y } in tetromino.get_cur_occupied_spaces() { + if (y as usize) >= self.field.len() + || (x as usize) >= PLAYFIELD_WIDTH + || self.field[y as usize][x as usize].is_some() + { + return Some(Position { x, y }); + } + } + None } pub fn try_swap_hold(&mut self) -> Result<(), ()> { diff --git a/src/srs.rs b/src/srs.rs index 563ee0f..73a2841 100644 --- a/src/srs.rs +++ b/src/srs.rs @@ -56,7 +56,10 @@ impl SRS { let y = offset.y; let test_position = active_piece.position.offset(x, y); test_tetromino.position = test_position; - if playfield.can_piece_be_at_position(&test_tetromino) { + if playfield + .can_piece_be_at_position(&test_tetromino) + .is_none() + { return Ok(offset); } }