2020-03-19 15:51:43 -07: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-20 21:05:39 -07:00
|
|
|
use crate::tetromino::Position;
|
|
|
|
use crate::TICKS_PER_SECOND;
|
2020-03-28 17:57:39 -07:00
|
|
|
use log::{error, info, trace};
|
2020-03-20 21:05:39 -07:00
|
|
|
use std::fmt;
|
2020-03-19 15:51:43 -07:00
|
|
|
|
2020-03-20 21:05:39 -07:00
|
|
|
// I think this was correct, can't find source
|
|
|
|
const LINE_CLEAR_DELAY: u64 = TICKS_PER_SECOND as u64 * 41 / 60;
|
|
|
|
|
2020-03-28 17:57:39 -07:00
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
pub enum LossReason {
|
|
|
|
TopOut,
|
|
|
|
LockOut,
|
|
|
|
BlockOut(Position),
|
|
|
|
}
|
2020-04-05 16:36:00 -07:00
|
|
|
|
|
|
|
#[derive(Clone)]
|
2020-03-20 21:05:39 -07:00
|
|
|
// Logic is based on 60 ticks / second
|
|
|
|
pub struct Game {
|
2020-03-19 15:51:43 -07:00
|
|
|
playfield: PlayField,
|
2020-03-20 21:05:39 -07:00
|
|
|
rotation_system: SRS,
|
2020-03-19 15:51:43 -07:00
|
|
|
level: u8,
|
2020-03-21 23:47:17 -07:00
|
|
|
score: u32,
|
2020-03-20 21:37:02 -07:00
|
|
|
tick: u64,
|
2020-03-20 21:05:39 -07:00
|
|
|
next_gravity_tick: u64,
|
|
|
|
next_lock_tick: u64,
|
|
|
|
next_spawn_tick: u64,
|
2020-03-28 17:57:39 -07:00
|
|
|
is_game_over: Option<LossReason>,
|
2020-03-22 18:56:11 -07:00
|
|
|
/// The last clear action performed, used for determining if a back-to-back
|
|
|
|
/// bonus is needed.
|
|
|
|
last_clear_action: ClearAction,
|
2020-04-05 13:34:33 -07:00
|
|
|
pub line_clears: u32,
|
2020-03-20 21:05:39 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Debug for Game {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
2020-03-21 23:47:17 -07:00
|
|
|
writeln!(f, "level: {}, points: {}", self.level, self.score)?;
|
2020-03-20 21:05:39 -07:00
|
|
|
writeln!(f, "tick: {}", self.tick)?;
|
|
|
|
write!(f, "{:?}", self.playfield)
|
|
|
|
}
|
2020-03-19 15:51:43 -07:00
|
|
|
}
|
|
|
|
|
2020-03-20 21:05:39 -07:00
|
|
|
impl Default for Game {
|
2020-03-19 16:47:50 -07:00
|
|
|
fn default() -> Self {
|
|
|
|
Game {
|
|
|
|
playfield: PlayField::new(),
|
2020-03-20 21:05:39 -07:00
|
|
|
rotation_system: SRS::default(),
|
|
|
|
level: 1,
|
2020-03-21 23:47:17 -07:00
|
|
|
score: 0,
|
2020-03-20 21:05:39 -07:00
|
|
|
tick: 0,
|
|
|
|
next_gravity_tick: 60,
|
|
|
|
next_lock_tick: 0,
|
|
|
|
next_spawn_tick: 0,
|
2020-03-28 17:57:39 -07:00
|
|
|
is_game_over: None,
|
2020-03-22 18:56:11 -07:00
|
|
|
last_clear_action: ClearAction::Single, // Doesn't matter what it's initialized to
|
2020-03-30 09:28:53 -07:00
|
|
|
line_clears: 0,
|
2020-03-19 16:47:50 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-20 21:05:39 -07:00
|
|
|
pub trait Tickable {
|
|
|
|
fn tick(&mut self);
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Tickable for Game {
|
|
|
|
fn tick(&mut self) {
|
2020-03-28 17:57:39 -07:00
|
|
|
if self.is_game_over().is_some() {
|
2020-03-20 21:05:39 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
self.tick += 1;
|
|
|
|
match self.tick {
|
2020-03-28 17:57:39 -07:00
|
|
|
t if t == self.next_spawn_tick => {
|
|
|
|
trace!("Spawn tick was met, spawning new Tetromino!");
|
|
|
|
self.spawn_tetromino();
|
|
|
|
}
|
2020-03-20 21:05:39 -07:00
|
|
|
t if t == self.next_lock_tick => {
|
2020-03-28 17:57:39 -07: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-20 21:05:39 -07:00
|
|
|
}
|
|
|
|
t if t == self.next_gravity_tick => {
|
2020-03-28 17:57:39 -07: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-20 21:05:39 -07:00
|
|
|
}
|
2020-03-28 17:57:39 -07:00
|
|
|
|
2020-03-20 21:05:39 -07:00
|
|
|
self.update_gravity_tick();
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-05 16:36:00 -07:00
|
|
|
#[derive(Clone, Copy)]
|
2020-03-22 18:56:11 -07:00
|
|
|
enum ClearAction {
|
|
|
|
Single,
|
|
|
|
Double,
|
|
|
|
Triple,
|
|
|
|
Tetris,
|
|
|
|
MiniTSpin,
|
|
|
|
TSpin,
|
|
|
|
TSpinSingle,
|
|
|
|
TSpinDouble,
|
|
|
|
TSpinTriple,
|
|
|
|
}
|
|
|
|
|
2020-03-20 21:05:39 -07:00
|
|
|
impl Game {
|
2020-03-30 09:28:53 -07:00
|
|
|
pub fn score(&self) -> u32 {
|
|
|
|
self.score
|
|
|
|
}
|
2020-03-30 14:23:51 -07:00
|
|
|
|
2020-03-28 17:57:39 -07: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-20 21:05:39 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
fn update_gravity_tick(&mut self) {
|
2020-03-22 14:33:38 -07:00
|
|
|
self.next_gravity_tick = self.tick + TICKS_PER_SECOND as u64;
|
2020-03-20 21:05:39 -07: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-22 18:56:11 -07:00
|
|
|
fn clear_lines(&mut self) -> usize {
|
|
|
|
let rows = self
|
|
|
|
.playfield
|
|
|
|
.active_piece
|
|
|
|
.map(|t| t.get_cur_occupied_spaces())
|
2020-03-28 17:57:39 -07:00
|
|
|
.map(|i| {
|
|
|
|
let mut a = i.iter().map(|p| p.y).collect::<Vec<_>>();
|
|
|
|
a.sort();
|
|
|
|
a
|
|
|
|
})
|
2020-03-22 18:56:11 -07: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-20 21:05:39 -07:00
|
|
|
}
|
2020-03-21 23:47:17 -07: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-03-28 17:57:39 -07:00
|
|
|
self.is_game_over = self.is_game_over.or_else(|| {
|
|
|
|
if positions.iter().map(|p| p.y).all(|y| y < 20) {
|
2020-03-30 09:28:53 -07:00
|
|
|
trace!("Loss due to topout! {:?}", positions);
|
|
|
|
Some(LossReason::TopOut)
|
2020-03-28 17:57:39 -07:00
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
});
|
2020-03-22 18:56:11 -07:00
|
|
|
|
2020-03-30 09:28:53 -07: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 13:34:33 -07:00
|
|
|
self.score += (cleared_lines * 100 * self.level as usize) as u32;
|
2020-03-30 09:28:53 -07:00
|
|
|
self.level = (self.line_clears / 10) as u8;
|
2020-03-22 18:56:11 -07:00
|
|
|
self.playfield.active_piece = None;
|
2020-03-21 23:47:17 -07:00
|
|
|
self.next_spawn_tick = self.tick + LINE_CLEAR_DELAY;
|
|
|
|
} else {
|
|
|
|
self.spawn_tetromino();
|
|
|
|
}
|
2020-03-22 18:56:11 -07:00
|
|
|
|
2020-03-21 23:47:17 -07:00
|
|
|
true
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
2020-03-20 21:05:39 -07:00
|
|
|
|
2020-03-30 09:28:53 -07:00
|
|
|
pub fn playfield(&self) -> &PlayField {
|
|
|
|
&self.playfield
|
2020-03-20 21:05:39 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-20 21:37:02 -07:00
|
|
|
pub trait Controllable {
|
2020-03-19 15:51:43 -07: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 09:28:53 -07:00
|
|
|
fn get_legal_actions(&self) -> Vec<Action>;
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Hash, Eq, PartialEq, Debug, Copy, Clone)]
|
|
|
|
pub enum Action {
|
|
|
|
Nothing, // Default value
|
|
|
|
MoveLeft,
|
|
|
|
MoveRight,
|
|
|
|
SoftDrop,
|
|
|
|
HardDrop,
|
|
|
|
Hold,
|
|
|
|
RotateLeft,
|
|
|
|
RotateRight,
|
2020-03-19 15:51:43 -07:00
|
|
|
}
|
|
|
|
|
2020-03-20 21:05:39 -07:00
|
|
|
impl Controllable for Game {
|
2020-03-20 21:37:02 -07:00
|
|
|
fn move_left(&mut self) {
|
2020-03-28 17:57:39 -07:00
|
|
|
if self.playfield.active_piece.is_none() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-20 21:37:02 -07:00
|
|
|
self.playfield.move_offset(-1, 0);
|
2020-03-22 14:33:38 -07:00
|
|
|
if !self.playfield.can_active_piece_move_down() {
|
|
|
|
self.update_lock_tick();
|
|
|
|
}
|
2020-03-20 21:37:02 -07:00
|
|
|
}
|
2020-03-30 14:23:51 -07:00
|
|
|
|
2020-03-20 21:37:02 -07:00
|
|
|
fn move_right(&mut self) {
|
2020-03-28 17:57:39 -07:00
|
|
|
if self.playfield.active_piece.is_none() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-20 21:37:02 -07:00
|
|
|
self.playfield.move_offset(1, 0);
|
2020-03-22 14:33:38 -07:00
|
|
|
if !self.playfield.can_active_piece_move_down() {
|
|
|
|
self.update_lock_tick();
|
|
|
|
}
|
2020-03-20 21:37:02 -07:00
|
|
|
}
|
2020-03-30 14:23:51 -07:00
|
|
|
|
2020-03-20 21:37:02 -07:00
|
|
|
fn move_down(&mut self) {
|
2020-03-28 17:57:39 -07:00
|
|
|
if self.playfield.active_piece.is_none() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-21 23:47:17 -07:00
|
|
|
if self.playfield.move_offset(0, 1) {
|
|
|
|
self.score += 1;
|
|
|
|
self.update_gravity_tick();
|
|
|
|
self.update_lock_tick();
|
|
|
|
}
|
2020-03-20 21:37:02 -07:00
|
|
|
}
|
2020-03-30 14:23:51 -07:00
|
|
|
|
2020-03-20 21:37:02 -07:00
|
|
|
fn rotate_left(&mut self) {
|
2020-03-28 17:57:39 -07:00
|
|
|
if self.playfield.active_piece.is_none() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-21 23:47:17 -07: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-05 16:36:00 -07:00
|
|
|
// self.update_lock_tick();
|
2020-03-21 23:47:17 -07:00
|
|
|
}
|
|
|
|
Err(_) => (),
|
|
|
|
}
|
2020-03-20 21:37:02 -07:00
|
|
|
}
|
2020-03-30 14:23:51 -07:00
|
|
|
|
2020-03-20 21:37:02 -07:00
|
|
|
fn rotate_right(&mut self) {
|
2020-03-28 17:57:39 -07:00
|
|
|
if self.playfield.active_piece.is_none() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-21 23:47:17 -07: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-05 16:36:00 -07:00
|
|
|
// self.update_lock_tick();
|
2020-03-21 23:47:17 -07:00
|
|
|
}
|
|
|
|
Err(_) => (),
|
|
|
|
}
|
2020-03-20 21:37:02 -07:00
|
|
|
}
|
2020-03-30 14:23:51 -07:00
|
|
|
|
2020-03-20 21:37:02 -07:00
|
|
|
fn hard_drop(&mut self) {
|
2020-03-28 17:57:39 -07:00
|
|
|
if self.playfield.active_piece.is_none() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut lines_fallen = 0;
|
|
|
|
trace!("Score before hard drop: {}", self.score);
|
2020-03-21 23:47:17 -07:00
|
|
|
while self.playfield.can_active_piece_move_down() {
|
|
|
|
self.score += 2;
|
2020-03-28 17:57:39 -07:00
|
|
|
lines_fallen += 1;
|
2020-03-21 23:47:17 -07:00
|
|
|
self.playfield.move_offset(0, 1);
|
|
|
|
}
|
2020-03-28 17:57:39 -07:00
|
|
|
trace!(
|
|
|
|
"Score after hard dropping {} lines: {}",
|
|
|
|
lines_fallen,
|
|
|
|
self.score
|
|
|
|
);
|
2020-03-21 23:47:17 -07:00
|
|
|
|
2020-03-28 17:57:39 -07: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-21 23:47:17 -07:00
|
|
|
}
|
2020-03-20 21:37:02 -07:00
|
|
|
}
|
2020-03-19 15:51:43 -07:00
|
|
|
|
|
|
|
fn hold(&mut self) {
|
2020-03-28 17:57:39 -07:00
|
|
|
if self.playfield.active_piece.is_none() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-22 19:44:27 -07:00
|
|
|
let _ = self.playfield.try_swap_hold();
|
2020-03-19 15:51:43 -07:00
|
|
|
}
|
2020-03-30 09:28:53 -07:00
|
|
|
|
|
|
|
fn get_legal_actions(&self) -> Vec<Action> {
|
|
|
|
let mut legal_actions = vec![
|
2020-03-30 14:23:51 -07:00
|
|
|
Action::Nothing,
|
2020-03-30 09:28:53 -07:00
|
|
|
Action::MoveLeft,
|
|
|
|
Action::MoveRight,
|
|
|
|
Action::SoftDrop,
|
|
|
|
Action::HardDrop,
|
|
|
|
Action::RotateLeft,
|
|
|
|
Action::RotateRight,
|
|
|
|
];
|
|
|
|
|
|
|
|
if self.playfield.can_swap() {
|
|
|
|
legal_actions.push(Action::Hold);
|
|
|
|
}
|
|
|
|
|
|
|
|
legal_actions
|
|
|
|
}
|
2020-03-19 15:51:43 -07:00
|
|
|
}
|