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 {}