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::tetromino::Position;
|
||||||
use crate::Renderable;
|
use crate::Renderable;
|
||||||
use crate::TICKS_PER_SECOND;
|
use crate::TICKS_PER_SECOND;
|
||||||
use log::trace;
|
use log::{error, info, trace};
|
||||||
use sdl2::{render::Canvas, video::Window};
|
use sdl2::{render::Canvas, video::Window};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
// I think this was correct, can't find source
|
// I think this was correct, can't find source
|
||||||
const LINE_CLEAR_DELAY: u64 = TICKS_PER_SECOND as u64 * 41 / 60;
|
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
|
// Logic is based on 60 ticks / second
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
playfield: PlayField,
|
playfield: PlayField,
|
||||||
|
@ -24,7 +30,7 @@ pub struct Game {
|
||||||
next_gravity_tick: u64,
|
next_gravity_tick: u64,
|
||||||
next_lock_tick: u64,
|
next_lock_tick: u64,
|
||||||
next_spawn_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
|
/// The last clear action performed, used for determining if a back-to-back
|
||||||
/// bonus is needed.
|
/// bonus is needed.
|
||||||
last_clear_action: ClearAction,
|
last_clear_action: ClearAction,
|
||||||
|
@ -49,7 +55,7 @@ impl Default for Game {
|
||||||
next_gravity_tick: 60,
|
next_gravity_tick: 60,
|
||||||
next_lock_tick: 0,
|
next_lock_tick: 0,
|
||||||
next_spawn_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
|
last_clear_action: ClearAction::Single, // Doesn't matter what it's initialized to
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,20 +67,32 @@ pub trait Tickable {
|
||||||
|
|
||||||
impl Tickable for Game {
|
impl Tickable for Game {
|
||||||
fn tick(&mut self) {
|
fn tick(&mut self) {
|
||||||
if self.is_game_over() {
|
if self.is_game_over().is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.tick += 1;
|
self.tick += 1;
|
||||||
match self.tick {
|
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 => {
|
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 => {
|
t if t == self.next_gravity_tick => {
|
||||||
self.playfield.tick_gravity();
|
if self.playfield.active_piece.is_some() {
|
||||||
if !self.playfield.can_active_piece_move_down() {
|
self.playfield.tick_gravity();
|
||||||
self.update_lock_tick();
|
trace!("Ticking gravity");
|
||||||
|
if !self.playfield.can_active_piece_move_down() {
|
||||||
|
self.update_lock_tick();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_gravity_tick();
|
self.update_gravity_tick();
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
|
@ -95,8 +113,12 @@ enum ClearAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
pub fn is_game_over(&self) -> bool {
|
pub fn is_game_over(&self) -> Option<LossReason> {
|
||||||
self.is_game_over || !self.playfield.is_active_piece_in_valid_position()
|
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) {
|
fn update_gravity_tick(&mut self) {
|
||||||
|
@ -119,7 +141,11 @@ impl Game {
|
||||||
.playfield
|
.playfield
|
||||||
.active_piece
|
.active_piece
|
||||||
.map(|t| t.get_cur_occupied_spaces())
|
.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();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut rows_cleared = 0;
|
let mut rows_cleared = 0;
|
||||||
|
@ -136,9 +162,17 @@ impl Game {
|
||||||
// It's possible that the player moved the piece in the meantime.
|
// It's possible that the player moved the piece in the meantime.
|
||||||
if !self.playfield.can_active_piece_move_down() {
|
if !self.playfield.can_active_piece_move_down() {
|
||||||
let positions = self.playfield.lock_active_piece();
|
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 {
|
if self.clear_lines() > 0 {
|
||||||
|
println!("Lines were cleared.");
|
||||||
self.playfield.active_piece = None;
|
self.playfield.active_piece = None;
|
||||||
self.next_spawn_tick = self.tick + LINE_CLEAR_DELAY;
|
self.next_spawn_tick = self.tick + LINE_CLEAR_DELAY;
|
||||||
} else {
|
} else {
|
||||||
|
@ -170,18 +204,30 @@ pub trait Controllable {
|
||||||
|
|
||||||
impl Controllable for Game {
|
impl Controllable for Game {
|
||||||
fn move_left(&mut self) {
|
fn move_left(&mut self) {
|
||||||
|
if self.playfield.active_piece.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.playfield.move_offset(-1, 0);
|
self.playfield.move_offset(-1, 0);
|
||||||
if !self.playfield.can_active_piece_move_down() {
|
if !self.playfield.can_active_piece_move_down() {
|
||||||
self.update_lock_tick();
|
self.update_lock_tick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn move_right(&mut self) {
|
fn move_right(&mut self) {
|
||||||
|
if self.playfield.active_piece.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.playfield.move_offset(1, 0);
|
self.playfield.move_offset(1, 0);
|
||||||
if !self.playfield.can_active_piece_move_down() {
|
if !self.playfield.can_active_piece_move_down() {
|
||||||
self.update_lock_tick();
|
self.update_lock_tick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn move_down(&mut self) {
|
fn move_down(&mut self) {
|
||||||
|
if self.playfield.active_piece.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if self.playfield.move_offset(0, 1) {
|
if self.playfield.move_offset(0, 1) {
|
||||||
self.score += 1;
|
self.score += 1;
|
||||||
self.update_gravity_tick();
|
self.update_gravity_tick();
|
||||||
|
@ -189,6 +235,10 @@ impl Controllable for Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn rotate_left(&mut self) {
|
fn rotate_left(&mut self) {
|
||||||
|
if self.playfield.active_piece.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match self.rotation_system.rotate_left(&self.playfield) {
|
match self.rotation_system.rotate_left(&self.playfield) {
|
||||||
Ok(Position { x, y }) => {
|
Ok(Position { x, y }) => {
|
||||||
let mut active_piece = self.playfield.active_piece.unwrap().clone();
|
let mut active_piece = self.playfield.active_piece.unwrap().clone();
|
||||||
|
@ -201,6 +251,10 @@ impl Controllable for Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn rotate_right(&mut self) {
|
fn rotate_right(&mut self) {
|
||||||
|
if self.playfield.active_piece.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match self.rotation_system.rotate_right(&self.playfield) {
|
match self.rotation_system.rotate_right(&self.playfield) {
|
||||||
Ok(Position { x, y }) => {
|
Ok(Position { x, y }) => {
|
||||||
let mut active_piece = self.playfield.active_piece.unwrap().clone();
|
let mut active_piece = self.playfield.active_piece.unwrap().clone();
|
||||||
|
@ -213,19 +267,40 @@ impl Controllable for Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn hard_drop(&mut self) {
|
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() {
|
while self.playfield.can_active_piece_move_down() {
|
||||||
self.score += 2;
|
self.score += 2;
|
||||||
|
lines_fallen += 1;
|
||||||
self.playfield.move_offset(0, 1);
|
self.playfield.move_offset(0, 1);
|
||||||
}
|
}
|
||||||
|
trace!(
|
||||||
|
"Score after hard dropping {} lines: {}",
|
||||||
|
lines_fallen,
|
||||||
|
self.score
|
||||||
|
);
|
||||||
|
|
||||||
if !self.try_lock_tetromino() {
|
trace!(
|
||||||
println!("couldn't lock tetromino despite hard dropping!");
|
"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) {
|
fn hold(&mut self) {
|
||||||
|
if self.playfield.active_piece.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let _ = self.playfield.try_swap_hold();
|
let _ = self.playfield.try_swap_hold();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
61
src/main.rs
61
src/main.rs
|
@ -1,8 +1,10 @@
|
||||||
use game::{Controllable, Game, Tickable};
|
use game::{Controllable, Game, Tickable};
|
||||||
use graphics::COLOR_BACKGROUND;
|
use graphics::COLOR_BACKGROUND;
|
||||||
|
use log::{debug, info, trace};
|
||||||
use sdl2::event::Event;
|
use sdl2::event::Event;
|
||||||
use sdl2::keyboard::Keycode;
|
use sdl2::keyboard::Keycode;
|
||||||
use sdl2::{render::Canvas, video::Window};
|
use sdl2::{render::Canvas, video::Window};
|
||||||
|
use simple_logger;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time::interval;
|
use tokio::time::interval;
|
||||||
|
|
||||||
|
@ -21,6 +23,7 @@ pub trait Renderable {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
simple_logger::init()?;
|
||||||
let sdl_context = sdl2::init()?;
|
let sdl_context = sdl2::init()?;
|
||||||
let video_subsystem = sdl_context.video()?;
|
let video_subsystem = sdl_context.video()?;
|
||||||
let window = video_subsystem
|
let window = video_subsystem
|
||||||
|
@ -32,36 +35,60 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut game = Game::default();
|
let mut game = Game::default();
|
||||||
let mut interval = interval(Duration::from_millis(1000 / TICKS_PER_SECOND as u64));
|
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() {
|
for event in event_pump.poll_iter() {
|
||||||
match event {
|
match event {
|
||||||
Event::Quit { .. }
|
Event::Quit { .. }
|
||||||
| Event::KeyDown {
|
| Event::KeyDown {
|
||||||
keycode: Some(Keycode::Escape),
|
keycode: Some(Keycode::Escape),
|
||||||
..
|
..
|
||||||
} => break 'running,
|
} => {
|
||||||
|
debug!("Escape registered");
|
||||||
|
break 'running;
|
||||||
|
}
|
||||||
Event::KeyDown {
|
Event::KeyDown {
|
||||||
keycode: Some(Keycode::Left),
|
keycode: Some(Keycode::Left),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
debug!("Move left registered");
|
||||||
game.move_left();
|
game.move_left();
|
||||||
}
|
}
|
||||||
Event::KeyDown {
|
Event::KeyDown {
|
||||||
keycode: Some(Keycode::Right),
|
keycode: Some(Keycode::Right),
|
||||||
..
|
..
|
||||||
} => game.move_right(),
|
} => {
|
||||||
|
debug!("Move right registered");
|
||||||
|
game.move_right();
|
||||||
|
}
|
||||||
Event::KeyDown {
|
Event::KeyDown {
|
||||||
keycode: Some(Keycode::Down),
|
keycode: Some(Keycode::Down),
|
||||||
..
|
..
|
||||||
} => game.move_down(),
|
} => {
|
||||||
|
debug!("Soft drop registered");
|
||||||
|
game.move_down();
|
||||||
|
}
|
||||||
Event::KeyDown {
|
Event::KeyDown {
|
||||||
keycode: Some(Keycode::Z),
|
keycode: Some(Keycode::Z),
|
||||||
..
|
..
|
||||||
} => game.rotate_left(),
|
} => {
|
||||||
|
debug!("Rotate left registered");
|
||||||
|
game.rotate_left();
|
||||||
|
}
|
||||||
Event::KeyDown {
|
Event::KeyDown {
|
||||||
keycode: Some(Keycode::X),
|
keycode: Some(Keycode::X),
|
||||||
..
|
..
|
||||||
} => game.rotate_right(),
|
} => {
|
||||||
|
debug!("Rotate right registered");
|
||||||
|
game.rotate_right();
|
||||||
|
}
|
||||||
Event::KeyDown {
|
Event::KeyDown {
|
||||||
keycode: Some(Keycode::Space),
|
keycode: Some(Keycode::Space),
|
||||||
..
|
..
|
||||||
|
@ -69,12 +96,28 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
| Event::KeyDown {
|
| Event::KeyDown {
|
||||||
keycode: Some(Keycode::Up),
|
keycode: Some(Keycode::Up),
|
||||||
..
|
..
|
||||||
} => game.hard_drop(),
|
} => {
|
||||||
|
debug!("Hard drop registered");
|
||||||
|
game.hard_drop();
|
||||||
|
}
|
||||||
Event::KeyDown {
|
Event::KeyDown {
|
||||||
keycode: Some(Keycode::LShift),
|
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();
|
game.tick();
|
||||||
|
|
|
@ -149,17 +149,13 @@ impl PlayField {
|
||||||
|
|
||||||
pub fn can_active_piece_move_down(&self) -> bool {
|
pub fn can_active_piece_move_down(&self) -> bool {
|
||||||
self.active_piece
|
self.active_piece
|
||||||
.and_then(|p| {
|
.expect("Tried to request the status of a non-existing active piece!")
|
||||||
Some(
|
.get_falling_occupied_spaces()
|
||||||
p.get_falling_occupied_spaces()
|
.iter()
|
||||||
.iter()
|
.fold(true, |acc, pos| {
|
||||||
.fold(true, |acc, pos| {
|
acc && (pos.y as usize) < self.field.len()
|
||||||
acc && (pos.y as usize) < self.field.len()
|
&& self.field[pos.y as usize][pos.x as usize].is_none()
|
||||||
&& 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> {
|
pub fn lock_active_piece(&mut self) -> Vec<Position> {
|
||||||
|
@ -173,26 +169,25 @@ impl PlayField {
|
||||||
|
|
||||||
new_pieces
|
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 {
|
pub fn is_active_piece_in_valid_position(&self) -> Option<Position> {
|
||||||
match self.active_piece {
|
self.active_piece
|
||||||
Some(active_piece) => self.can_piece_be_at_position(&active_piece),
|
.and_then(|t| self.can_piece_be_at_position(&t))
|
||||||
None => true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_piece_be_at_position(&self, tetromino: &Tetromino) -> bool {
|
pub fn can_piece_be_at_position(&self, tetromino: &Tetromino) -> Option<Position> {
|
||||||
tetromino
|
for Position { x, y } in tetromino.get_cur_occupied_spaces() {
|
||||||
.get_cur_occupied_spaces()
|
if (y as usize) >= self.field.len()
|
||||||
.iter()
|
|| (x as usize) >= PLAYFIELD_WIDTH
|
||||||
.all(|Position { x, y }| {
|
|| self.field[y as usize][x as usize].is_some()
|
||||||
(*y as usize) < self.field.len()
|
{
|
||||||
&& (*x as usize) < PLAYFIELD_WIDTH
|
return Some(Position { x, y });
|
||||||
&& self.field[*y as usize][*x as usize].is_none()
|
}
|
||||||
})
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_swap_hold(&mut self) -> Result<(), ()> {
|
pub fn try_swap_hold(&mut self) -> Result<(), ()> {
|
||||||
|
|
|
@ -56,7 +56,10 @@ impl SRS {
|
||||||
let y = offset.y;
|
let y = offset.y;
|
||||||
let test_position = active_piece.position.offset(x, y);
|
let test_position = active_piece.position.offset(x, y);
|
||||||
test_tetromino.position = test_position;
|
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);
|
return Ok(offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue