fixed early game over bug
This commit is contained in:
parent
7fab1b2cce
commit
e5d3bbb628
4 changed files with 168 additions and 52 deletions
109
src/game.rs
109
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<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();
|
||||
}
|
||||
}
|
||||
|
|
61
src/main.rs
61
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<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();
|
||||
|
|
|
@ -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<(), ()> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue