# turnbase - Complete API Documentation > Headless, deterministic turn-based game engine core **Version:** 0.0.0 **Authors:** Matan Lurey **License:** MIT OR Apache-2.0 **Repository:** https://github.com/crates-lurey-io/turnbase **Keywords:** game-engine, turn-based, deterministic, gamedev, ai Generated: 2026-07-25 17:07:54 UTC Created by: [cargo-llms-txt](https://github.com/masinc/cargo-llms-txt) ## Table of Contents ### src/player.rs - pub struct PlayerId - impl PlayerId - impl core::fmt::Display for PlayerId ### src/error.rs - pub enum Error - impl core::fmt::Display for Error - impl std::error::Error for Error ### src/chance.rs - pub fn sample_chance - impl Game for tests::Weighted ### src/game.rs - pub trait Game - pub trait Reversible - pub trait Determinize - impl Game for tests::RollGame - impl Reversible for tests::RollGame ### src/lib.rs - pub use active::ActivePlayers - pub use chance::sample_chance - pub use effects::{EffectSystem, MAX_EFFECTS, resolve_effects} - pub use error::Error - pub use game::{Determinize, Game, Reversible} - pub use pile::Pile - pub use player::PlayerId - pub use rng::Prng - pub use state::{PlayerView, State} ### src/active.rs - pub struct ActivePlayers - impl ActivePlayers - impl FromIterator for ActivePlayers - impl IntoIterator for Unknown ### src/effects.rs - pub trait EffectSystem - pub const MAX_EFFECTS - pub fn resolve_effects - impl EffectSystem for tests::Dominoes ### src/pile.rs - pub struct Pile - impl Pile - impl Pile - impl FromIterator for Pile - impl IntoIterator for Unknown ### src/state.rs - pub struct State - impl State - impl State - pub struct PlayerView ### src/rng.rs - pub struct Prng - impl Prng ### src/serde_roundtrip.rs - impl Game for Roll --- ## README.md ### turnbase Headless, deterministic turn-based game engine core Part of the [turnbase](https://github.com/crates-lurey-io/turnbase) workspace. --- ## src/player.rs ### PlayerId ```rust #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct PlayerId(); ``` Identifies one seat in a match. A seat is not necessarily a human: scripted opponents, a blackjack dealer, and the reserved chance "player" ([`PlayerId::CHANCE`]) are all seats. Real players are numbered from 0; games map their own seat concepts onto these indices. ### impl PlayerId ```rust impl PlayerId { pub const CHANCE: Type; pub fn new(index: u32) -> Self; pub fn index(self) -> u32; pub fn is_chance(self) -> bool; } ``` ### impl core::fmt::Display for PlayerId ```rust impl core::fmt::Display for PlayerId { } ``` ## src/error.rs ### Error ```rust #[derive(Clone, PartialEq, Eq, Debug)] pub enum Error { IllegalAction { player: PlayerId }, NotActive { player: PlayerId }, } ``` Errors returned by the engine's checked entry points. The in-place `apply` primitive assumes a legal action and does not return errors; these surface from checked helpers such as `apply_cloned`, which validate before mutating. More variants may be added, so match with a wildcard arm. ### impl core::fmt::Display for Error ```rust impl core::fmt::Display for Error { } ``` ### impl std::error::Error for Error ```rust impl std::error::Error for Error { } ``` ## src/chance.rs ### sample_chance ```rust pub fn sample_chance(game: &G, state: &G::State, rng: &mut Prng) -> Option ``` Samples one outcome from `game.chance_outcomes(state)`, weighted by probability, using `rng`. Returns `None` if there are no outcomes. `rng` is the chance sampler's generator, owned by the driver or search loop, deliberately separate from any per-state generator a game uses for implicit rolls inside `apply`. Given the same generator position and the same outcome list, the draw is reproducible, which is what makes replay and undo/redo of a dealt card land on the same result. The returned action is one of the outcomes; the caller applies it with [`PlayerId::CHANCE`](crate::PlayerId::CHANCE) to commit it to state. Use it in a driver or rollout loop whenever the chance pseudo-player is active. #### Example ``` use turnbase::{ActivePlayers, Game, PlayerId, Prng, sample_chance}; // One chance node that reveals a card 0, 1, or 2 (uniform by default). struct Reveal; impl Game for Reveal { type State = (); type Action = u8; type View = (); fn new_initial_state(&self, _seed: u64) {} fn num_players(&self) -> usize { 0 } fn active_players(&self, _s: &()) -> ActivePlayers { ActivePlayers::one(PlayerId::CHANCE) } fn legal_actions(&self, _s: &(), p: PlayerId) -> Vec { if p.is_chance() { vec![0, 1, 2] } else { vec![] } } fn apply(&self, _s: &mut (), _p: PlayerId, _a: u8) {} fn is_terminal(&self, _s: &()) -> bool { false } fn reward(&self, _s: &(), _p: PlayerId) -> f64 { 0.0 } fn view(&self, _s: &(), _v: Option) {} } let mut sampler = Prng::new(42); let mut state = (); if Reveal.active_players(&state).contains(PlayerId::CHANCE) { let card = sample_chance(&Reveal, &state, &mut sampler).unwrap(); Reveal.apply(&mut state, PlayerId::CHANCE, card); // commit the draw assert!(card <= 2); } ``` ### impl Game for Weighted ```rust impl Game for Weighted { } ``` ## src/game.rs ### Game ```rust pub trait Game { type State; type Action; type View; fn new_initial_state(&self, seed: u64) -> Self::State; fn num_players(&self) -> usize; fn active_players(&self, state: &Self::State) -> ActivePlayers; fn legal_actions(&self, state: &Self::State, player: PlayerId) -> Vec; fn is_legal(&self, state: &Self::State, player: PlayerId, action: &Self::Action) -> bool; fn chance_outcomes(&self, state: &Self::State) -> Vec<(Self::Action, f64)>; fn apply(&self, state: &mut Self::State, player: PlayerId, action: Self::Action); fn apply_cloned(&self, state: &Self::State, player: PlayerId, action: Self::Action) -> Result where Self::State: Clone; fn is_terminal(&self, state: &Self::State) -> bool; fn reward(&self, state: &Self::State, player: PlayerId) -> f64; fn step_reward(&self, state: &Self::State, player: PlayerId) -> f64; fn view(&self, state: &Self::State, viewer: Option) -> Self::View; } ``` A turn-based game defined as pure functions from state and action to new state. Per-match configuration (player count, board size, variant rules) lives on the implementing value (`&self`), so [`Self::State`] stays lean and cheap to clone (cloning is the default backtracking primitive) and there is one home for `num_players` and variant flags. Everything is synchronous and side-effect-free; randomness comes from a generator stored inside the state. ### Reversible ```rust pub trait Reversible: Game { type UndoRecord; fn apply_undoable(&self, state: &mut Self::State, player: PlayerId, action: Self::Action) -> Self::UndoRecord; fn undo(&self, state: &mut Self::State, record: Self::UndoRecord); } ``` Opt-in make/unmake for games where cloning the whole state per search node is too slow (chess-scale branching). Cloning ([`Game::apply_cloned`]) is the default and is always correct; implement this only when per-node cloning dominates the search budget. A wrong `undo` corrupts search silently instead of crashing, so it is a deliberate opt-in. RNG invariant: [`Self::UndoRecord`] must capture the generator's position as it was *before* the move (a small `Copy` value, [`Prng::position`]) and [`Self::undo`] must restore it. A move may consume a variable number of draws, so the pre-move position cannot be recovered by counting draws; it must be snapshotted up front. The clone path gets this for free by cloning the whole state. [`Prng::position`]: crate::Prng::position ### Determinize ```rust pub trait Determinize: Game { fn determinize(&self, state: &Self::State, observer: PlayerId, rng: &mut Prng) -> Self::State; } ``` Opt-in resampling of hidden information for imperfect-information search. The imperfect-info analog of [`Reversible`]: an optional capability a game implements to unlock a bot that could not otherwise work. A perfect-info game implements it trivially as `state.clone()`; a hidden-info game randomly fills in what `observer` cannot see. Information-set MCTS (`Ismcts`) calls this once per simulation to search over sampled worlds. The one invariant that makes search sound: the result must be **consistent with `observer`'s information set**. Everything `observer` can already see is preserved exactly (so [`Game::view`] for `observer` is unchanged); only the cards, tiles, or rolls they cannot see are resampled. Draw the resampling randomness from `rng`, not from the state, so repeated calls explore different worlds. This is the game-specific knowledge that a generic engine cannot supply (mirroring OpenSpiel's `ResampleFromInfostate`), which is why it is a trait a game opts into rather than a core method. ### impl Game for RollGame ```rust impl Game for RollGame { } ``` ### impl Reversible for RollGame ```rust impl Reversible for RollGame { } ``` ## src/lib.rs ### active::ActivePlayers ```rust pub use active::ActivePlayers; ``` ### chance::sample_chance ```rust pub use chance::sample_chance; ``` ### effects::{EffectSystem, MAX_EFFECTS, resolve_effects} ```rust pub use effects::{EffectSystem, MAX_EFFECTS, resolve_effects}; ``` ### error::Error ```rust pub use error::Error; ``` ### game::{Determinize, Game, Reversible} ```rust pub use game::{Determinize, Game, Reversible}; ``` ### pile::Pile ```rust pub use pile::Pile; ``` ### player::PlayerId ```rust pub use player::PlayerId; ``` ### rng::Prng ```rust pub use rng::Prng; ``` ### state::{PlayerView, State} ```rust pub use state::{PlayerView, State}; ``` ## src/active.rs ### ActivePlayers ```rust #[derive(Clone, PartialEq, Eq, Default, Debug)] pub struct ActivePlayers(); ``` An ordered, deterministic set of players who owe a decision. Wraps a [`BTreeSet`] so iteration order is stable (ascending by seat index) rather than the per-process random order of a hashed set. Determinism is the engine's core promise, so ordering is part of the observable contract: two replays of the same seed and inputs must visit active players in the same order. The backing collection is intentionally not exposed. Cardinality carries meaning: - empty during engine-only resolution steps (adjudicating simultaneous orders, running a deterministic resolution pass), - one for strictly alternating games (the common case), - many during simultaneous or secret phases. ### impl ActivePlayers ```rust impl ActivePlayers { pub fn none() -> Self; pub fn one(player: PlayerId) -> Self; pub fn all(num_players: u32) -> Self; pub fn contains(&self, player: PlayerId) -> bool; pub fn len(&self) -> usize; pub fn is_empty(&self) -> bool; pub fn iter(&self) -> impl Trait; } ``` ### impl FromIterator for ActivePlayers ```rust impl FromIterator for ActivePlayers { } ``` ### impl IntoIterator for Unknown ```rust impl<'a> IntoIterator for Unknown { } ``` ## src/effects.rs ### EffectSystem ```rust pub trait EffectSystem { type State; type Effect; fn apply(&self, state: &mut Self::State, effect: &Self::Effect); fn react(&self, state: &mut Self::State, effect: &Self::Effect) -> Vec; } ``` A game that resolves effects through a queue. The move logic ([`Game::apply`](crate::Game::apply)) builds the initial effects of a move and calls [`resolve_effects`]; the queue does the rest. ### MAX_EFFECTS ```rust pub const MAX_EFFECTS: usize ``` Upper bound on effects resolved by one [`resolve_effects`] call, a guard against a game whose triggers never terminate. A well-formed game resolves in far fewer steps. ### resolve_effects ```rust pub fn resolve_effects(system: &S, state: &mut S::State, initial: impl Trait) -> usize ``` Resolves `initial` and everything it triggers, applying each effect then enqueuing its follow-ups, in FIFO order. Returns the number of effects resolved. Stops at [`MAX_EFFECTS`] to bound a non-terminating trigger loop (a game bug). ### impl EffectSystem for Dominoes ```rust impl EffectSystem for Dominoes { } ``` ## src/pile.rs ### Pile ```rust #[derive(Clone, PartialEq, Eq, Debug, Default)] pub struct Pile { } ``` An ordered pile of items with deterministic operations. The "top" is the end of the pile: [`draw`](Pile::draw) takes from the top and [`put`](Pile::put) adds to it. Shuffling is deterministic given a [`Prng`], and the pile carries no generator of its own, so it snapshots and replays with whatever state it lives in. This is an opt-in helper; a game's `State` can hold piles or plain fields as it prefers. ### impl Pile ```rust impl Pile { pub fn new() -> Self; pub fn from_items(items: Vec) -> Self; pub fn len(&self) -> usize; pub fn is_empty(&self) -> bool; pub fn draw(&mut self) -> Option; pub fn draw_n(&mut self, n: usize) -> Vec; pub fn put(&mut self, item: T); pub fn put_bottom(&mut self, item: T); pub fn insert(&mut self, index: usize, item: T); pub fn remove(&mut self, index: usize) -> Option; pub fn shuffle(&mut self, rng: &mut Prng); pub fn as_slice(&self) -> &[T]; pub fn iter(&self) -> std::slice::Iter<'_, T>; } ``` ### impl Pile ```rust impl Pile { pub fn contains(&self, item: &T) -> bool; pub fn position(&self, item: &T) -> Option; pub fn remove_item(&mut self, item: &T) -> Option; } ``` ### impl FromIterator for Pile ```rust impl FromIterator for Pile { } ``` ### impl IntoIterator for Unknown ```rust impl<'a, T> IntoIterator for Unknown { } ``` ## src/state.rs ### State ```rust #[derive(Clone, PartialEq, Eq, Debug)] pub struct State { } ``` State split into a public zone and per-player private zones, plus the match's random generator. Redaction is mechanical: a player's observation is the public zone plus *their own* private entry, so there is no field to forget to strip. The backing map is not exposed; games reach private data through the accessors. Using this type is a convenience, not a requirement (`Game::State` is an associated type and can be any shape) but it makes the common hidden-info game turnkey. The [`Prng`] lives here so it clones, serializes, and rewinds together with everything else: snapshot and resume are O(1), and a `Reversible` undo record can restore the stream position (see `ARCHITECTURE.md`). Games with their own `State` type should embed a [`Prng`] the same way. ### impl State ```rust impl State { pub fn new(public: P, seed: u64) -> Self; pub fn public(&self) -> &P; pub fn public_mut(&mut self) -> &mut P; pub fn insert_private(&mut self, player: PlayerId, value: Q) -> Option; pub fn private(&self, player: PlayerId) -> Option<&Q>; pub fn private_mut(&mut self, player: PlayerId) -> Option<&mut Q>; pub fn remove_private(&mut self, player: PlayerId) -> Option; pub fn rng(&self) -> &Prng; pub fn rng_mut(&mut self) -> &mut Prng; } ``` ### impl State ```rust impl State { pub fn view_for(&self, viewer: Option) -> PlayerView; } ``` ### PlayerView ```rust #[derive(Clone, PartialEq, Eq, Debug)] pub struct PlayerView { pub public: P, pub own_private: Option, } ``` The standard observation produced by [`State::view_for`]: the public zone and the viewer's own private zone, if any. ## src/rng.rs ### Prng ```rust #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct Prng { } ``` A small, deterministic pseudo-random generator that lives inside game state. The whole reproducible position is a single `u64` ([`Prng::position`]), so the generator is `Copy`, serializes with the state it lives in (O(1) snapshot and resume), and a `Reversible` game's undo record can restore its position exactly. Everything is integer-only with a fixed algorithm, so a seed reproduces the same stream on every platform. Do not rely on the concrete algorithm (currently PCG XSH-RR 64/32, single stream): it is an implementation detail of this newtype and may change. This is not cryptographically secure and must not be used for security. ### impl Prng ```rust impl Prng { pub fn new(seed: u64) -> Self; pub fn position(&self) -> u64; pub fn set_position(&mut self, position: u64); pub fn next_u32(&mut self) -> u32; pub fn next_u64(&mut self) -> u64; pub fn below(&mut self, bound: u64) -> u64; pub fn range(&mut self, low: u64, high: u64) -> u64; pub fn choose<'a, T>(&mut self, items: &'a [T]) -> Option<&'a T>; pub fn shuffle(&mut self, items: &mut [T]); } ``` ## src/serde_roundtrip.rs ### impl Game for Roll ```rust impl Game for Roll { } ```