diff --git a/src/main.rs b/src/main.rs index 45d02de..07c964a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use crossterm::{ }; use std::time::{Duration, Instant}; use std::{io, io::Write}; -use tetromino::{Rotation, WIDTH}; +use tetromino::{BlockGrid, Rotation}; const RED_BLOCK: &str = "\x1b[48;2;255;0;0m \x1b[0m"; const GREEN_BLOCK: &str = "\x1b[48;2;0;255;0m \x1b[0m"; @@ -17,58 +17,43 @@ const PURPLE_BLOCK: &str = "\x1b[48;2;128;0;128m \x1b[0m"; const CYAN_BLOCK: &str = "\x1b[48;2;0;255;255m \x1b[0m"; mod tetromino; -use tetromino::{Block, Tetromino, TetrominoKind}; +use tetromino::{Block, Tetromino, TetrominoKind, HEIGHT, WIDTH}; fn main() { let mut rng = rand::thread_rng(); terminal::enable_raw_mode().unwrap(); - let frame_time = Duration::from_millis(200); + let mut level = 0; print!("\x1b[?25l"); let mut tetromino = Tetromino::new(TetrominoKind::new(&mut rng)); - let mut blocks = vec![]; + let mut block_grid = BlockGrid::new(); let mut prev_frame_dur = Instant::now().elapsed(); + let mut score = Score::new(); loop { let frame_dur = Instant::now(); - if !tetromino.move_tetromino((0, -1), &blocks) { - spawn_blocks(&tetromino, &mut blocks); - tetromino = Tetromino::new(TetrominoKind::new(&mut rng)); - } - render(&tetromino, &blocks, &prev_frame_dur); + let frame_time = Duration::from_secs_f64( + (0.8f64 - ((level as f64 - 1.) * 0.007)).powf(level as f64 - 1.), + ); + render(&tetromino, &block_grid, &score, &prev_frame_dur, level); while frame_time.checked_sub(frame_dur.elapsed()).is_some() { if let Some(input) = input(&frame_dur, frame_time) { - handle_action(&input, &mut tetromino, &blocks); - render(&tetromino, &blocks, &prev_frame_dur); + handle_action(&input, &mut tetromino, &block_grid); + render(&tetromino, &block_grid, &score, &prev_frame_dur, level); } } - render(&tetromino, &blocks, &prev_frame_dur); + if !tetromino.move_tetromino((0, -1), &block_grid) { + if tetromino.pos.1 == 17 { + exit(); + } + spawn_blocks(&tetromino, &mut block_grid); + tetromino = Tetromino::new(TetrominoKind::new(&mut rng)); + } + clear_lines(&mut block_grid, &mut score); + render(&tetromino, &block_grid, &score, &prev_frame_dur, level); prev_frame_dur = frame_dur.elapsed(); + level = score.lines / 10; } } - -// fn clear_lines(blocks: &mut Vec) { -// let mut levels = [0; 10]; -// for block in blocks.iter() { -// } -// let mut to_remove = vec![]; -// for (y, level) in levels.iter().enumerate() { -// if *level == WIDTH { -// let mut to_remove = None; -// for (i, block) in blocks.iter().enumerate() { -// } -// } -// } -// } - -fn spawn_blocks(tetromino: &Tetromino, blocks: &mut Vec) { - tetromino.b.iter().for_each(|m| { - blocks.push(Block { - pos: (tetromino.pos.0 + m.0, tetromino.pos.1 + m.1), - kind: tetromino.kind.clone(), - }) - }); -} - enum Action { MoveRight, MoveLeft, @@ -77,17 +62,75 @@ enum Action { HardDrop, } -fn handle_action(action: &Action, tetromino: &mut Tetromino, blocks: &Vec) { +struct Score { + points: u32, + lines: u32, +} + +impl Score { + fn new() -> Self { + Self { + points: 0, + lines: 0, + } + } +} + +fn clear_lines(block_grid: &mut BlockGrid, score: &mut Score) { + let mut to_clear = vec![]; + for (i, line) in block_grid.0.iter_mut().enumerate().rev() { + let count = line.iter().flatten().count(); + if count == 10 { + to_clear.push(i); + } + } + + let cleared_lines = to_clear.len(); + if cleared_lines == 0 { + return; + } + + match cleared_lines { + 1 => score.points += 100, + 2 => score.points += 300, + 3 => score.points += 500, + 4 => score.points += 800, + _ => unreachable!(), + } + score.lines += cleared_lines as u32; + + for line in to_clear { + for i in line..HEIGHT as usize - 1 { + block_grid.0[i] = block_grid.0[i + 1]; + } + } +} + +fn spawn_blocks(tetromino: &Tetromino, block_grid: &mut BlockGrid) { + for y in 0..HEIGHT as usize { + for x in 0..WIDTH as usize { + for piece in tetromino.b { + let pos = (tetromino.pos.0 + piece.0, tetromino.pos.1 + piece.1); + if pos == (x as i8, y as i8) { + assert!(block_grid.0[y][x].is_none()); + block_grid.0[y][x].replace(Block(tetromino.kind)); + } + } + } + } +} + +fn handle_action(action: &Action, tetromino: &mut Tetromino, block_grid: &BlockGrid) { match action { Action::MoveRight => { - tetromino.move_tetromino((1, 0), blocks); + tetromino.move_tetromino((1, 0), block_grid); } Action::MoveLeft => { - tetromino.move_tetromino((-1, 0), blocks); + tetromino.move_tetromino((-1, 0), block_grid); } - Action::Rotate(r) => tetromino.rotate(r, blocks), + Action::Rotate(r) => tetromino.rotate(r, block_grid), Action::SoftDrop => (), - Action::HardDrop => (), + Action::HardDrop => while tetromino.move_tetromino((0, -1), block_grid) {}, } } @@ -114,7 +157,13 @@ fn input(now: &Instant, frame_time: Duration) -> Option { None } -fn render(tetromino: &Tetromino, blocks: &Vec, prev_frame_dur: &Duration) { +fn render( + tetromino: &Tetromino, + block_grid: &BlockGrid, + score: &Score, + prev_frame_dur: &Duration, + level: u32, +) { let mut s = String::with_capacity(2 * 22 * 22); s.push('┏'); s.push_str("━━━━━━━━━━━━━━━━━━━━"); @@ -123,15 +172,12 @@ fn render(tetromino: &Tetromino, blocks: &Vec, prev_frame_dur: &Duration) s.push('┃'); for x in 0..10 { let mut found = false; - for block in blocks { - if block.pos == (x, y) { - s.push_str(block.kind.to_block()); - found = true; - break; - } + if let Some(block) = block_grid.0[y][x] { + s.push_str(block.0.to_block()); + found = true; } for block in tetromino.b { - if (block.0 + tetromino.pos.0, block.1 + tetromino.pos.1) == (x, y) { + if (block.0 + tetromino.pos.0, block.1 + tetromino.pos.1) == (x as i8, y as i8) { s.push_str(tetromino.kind.to_block()); found = true; break; @@ -147,7 +193,15 @@ fn render(tetromino: &Tetromino, blocks: &Vec, prev_frame_dur: &Duration) s.push_str("━━━━━━━━━━━━━━━━━━━━"); s.push('┛'); s.push('\n'); + s.push_str("frame_time: "); s.push_str(&prev_frame_dur.as_millis().to_string()); + s.push_str(" ms, points: "); + s.push_str(&score.points.to_string()); + s.push_str(", lines: "); + s.push_str(&score.lines.to_string()); + s.push_str(", level: "); + s.push_str(&level.to_string()); + s.push('\n'); terminal::disable_raw_mode().unwrap(); print!("\x1b[2J\x1b[H{s}"); terminal::enable_raw_mode().unwrap(); @@ -163,14 +217,55 @@ fn exit() -> ! { #[cfg(test)] mod tests { use super::*; + use rand::prelude::*; #[test] - fn color() { - println!("red: {RED_BLOCK}"); - println!("green: {GREEN_BLOCK}"); - println!("blue: {BLUE_BLOCK}"); - println!("yellow: {YELLOW_BLOCK}"); - println!("orange: {ORANGE_BLOCK}"); - println!("purple: {PURPLE_BLOCK}"); - println!("cyan: {CYAN_BLOCK}"); + fn clearing() { + let mut rng = thread_rng(); + let mut block_grid = BlockGrid([ + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH as usize], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH as usize], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH as usize], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH as usize], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH as usize], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH as usize], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH as usize], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH as usize], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH as usize], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH as usize], + [None; WIDTH as usize], + [None; WIDTH as usize], + [None; WIDTH as usize], + [None; WIDTH as usize], + [None; WIDTH as usize], + [None; WIDTH as usize], + [None; WIDTH as usize], + [None; WIDTH as usize], + [None; WIDTH as usize], + [None; WIDTH as usize], + ]); + block_grid.0[1][3] = None; + block_grid.0[4][4] = None; + block_grid.0[5][1] = None; + block_grid.0[6][3] = None; + block_grid.0[7][2] = None; + block_grid.0[8][8] = None; + block_grid.0[9][7] = None; + block_grid.0[10][0] = None; + block_grid.0[11][9] = None; + + let mut copy = BlockGrid(block_grid.0.clone()); + copy.0[0] = copy.0[1]; + copy.0[1] = copy.0[4]; + copy.0[2] = copy.0[5]; + copy.0[3] = copy.0[6]; + copy.0[4] = copy.0[7]; + copy.0[5] = copy.0[8]; + copy.0[6] = copy.0[9]; + copy.0[7] = copy.0[10]; + copy.0[8] = copy.0[11]; + copy.0[9] = copy.0[12]; + let mut score = Score::new(); + clear_lines(&mut block_grid, &mut score); + assert_eq!(block_grid, copy); } } diff --git a/src/tetromino.rs b/src/tetromino.rs index f2767d1..1842827 100644 --- a/src/tetromino.rs +++ b/src/tetromino.rs @@ -4,7 +4,7 @@ use crate::{ use rand::prelude::*; pub const WIDTH: i8 = 10; -const HEIGHT: i8 = 20; +pub const HEIGHT: i8 = 20; const I: [[(i8, i8); 4]; 4] = [ [(0, 2), (1, 2), (2, 2), (3, 2)], @@ -64,7 +64,7 @@ pub enum Rotation { Left, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum TetrominoKind { I, O, @@ -75,16 +75,14 @@ pub enum TetrominoKind { Z, } -#[derive(Copy, Clone)] -pub struct Block { - pub pos: (i8, i8), - pub kind: TetrominoKind, -} +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Block(pub TetrominoKind); -struct BlockGrid([[Option; 10]; 20]); +#[derive(Debug, PartialEq)] +pub struct BlockGrid(pub [[Option; 10]; 20]); impl BlockGrid { - fn new() -> Self { + pub fn new() -> Self { Self([[None; 10]; 20]) } } @@ -95,14 +93,14 @@ impl Tetromino { b: kind.get_blocks(&Rotation::Neutral), kind, rotation: Rotation::Neutral, - pos: (5, 20), + pos: (3, 17), } } - pub fn move_tetromino(&mut self, (x, y): (i8, i8), blocks: &Vec) -> bool { + pub fn move_tetromino(&mut self, (x, y): (i8, i8), block_grid: &BlockGrid) -> bool { self.pos.0 += x; self.pos.1 += y; - if !self.test_pos(blocks) { + if !self.test_pos(block_grid) { self.pos.0 -= x; self.pos.1 -= y; false @@ -111,7 +109,7 @@ impl Tetromino { } } - pub fn rotate(&mut self, rotation: &Rotation, blocks: &Vec) { + pub fn rotate(&mut self, rotation: &Rotation, block_grid: &BlockGrid) { let (r, b) = (rotation.clone(), self.b); match self.rotation { Rotation::Neutral => match rotation { @@ -143,42 +141,46 @@ impl Tetromino { }, } self.b = self.kind.get_blocks(&self.rotation); - if !self.test_pos(blocks) { + if !self.test_pos(block_grid) { self.b = b; self.rotation = r; } } - fn test_pos(&self, blocks: &Vec) -> bool { + fn test_pos(&self, block_grid: &BlockGrid) -> bool { for piece in self.b { let (x, y) = (piece.0 + self.pos.0, piece.1 + self.pos.1); if x >= WIDTH || x < 0 || y < 0 { return false; } - for block in blocks { - if block.pos == (x, y) { - return false; - } + } + for piece in self.b { + let (x, y) = (piece.0 + self.pos.0, piece.1 + self.pos.1); + if y > HEIGHT - 2 { + return true; + } + if block_grid.0[y as usize][x as usize].is_some() { + return false; } } + true } } impl TetrominoKind { pub fn new(rng: &mut ThreadRng) -> Self { - // match rng.gen_range(0..=7) { - // 0 => Self::I, - // 1 => Self::O, - // 2 => Self::T, - // 3 => Self::J, - // 4 => Self::L, - // 5 => Self::S, - // 6 => Self::Z, - // 7 => Self::O, - // _ => unreachable!(), - // } - Self::Z + match rng.gen_range(0..=7) { + 0 => Self::I, + 1 => Self::O, + 2 => Self::T, + 3 => Self::J, + 4 => Self::L, + 5 => Self::S, + 6 => Self::Z, + 7 => Self::O, + _ => unreachable!(), + } } fn get_blocks(&self, rotation: &Rotation) -> [(i8, i8); 4] {