line clears

This commit is contained in:
Theis Pieter Hollebeek 2025-03-03 11:55:50 +01:00
parent 8e588a8a70
commit 8199928d7d
4 changed files with 252 additions and 16 deletions

View File

@ -6,10 +6,30 @@ pub enum Controls {
Right,
SoftDrop,
HardDrop,
Swap,
RotateCw,
RotateCcw,
}
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 {
type Target = HashMap<Controls, usize>;

View File

@ -2,6 +2,7 @@ use std::ops::{Deref, DerefMut};
use crate::{CurrentTetromino, Tetromino};
#[derive(PartialEq)]
pub struct Board([[Option<Tetromino>; Self::WIDTH]; Self::HEIGHT]);
impl Deref for Board {
@ -68,4 +69,142 @@ impl Board {
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);
}
}

View File

@ -1,5 +1,3 @@
#![allow(dead_code)]
use actions::{Controls, ControlsHeld};
use board::Board;
use tetromino::{Direction, DirectionDiff, Tetromino};
@ -44,6 +42,7 @@ struct Score {
points: usize,
lines: usize,
combo: usize,
back_to_back: bool,
}
impl Score {
@ -53,6 +52,34 @@ impl Score {
points: 0,
lines: 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) {
if controls.contains_key(&Controls::HardDrop) {
loop {
todo!()
if !controls.just_pressed(self.ticks, &Controls::HardDrop) {
return;
}
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.place_current_tetromino();
self.check_line_clears();
self.has_swapped_held = false;
} else if controls.contains_key(&Controls::SoftDrop) {
self.score.points += 1;
}
@ -96,11 +132,9 @@ impl Game {
fn move_horizontally(&mut self, controls: &ControlsHeld) {
for key in [Controls::Left, Controls::Right] {
let Some(held_since) = controls.get(&key) else {
continue;
};
let held_for = self.ticks - held_since;
if held_for < 15 {
let just_pressed = controls.just_pressed(self.ticks, &key);
let long_press = controls.held_for(self.ticks, &key, |held_for| held_for > 15);
if !just_pressed && !long_press {
continue;
}
let offset = match key {
@ -115,15 +149,57 @@ impl Game {
}
}
fn check_line_clears(&self) {
todo!()
fn check_line_clears(&mut self) {
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) {
// TODO: ensure game is running at 60fps (`if !check_update_time(context, 60) { return; }`)
self.ticks += 1;
// TODO: ensure game is running at 60 ticks per second? (`if !check_update_time(context, 60) { return; }`)
self.hard_drop(controls);
self.soft_drop(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 {
@ -168,6 +244,8 @@ impl Game {
self.board[y][x] = Some(current.tetromino.clone());
}
}
self.has_swapped_held = false;
}
fn try_swap_tetromino(&mut self) {

View File

@ -1219,4 +1219,3 @@ unsafe fn create_popup_message(info: &panic::PanicInfo, path: PathBuf) {
fn create_popup_message(info: &panic::PanicInfo) {
// Do nothing
}