From ce71676b0fecb6f58fcf71104c8b225971160c4f Mon Sep 17 00:00:00 2001 From: Vegard Matthey Date: Tue, 28 May 2024 22:56:57 +0200 Subject: [PATCH] game object, nes point system --- .gitignore | 1 + Cargo.toml | 7 ++ src/main.rs | 273 ++++++++++++++++++++++++++++++----------------- src/tetromino.rs | 14 ++- 4 files changed, 191 insertions(+), 104 deletions(-) diff --git a/.gitignore b/.gitignore index 96ef6c0..0f3ff49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +release.sh diff --git a/Cargo.toml b/Cargo.toml index bbfbba8..ebda2ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,13 @@ name = "term_tetris" version = "0.1.0" edition = "2021" +[profile.release] +strip = true +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" + [dependencies] crossterm = "0.27.0" rand = "0.8.5" diff --git a/src/main.rs b/src/main.rs index 07c964a..d770c27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,19 @@ -// https://tetris.wiki/Super_Rotation_System +// Rotation: https://tetris.wiki/Super_Rotation_System +// Gravity: https://tetris.wiki/Tetris_(NES,_Nintendo) +// https://en.wikipedia.org/wiki/Tetris_(NES_video_game)#Scoring use crossterm::{ event, event::{Event, KeyCode}, terminal, }; +use rand::prelude::*; use std::time::{Duration, Instant}; use std::{io, io::Write}; use tetromino::{BlockGrid, Rotation}; +const WIDTH: usize = 10; +const HEIGHT: usize = 20; + const RED_BLOCK: &str = "\x1b[48;2;255;0;0m \x1b[0m"; const GREEN_BLOCK: &str = "\x1b[48;2;0;255;0m \x1b[0m"; const BLUE_BLOCK: &str = "\x1b[48;2;0;0;255m \x1b[0m"; @@ -17,40 +23,94 @@ 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, HEIGHT, WIDTH}; +use tetromino::{Block, Tetromino, TetrominoKind}; fn main() { - let mut rng = rand::thread_rng(); - terminal::enable_raw_mode().unwrap(); - let mut level = 0; - print!("\x1b[?25l"); - let mut tetromino = Tetromino::new(TetrominoKind::new(&mut rng)); - let mut block_grid = BlockGrid::new(); + let mut game = Game::new(); let mut prev_frame_dur = Instant::now().elapsed(); - let mut score = Score::new(); + let mut soft = false; + let mut speed_time = frame_time_for_level(0); + + terminal::enable_raw_mode().unwrap(); + print!("\x1b[?25l"); + loop { + let frame_time = frame_time_for_level(game.level()); let frame_dur = Instant::now(); - 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() { + render(&game, &prev_frame_dur); + while speed_time.checked_sub(frame_dur.elapsed()).is_some() { if let Some(input) = input(&frame_dur, frame_time) { - handle_action(&input, &mut tetromino, &block_grid); - render(&tetromino, &block_grid, &score, &prev_frame_dur, level); + handle_action(&input, &mut game.tetromino, &game.block_grid, &mut soft); + render(&game, &prev_frame_dur); } } - if !tetromino.move_tetromino((0, -1), &block_grid) { - if tetromino.pos.1 == 17 { + if soft { + game.score.points += 1; + if game.level() < 19 { + speed_time = Duration::from_secs_f64(2. / 60.); + } + soft = false; + } else { + speed_time = frame_time; + } + + if !game.tetromino.move_tetromino((0, -1), &game.block_grid) { + if game.tetromino.pos.1 == 17 { exit(); } - spawn_blocks(&tetromino, &mut block_grid); - tetromino = Tetromino::new(TetrominoKind::new(&mut rng)); + game.spawn_new_tetromino(); } - clear_lines(&mut block_grid, &mut score); - render(&tetromino, &block_grid, &score, &prev_frame_dur, level); + clear_lines(&mut game.block_grid, &mut game.score); + render(&game, &prev_frame_dur); prev_frame_dur = frame_dur.elapsed(); - level = score.lines / 10; + } +} + +struct Game { + score: Score, + tetromino: Tetromino, + next_tetromino: TetrominoKind, + block_grid: BlockGrid, + rng: ThreadRng, +} + +impl Game { + fn new() -> Self { + let mut rng = thread_rng(); + Self { + score: Score::new(), + tetromino: Tetromino::new(TetrominoKind::new(&mut rng)), + next_tetromino: TetrominoKind::new(&mut rng), + block_grid: BlockGrid::new(), + rng, + } + } + + fn spawn_new_tetromino(&mut self) { + self.spawn_blocks(); + self.tetromino = Tetromino::new(self.next_tetromino); + self.next_tetromino = TetrominoKind::new(&mut self.rng); + } + + fn spawn_blocks(&mut self) { + for y in 0..HEIGHT { + for x in 0..WIDTH { + for piece in self.tetromino.b { + let pos = ( + self.tetromino.pos.0 + piece.0, + self.tetromino.pos.1 + piece.1, + ); + if pos == (x as i8, y as i8) { + assert!(self.block_grid.0[y][x].is_none()); + self.block_grid.0[y][x].replace(Block(self.tetromino.kind)); + } + } + } + } + } + + fn level(&self) -> u32 { + self.score.lines / 10 } } @@ -77,50 +137,43 @@ impl Score { } fn clear_lines(block_grid: &mut BlockGrid, score: &mut Score) { + let level = score.lines / 10; 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 { + if count == WIDTH { to_clear.push(i); } } - let cleared_lines = to_clear.len(); - if cleared_lines == 0 { + if to_clear.is_empty() { return; } + let cleared_lines = to_clear.len(); + match cleared_lines { - 1 => score.points += 100, - 2 => score.points += 300, - 3 => score.points += 500, - 4 => score.points += 800, + 1 => score.points += 40 * (level + 1), + 2 => score.points += 100 * (level + 1), + 3 => score.points += 300 * (level + 1), + 4 => score.points += 1200 * (level + 1), _ => unreachable!(), } score.lines += cleared_lines as u32; for line in to_clear { - for i in line..HEIGHT as usize - 1 { + for i in line..HEIGHT - 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) { +fn handle_action( + action: &Action, + tetromino: &mut Tetromino, + block_grid: &BlockGrid, + soft: &mut bool, +) { match action { Action::MoveRight => { tetromino.move_tetromino((1, 0), block_grid); @@ -129,7 +182,7 @@ fn handle_action(action: &Action, tetromino: &mut Tetromino, block_grid: &BlockG tetromino.move_tetromino((-1, 0), block_grid); } Action::Rotate(r) => tetromino.rotate(r, block_grid), - Action::SoftDrop => (), + Action::SoftDrop => *soft = true, Action::HardDrop => while tetromino.move_tetromino((0, -1), block_grid) {}, } } @@ -157,28 +210,46 @@ fn input(now: &Instant, frame_time: Duration) -> Option { None } -fn render( - tetromino: &Tetromino, - block_grid: &BlockGrid, - score: &Score, - prev_frame_dur: &Duration, - level: u32, -) { +fn frame_time_for_level(level: u32) -> Duration { + match level { + 0 => Duration::from_secs_f64(48. / 60.), + 1 => Duration::from_secs_f64(43. / 60.), + 2 => Duration::from_secs_f64(38. / 60.), + 3 => Duration::from_secs_f64(33. / 60.), + 4 => Duration::from_secs_f64(28. / 60.), + 5 => Duration::from_secs_f64(23. / 60.), + 6 => Duration::from_secs_f64(18. / 60.), + 7 => Duration::from_secs_f64(13. / 60.), + 8 => Duration::from_secs_f64(8. / 60.), + 9 => Duration::from_secs_f64(6. / 60.), + 10..=12 => Duration::from_secs_f64(5. / 60.), + 13..=15 => Duration::from_secs_f64(4. / 60.), + 16..=18 => Duration::from_secs_f64(3. / 60.), + 19..=28 => Duration::from_secs_f64(2. / 60.), + _ => Duration::from_secs_f64(1. / 60.), + } +} + +fn render(game: &Game, prev_frame_dur: &Duration) { let mut s = String::with_capacity(2 * 22 * 22); - s.push('┏'); - s.push_str("━━━━━━━━━━━━━━━━━━━━"); - s.push_str("┓\n"); + s.push_str("\x1b[48;2;128;128;128m \x1b[0m\n"); + let next_pos = game.next_tetromino.get_blocks(&Rotation::Neutral); + let next_block = game.next_tetromino.string_block(); for y in (0..20).rev() { - s.push('┃'); + s.push_str("\x1b[48;2;128;128;128m \x1b[0m"); for x in 0..10 { let mut found = false; - if let Some(block) = block_grid.0[y][x] { - s.push_str(block.0.to_block()); + if let Some(block) = game.block_grid.0[y][x] { + s.push_str(block.0.string_block()); found = true; } - for block in tetromino.b { - if (block.0 + tetromino.pos.0, block.1 + tetromino.pos.1) == (x as i8, y as i8) { - s.push_str(tetromino.kind.to_block()); + for block in game.tetromino.b { + if ( + block.0 + game.tetromino.pos.0, + block.1 + game.tetromino.pos.1, + ) == (x as i8, y as i8) + { + s.push_str(game.tetromino.kind.string_block()); found = true; break; } @@ -187,21 +258,31 @@ fn render( s.push_str(" "); } } - s.push_str("┃\n"); + s.push_str("\x1b[48;2;128;128;128m \x1b[0m"); + if y < 13 && y > 7 { + let y = y - 8; + s.push_str(" "); + for x in 0..4 { + if next_pos.contains(&(x, y as i8)) { + s.push_str(next_block); + } else { + s.push_str(" "); + } + } + } + s.push('\n'); } - s.push('┗'); - s.push_str("━━━━━━━━━━━━━━━━━━━━"); - s.push('┛'); - s.push('\n'); - s.push_str("frame_time: "); + s.push_str("\x1b[48;2;128;128;128m \x1b[0m\n"); + + s.push_str("level: "); + s.push_str(&(game.score.lines / 10).to_string()); + s.push_str("\nlines: "); + s.push_str(&game.score.lines.to_string()); + s.push_str("\npoints: "); + s.push_str(&game.score.points.to_string()); + s.push_str("\nframe_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'); + s.push_str(" ms\n"); terminal::disable_raw_mode().unwrap(); print!("\x1b[2J\x1b[H{s}"); terminal::enable_raw_mode().unwrap(); @@ -222,26 +303,26 @@ mod tests { 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], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH], + [Some(Block(TetrominoKind::new(&mut rng))); WIDTH], + [None; WIDTH], + [None; WIDTH], + [None; WIDTH], + [None; WIDTH], + [None; WIDTH], + [None; WIDTH], + [None; WIDTH], + [None; WIDTH], + [None; WIDTH], + [None; WIDTH], ]); block_grid.0[1][3] = None; block_grid.0[4][4] = None; diff --git a/src/tetromino.rs b/src/tetromino.rs index 1842827..7da138c 100644 --- a/src/tetromino.rs +++ b/src/tetromino.rs @@ -1,11 +1,9 @@ use crate::{ - BLUE_BLOCK, CYAN_BLOCK, GREEN_BLOCK, ORANGE_BLOCK, PURPLE_BLOCK, RED_BLOCK, YELLOW_BLOCK, + BLUE_BLOCK, CYAN_BLOCK, GREEN_BLOCK, HEIGHT, ORANGE_BLOCK, PURPLE_BLOCK, RED_BLOCK, WIDTH, + YELLOW_BLOCK, }; use rand::prelude::*; -pub const WIDTH: i8 = 10; -pub const HEIGHT: i8 = 20; - const I: [[(i8, i8); 4]; 4] = [ [(0, 2), (1, 2), (2, 2), (3, 2)], [(2, 0), (2, 1), (2, 2), (2, 3)], @@ -150,13 +148,13 @@ impl Tetromino { 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 { + if x >= WIDTH as i8 || x < 0 || y < 0 { return false; } } for piece in self.b { let (x, y) = (piece.0 + self.pos.0, piece.1 + self.pos.1); - if y > HEIGHT - 2 { + if y > HEIGHT as i8 - 2 { return true; } if block_grid.0[y as usize][x as usize].is_some() { @@ -183,7 +181,7 @@ impl TetrominoKind { } } - fn get_blocks(&self, rotation: &Rotation) -> [(i8, i8); 4] { + pub fn get_blocks(&self, rotation: &Rotation) -> [(i8, i8); 4] { let i = match rotation { Rotation::Neutral => 0, Rotation::Right => 1, @@ -201,7 +199,7 @@ impl TetrominoKind { } } - pub fn to_block(&self) -> &str { + pub fn string_block(&self) -> &str { match self { Self::I => CYAN_BLOCK, Self::O => YELLOW_BLOCK,