fixed early game over bug

This commit is contained in:
Edward Shen 2020-03-28 20:57:39 -04:00
parent 7fab1b2cce
commit e5d3bbb628
Signed by: edward
GPG key ID: 19182661E818369F
4 changed files with 168 additions and 52 deletions

View file

@ -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<LossReason>,
/// 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<LossReason> {
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::<Vec<_>>())
.map(|i| {
let mut a = i.iter().map(|p| p.y).collect::<Vec<_>>();
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();
}
}

View file

@ -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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
| 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();

View file

@ -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<Position> {
@ -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<Position> {
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<Position> {
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<(), ()> {

View file

@ -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);
}
}