Skip to main content

falling_tetromino_engine/
lib.rs

1/*!
2# Falling Tetromino Engine
3
4`falling_tetromino_engine` is an implementation of a tetromino game engine, able to handle numerous modern
5mechanics.
6
7# Example
8
9```
10use falling_tetromino_engine::*;
11
12// Starting up a game - note that in-game time starts at 0s.
13let mut game = Game::builder()
14    .seed(1234)
15    /* ...Further optional configuration possible... */
16    .build();
17
18// Updating the game with the info that 'left' should be pressed at second 4.2;
19// If a piece is in the game, it will try to move left.
20let input = Input::Activate(Button::MoveLeft);
21game.update(InGameTime::from_secs_f64(4.2), Some(input));
22
23// ...
24
25// Updating the game with the info that no input change has occurred up to second 6.79;
26// This updates the game, e.g., pieces fall and lock.
27game.update(InGameTime::from_secs_f64(13.37), None);
28
29// Read most recent game state;
30// This is how a UI can know how to render the board, etc.
31let State { board, .. } = game.state();
32```
33
34[FIXME: Document *all* features in detail (including IRS, etc., cargo feature `serde` etc.).]
35*/
36
37#![warn(missing_docs)]
38
39mod builder;
40pub mod extduration;
41pub mod extnonnegf64;
42pub mod modding;
43pub mod randomization;
44pub mod rotation;
45mod update;
46
47use std::{collections::VecDeque, fmt, num::NonZeroU8, ops, time::Duration};
48
49use rand_chacha::{rand_core::SeedableRng, ChaCha8Rng};
50
51pub use builder::GameBuilder;
52pub use extduration::ExtDuration;
53pub use extnonnegf64::ExtNonNegF64;
54pub use modding::{GameAccess, GameModifier};
55pub use randomization::TetrominoGenerator;
56pub use rotation::RotationSystem;
57
58/// Abstract identifier for which type of tile occupies a cell in the grid.
59pub type TileTypeID = NonZeroU8;
60/// The type of horizontal lines of the playing grid.
61pub type Line = [Option<TileTypeID>; Game::WIDTH];
62// NOTE: Would've liked to use `impl Game { type Board = ...` (https://github.com/rust-lang/rust/issues/8995)
63/// The type of the entire two-dimensional playing grid.
64pub type Board = [Line; Game::HEIGHT];
65/// Coordinates conventionally used to index into the [`Board`], starting in the bottom left.
66pub type Coord = (isize, isize);
67/// Coordinates offsets that can be [`add`]ed to [`Coord`]inates.
68pub type CoordOffset = (isize, isize);
69/// Type describing the state that is stored about buttons.
70///
71/// Specifically, it stores which buttons are considered active, and if yes, since when.
72pub type ButtonsState = [Option<InGameTime>; Button::VARIANTS.len()];
73/// The type used to identify points in time in a game's internal timeline.
74pub type InGameTime = Duration;
75/// The internal RNG used by a game.
76pub type GameRng = ChaCha8Rng;
77/// Type alias for a stream of notifications with timestamps.
78pub type NotificationFeed = Vec<(Notification, InGameTime)>;
79
80/// Represents one of the seven "Tetrominos";
81///
82/// A *tetromino* is a two-dimensional, geometric shape made by
83/// connecting four squares (orthogonally / at along the edges).
84#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash, Debug)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
86pub enum Tetromino {
87    /// 'O'-Tetromino.
88    /// Four squares connected as one big square; `⠶`, `██`.
89    ///
90    /// 'O' has 90° rotational symmetry + 2 axes of mirror symmetry.
91    O = 0,
92    /// 'I'-Tetromino.
93    /// Four squares connected as one straight line; `⡇`, `▄▄▄▄`.
94    ///
95    /// 'I' has 180° rotational symmetry + 2 axes of mirror symmetry.
96    I = 1,
97    /// 'S'-Tetromino.
98    /// Four squares connected in an 'S'-snaking manner; `⠳`, `▄█▀`.
99    ///
100    /// 'S' has 180° rotational symmetry + 0 axes of mirror symmetry.
101    S = 2,
102    /// 'Z'-Tetromino:
103    /// Four squares connected in a 'Z'-snaking manner; `⠞`, `▀█▄`.
104    ///
105    /// 'Z' has 180° rotational symmetry + 0 axes of mirror symmetry.
106    Z = 3,
107    /// 'T'-Tetromino:
108    /// Four squares connected in a 'T'-junction shape; `⠗`, `▄█▄`.
109    ///
110    /// 'T' has 360° rotational symmetry + 1 axis of mirror symmetry.
111    T = 4,
112    /// 'L'-Tetromino:
113    /// Four squares connected in an 'L'-shape; `⠧`, `▄▄█`.
114    ///
115    /// 'L' has 360° rotational symmetry + 0 axes of mirror symmetry.
116    L = 5,
117    /// 'J'-Tetromino:
118    /// Four squares connected in a 'J'-shape; `⠼`, `█▄▄`.
119    ///
120    /// 'J' has 360° rotational symmetry + 0 axes of mirror symmetry.
121    J = 6,
122}
123
124/// Represents the orientation an active piece can be in.
125#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash, Debug)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127pub enum Orientation {
128    /// North.
129    N = 0,
130    /// East.
131    E,
132    /// South.
133    S,
134    /// West.
135    W,
136}
137
138/// An active tetromino in play.
139#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash, Debug)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141pub struct Piece {
142    /// Type of tetromino the active piece is.
143    pub tetromino: Tetromino,
144    /// In which way the tetromino is re-oriented.
145    pub orientation: Orientation,
146    /// The position of the active piece on a playing grid.
147    pub position: Coord,
148}
149
150/// A struct holding information on how certain time 'delay' values progress during a game's lifetime.
151///
152/// # Example
153/// The formulation used for calculation of fall delay is conceptually:
154/// ```ignore
155/// let fall_delay = |lineclears| {
156///     initial_fall_delay.mul_ennf64(
157///         multiplier.get().powf(lineclears) - subtrahend.get() * lineclears
158///     )
159/// }
160/// ```
161#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash, Debug)]
162#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
163pub struct DelayParameters {
164    /// The duration at which the delay starts.
165    base_delay: ExtDuration,
166    /// The base factor that gets exponentiated by number of line clears;
167    /// `factor ^ lineclears ...`.
168    ///
169    /// Should be in the range `0.0 ≤ .. ≤ 1.0`, where
170    /// - `0.0` means 'zero-out initial delay at every line clear',
171    /// - `0.5` means 'halve initial delay for every line clear',
172    /// - `1.0` means 'keep initial delay at 100%'.
173    factor: ExtNonNegF64,
174    /// The base subtrahend that gets multiplied by number of line clears;
175    /// `... - subtrahend * lineclears`.
176    ///
177    /// Should be in the range `0.0 ≤ .. ≤ 1.0`, where
178    /// - `0.0` means 'subtract 0% of initial delay for every line clear',
179    /// - `0.5` means 'subtract 50% of initial delay for every line clear',
180    /// - `1.0` means 'subtract 100% of initial delay for every line clear'.
181    subtrahend: ExtDuration,
182    /// The duration below which delay cannot decrease.
183    lowerbound: ExtDuration,
184}
185
186/// Certain statistics for which an instance of [`Game`] can be checked against.
187#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Debug, Default)]
188#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
189pub struct GameLimits {
190    /// A given amount of total time that can elapse in-game.
191    pub time_elapsed: Option<(InGameTime, bool)>,
192    /// A given number of [`Tetromino`]s that can be locked/placed on the game's [`Board`].
193    pub pieces_locked: Option<(u32, bool)>,
194    /// A given number of lines that can be cleared from the [`Board`].
195    pub lines_cleared: Option<(u32, bool)>,
196    /// A given number of points that can be scored.
197    pub points_scored: Option<(u32, bool)>,
198}
199
200/// Certain statistics for which an instance of [`Game`] can be checked against.
201#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Debug)]
202#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
203pub enum Stat {
204    /// A given amount of total time that elapsed in-game.
205    TimeElapsed(InGameTime),
206    /// A given number of [`Tetromino`]s that have been locked/placed on the game's [`Board`].
207    PiecesLocked(u32),
208    /// A given number of lines that have been cleared from the [`Board`].
209    LinesCleared(u32),
210    /// A given number of points that have been scored.
211    PointsScored(u32),
212}
213
214/// The amount of feedback information that is to be generated.
215#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash, Debug)]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217pub enum NotificationLevel {
218    /// No feedback generated by base engine.
219    /// Note that game modifiers called may choose to generate feedback messages
220    /// themselves, which will not again be discarded once received by
221    /// the base game engine.
222    Silent,
223    /// Base level of feedback about in-game events.
224    Standard,
225    /// Highest level of feedback, which includes emitting every
226    /// internal game event processed
227    Debug,
228}
229
230/// Configuration options of the game, which can be modified without hurting internal invariants.
231///
232/// # Reproducibility
233/// Modifying a [`Game`]'s configuration after it was created might not make it easily
234/// reproducible anymore.
235#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Debug)]
236#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
237pub struct Configuration {
238    /// How many pieces should be pre-generated and accessible/visible in the game state.
239    pub piece_preview_count: usize,
240    /// Whether holding a 'rotate' button lets a piece be smoothly spawned in a rotated state,
241    /// or holding the 'hold' button lets a piece be swapped immediately before it evens spawns.
242    pub allow_initial_actions: bool,
243    /// The method of tetromino rotation used.
244    pub rotation_system: RotationSystem,
245    /// How long the game should take to spawn a new piece.
246    pub spawn_delay: Duration,
247    /// How long it takes for the active piece to start automatically shifting more to the side
248    /// after the initial time a 'move' button has been pressed.
249    pub delayed_auto_shift: Duration,
250    /// How long it takes for automatic side movement to repeat once it has started.
251    pub auto_repeat_rate: Duration,
252    /// Specification of how fall delay gets calculated from the rest of the state.
253    pub fall_delay_params: DelayParameters,
254    /// How many times faster than normal drop speed a piece should fall while 'soft drop' is being held.
255    pub soft_drop_factor: ExtNonNegF64,
256    /// Specification of how fall delay gets calculated from the rest of the state.
257    pub lock_delay_params: DelayParameters,
258    /// Whether engine should try to ensure that delays for autonomous moves - which are determined by
259    /// `delayed_auto_shift` and `auto_repeat_rate` - should be less than `lock_delay` runs out.
260    /// This allows DAS and ARR to function at extreme game speeds.
261    pub ensure_move_delay_lt_lock_delay: bool,
262    /// Whether just pressing a rotation- or movement button is enough to refresh lock delay.
263    /// Normally, lock delay only resets if rotation or movement actually succeeds.
264    pub allow_lenient_lock_reset: bool,
265    /// How long each spawned active piece may touch the ground in total until it should lock down
266    /// immediately.
267    pub lock_reset_cap_factor: ExtNonNegF64,
268    /// How long the game should take to clear a line.
269    pub line_clear_duration: Duration,
270    /// When to update the fall and lock delays in [`State`].
271    pub update_delays_every_n_lineclears: u32,
272    /// Stores the ways in which a round of the game should be limited.
273    ///
274    /// Each limitation may be either of positive ('game completed') or negative ('game over'), as
275    /// designated by the `bool` stored with it.
276    ///
277    /// No limitations may allow for endless games.
278    pub game_limits: GameLimits,
279    /// The amount of feedback information that is to be generated.
280    pub notification_level: NotificationLevel,
281}
282
283/// Some values that were used to help initialize the game.
284///
285/// Used for game reproducibility.
286#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash, Debug)]
287#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
288pub struct StateInitialization {
289    /// The value to seed the game's PRNG with.
290    pub seed: u64,
291    /// The method (and internal state) of tetromino generation used.
292    pub tetromino_generator: TetrominoGenerator,
293}
294
295/// Represents an abstract game input.
296// NOTE: We could consider calling this `Action` judging from its variants, however the Game stores a mapping of whether a given `Button` is active over a period of time. `Intents` could work but `Button` is less abstract and often corresponds directly to IRL player inputs.
297#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash, Debug)]
298#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
299pub enum Button {
300    /// Moves the piece once to the left.
301    MoveLeft = 0,
302    /// Moves the piece once to the right.
303    MoveRight,
304    /// Rotate the piece by +90° (clockwise).
305    RotateLeft,
306    /// Rotate the piece by -90° (counter-clockwise).
307    RotateRight,
308    /// Rotate the piece by 180° (flip around).
309    Rotate180,
310    /// "Soft" dropping.
311    /// This drops a piece down by one, locking it immediately if it hit a surface,
312    /// Otherwise holding this button decreases fall speed by the game [`Configuration`]'s `soft_drop_factor`.
313    DropSoft,
314    /// "Hard" dropping.
315    /// This immediately drops a piece all the way down until it hits a surface,
316    /// locking it there (almost) instantly, too.
317    DropHard,
318    /// Teleport the piece down, also known as "Sonic" dropping.
319    /// This immediately drops a piece all the way down until it hits a surface,
320    /// but without locking it (unlike [`Button::DropHard`]).
321    TeleDown,
322    /// Instantly 'teleports' (moves) a piece left until it hits a surface.
323    TeleLeft,
324    /// Instantly 'teleports' (moves) a piece right until it hits a surface.
325    TeleRight,
326    /// Holding the current piece; and swapping in a new piece if one was held previously.
327    HoldPiece,
328}
329
330/// A change in button state, between being held down or unpressed.
331#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash, Debug)]
332#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
333pub enum Input {
334    /// The signal of a button now being activated.
335    Activate(Button),
336    /// The signal of a button now being deactivated.
337    Deactivate(Button),
338}
339
340/// Struct storing internal game state that changes over the course of play.
341#[derive(Eq, PartialEq, Clone, Debug)]
342#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
343pub struct State {
344    /// Current in-game time.
345    pub time: InGameTime,
346    /// The stores which buttons are considered active and since when.
347    pub active_buttons: ButtonsState,
348    /// The internal pseudo random number generator used.
349    pub rng: GameRng,
350    /// The method (and internal state) of tetromino generation used.
351    pub piece_generator: TetrominoGenerator,
352    /// Upcoming pieces to be played.
353    pub piece_preview: VecDeque<Tetromino>,
354    /// Data about the piece being held. `true` denotes that the held piece can be swapped back in.
355    pub piece_held: Option<(Tetromino, bool)>,
356    /// The main playing grid storing empty (`None`) and filled, fixed tiles (`Some(nz_u32)`).
357    pub board: Board,
358    /// The current duration a piece takes to fall one unit.
359    pub fall_delay: ExtDuration,
360    /// The point (number of lines cleared) at which fall delay was updated to zero (possibly capped if formula yielded negative).
361    pub fall_delay_lowerbound_hit_at_n_lineclears: Option<u32>,
362    /// The current duration a piece takes to try and lock down.
363    pub lock_delay: ExtDuration,
364    /// Tallies of how many pieces of each type have been played so far.
365    pub pieces_locked: [u32; Tetromino::VARIANTS.len()],
366    /// The total number of lines that have been cleared.
367    pub lineclears: u32,
368    /// The number of consecutive pieces that have been played and caused a line clear.
369    pub consecutive_line_clears: u32,
370    /// The current total score the player has achieved in this round of play.
371    pub points: u32,
372}
373
374/// Represents how a game can end.
375#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Debug)]
376#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
377pub enum GameEndCause {
378    /// 'Lock out' denotes the most recent piece would be completely locked down at
379    /// or above [`Game::SKYLINE_HEIGHT`].
380    LockOut {
381        /// The offending piece that does not fit below [`Game::SKYLINE_HEIGHT`].
382        locking_piece: Piece,
383    },
384    /// 'Block out' denotes a new piece being unable to spawn due to existing board tile(s)
385    /// blocking one or several of the cells of a piece to be spawned.
386    BlockOut {
387        /// The offending piece that does not fit onto board.
388        blocked_piece: Piece,
389    },
390    // 'Top out' denotes a number of new lines being unable to enter the existing board.
391    /// This is currently unused in the base engine.
392    TopOut {
393        /// The offending lines that did not fit onto the existing board.
394        blocked_lines: Vec<Line>,
395    },
396    /// Game over by having reached a [`Stat`] limit.
397    Limit(Stat),
398    /// Game ended by player forfeit.
399    Forfeit {
400        /// Piece that was in play at time of forfeit.
401        piece_in_play: Option<Piece>,
402    },
403    /// Custom game over.
404    /// This is unused in the base engine and intended for modding.
405    Custom(String),
406}
407
408/// An event that is scheduled by the game engine to execute some action.
409#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Debug)]
410#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
411pub enum Phase {
412    /// The state of the game "taking its time" to spawn a piece.
413    /// This is the state the board will have right before attempting to spawn a new piece.
414    Spawning {
415        /// The in-game time at which the game moves on to the next `Phase.`
416        spawn_time: InGameTime,
417    },
418    /// The state of the game having an active piece in-play, which can be controlled by a player.
419    PieceInPlay {
420        /// The tetromino game piece itself.
421        piece: Piece,
422        /// Optional time of the next move event.
423        auto_move_scheduled: Option<InGameTime>,
424        /// The time of the next fall or lock event.
425        fall_or_lock_time: InGameTime,
426        /// The time after which the active piece will immediately lock upon touching ground.
427        lock_time_cap: InGameTime,
428        /// The lowest recorded vertical position of the main piece.
429        lowest_y: isize,
430    },
431    /// The state of the game "taking its time" to clear out lines.
432    /// In this state the board is as it was at the time of the piece locking down,
433    /// i.e. with some horizontally completed lines.
434    /// After exiting this state, the
435    LinesClearing {
436        /// The in-game time at which the game moves on to the next `Phase.`
437        clear_finish_time: InGameTime,
438        /// The score bonus that will be earned once the lines are cleared out.
439        score_bonus: u32,
440    },
441    /// The state of the game being irreversibly over, and not playable anymore.
442    GameEnd {
443        /// The cause of why the game ended.
444        cause: GameEndCause,
445        /// Whether the ending is considered a win.
446        is_win: bool,
447    },
448}
449
450/// Main game struct representing a round of play.
451#[derive(Debug)]
452pub struct Game {
453    /// Some internal configuration options of the `Game`.
454    ///
455    /// # Reproducibility
456    /// Modifying a `Game`'s configuration after it was created might not make it easily
457    /// reproducible anymore.
458    pub config: Configuration,
459    state_init: StateInitialization,
460    state: State,
461    phase: Phase,
462    /// A list of special modifiers that apply to the `Game`.
463    ///
464    /// # Reproducibility
465    /// Modifying a `Game`'s modifiers after it was created might not make it easily
466    /// reproducible anymore.
467    pub modifiers: Vec<Box<dyn GameModifier>>,
468}
469
470/// A number of feedback notifications that can be returned by the game.
471///
472/// These can be used to more easily render visual feedback to the player.
473///
474/// The [`Notification::Debug`] variant is accessible if [`NotificationLevel::Debug`] is toggled.
475/// All other events are generally variants of `Notification::Debug` but provide additional info to possibly
476/// reconstruct visual effects (e.g. location of where a lock actually occurred, or how long a lineclear took at the time it happened).
477#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Debug)]
478#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
479pub enum Notification {
480    /// A piece was locked down in a certain configuration.
481    PieceLocked {
482        /// Information about the [`Piece`] that was locked.
483        piece: Piece,
484    },
485    /// A number of lines were cleared.
486    ///
487    /// The duration indicates the line clear delay the game was configured with at the time.
488    LinesClearing {
489        /// A list of height coordinates/indices signifying where lines where cleared.
490        y_coords: Vec<usize>,
491        /// Game time where lines started clearing.
492        /// Starts simultaneously to when a piece was locked and successfully completed some horizontal [`Line`]s,
493        /// therefore this will coincide with the time same value in a nearby [`Notification::PieceLocked`].
494        line_clear_duration: InGameTime,
495    },
496    /// A piece was quickly dropped from its original position to a new one.
497    HardDrop {
498        /// Information about the old state of the hard-dropped piece.
499        previous_piece: Piece,
500        /// Information about the new state of the hard-dropped piece.
501        updated_piece: Piece,
502    },
503    /// The player cleared some lines with a number of other stats that might have increased their
504    /// score bonus.
505    Accolade {
506        /// The final computed score bonus caused by the action.
507        score_bonus: u32,
508        /// How many lines were cleared by the piece simultaneously
509        lineclears: u32,
510        /// The number of consecutive pieces played that caused a lineclear.
511        combo: u32,
512        /// Whether the piece was spun into place.
513        is_spin: bool,
514        /// Whether the entire board was cleared empty by this action.
515        is_perfect_clear: bool,
516        /// The tetromino type that was locked.
517        tetromino: Tetromino,
518    },
519    /// Message that the game has ended.
520    GameEnded {
521        /// Whether it was a win or a loss.
522        is_win: bool,
523    },
524    /// A message containing debug information.
525    ///
526    /// This feedback type is only generated on [`NotificationLevel::Debug`]
527    Debug(String),
528    /// Generic text feedback message.
529    ///
530    /// This is currently unused in the base engine.
531    Custom(String),
532}
533
534/// An error that can be thrown by [`Game::update`].
535#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash, Debug)]
536pub enum UpdateGameError {
537    /// Error variant caused by an attempt to update the game with a requested `update_time` that lies in
538    /// the game's past (` < game.state().time`).
539    TargetTimeInPast,
540    /// Error variant caused by an attempt to update a game that has ended (`game.ended() == true`).
541    AlreadyEnded,
542}
543
544impl Tetromino {
545    /// All `Tetromino` enum variants in order.
546    ///
547    /// Note that `Tetromino::VARIANTS[t as usize] == t` always holds.
548    pub const VARIANTS: [Self; 7] = {
549        use Tetromino::*;
550        [O, I, S, Z, T, L, J]
551    };
552
553    /// Returns the mino offsets of a tetromino shape, given an orientation.
554    pub const fn minos(&self, oriented: Orientation) -> [Coord; 4] {
555        use Orientation::*;
556        match self {
557            Tetromino::O => [(0, 0), (1, 0), (0, 1), (1, 1)], // ⠶
558            Tetromino::I => match oriented {
559                N | S => [(0, 0), (1, 0), (2, 0), (3, 0)], // ⠤⠤
560                E | W => [(0, 0), (0, 1), (0, 2), (0, 3)], // ⡇
561            },
562            Tetromino::S => match oriented {
563                N | S => [(0, 0), (1, 0), (2, 1), (1, 1)], // ⠴⠂
564                E | W => [(1, 0), (0, 1), (1, 1), (0, 2)], // ⠳
565            },
566            Tetromino::Z => match oriented {
567                N | S => [(1, 0), (2, 0), (0, 1), (1, 1)], // ⠲⠄
568                E | W => [(0, 0), (1, 1), (0, 1), (1, 2)], // ⠞
569            },
570            Tetromino::T => match oriented {
571                N => [(0, 0), (1, 0), (2, 0), (1, 1)], // ⠴⠄
572                E => [(0, 0), (1, 1), (0, 1), (0, 2)], // ⠗
573                S => [(1, 0), (0, 1), (2, 1), (1, 1)], // ⠲⠂
574                W => [(1, 0), (0, 1), (1, 1), (1, 2)], // ⠺
575            },
576            Tetromino::L => match oriented {
577                N => [(0, 0), (1, 0), (2, 0), (2, 1)], // ⠤⠆
578                E => [(0, 0), (1, 0), (0, 1), (0, 2)], // ⠧
579                S => [(0, 0), (1, 1), (2, 1), (0, 1)], // ⠖⠂
580                W => [(1, 0), (0, 2), (1, 1), (1, 2)], // ⠹
581            },
582            Tetromino::J => match oriented {
583                N => [(0, 0), (1, 0), (2, 0), (0, 1)], // ⠦⠄
584                E => [(0, 0), (1, 2), (0, 1), (0, 2)], // ⠏
585                S => [(2, 0), (0, 1), (1, 1), (2, 1)], // ⠒⠆
586                W => [(0, 0), (1, 0), (1, 1), (1, 2)], // ⠼
587            },
588        }
589    }
590
591    /// Returns the convened-on standard tile id corresponding to the given tetromino.
592    pub const fn tiletypeid(&self) -> TileTypeID {
593        use Tetromino::*;
594        let u8 = match self {
595            O => 1,
596            I => 2,
597            S => 3,
598            Z => 4,
599            T => 5,
600            L => 6,
601            J => 7,
602        };
603        // SAFETY: Ye, `u8 > 0`;
604        unsafe { NonZeroU8::new_unchecked(u8) }
605    }
606}
607
608impl Orientation {
609    /// All `Orientation` enum variants in order.
610    ///
611    /// Note that `Orientation::VARIANTS[o as usize] == o` always holds.
612    pub const VARIANTS: [Self; 4] = {
613        use Orientation::*;
614        [N, E, S, W]
615    };
616
617    /// Find a new direction by turning right some number of times.
618    ///
619    /// This accepts `i32` to allow for left rotation.
620    pub const fn reorient_right(&self, right_turns: i8) -> Self {
621        Orientation::VARIANTS[((*self as i8 + right_turns) as usize).rem_euclid(4)]
622    }
623}
624
625impl Piece {
626    /// Returns the coordinates and tile types for he piece on the board.
627    pub fn tiles(&self) -> [(Coord, TileTypeID); 4] {
628        let Self {
629            tetromino,
630            orientation,
631            position: (x, y),
632        } = self;
633        let tile_type_id = tetromino.tiletypeid();
634        tetromino
635            .minos(*orientation)
636            .map(|(dx, dy)| ((x + dx, y + dy), tile_type_id))
637    }
638
639    /// Checks whether the piece fits at its current location onto the board.
640    pub fn fits_onto(&self, board: &Board) -> bool {
641        self.tiles().iter().all(|&((x, y), _)| {
642            0 <= x
643                && (x as usize) < Game::WIDTH
644                && 0 <= y
645                && (y as usize) < Game::HEIGHT
646                && board[y as usize][x as usize].is_none()
647        })
648    }
649
650    /// Checks whether the piece fits a given offset from its current location onto the board.
651    pub fn offset_on(&self, board: &Board, offset: CoordOffset) -> Result<Piece, Piece> {
652        let offset_piece = Piece {
653            tetromino: self.tetromino,
654            orientation: self.orientation,
655            position: add(self.position, offset),
656        };
657
658        if offset_piece.fits_onto(board) {
659            Ok(offset_piece)
660        } else {
661            Err(offset_piece)
662        }
663    }
664
665    /// Checks whether the piece fits a given offset from its current location onto the board, with
666    /// its rotation changed by some number of right turns.
667    pub fn reoriented_offset_on(
668        &self,
669        board: &Board,
670        right_turns: i8,
671        offset: CoordOffset,
672    ) -> Result<Piece, Piece> {
673        let reoriented_offset_piece = Piece {
674            tetromino: self.tetromino,
675            orientation: self.orientation.reorient_right(right_turns),
676            position: add(self.position, offset),
677        };
678
679        if reoriented_offset_piece.fits_onto(board) {
680            Ok(reoriented_offset_piece)
681        } else {
682            Err(reoriented_offset_piece)
683        }
684    }
685
686    /// Check whether piece could fall one unit down or not.
687    pub fn is_airborne(&self, board: &Board) -> bool {
688        self.offset_on(board, (0, -1)).is_ok()
689    }
690
691    /// Given an iterator over some offsets, checks whether the rotated piece fits at any offset
692    /// location onto the board.
693    pub fn find_reoriented_offset_on(
694        &self,
695        board: &Board,
696        right_turns: i8,
697        offsets: impl IntoIterator<Item = CoordOffset>,
698    ) -> Option<Piece> {
699        let original_pos = self.position;
700
701        let mut updated_piece = *self;
702        updated_piece.orientation = updated_piece.orientation.reorient_right(right_turns);
703        for offset in offsets {
704            updated_piece.position = add(original_pos, offset);
705            if updated_piece.fits_onto(board) {
706                return Some(updated_piece);
707            }
708        }
709
710        None
711    }
712
713    /// Returns the position the piece would hit if it kept moving at `offset` steps.
714    /// For offset `(0,0)` this function return immediately.
715    pub fn teleported(&self, board: &Board, offset: CoordOffset) -> Piece {
716        let mut updated_piece = *self;
717
718        if offset != (0, 0) {
719            // Move piece as far as possible.
720            while let Ok(offset_updated_piece) = updated_piece.offset_on(board, offset) {
721                if offset_updated_piece == updated_piece {
722                    break;
723                }
724                updated_piece = offset_updated_piece;
725            }
726        }
727
728        updated_piece
729    }
730}
731
732impl std::fmt::Display for GameEndCause {
733    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
734        let s = match self {
735            GameEndCause::LockOut { .. } => "Lock out",
736            GameEndCause::BlockOut { .. } => "Block out",
737            GameEndCause::TopOut { .. } => "Top out",
738            GameEndCause::Limit(stat) => match stat {
739                Stat::TimeElapsed(_) => "Time limit reached",
740                Stat::PiecesLocked(_) => "Piece limit reached",
741                Stat::LinesCleared(_) => "Line limit reached",
742                Stat::PointsScored(_) => "Score limit reached",
743            },
744            GameEndCause::Forfeit { .. } => "Forfeited",
745            GameEndCause::Custom(text) => text,
746        };
747        write!(f, "{s}")
748    }
749}
750
751impl DelayParameters {
752    /// The duration at which the delay starts.
753    pub fn base_delay(&self) -> ExtDuration {
754        self.base_delay
755    }
756
757    /// The base factor that gets exponentiated by number of line clears;
758    /// `factor ^ lineclears ...`.
759    ///
760    /// Should be in the range `0.0 ≤ .. ≤ 1.0`, where
761    /// - `0.0` means 'zero-out initial delay at every line clear',
762    /// - `0.5` means 'halve initial delay for every line clear',
763    /// - `1.0` means 'keep initial delay at 100%'.
764    pub fn factor(&self) -> ExtNonNegF64 {
765        self.factor
766    }
767
768    /// The base subtrahend that gets multiplied by number of line clears;
769    /// `... - subtrahend * lineclears`.
770    ///
771    /// Should be in the range `0.0 ≤ .. ≤ 1.0`, where
772    /// - `0.0` means 'subtract 0% of initial delay for every line clear',
773    /// - `0.5` means 'subtract 50% of initial delay for every line clear',
774    /// - `1.0` means 'subtract 100% of initial delay for every line clear'.
775    pub fn subtrahend(&self) -> ExtDuration {
776        self.subtrahend
777    }
778
779    /// The duration below which delay cannot decrease.
780    pub fn lowerbound(&self) -> ExtDuration {
781        self.lowerbound
782    }
783
784    /// Delay equation which decreases/decays exponentially in number of linescleared.
785    pub fn new(
786        base_delay: ExtDuration,
787        lowerbound: ExtDuration,
788        factor: ExtNonNegF64,
789        subtrahend: ExtDuration,
790    ) -> Option<Self> {
791        Self::constant(Default::default())
792            .with_bounds(base_delay, lowerbound)?
793            .with_coefficients(factor, subtrahend)
794    }
795
796    /// Create a modified delay parameters where only the bounds are changed.
797    pub fn with_bounds(&self, base_delay: ExtDuration, lowerbound: ExtDuration) -> Option<Self> {
798        let correct_bounds = lowerbound <= base_delay;
799        correct_bounds.then_some(Self {
800            base_delay,
801            lowerbound,
802            ..*self
803        })
804    }
805
806    /// Create a modified delay parameters where only the coefficients are changed.
807    pub fn with_coefficients(&self, factor: ExtNonNegF64, subtrahend: ExtDuration) -> Option<Self> {
808        let correct_coefficients = factor <= 1.into();
809        correct_coefficients.then_some(Self {
810            factor,
811            subtrahend,
812            ..*self
813        })
814    }
815
816    /// Delay equation which does not change at all with number of linescleared.
817    pub fn constant(delay: ExtDuration) -> Self {
818        Self {
819            base_delay: delay,
820            factor: 1.into(),
821            subtrahend: ExtDuration::ZERO,
822            lowerbound: delay,
823        }
824    }
825
826    /// Whether the delay curve is invariant to number of lineclears.
827    pub fn is_constant(&self) -> bool {
828        self.factor == 1.into() && self.subtrahend.is_zero()
829    }
830
831    /// Delay equation which implements guideline-like fall delays:
832    /// *   0.0  lineclears ~> 20s to fall 20 units (1s/unit).
833    /// *  28.8_ lineclears ~> 10s to fall 20 units.
834    /// *  94.4_ lineclears ~>  2s to fall 20 units.
835    /// * 120.9_ lineclears ~>  1s to fall 20 units.
836    /// * 156.8_ lineclears ~> 1/3s to fall 20 units (NES max; 1 unit/frame).
837    /// * 196.1_ lineclears ~> 1/60s to fall 20 units (1frame/20units).
838    /// * 199.4_ lineclears ~>  0s to fall (instant gravity).
839    pub fn standard_fall() -> Self {
840        Self {
841            base_delay: Duration::from_millis(1000).into(),
842            factor: ExtNonNegF64::new(0.9763).unwrap(),
843            subtrahend: Duration::from_secs_f64(0.000042).into(),
844            lowerbound: Duration::ZERO.into(),
845        }
846    }
847
848    /// Delay equation which implements guideline-like lock delays:
849    /// * 0 lineclears ~> 500ms lock delay.
850    /// * Decrease lock_delay by 10 ms every 10 lineclears (= 1 ms every lineclear).
851    /// * End at 100ms lock delay.
852    pub fn standard_lock() -> Self {
853        Self {
854            base_delay: Duration::from_millis(500).into(),
855            factor: 1.into(),
856            subtrahend: Duration::from_millis(1).into(),
857            lowerbound: Duration::from_millis(100).into(),
858        }
859    }
860
861    /// Calculates an actual delay value given a number of lineclears to determine progression.
862    pub fn calculate(&self, lineclears: u32) -> ExtDuration {
863        // Multiplicative factor computed from lineclears;
864        let raw_mul = self.factor.get().powf(f64::from(lineclears));
865        // Wrap it back in ExtNonNegF64.
866        // SAFETY: ∀e:int, ∀b:f64 ≤ 1, (b^e ≤ 1).
867        let mul = ExtNonNegF64::new(raw_mul).unwrap();
868
869        // Subtractive offset computed from lineclears.
870        let sub = self.subtrahend.mul_ennf64(lineclears.into());
871
872        // Calculate intended delay;
873        let raw_delay = self.base_delay.mul_ennf64(mul).saturating_sub(sub);
874        // Return delay capped by lower bound.
875        self.lowerbound.max(raw_delay)
876    }
877}
878
879impl GameLimits {
880    /// Create a fresh [`GameLimits`] without any limits.
881    pub fn new() -> Self {
882        Self::default()
883    }
884
885    /// Create a new [`GameLimits`] with a single [`Stat`] as the limit.
886    pub fn single(stat: Stat, is_win: bool) -> Self {
887        let mut new = Self::new();
888
889        match stat {
890            Stat::TimeElapsed(t) => new.time_elapsed = Some((t, is_win)),
891            Stat::PiecesLocked(p) => new.pieces_locked = Some((p, is_win)),
892            Stat::LinesCleared(l) => new.lines_cleared = Some((l, is_win)),
893            Stat::PointsScored(s) => new.points_scored = Some((s, is_win)),
894        };
895
896        new
897    }
898
899    /// Iterate over all limiting [`Stat`] contained in a [`GameLimits`] struct.
900    pub fn iter(&self) -> impl Iterator<Item = (Stat, bool)> {
901        [
902            self.time_elapsed
903                .map(|(t, is_win)| (Stat::TimeElapsed(t), is_win)),
904            self.pieces_locked
905                .map(|(p, is_win)| (Stat::PiecesLocked(p), is_win)),
906            self.lines_cleared
907                .map(|(l, is_win)| (Stat::LinesCleared(l), is_win)),
908            self.points_scored
909                .map(|(s, is_win)| (Stat::PointsScored(s), is_win)),
910        ]
911        .into_iter()
912        .flatten()
913    }
914}
915
916impl Button {
917    /// All `Button` enum variants.
918    ///
919    /// Note that `Button::VARIANTS[b as usize] == b` always holds.
920    pub const VARIANTS: [Self; 11] = {
921        use Button as B;
922        [
923            B::MoveLeft,
924            B::MoveRight,
925            B::RotateLeft,
926            B::RotateRight,
927            B::Rotate180,
928            B::DropSoft,
929            B::DropHard,
930            B::TeleDown,
931            B::TeleLeft,
932            B::TeleRight,
933            B::HoldPiece,
934        ]
935    };
936}
937
938impl<T> ops::Index<Button> for [T; Button::VARIANTS.len()] {
939    type Output = T;
940
941    fn index(&self, idx: Button) -> &Self::Output {
942        &self[idx as usize]
943    }
944}
945
946impl<T> ops::IndexMut<Button> for [T; Button::VARIANTS.len()] {
947    fn index_mut(&mut self, idx: Button) -> &mut Self::Output {
948        &mut self[idx as usize]
949    }
950}
951
952impl Default for Configuration {
953    fn default() -> Self {
954        Self {
955            piece_preview_count: 3,
956            allow_initial_actions: true,
957            rotation_system: RotationSystem::default(),
958            spawn_delay: Duration::from_millis(50),
959            delayed_auto_shift: Duration::from_millis(167),
960            auto_repeat_rate: Duration::from_millis(33),
961            fall_delay_params: DelayParameters::constant(Duration::from_millis(1000).into()),
962            soft_drop_factor: ExtNonNegF64::new(15.0).unwrap(),
963            lock_delay_params: DelayParameters::constant(Duration::from_millis(500).into()),
964            allow_lenient_lock_reset: false,
965            ensure_move_delay_lt_lock_delay: false,
966            lock_reset_cap_factor: ExtNonNegF64::new(8.0).unwrap(),
967            line_clear_duration: Duration::from_millis(200),
968            update_delays_every_n_lineclears: 10,
969            game_limits: Default::default(),
970            notification_level: NotificationLevel::Standard,
971        }
972    }
973}
974
975impl Phase {
976    /// Read accessor to a `Phase`'s possible [`Piece`].
977    pub fn piece(&self) -> Option<&Piece> {
978        if let Phase::PieceInPlay { piece, .. } = self {
979            Some(piece)
980        } else {
981            None
982        }
983    }
984
985    /// Mutable accessor to a `Phase`'s possible [`Piece`].
986    pub fn piece_mut(&mut self) -> Option<&mut Piece> {
987        if let Phase::PieceInPlay { piece, .. } = self {
988            Some(piece)
989        } else {
990            None
991        }
992    }
993}
994
995impl Game {
996    /// The maximum height *any* piece tile could reach *before* `GameOver::LockOut` occurs.
997    pub const HEIGHT: usize = Self::LOCK_OUT_HEIGHT + 7;
998    /// The game field width.
999    pub const WIDTH: usize = 10;
1000    /// The height of the (conventionally) visible playing grid that can be played in.
1001    /// No tile piece may have all its tiles locked entirely at or above this index height (see [`GameEndCause::LockOut`]), although it may do so partially.
1002    pub const LOCK_OUT_HEIGHT: usize = 20;
1003
1004    /// Creates a blank new template representing a yet-to-be-started [`Game`] ready for configuration.
1005    pub fn builder() -> GameBuilder {
1006        GameBuilder::default()
1007    }
1008
1009    /// Read accessor for the game's initial values.
1010    pub const fn state_init(&self) -> &StateInitialization {
1011        &self.state_init
1012    }
1013
1014    /// Read accessor for the current game state.
1015    pub const fn state(&self) -> &State {
1016        &self.state
1017    }
1018
1019    /// Read accessor for the current game state.
1020    pub const fn phase(&self) -> &Phase {
1021        &self.phase
1022    }
1023
1024    /// Whether the game has ended, and whether it can continue to update.
1025    pub const fn has_ended(&self) -> bool {
1026        matches!(self.phase, Phase::GameEnd { .. })
1027    }
1028
1029    /// Retrieve the when the next *autonomous* in-game update is scheduled.
1030    /// I.e., compute the next time the game would change state assuming no button updates
1031    ///
1032    /// Returns `None` when game ended.
1033    ///
1034    /// # Modifiers
1035    /// Note that this only predicts what an unmodded game would do;
1036    /// [`Modifier`]s may arbitrarily change game state and change or prevent precise update predictions.
1037    pub fn peek_next_update_time(&self) -> Option<InGameTime> {
1038        // Find the next autonomous game update.
1039        let mut update_time = match self.phase {
1040            Phase::GameEnd { .. } => return None,
1041            Phase::LinesClearing {
1042                clear_finish_time, ..
1043            } => clear_finish_time,
1044            Phase::Spawning { spawn_time } => spawn_time,
1045            Phase::PieceInPlay {
1046                auto_move_scheduled,
1047                fall_or_lock_time,
1048                ..
1049            } => 'exp: {
1050                if let Some(move_time) = auto_move_scheduled {
1051                    if move_time < fall_or_lock_time {
1052                        break 'exp move_time;
1053                    }
1054                }
1055                fall_or_lock_time
1056            }
1057        };
1058
1059        // Check against time-related end conditions.
1060        if let Some((time_limit, _)) = self.config.game_limits.time_elapsed {
1061            if time_limit < update_time {
1062                update_time = time_limit;
1063            }
1064        }
1065
1066        Some(update_time)
1067    }
1068
1069    /// Check whether a certain stat value has been met or exceeded.
1070    pub fn check_stat_met(&self, stat: Stat) -> bool {
1071        match stat {
1072            Stat::TimeElapsed(t) => t <= self.state.time,
1073            Stat::PiecesLocked(p) => p <= self.state.pieces_locked.iter().sum(),
1074            Stat::LinesCleared(l) => l <= self.state.lineclears,
1075            Stat::PointsScored(s) => s <= self.state.points,
1076        }
1077    }
1078
1079    /// Try to create a cloned instance of the game.
1080    pub fn try_clone(&self) -> Result<Self, String> {
1081        let mut modifiers = Vec::new();
1082        for modifier in self.modifiers.iter() {
1083            modifiers.push(modifier.try_clone()?);
1084        }
1085
1086        Ok(Self {
1087            config: self.config.clone(),
1088            state_init: self.state_init,
1089            state: self.state.clone(),
1090            phase: self.phase.clone(),
1091            modifiers,
1092        })
1093    }
1094}
1095
1096impl std::fmt::Display for UpdateGameError {
1097    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1098        let s = match self {
1099            UpdateGameError::TargetTimeInPast => {
1100                "attempt to update game to timestamp it already passed"
1101            }
1102            UpdateGameError::AlreadyEnded => "attempt to update game after it already ended",
1103        };
1104        write!(f, "{s}")
1105    }
1106}
1107
1108impl std::error::Error for UpdateGameError {}
1109
1110/// Adds an offset to a coordinate, failing if the result overflows
1111/// (negative or positive).
1112pub fn add((x, y): Coord, (dx, dy): CoordOffset) -> Coord {
1113    (x + dx, y + dy)
1114}
1115
1116/*#[cfg(test)]
1117mod tests {
1118    use super::*;
1119
1120    #[test]
1121    fn it_works() {
1122        let res = add((1,2),(3,4));
1123        assert_eq!(res, (4,6));
1124    }
1125}*/