falling-tetromino-engine 1.5.0

A backend with ergonomic API for games where tetrominos fall and stack.
Documentation

Falling Tetromino Engine

Crates.io Documentation License

A backend with ergonomic API for games where tetrominos fall and stack.

Installation

Add this to your Cargo.toml:

[dependencies]
falling-tetromino-engine = "1.2.0"

About

The engine is completely frontend-agnostic, although calls to Game::update may optionally return additional info to facilitate implementation of visual effects.

The engine allows for compile-time mods which may arbitrarily access and modify game state during gameplay.

The engine aims to compete on the order of modern tetromino stackers; It incorporates many features found in such games. Experienced players may be familiar with most of the following mechanics:

  • Variable gravity/fall delay (frame-agnostic); '20G' (= 0s fall delay),
  • Simple but flexible programming of custom fall and lock delay progressions (DelayParameters),
  • (Arbitrary) piece preview,
  • Pre-spawn actions toggle ('Initial Hold/Rotation System'),
  • Rotation systems: 'Ocular' (engine-specific, playtested), 'Classic', 'Super',
  • Tetromino generators: 'Uniform', 'Stock' (generalized Bag), 'Recency' (history), 'Balancerelative',
  • Spawn delay (ARE),
  • Delayed auto-shift (DAS),
  • Auto-repeat rate (ARR),
  • Soft drop factor (SDF),
  • Lenient lock delay reset toggle (reset lock delay even if rotate/move fails),
  • Ensure move delay less than lock delay toggle (DAS/ARR automatically shortened when lock delay is very low),
  • Lock-reset-cap factor (~maximum time before lock delay cannot be reset),
  • Line clear duration (LCD),
  • Custom win/loss conditions based on stats: time, pieces, lines, score,
  • Hold piece,
  • Higher score for larger lineclears and spins ('allspin')
  • Game reproducibility (PRNG),
  • Available player actions: MoveLeft, MoveRight; RotateLeft, RotateRight, Rotate180; DropSoft, DropHard, TeleDown ('Sonic drop'), TeleLeft, TeleRight, HoldPiece.

Example Usage

use falling_tetromino_engine::*;

// Starting up a game - note that in-game time starts at 0s.
let mut game = Game::builder()
    .seed(1234)
    /* ...Further optional configuration possible... */
    .build();

// Updating the game with the info that 'left' should be pressed at second 4.2;
// If a piece is in the game, it will try to move left.
let input = Input::Activate(Button::MoveLeft);
game.update(InGameTime::from_secs(4.2), Some(input));

// ...

// Updating the game with the info that no input change has occurred up to second 6.79;
// This updates the game, e.g., pieces fall and lock.
game.update(InGameTime::from_secs(6.79), None);

// Read most recent game state;
// This is how a UI can know how to render the board, etc.
let State { board, .. } = game.state();

Internally, the game keeps its own timeline:

    0s - Game start; Piece is spawned.
   /        4.2s - Player input; Piece starts moving left.
  /        /            6.79s - Piece will have moved left some more,
 /        /            /        has been affected by gravity etc.
[--------|------------¦--------------- - - - ->

Overview by Types

Much of the implementation is tightly encoded into types.

// The central engine type.
struct Game {
    config: Configuration, // Can be safely modified during play.
    state_init: StateInitialization, // For reproducibility.
    state: State,
    phase: Phase, // Macro-state-machine.
    modifiers: Vec<Modifier>, // Modding facility.
}

It may be instructive to simply consider the main type definitions as a tentative overview of the deeper engine mechanics.

Types used as Game fields

struct Configuration {
    piece_preview_count: usize,
    allow_initial_actions: bool,
    rotation_system: RotationSystem,
    spawn_delay: Duration,
    delayed_auto_shift: Duration,
    auto_repeat_rate: Duration,
    fall_delay_params: DelayParameters,
    soft_drop_factor: ExtNonNegF64,
    lock_delay_params: DelayParameters,
    ensure_move_delay_lt_lock_delay: bool,
    allow_lenient_lock_reset: bool,
    lock_reset_cap_factor: ExtNonNegF64,
    line_clear_duration: Duration,
    update_delays_every_n_lineclears: u32,
    game_limits: Vec<(Stat, bool)>,
    notification_level: NotificationLevel,
}

struct StateInitialization {
    seed: u64,
    tetromino_generator: TetrominoGenerator,
}

struct State {
    time: InGameTime,
    buttons_pressed: [Option<InGameTime>; Button::VARIANTS.len()],
    rng: GameRng,
    piece_generator: TetrominoGenerator,
    piece_preview: VecDeque<Tetromino>,
    piece_held: Option<(Tetromino, bool)>,
    board: [[Option<TileTypeID>; Game::WIDTH]; Game::HEIGHT],
    fall_delay: ExtDuration,
    fall_delay_lowerbound_hit_at_n_lineclears: Option<u32>,
    lock_delay: ExtDuration,
    pieces_locked: [u32; Tetromino::VARIANTS.len()],
    lineclears: u32,
    consecutive_line_clears: u32,
    score: u32,
}

enum Phase {
    Spawning { spawn_time: InGameTime },
    PieceInPlay {
        piece: Piece,
        auto_move_scheduled: Option<InGameTime>,
        fall_or_lock_time: InGameTime,
        lock_time_cap: InGameTime,
        lowest_y: isize,
    },
    LinesClearing { clears_finish_time: InGameTime },
    GameEnd { cause: GameEndCause, is_win: bool },
}

struct Modifier {
    descriptor: String,
    mod_function: Box<GameModFn>,
}

Other Important Types

enum Tetromino {
    O, I, S, Z, T, L, J
}

struct Piece {
    tetromino: Tetromino,
    orientation: Orientation,
    position: Coord,
}

enum Button {
    MoveLeft, MoveRight,
    RotateLeft, RotateRight, Rotate180,
    DropSoft, DropHard,
    TeleLeft, TeleRight, TeleDown, 
    HoldPiece,
}

struct DelayParameters {
    base_delay: ExtDuration,
    factor: ExtNonNegF64,
    subtrahend: ExtDuration,
    lowerbound: ExtDuration,
}

See full documentation.