// 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; use crate::tetromino::Position; use crate::TICKS_PER_SECOND; use log::{error, info, trace}; 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, PieceLimitReached, TickLimitReached, BlockOut(Position), } #[derive(Clone)] // Logic is based on 60 ticks / second pub struct Game { playfield: PlayField, rotation_system: SRS, level: u8, score: u32, tick: u64, next_gravity_tick: u64, next_lock_tick: u64, next_spawn_tick: u64, is_game_over: Option, /// The last clear action performed, used for determining if a back-to-back /// bonus is needed. last_clear_action: ClearAction, pub line_clears: u32, // used if we set a limit on how long a game can last. pieces_placed: usize, piece_limit: usize, // used if we set a limit on how long the game can be played. tick_limit: u64, } impl fmt::Debug for Game { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "level: {}, points: {}", self.level, self.score)?; writeln!(f, "tick: {}", self.tick)?; write!(f, "{:?}", self.playfield) } } impl Default for Game { fn default() -> Self { Game { playfield: PlayField::new(), rotation_system: SRS::default(), level: 1, score: 0, tick: 0, next_gravity_tick: 60, next_lock_tick: 0, next_spawn_tick: 0, is_game_over: None, last_clear_action: ClearAction::Single, // Doesn't matter what it's initialized to line_clears: 0, pieces_placed: 0, piece_limit: 0, tick_limit: 0, } } } pub trait Tickable { fn tick(&mut self); } impl Tickable for Game { fn tick(&mut self) { if self.is_game_over().is_some() { return; } self.tick += 1; match self.tick { 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 => { 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 => { 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(); } _ => (), } if self.tick == self.tick_limit { self.is_game_over = Some(LossReason::TickLimitReached); } } } #[derive(Clone, Copy)] enum ClearAction { Single, Double, Triple, Tetris, MiniTSpin, TSpin, TSpinSingle, TSpinDouble, TSpinTriple, } impl Game { pub fn score(&self) -> u32 { self.score } 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) { self.next_gravity_tick = self.tick + TICKS_PER_SECOND as u64; } 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 fn clear_lines(&mut self) -> usize { let rows = self .playfield .active_piece .map(|t| t.get_cur_occupied_spaces()) .map(|i| { let mut a = i.iter().map(|p| p.y).collect::>(); a.sort(); a }) .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 } 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(); 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); } } self.is_game_over = self.is_game_over.or_else(|| { if positions.iter().map(|p| p.y).all(|y| y < 20) { trace!("Loss due to lockout! {:?}", positions); Some(LossReason::LockOut) } else { None } }); let cleared_lines = self.clear_lines(); if cleared_lines > 0 { trace!("Lines were cleared."); self.line_clears += cleared_lines as u32; self.score += (cleared_lines * 100 * self.level as usize) as u32; self.level = (self.line_clears / 10) as u8; self.playfield.active_piece = None; self.next_spawn_tick = self.tick + LINE_CLEAR_DELAY; } else { self.spawn_tetromino(); } true } else { false } } pub fn set_piece_limit(&mut self, size: usize) { self.piece_limit = size; } pub fn playfield(&self) -> &PlayField { &self.playfield } pub fn set_time_limit(&mut self, duration: std::time::Duration) { self.tick_limit = duration.as_secs() * TICKS_PER_SECOND as u64; } } pub trait Controllable { 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); fn get_legal_actions(&self) -> Vec; } #[derive(Hash, Eq, PartialEq, Copy, Clone, Debug)] pub enum Action { Nothing, // Default value MoveLeft, MoveRight, SoftDrop, HardDrop, Hold, RotateLeft, RotateRight, } impl Controllable for Game { fn move_left(&mut self) { if self.playfield.active_piece.is_none() { return; } if self.playfield.move_offset(-1, 0) && !self.playfield.can_active_piece_move_down() { self.update_lock_tick(); } } fn move_right(&mut self) { if self.playfield.active_piece.is_none() { return; } if self.playfield.move_offset(1, 0) && !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(); self.update_lock_tick(); } } 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(); active_piece.position = active_piece.position.offset(x, y); active_piece.rotate_left(); self.playfield.active_piece = Some(active_piece); self.update_lock_tick(); } Err(_) => (), } } 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(); active_piece.position = active_piece.position.offset(x, y); active_piece.rotate_right(); self.playfield.active_piece = Some(active_piece); self.update_lock_tick(); } Err(_) => (), } } 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 ); 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!"); } } fn hold(&mut self) { if self.playfield.active_piece.is_none() { return; } let _ = self.playfield.try_swap_hold(); } fn get_legal_actions(&self) -> Vec { let mut legal_actions = vec![ Action::RotateLeft, Action::RotateRight, Action::SoftDrop, Action::HardDrop, Action::Nothing, Action::MoveLeft, Action::MoveRight, ]; if self.playfield.can_swap() { legal_actions.push(Action::Hold); } legal_actions } }