Skip to content

Commit

Permalink
add hold, refactor code
Browse files Browse the repository at this point in the history
  • Loading branch information
Strophox committed Aug 24, 2024
1 parent b131208 commit bcd9342
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 78 deletions.
53 changes: 30 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,23 @@

# Features of the Application

### Gamemodes
- 'Sprint': Clear 40-Lines (as quickly as possible).
- 'Marathon': Reach level 20 (with the highest score possible).
- Master: Clear 300 lines (at 20G = instant falling speed).
- Cheese: Eat yourself through 32 lines with random holes (with as few pieces as possible).
- Puzzle Mode: Advance through all 24 puzzle stages using perfect clears (and up to 5 attempts), enabled by piece acrobatics of the 'ocular' rotation system.
- Custom Mode: Change start level, toggle level increment, set game limit *(Time, Score, Pieces, Lines, Level, or No limit)*.

### Gameplay
- Familiar game experience of moving, rotating, hard-/softdropping *tetrominos* with the aim to clear lines.
- Colorful pieces (following guideline).
- Familiar stacker experience of moving, rotating, soft-/hard-dropping and holding *tetrominos* and clearing completed rows.
- Colorful pieces.
- Next piece preview.
- Ghost piece.
- Animations: Hard drop, Line clears and Piece locking.
- Game stats: Level, Score, Lines, Time, Pieces generated.

