line clears
This commit is contained in:
parent
8e588a8a70
commit
8199928d7d
@ -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>;
|
||||
|
||||
|
139
src/board.rs
139
src/board.rs
@ -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);
|
||||
}
|
||||
}
|
||||
|
108
src/main.rs
108
src/main.rs
@ -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) {
|
||||
|
@ -1219,4 +1219,3 @@ unsafe fn create_popup_message(info: &panic::PanicInfo, path: PathBuf) {
|
||||
fn create_popup_message(info: &panic::PanicInfo) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user