Skip to main content

falling_tetromino_engine/
core.rs

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