game object, nes point system
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
release.sh
|
||||
|
||||
@@ -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"
|
||||
|
||||
273
src/main.rs
273
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<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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user