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