game object, nes point system

This commit is contained in:
2024-05-28 22:56:57 +02:00
parent 898ee72a88
commit ce71676b0f
4 changed files with 191 additions and 104 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target
Cargo.lock
release.sh

View File

@@ -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"

View File

@@ -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<Action> {
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;

View File

@@ -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,