For more technical details see [Features of the Tetrs Engine](#features-of-the-tetrs-engine).

### Gamemodes
- **40-Lines**: Clear 40-Lines as quickly as possible.
- **Marathon**: Reach the highest speed level (with the highest score possible).
- **Master**: Clear 300 lines starting *at* the highest speed level.
- **Cheese**: Eat yourself through 32 lines with random holes (with as few pieces as possible).
- **Puzzle**: Advance through all 24 puzzle stages using perfect clears (and up to 5 attempts), enabled by piece acrobatics of the 'ocular' rotation system.
- **Custom**: Change start level, toggle level increment, set game limit *(Time, Score, Pieces, Lines, Level, or No limit)*.

### Settings
- Look of the game:
Expand Down Expand Up @@ -145,16 +145,16 @@ For more technical details see [Features of the Tetrs Engine](#features-of-the-t
- **Keep Savefile**: By default this program won't store anything and just let you play the game. If you **do** want `tetrs_terminal` to restore your settings and past games in the future then make sure this is set to **"On"**!

### Scoreboard
- History of games played in the current session (/in the past, if "Keep savefile" is turned on).
- *(\*Games where 0 lines have been cleared are auto-deleted upon exit.)*
- History of games played in the current session (or in the past, if "keep save file" is toggled on).
- *(\*Games where 0 lines have been cleared are auto-deleted on exit.)*

> [!NOTE]
> If "Keep savefile for tetrs" is turned on then your settings and games will be stored in `.tetrs_terminal.json` under a directory that tries to follow OS conventions [[1](https://softwareengineering.stackexchange.com/questions/3956/best-way-to-save-application-settings), [2](https://softwareengineering.stackexchange.com/questions/299869/where-is-the-appropriate-place-to-put-application-configuration-files-for-each-p)]:
> If "keep save file for tetrs" is toggled ON then your settings and games will be stored in `.tetrs_terminal.json` under a directory that tries to follow OS conventions [[1](https://softwareengineering.stackexchange.com/questions/3956/best-way-to-save-application-settings), [2](https://softwareengineering.stackexchange.com/questions/299869/where-is-the-appropriate-place-to-put-application-configuration-files-for-each-p)]:
> | | Windows | Linux | macOS | other |
> | -: | - | - | - | - |
> | location | `%APPDATA%` | `~/.config/` | `~/Library/Application Support/` | (home directory) |
>
> (If this fails it tries to store it locally (`./`)!)
> (If this fails it tries to store it locally, `./`.)

# Features of the Tetrs Engine
Expand Down Expand Up @@ -248,19 +248,26 @@ The game provides some useful feedback events upon every `update`, usually used

</details>

*\*Many small details of the `tetrs_engine` may have been left out of this readme (such as the initial rotation mechanic that immediately rotates a piece if a rotation button is pressed upon spawning). It is intended that this might be elaborated on at a later point in time.*

*\*Another minor defect is current crate documentation (`cargo doc --open`) being unfortunately sparse at this time. Apologies from the author; I hope this can be fixed sometime!*


# State of the Code
# State of the Project

As much love and care went into building this project, it is of course not without its flaws;

- The engine itself might contain niche bugs as of this time. A personal dream is to sometime go through `Game::update`, establish proper invariants, and prove that it is safe / panic-free!
- With regards to the terminal game experience, the frontend, I argue is polished enough (much dedication went into making it nice!). Regardless of how it looks, it is very much lacking in aspects of code style. The code for the menus is ad-hoc, with rampant code duplication, no comments, and panic-freedom yet unproven.
- The README is not comprehensive:
- Many small details of the `tetrs_engine` are not properly explained (e.g. the initial rotation mechanic, which allows spawning a piece immediately rotated if a rotation button was held).
- The engine itself might contain niche bugs as of this time. Concrete improvements include:
- Better API documentation (`cargo doc --open`).
- Simplification of code.
- Proper commenting of implementation.
- Refactor of complicated systems (e.g. locking).
- Ideally: ensuring that `Game::update` is safe / actually panic-free.
- With regards to the terminal game experience, I'd like to argue the frontend is polished enough (much dedication went into making it nice for a 'proof-of-concept'). Regardless of whether this is actually the case, it is very lacking in aspects of code style, defects include:
- The code for the menus is ad-hoc,
- code duplication runs rampant,
- no (or even worse, *wrong*) comments, and
- possible panics may still be hiding around the corner.

A goal would be to amend these problems sometime, step-by-step.
A goal of mine would be to (at least partially) amend these problems, step-by-step.


# Project Highlights
Expand Down Expand Up @@ -347,7 +354,7 @@ With SRS one starts to spot some rotational symmetries (you can always rotate ba
</details>

In the end I'm happy with how the custom rotation system turned out.
It vaguely follows the mantra "if the rotation looks like it could reasonably work visually, it should" (+ some added kicks for flexibility and fun).
It vaguely follows the mantra "if the rotation looks like it could reasonably work visually, it should" (+ some added kicks for flexibility and fun :-), hence it's name, *Ocular* rotation system[.](https://ocularnebula.newgrounds.com/)

<details>

Expand Down
56 changes: 45 additions & 11 deletions tetrs_engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub mod piece_rotation;
use std::{
collections::{HashMap, VecDeque},
fmt,
num::NonZeroU32,
num::{NonZeroU32, NonZeroU8},
ops,
time::Duration,
};
Expand All @@ -55,9 +55,9 @@ pub use piece_rotation::RotationSystem;
use rand::rngs::ThreadRng;

/// A mapping for which buttons are pressed, usable through `impl Index<Button> for [T; 8]`.
pub type ButtonsPressed = [bool; 8];
pub type ButtonsPressed = [bool; 9];
/// Abstract identifier for which type of tile occupies a cell in the grid.
pub type TileTypeID = NonZeroU32;
pub type TileTypeID = NonZeroU8;
/// The type of horizontal lines of the playing grid.
pub type Line = [Option<TileTypeID>; Game::WIDTH];
// NOTE: Would've liked to use `impl Game { type Board = ...` (https://github.com/rust-lang/rust/issues/8995)
Expand Down Expand Up @@ -104,6 +104,8 @@ pub enum Button {
/// **without** locking it immediately or performing any other special handling
/// with respect to locking.
DropSonic,
/// Holding and swapping in a held piece.
Hold,
}

/// Represents the orientation an active piece can be in.
Expand Down Expand Up @@ -254,6 +256,8 @@ pub enum InternalEvent {
/// Event of the current [`ActivePiece`] being fixed on the board, allowing no further updates
/// to its state.
Lock,
/// Event of trying to hold / swap out the current piece.
HoldPiece,
/// Event of the active piece being dropped down and a fast [`InternalEvent::LockTimer`] being initiated.
HardDrop,
/// Event of the active piece being dropped down (without any further action or locking).
Expand Down Expand Up @@ -308,6 +312,8 @@ pub struct GameState {
pub board: Board,
/// All relevant data of the current piece in play.
pub active_piece_data: Option<(ActivePiece, LockingData)>,
/// Data about the piece being held. `true` denotes that the held piece can be swapped back in.
pub holding_piece: Option<(Tetromino, bool)>,
/// Upcoming pieces to be played.
pub next_pieces: VecDeque<Tetromino>,
/// Tallies of how many pieces of each type have been played so far.
Expand Down Expand Up @@ -472,7 +478,7 @@ impl Tetromino {
J => 7,
};
// SAFETY: Ye, `u8 > 0`;
unsafe { NonZeroU32::new_unchecked(u8) }
unsafe { NonZeroU8::new_unchecked(u8) }
}
}

Expand Down Expand Up @@ -689,7 +695,7 @@ impl GameMode {
}
}

impl<T> ops::Index<Button> for [T; 8] {
impl<T> ops::Index<Button> for [T; 9] {
type Output = T;

fn index(&self, idx: Button) -> &Self::Output {
Expand All @@ -702,11 +708,12 @@ impl<T> ops::Index<Button> for [T; 8] {
Button::DropSoft => &self[5],
Button::DropHard => &self[6],
Button::DropSonic => &self[7],
Button::Hold => &self[8],
}
}
}

impl<T> ops::IndexMut<Button> for [T; 8] {
impl<T> ops::IndexMut<Button> for [T; 9] {
fn index_mut(&mut self, idx: Button) -> &mut Self::Output {
match idx {
Button::MoveLeft => &mut self[0],
Expand All @@ -717,6 +724,7 @@ impl<T> ops::IndexMut<Button> for [T; 8] {
Button::DropSoft => &mut self[5],
Button::DropHard => &mut self[6],
Button::DropSonic => &mut self[7],
Button::Hold => &mut self[8],
}
}
}
Expand Down Expand Up @@ -776,6 +784,7 @@ impl Game {
.take(Self::HEIGHT)
.collect(),
active_piece_data: None,
holding_piece: None,
next_pieces: VecDeque::new(),
pieces_played: [0; 7],
lines_cleared: 0,
Expand Down Expand Up @@ -997,9 +1006,9 @@ impl Game {
/// player in form of a change of button states.
fn add_input_events(&mut self, next_buttons_pressed: ButtonsPressed, update_time: GameTime) {
#[allow(non_snake_case)]
let [mL0, mR0, rL0, rR0, rA0, dS0, dH0, dC0] = self.state.buttons_pressed;
let [mL0, mR0, rL0, rR0, rA0, dS0, dH0, dC0, h0] = self.state.buttons_pressed;
#[allow(non_snake_case)]
let [mL1, mR1, rL1, rR1, rA1, dS1, dH1, dC1] = next_buttons_pressed;
let [mL1, mR1, rL1, rR1, rA1, dS1, dH1, dC1, h1] = next_buttons_pressed;
/*
Table: Karnaugh map:
| mL0 mR0 mL1 mR1 | !mL1 !mL1 mL1 mL1
Expand Down Expand Up @@ -1084,17 +1093,23 @@ impl Game {
update_time + Self::drop_delay(self.state.level, None),
);
}
// Hard drop button pressed.
if !dH0 && dH1 {
self.state
.events
.insert(InternalEvent::HardDrop, update_time);
}
// Sonic drop button pressed
if !dC0 && dC1 {
self.state
.events
.insert(InternalEvent::SonicDrop, update_time);
}
// Hard drop button pressed.
if !dH0 && dH1 {
// Hold button pressed
if !h0 && h1 {
self.state
.events
.insert(InternalEvent::HardDrop, update_time);
.insert(InternalEvent::HoldPiece, update_time);
}
}

Expand Down Expand Up @@ -1181,6 +1196,21 @@ impl Game {
self.state.events.insert(InternalEvent::Fall, event_time);
Some(next_piece)
}
InternalEvent::HoldPiece => {
let prev_piece = prev_piece.expect("hold piece event but no active piece");
match self.state.holding_piece {
None | Some((_, true)) => {
if let Some((held_piece, _)) = self.state.holding_piece {
self.state.next_pieces.push_front(held_piece);
}
self.state.holding_piece = Some((prev_piece.shape, false));
self.state.events.clear();
self.state.events.insert(InternalEvent::Spawn, event_time);
None
}
_ => Some(prev_piece),
}
}
InternalEvent::Rotate(turns) => {
let prev_piece = prev_piece.expect("rotate event but no active piece");
self.config
Expand Down Expand Up @@ -1357,6 +1387,10 @@ impl Game {
event_time + self.config.appearance_delay,
);
}
self.state.holding_piece = self
.state
.holding_piece
.map(|(held_piece, _swap_allowed)| (held_piece, true));
None
}
InternalEvent::LineClear => {
Expand Down
6 changes: 3 additions & 3 deletions tetrs_terminal/src/game_mods/cheese_mode.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::num::NonZeroU32;
use std::num::{NonZeroU32, NonZeroU8};

use rand::{self, prelude::SliceRandom};

Expand All @@ -8,7 +8,7 @@ use tetrs_engine::{
};

pub fn random_hole_lines() -> impl Iterator<Item = Line> {
let grey_tile = Some(NonZeroU32::try_from(254).unwrap());
let grey_tile = Some(NonZeroU8::try_from(254).unwrap());
let mut rng = rand::thread_rng();
std::iter::from_fn(move || {
let mut line = [grey_tile; 10];
Expand All @@ -20,7 +20,7 @@ pub fn random_hole_lines() -> impl Iterator<Item = Line> {

fn is_cheese_line(line: &Line) -> bool {
line.iter()
.any(|cell| *cell == Some(NonZeroU32::try_from(254).unwrap()))
.any(|cell| *cell == Some(NonZeroU8::try_from(254).unwrap()))
}

// TODO: Why do I have to specify 'static here??
Expand Down
7 changes: 5 additions & 2 deletions tetrs_terminal/src/game_mods/puzzle_mode.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::{collections::VecDeque, num::NonZeroU32};
use std::{
collections::VecDeque,
num::{NonZeroU32, NonZeroU8},
};

use tetrs_engine::{
Feedback, FeedbackEvents, FnGameMod, Game, GameConfig, GameMode, GameOver, GameState,
Expand Down Expand Up @@ -46,7 +49,7 @@ pub fn new_game() -> Game {
if b == b' ' {
None
} else {
Some(unsafe { NonZeroU32::new_unchecked(254) })
Some(unsafe { NonZeroU8::new_unchecked(254) })
}
})
})
Expand Down
Loading

0 comments on commit bcd9342

Please sign in to comment.