// 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::Renderable; use crate::TICKS_PER_SECOND; use log::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; // 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: bool, /// The last clear action performed, used for determining if a back-to-back /// bonus is needed. last_clear_action: ClearAction, } 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: false, last_clear_action: ClearAction::Single, // Doesn't matter what it's initialized to } } } pub trait Tickable { fn tick(&mut self); } impl Tickable for Game { fn tick(&mut self) { if self.is_game_over() { return; } self.tick += 1; match self.tick { t if t == self.next_spawn_tick => self.spawn_tetromino(), t if t == self.next_lock_tick => { self.try_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(); } self.update_gravity_tick(); } _ => (), } } } enum ClearAction { Single, Double, Triple, Tetris, MiniTSpin, TSpin, TSpinSingle, TSpinDouble, TSpinTriple, } impl Game { pub fn is_game_over(&self) -> bool { self.is_game_over || !self.playfield.is_active_piece_in_valid_position() } fn update_gravity_tick(&mut self) { // self.next_gravity_tick = (-1 as i64) as u64; 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| i.iter().map(|p| p.y).collect::>()) .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(); self.is_game_over = self.is_game_over || positions.iter().all(|Position { x: _, y }| *y < 20); if self.clear_lines() > 0 { self.playfield.active_piece = None; self.next_spawn_tick = self.tick + LINE_CLEAR_DELAY; } else { self.spawn_tetromino(); } true } else { false } } } impl Renderable for Game { fn render(&self, canvas: &mut Canvas) -> Result<(), String> { self.playfield.render(canvas) } } 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); } impl Controllable for Game { fn move_left(&mut self) { self.playfield.move_offset(-1, 0); if !self.playfield.can_active_piece_move_down() { self.update_lock_tick(); } } fn move_right(&mut self) { 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.move_offset(0, 1) { self.score += 1; self.update_gravity_tick(); self.update_lock_tick(); } } fn rotate_left(&mut self) { 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) { 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) { while self.playfield.can_active_piece_move_down() { self.score += 2; self.playfield.move_offset(0, 1); } if !self.try_lock_tetromino() { println!("couldn't lock tetromino despite hard dropping!"); } self.next_lock_tick = std::u64::MAX; } fn hold(&mut self) { match self.playfield.try_swap_hold() { Ok(_) => {} Err(_) => (), } } }