tetris/src/game.rs

382 lines
10 KiB
Rust
Raw Permalink Normal View History

2020-03-19 22:51:43 +00:00
// A game is a self-contained struct that holds everything that an instance of
// Tetris needs to run, except for something to tick the time forward.
use crate::playfield::PlayField;
use crate::srs::RotationSystem;
use crate::srs::SRS;
2020-03-21 04:05:39 +00:00
use crate::tetromino::Position;
use crate::TICKS_PER_SECOND;
2020-03-29 00:57:39 +00:00
use log::{error, info, trace};
2020-03-21 04:05:39 +00:00
use std::fmt;
2020-03-19 22:51:43 +00:00
2020-03-21 04:05:39 +00:00
// I think this was correct, can't find source
const LINE_CLEAR_DELAY: u64 = TICKS_PER_SECOND as u64 * 41 / 60;
2020-03-29 00:57:39 +00:00
#[derive(Debug, Clone, Copy)]
pub enum LossReason {
TopOut,
LockOut,
2020-04-06 03:39:19 +00:00
PieceLimitReached,
2020-04-06 20:07:30 +00:00
TickLimitReached,
2020-03-29 00:57:39 +00:00
BlockOut(Position),
}
2020-04-05 23:36:00 +00:00
#[derive(Clone)]
2020-03-21 04:05:39 +00:00
// Logic is based on 60 ticks / second
pub struct Game {
2020-03-19 22:51:43 +00:00
playfield: PlayField,
2020-03-21 04:05:39 +00:00
rotation_system: SRS,
2020-03-19 22:51:43 +00:00
level: u8,
2020-03-22 06:47:17 +00:00
score: u32,
2020-03-21 04:37:02 +00:00
tick: u64,
2020-03-21 04:05:39 +00:00
next_gravity_tick: u64,
next_lock_tick: u64,
next_spawn_tick: u64,
2020-03-29 00:57:39 +00:00
is_game_over: Option<LossReason>,
2020-03-23 01:56:11 +00:00
/// The last clear action performed, used for determining if a back-to-back
/// bonus is needed.
last_clear_action: ClearAction,
2020-04-05 20:34:33 +00:00
pub line_clears: u32,
2020-04-06 03:39:19 +00:00
// used if we set a limit on how long a game can last.
pieces_placed: usize,
piece_limit: usize,
2020-04-06 20:07:30 +00:00
// used if we set a limit on how long the game can be played.
tick_limit: u64,
2020-03-21 04:05:39 +00:00
}
impl fmt::Debug for Game {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2020-03-22 06:47:17 +00:00
writeln!(f, "level: {}, points: {}", self.level, self.score)?;
2020-03-21 04:05:39 +00:00
writeln!(f, "tick: {}", self.tick)?;
write!(f, "{:?}", self.playfield)
}
2020-03-19 22:51:43 +00:00
}
2020-03-21 04:05:39 +00:00
impl Default for Game {
2020-03-19 23:47:50 +00:00
fn default() -> Self {
Game {
playfield: PlayField::new(),
2020-03-21 04:05:39 +00:00
rotation_system: SRS::default(),
level: 1,
2020-03-22 06:47:17 +00:00
score: 0,
2020-03-21 04:05:39 +00:00
tick: 0,
next_gravity_tick: 60,
next_lock_tick: 0,
next_spawn_tick: 0,
2020-03-29 00:57:39 +00:00
is_game_over: None,
2020-03-23 01:56:11 +00:00
last_clear_action: ClearAction::Single, // Doesn't matter what it's initialized to
2020-03-30 16:28:53 +00:00
line_clears: 0,
2020-04-06 03:39:19 +00:00
pieces_placed: 0,
piece_limit: 0,
2020-04-06 20:07:30 +00:00
tick_limit: 0,
2020-03-19 23:47:50 +00:00
}
}
}
2020-03-21 04:05:39 +00:00
pub trait Tickable {
fn tick(&mut self);
}
impl Tickable for Game {
fn tick(&mut self) {
2020-03-29 00:57:39 +00:00
if self.is_game_over().is_some() {
2020-03-21 04:05:39 +00:00
return;
}
self.tick += 1;
2020-04-06 20:07:30 +00:00
2020-03-21 04:05:39 +00:00
match self.tick {
2020-03-29 00:57:39 +00:00
t if t == self.next_spawn_tick => {
trace!("Spawn tick was met, spawning new Tetromino!");
self.spawn_tetromino();
}
2020-03-21 04:05:39 +00:00
t if t == self.next_lock_tick => {
2020-03-29 00:57:39 +00:00
trace!("Lock tick was met, trying to locking tetromino");
if self.try_lock_tetromino() {
trace!("Successfully locked Tetromino");
} else {
trace!("Failed to lock Tetromino.");
}
2020-03-21 04:05:39 +00:00
}
t if t == self.next_gravity_tick => {
2020-03-29 00:57:39 +00:00
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();
}
2020-03-21 04:05:39 +00:00
}
2020-03-29 00:57:39 +00:00
2020-03-21 04:05:39 +00:00
self.update_gravity_tick();
}
_ => (),
}
2020-04-06 20:07:30 +00:00
if self.tick == self.tick_limit {
self.is_game_over = Some(LossReason::TickLimitReached);
}
2020-03-21 04:05:39 +00:00
}
}
2020-04-05 23:36:00 +00:00
#[derive(Clone, Copy)]
2020-03-23 01:56:11 +00:00
enum ClearAction {
Single,
Double,
Triple,
Tetris,
MiniTSpin,
TSpin,
TSpinSingle,
TSpinDouble,
TSpinTriple,
}
2020-03-21 04:05:39 +00:00
impl Game {
2020-03-30 16:28:53 +00:00
pub fn score(&self) -> u32 {
self.score
}
2020-03-30 21:23:51 +00:00
2020-03-29 00:57:39 +00:00
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))
})
2020-03-21 04:05:39 +00:00
}
fn update_gravity_tick(&mut self) {
2020-03-22 21:33:38 +00:00
self.next_gravity_tick = self.tick + TICKS_PER_SECOND as u64;
2020-03-21 04:05:39 +00:00
}
fn update_lock_tick(&mut self) {
self.next_lock_tick = self.tick + TICKS_PER_SECOND as u64 / 2;
}
fn spawn_tetromino(&mut self) {
self.playfield.spawn_tetromino();
self.update_gravity_tick();
}
/// Returns if some lines were cleared
2020-03-23 01:56:11 +00:00
fn clear_lines(&mut self) -> usize {
let rows = self
.playfield
.active_piece
.map(|t| t.get_cur_occupied_spaces())
2020-03-29 00:57:39 +00:00
.map(|i| {
let mut a = i.iter().map(|p| p.y).collect::<Vec<_>>();
a.sort();
a
})
2020-03-23 01:56:11 +00:00
.unwrap_or_default();
let mut rows_cleared = 0;
for row in rows {
if self.playfield.try_clear_row(row as usize).is_ok() {
rows_cleared += 1;
}
}
rows_cleared
2020-03-21 04:05:39 +00:00
}
2020-03-22 06:47:17 +00:00
fn try_lock_tetromino(&mut self) -> bool {
// 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();
2020-04-06 03:39:19 +00:00
if self.pieces_placed < self.piece_limit {
self.pieces_placed += 1;
if self.pieces_placed >= self.piece_limit {
trace!("Loss due to piece limit!");
self.is_game_over = Some(LossReason::PieceLimitReached);
}
}
2020-03-29 00:57:39 +00:00
self.is_game_over = self.is_game_over.or_else(|| {
if positions.iter().map(|p| p.y).all(|y| y < 20) {
2020-04-06 03:39:19 +00:00
trace!("Loss due to lockout! {:?}", positions);
Some(LossReason::LockOut)
2020-03-29 00:57:39 +00:00
} else {
None
}
});
2020-03-23 01:56:11 +00:00
2020-03-30 16:28:53 +00:00
let cleared_lines = self.clear_lines();
if cleared_lines > 0 {
trace!("Lines were cleared.");
self.line_clears += cleared_lines as u32;
2020-04-05 20:34:33 +00:00
self.score += (cleared_lines * 100 * self.level as usize) as u32;
2020-03-30 16:28:53 +00:00
self.level = (self.line_clears / 10) as u8;
2020-04-06 03:39:19 +00:00
2020-03-23 01:56:11 +00:00
self.playfield.active_piece = None;
2020-03-22 06:47:17 +00:00
self.next_spawn_tick = self.tick + LINE_CLEAR_DELAY;
} else {
self.spawn_tetromino();
}
2020-03-23 01:56:11 +00:00
2020-03-22 06:47:17 +00:00
true
} else {
false
}
}
2020-04-06 03:39:19 +00:00
pub fn set_piece_limit(&mut self, size: usize) {
self.piece_limit = size;
}
2020-03-21 04:05:39 +00:00
2020-03-30 16:28:53 +00:00
pub fn playfield(&self) -> &PlayField {
&self.playfield
2020-03-21 04:05:39 +00:00
}
2020-04-06 20:07:30 +00:00
pub fn set_time_limit(&mut self, duration: std::time::Duration) {
self.tick_limit = duration.as_secs() * TICKS_PER_SECOND as u64;
}
2020-03-21 04:05:39 +00:00
}
2020-03-21 04:37:02 +00:00
pub trait Controllable {
2020-03-19 22:51:43 +00:00
fn move_left(&mut self);
fn move_right(&mut self);
fn move_down(&mut self);
fn rotate_left(&mut self);
fn rotate_right(&mut self);
fn hard_drop(&mut self);
fn hold(&mut self);
2020-03-30 16:28:53 +00:00
fn get_legal_actions(&self) -> Vec<Action>;
}
2020-04-06 20:07:30 +00:00
#[derive(Hash, Eq, PartialEq, Copy, Clone, Debug)]
2020-03-30 16:28:53 +00:00
pub enum Action {
Nothing, // Default value
MoveLeft,
MoveRight,
SoftDrop,
HardDrop,
Hold,
RotateLeft,
RotateRight,
2020-03-19 22:51:43 +00:00
}
2020-03-21 04:05:39 +00:00
impl Controllable for Game {
2020-03-21 04:37:02 +00:00
fn move_left(&mut self) {
2020-03-29 00:57:39 +00:00
if self.playfield.active_piece.is_none() {
return;
}
2020-04-06 20:07:30 +00:00
if self.playfield.move_offset(-1, 0) && !self.playfield.can_active_piece_move_down() {
2020-03-22 21:33:38 +00:00
self.update_lock_tick();
}
2020-03-21 04:37:02 +00:00
}
2020-03-30 21:23:51 +00:00
2020-03-21 04:37:02 +00:00
fn move_right(&mut self) {
2020-03-29 00:57:39 +00:00
if self.playfield.active_piece.is_none() {
return;
}
2020-04-06 20:07:30 +00:00
if self.playfield.move_offset(1, 0) && !self.playfield.can_active_piece_move_down() {
2020-03-22 21:33:38 +00:00
self.update_lock_tick();
}
2020-03-21 04:37:02 +00:00
}
2020-03-30 21:23:51 +00:00
2020-03-21 04:37:02 +00:00
fn move_down(&mut self) {
2020-03-29 00:57:39 +00:00
if self.playfield.active_piece.is_none() {
return;
}
2020-03-22 06:47:17 +00:00
if self.playfield.move_offset(0, 1) {
self.score += 1;
self.update_gravity_tick();
self.update_lock_tick();
}
2020-03-21 04:37:02 +00:00
}
2020-03-30 21:23:51 +00:00
2020-03-21 04:37:02 +00:00
fn rotate_left(&mut self) {
2020-03-29 00:57:39 +00:00
if self.playfield.active_piece.is_none() {
return;
}
2020-03-22 06:47:17 +00:00
match self.rotation_system.rotate_left(&self.playfield) {
Ok(Position { x, y }) => {
let mut active_piece = self.playfield.active_piece.unwrap().clone();
active_piece.position = active_piece.position.offset(x, y);
active_piece.rotate_left();
self.playfield.active_piece = Some(active_piece);
2020-04-06 20:07:30 +00:00
self.update_lock_tick();
2020-03-22 06:47:17 +00:00
}
Err(_) => (),
}
2020-03-21 04:37:02 +00:00
}
2020-03-30 21:23:51 +00:00
2020-03-21 04:37:02 +00:00
fn rotate_right(&mut self) {
2020-03-29 00:57:39 +00:00
if self.playfield.active_piece.is_none() {
return;
}
2020-03-22 06:47:17 +00:00
match self.rotation_system.rotate_right(&self.playfield) {
Ok(Position { x, y }) => {
let mut active_piece = self.playfield.active_piece.unwrap().clone();
active_piece.position = active_piece.position.offset(x, y);
active_piece.rotate_right();
self.playfield.active_piece = Some(active_piece);
2020-04-06 20:07:30 +00:00
self.update_lock_tick();
2020-03-22 06:47:17 +00:00
}
Err(_) => (),
}
2020-03-21 04:37:02 +00:00
}
2020-03-30 21:23:51 +00:00
2020-03-21 04:37:02 +00:00
fn hard_drop(&mut self) {
2020-03-29 00:57:39 +00:00
if self.playfield.active_piece.is_none() {
return;
}
let mut lines_fallen = 0;
trace!("Score before hard drop: {}", self.score);
2020-03-22 06:47:17 +00:00
while self.playfield.can_active_piece_move_down() {
self.score += 2;
2020-03-29 00:57:39 +00:00
lines_fallen += 1;
2020-03-22 06:47:17 +00:00
self.playfield.move_offset(0, 1);
}
2020-03-29 00:57:39 +00:00
trace!(
"Score after hard dropping {} lines: {}",
lines_fallen,
self.score
);
2020-03-22 06:47:17 +00:00
2020-03-29 00:57:39 +00:00
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!");
2020-03-22 06:47:17 +00:00
}
2020-03-21 04:37:02 +00:00
}
2020-03-19 22:51:43 +00:00
fn hold(&mut self) {
2020-03-29 00:57:39 +00:00
if self.playfield.active_piece.is_none() {
return;
}
2020-03-23 02:44:27 +00:00
let _ = self.playfield.try_swap_hold();
2020-03-19 22:51:43 +00:00
}
2020-03-30 16:28:53 +00:00
fn get_legal_actions(&self) -> Vec<Action> {
let mut legal_actions = vec![
2020-04-06 20:07:30 +00:00
Action::RotateLeft,
Action::RotateRight,
Action::SoftDrop,
Action::HardDrop,
2020-03-30 21:23:51 +00:00
Action::Nothing,
2020-03-30 16:28:53 +00:00
Action::MoveLeft,
Action::MoveRight,
];
if self.playfield.can_swap() {
legal_actions.push(Action::Hold);
}
legal_actions
}
2020-03-19 22:51:43 +00:00
}