line clears
This commit is contained in:
parent
8e588a8a70
commit
8199928d7d
@ -6,10 +6,30 @@ pub enum Controls {
|
|||||||
Right,
|
Right,
|
||||||
SoftDrop,
|
SoftDrop,
|
||||||
HardDrop,
|
HardDrop,
|
||||||
|
Swap,
|
||||||
|
RotateCw,
|
||||||
|
RotateCcw,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ControlsHeld(HashMap<Controls, usize>);
|
pub struct ControlsHeld(HashMap<Controls, usize>);
|
||||||
|
|
||||||
|
impl ControlsHeld {
|
||||||
|
pub fn just_pressed(&self, ticks: usize, control: &Controls) -> bool {
|
||||||
|
self.held_for(ticks, control, |held_for| held_for == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn held_for<F: Fn(usize) -> bool>(
|
||||||
|
&self,
|
||||||
|
ticks: usize,
|
||||||
|
control: &Controls,
|
||||||
|
functor: F,
|
||||||
|
) -> bool {
|
||||||
|
self.get(control)
|
||||||
|
.map(|&held_since| ticks - held_since)
|
||||||
|
.is_some_and(functor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for ControlsHeld {
|
impl std::ops::Deref for ControlsHeld {
|
||||||
type Target = HashMap<Controls, usize>;
|
type Target = HashMap<Controls, usize>;
|
||||||
|
|
||||||
|
139
src/board.rs
139
src/board.rs
@ -2,6 +2,7 @@ use std::ops::{Deref, DerefMut};
|
|||||||
|
|
||||||
use crate::{CurrentTetromino, Tetromino};
|
use crate::{CurrentTetromino, Tetromino};
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
pub struct Board([[Option<Tetromino>; Self::WIDTH]; Self::HEIGHT]);
|
pub struct Board([[Option<Tetromino>; Self::WIDTH]; Self::HEIGHT]);
|
||||||
|
|
||||||
impl Deref for Board {
|
impl Deref for Board {
|
||||||
@ -68,4 +69,142 @@ impl Board {
|
|||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn lines_cleared(&mut self) -> usize {
|
||||||
|
let line_clears: Vec<_> = self
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, row)| if !row.contains(&None) { Some(i) } else { None })
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut lines_cleared = 0;
|
||||||
|
for i in (0..self.len()).rev() {
|
||||||
|
let blank_line = std::array::from_fn(|_| None);
|
||||||
|
let line = std::mem::replace(&mut self[i], blank_line);
|
||||||
|
self[i + lines_cleared] = line;
|
||||||
|
|
||||||
|
if line_clears.contains(&i) {
|
||||||
|
lines_cleared += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines_cleared
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Board {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let t = self
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|row| {
|
||||||
|
row.iter()
|
||||||
|
.map(|t| match t {
|
||||||
|
Some(t) => match t {
|
||||||
|
Tetromino::I => "I",
|
||||||
|
Tetromino::J => "J",
|
||||||
|
Tetromino::L => "L",
|
||||||
|
Tetromino::O => "O",
|
||||||
|
Tetromino::S => "S",
|
||||||
|
Tetromino::T => "T",
|
||||||
|
Tetromino::Z => "Z",
|
||||||
|
},
|
||||||
|
None => ".",
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("")
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
write!(f, "{t}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::Board;
|
||||||
|
|
||||||
|
fn board_from_str(str: &'static str) -> Board {
|
||||||
|
use crate::Tetromino::*;
|
||||||
|
Board(
|
||||||
|
str.split_whitespace()
|
||||||
|
.map(|row| {
|
||||||
|
let row: [char; Board::WIDTH] = row
|
||||||
|
.chars()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.try_into()
|
||||||
|
.expect("invalid board row");
|
||||||
|
row.map(|char| match char {
|
||||||
|
'.' => None,
|
||||||
|
'I' => Some(I),
|
||||||
|
'J' => Some(J),
|
||||||
|
'L' => Some(L),
|
||||||
|
'O' => Some(O),
|
||||||
|
'S' => Some(S),
|
||||||
|
'T' => Some(T),
|
||||||
|
'Z' => Some(Z),
|
||||||
|
c => panic!("invalid board char '{c}'"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.try_into()
|
||||||
|
.expect("invalid board content"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_clear() {
|
||||||
|
let mut board = board_from_str(
|
||||||
|
"
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..OOOOOOOO
|
||||||
|
OOOOOOOO..
|
||||||
|
JJJJJJJJJJ
|
||||||
|
JJJJJJJJJJ
|
||||||
|
..JJJJJJJJ
|
||||||
|
JJJJJJJJ..
|
||||||
|
JJJJJJJJ..
|
||||||
|
JJJJJJJJ..
|
||||||
|
JJJJJJJJJJ
|
||||||
|
JJJJJJJJJJ
|
||||||
|
JJJJJJJJJJ
|
||||||
|
JJJJJJJJJJ
|
||||||
|
JJJJJJJJJJ
|
||||||
|
JJJJJJJJJJ
|
||||||
|
JJJJJJJJJJ
|
||||||
|
JJJJJJJJJJ
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
let after = board_from_str(
|
||||||
|
"
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..........
|
||||||
|
..OOOOOOOO
|
||||||
|
OOOOOOOO..
|
||||||
|
..JJJJJJJJ
|
||||||
|
JJJJJJJJ..
|
||||||
|
JJJJJJJJ..
|
||||||
|
JJJJJJJJ..
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(board.lines_cleared(), 10);
|
||||||
|
|
||||||
|
assert_eq!(board, after);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
108
src/main.rs
108
src/main.rs
@ -1,5 +1,3 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use actions::{Controls, ControlsHeld};
|
use actions::{Controls, ControlsHeld};
|
||||||
use board::Board;
|
use board::Board;
|
||||||
use tetromino::{Direction, DirectionDiff, Tetromino};
|
use tetromino::{Direction, DirectionDiff, Tetromino};
|
||||||
@ -44,6 +42,7 @@ struct Score {
|
|||||||
points: usize,
|
points: usize,
|
||||||
lines: usize,
|
lines: usize,
|
||||||
combo: usize,
|
combo: usize,
|
||||||
|
back_to_back: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Score {
|
impl Score {
|
||||||
@ -53,6 +52,34 @@ impl Score {
|
|||||||
points: 0,
|
points: 0,
|
||||||
lines: 0,
|
lines: 0,
|
||||||
combo: 0,
|
combo: 0,
|
||||||
|
back_to_back: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn level_up(&mut self, lines_cleared: usize) {
|
||||||
|
self.lines += lines_cleared;
|
||||||
|
if self.lines > self.level * 5 {
|
||||||
|
self.level += 1;
|
||||||
|
self.lines = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_multiplier_from_lines_cleared(lines_cleared: usize) -> f32 {
|
||||||
|
match lines_cleared {
|
||||||
|
0 => 0.0,
|
||||||
|
1 => 100.0,
|
||||||
|
2 => 300.0,
|
||||||
|
3 => 500.0,
|
||||||
|
4 => 800.0,
|
||||||
|
_ => unreachable!("we cannot clear more than 4 lines"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn combos(&self, lines_cleared: usize) -> usize {
|
||||||
|
if lines_cleared > 0 {
|
||||||
|
self.combo * 50 * self.level
|
||||||
|
} else {
|
||||||
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,10 +93,20 @@ impl Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn hard_drop(&mut self, controls: &ControlsHeld) {
|
fn hard_drop(&mut self, controls: &ControlsHeld) {
|
||||||
if controls.contains_key(&Controls::HardDrop) {
|
if !controls.just_pressed(self.ticks, &Controls::HardDrop) {
|
||||||
loop {
|
return;
|
||||||
todo!()
|
}
|
||||||
|
let start_y = self.current_tetromino.y;
|
||||||
|
loop {
|
||||||
|
self.current_tetromino.y += 1;
|
||||||
|
if !self.board.colliding(&self.current_tetromino) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
self.current_tetromino.y -= 1;
|
||||||
|
self.score.points += (self.current_tetromino.y - start_y) as usize * 2;
|
||||||
|
self.place_current_tetromino();
|
||||||
|
self.check_line_clears();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +125,6 @@ impl Game {
|
|||||||
self.current_tetromino.y -= 1;
|
self.current_tetromino.y -= 1;
|
||||||
self.place_current_tetromino();
|
self.place_current_tetromino();
|
||||||
self.check_line_clears();
|
self.check_line_clears();
|
||||||
self.has_swapped_held = false;
|
|
||||||
} else if controls.contains_key(&Controls::SoftDrop) {
|
} else if controls.contains_key(&Controls::SoftDrop) {
|
||||||
self.score.points += 1;
|
self.score.points += 1;
|
||||||
}
|
}
|
||||||
@ -96,11 +132,9 @@ impl Game {
|
|||||||
|
|
||||||
fn move_horizontally(&mut self, controls: &ControlsHeld) {
|
fn move_horizontally(&mut self, controls: &ControlsHeld) {
|
||||||
for key in [Controls::Left, Controls::Right] {
|
for key in [Controls::Left, Controls::Right] {
|
||||||
let Some(held_since) = controls.get(&key) else {
|
let just_pressed = controls.just_pressed(self.ticks, &key);
|
||||||
continue;
|
let long_press = controls.held_for(self.ticks, &key, |held_for| held_for > 15);
|
||||||
};
|
if !just_pressed && !long_press {
|
||||||
let held_for = self.ticks - held_since;
|
|
||||||
if held_for < 15 {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let offset = match key {
|
let offset = match key {
|
||||||
@ -115,15 +149,57 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_line_clears(&self) {
|
fn check_line_clears(&mut self) {
|
||||||
todo!()
|
let lines_cleared = self.board.lines_cleared();
|
||||||
|
|
||||||
|
self.score.level_up(lines_cleared);
|
||||||
|
|
||||||
|
let mut points =
|
||||||
|
self.score.level as f32 * Score::point_multiplier_from_lines_cleared(lines_cleared);
|
||||||
|
|
||||||
|
// Back to back tetris
|
||||||
|
if self.score.back_to_back && lines_cleared == 4 {
|
||||||
|
points *= 1.5;
|
||||||
|
}
|
||||||
|
points += self.score.combos(lines_cleared) as f32;
|
||||||
|
|
||||||
|
self.score.points += points as usize;
|
||||||
|
|
||||||
|
if lines_cleared == 4 {
|
||||||
|
self.score.back_to_back = true;
|
||||||
|
} else if lines_cleared > 0 {
|
||||||
|
self.score.back_to_back = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if lines_cleared > 0 {
|
||||||
|
self.score.combo += 1;
|
||||||
|
// play_line_clears_sound();
|
||||||
|
} else {
|
||||||
|
self.score.combo = 0;
|
||||||
|
// play_hard_drop_sound();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step(&mut self, controls: &ControlsHeld) {
|
fn step(&mut self, controls: &ControlsHeld) {
|
||||||
// TODO: ensure game is running at 60fps (`if !check_update_time(context, 60) { return; }`)
|
// TODO: ensure game is running at 60 ticks per second? (`if !check_update_time(context, 60) { return; }`)
|
||||||
self.ticks += 1;
|
self.hard_drop(controls);
|
||||||
self.soft_drop(controls);
|
self.soft_drop(controls);
|
||||||
self.move_horizontally(controls);
|
self.move_horizontally(controls);
|
||||||
|
|
||||||
|
if controls.just_pressed(self.ticks, &Controls::Swap) {
|
||||||
|
self.try_swap_tetromino();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (control, direction) in [
|
||||||
|
(Controls::RotateCw, DirectionDiff::Cw),
|
||||||
|
(Controls::RotateCcw, DirectionDiff::Ccw),
|
||||||
|
] {
|
||||||
|
if !controls.just_pressed(self.ticks, &control) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.try_rotate(direction);
|
||||||
|
}
|
||||||
|
self.ticks += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_rotate(&mut self, diff: DirectionDiff) -> bool {
|
fn try_rotate(&mut self, diff: DirectionDiff) -> bool {
|
||||||
@ -168,6 +244,8 @@ impl Game {
|
|||||||
self.board[y][x] = Some(current.tetromino.clone());
|
self.board[y][x] = Some(current.tetromino.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.has_swapped_held = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_swap_tetromino(&mut self) {
|
fn try_swap_tetromino(&mut self) {
|
||||||
|
@ -1219,4 +1219,3 @@ unsafe fn create_popup_message(info: &panic::PanicInfo, path: PathBuf) {
|
|||||||
fn create_popup_message(info: &panic::PanicInfo) {
|
fn create_popup_message(info: &panic::PanicInfo) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user