HerdingCats
A deterministic, turn-based game engine for Rust.
Overview
HerdingCats brings order to complex rule interactions by evaluating an input through a
statically known, ordered set of Behaviors. The engine dispatches each Input atomically:
behaviors run in deterministic (order_key, name) order, each contributing Diffs to a
speculative working State. If dispatch succeeds and produces at least one Diff, the engine
commits a Frame and returns Outcome::Committed. If anything stops dispatch early, the
working State is discarded and no committed state changes occur.
The engine never advances on its own. It only transitions when the caller submits an Input or explicitly calls undo/redo.
Core Concepts
Input
A user-defined message submitted to the engine. Every engine transition begins with an
Input. Examples: Move, Attack, BeginTurn, RollDice.
State
The full game state, user-defined. Provided to Engine::new() — the engine does not
require a Default implementation. Behavior-local state lives inside the main State tree
(not hidden inside the engine), ensuring undo/redo correctness and serializability.
Behavior
A single game rule expressed as a BehaviorDef struct with a name, order_key, and
evaluate fn pointer. During dispatch the engine calls each entry's evaluate fn pointer
with an immutable borrow of the current working State and the Input, then either:
- returns
Continue(diffs)— zero or more Diffs to apply, dispatch continues - returns
Stop(outcome)— halts dispatch immediately with a non-committed Outcome
BehaviorDef entries are sorted once at construction by (order_key, name) and run in
that order for every dispatch call. The shared borrow prevents mutation — behaviors cannot
mutate State directly.
Diff
An atomic, user-defined description of a state mutation. Behaviors emit Diffs; the engine
applies them. Each Diff implements the Apply trait, which mutates the working State and
returns at least one Trace entry. Applied immediately and sequentially — later Behaviors
see earlier Diffs.
Trace
Per-Diff metadata generated at the exact moment each Diff is applied. Trace exists only for successful committed transitions and records what happened in execution order. Useful for UI presentation, replay explanation, and isolating UI logic from direct State inspection.
Frame
The canonical record of a successful, non-empty state transition:
Frame
Frames are immutable records. The same Frame struct is returned by Committed, Undone,
and Redone outcomes — the variant carries the meaning, not the frame.
Outcome
The result of every dispatch, undo, or redo call:
Engine
The central coordinator. Holds the committed State, the ordered list of BehaviorDef
entries, and the undo/redo history stacks. Exposes dispatch, undo, and redo.
How Dispatch Works
- Caller submits an Input with a
Reversibilitydeclaration (ReversibleorIrreversible). - Engine creates a copy-on-write working State — no clone until the first Diff is applied.
- Behaviors are evaluated in deterministic
(order_key, behavior_name)order. - Each Behavior either emits Diffs (
Continue) or halts dispatch (Stopwith aNonCommittedOutcome). - Diffs are applied immediately to the working State; Trace entries are appended at application time.
- If any Behavior returns
Stop, the working State is discarded and the corresponding non-committed Outcome is returned — no state change is visible. - If dispatch completes with no Diffs,
Outcome::NoChangeis returned. - If one or more Diffs were applied, the working State is committed atomically, a Frame
is created, and
Outcome::Committed(frame)is returned.
Undo / Redo
Undo and redo operate on stored Frames, not reverse-diff computations. No Reversible
trait is required on your types — the engine stores a prior-State snapshot alongside each
Frame. Irreversible dispatches erase undo/redo history across that boundary (once dice
are rolled, you cannot undo before the roll).
Quick Start
use ;
// 1. Define your spec (bundles all associated types)
;
;
// 2. Implement Apply so each Diff can mutate State and emit Trace
// 3. Define a BehaviorDef with an evaluate fn pointer
// 4. Construct the Engine with initial State and BehaviorDef entries
let behaviors = vec!;
let mut engine = new;
// 5. Dispatch an Input
let outcome = engine.dispatch.unwrap;
match outcome
Architecture Status (MVP)
v0.5.0 implements the full MVP architecture described in ARCHITECTURE.md:
- Static behavior set via
BehaviorDefstructs (fn pointers, no trait objects) - Copy-on-write working state (zero-clone until first diff)
- Snapshot-based undo/redo (no
Reversibletrait burden) - Deterministic
(order_key, name)behavior ordering Applytrace contract enforced bydebug_assert!in dispatch
Zero Dependencies
HerdingCats has no runtime dependencies. proptest is a dev-only dependency used for
property-based testing.