pub struct Engine<S, M, I, P>{
pub state: S,
/* private fields */
}Expand description
The runtime that manages game state, behaviors, and commit history.
Engine<S, M, I, P> is the central coordinator: it holds the current
state S, a sorted list of Behaviors, and an undo/redo stack of commit
frames. To advance state, callers construct an Action and pass
it — along with an event value — to dispatch. The
engine runs all enabled behaviors in priority order, applies the resulting
mutations, and records the frame for undo. Direct mutation of
state is possible but bypasses the behavior system; use
write for intentional resets.
Fields§
§state: SThe current committed game state.
Readable directly for ergonomic access in non-behavior contexts. Prefer
read when you want a clone rather than a borrow.
Direct mutation of this field bypasses the behavior system and undo
stack — use write for intentional state resets
(e.g., loading a saved game), which also clears both stacks and
resets the replay_hash.
Implementations§
Source§impl<S, M, I, P> Engine<S, M, I, P>
impl<S, M, I, P> Engine<S, M, I, P>
Sourcepub fn new(state: S) -> Self
pub fn new(state: S) -> Self
Create a new engine with the given initial state and no behaviors.
The undo and redo stacks start empty. replay_hash is initialized to
FNV_OFFSET (the hash of an empty sequence). Add behaviors with
add_behavior before dispatching events.
§Examples
use herdingcats::{Engine, Mutation};
#[derive(Clone)]
enum CounterOp { Inc }
impl Mutation<i32> for CounterOp {
fn apply(&self, s: &mut i32) { *s += 1; }
fn undo(&self, s: &mut i32) { *s -= 1; }
fn hash_bytes(&self) -> Vec<u8> { vec![1] }
}
let engine: Engine<i32, CounterOp, (), u8> = Engine::new(0i32);
assert_eq!(engine.state, 0);Sourcepub fn replay_hash(&self) -> u64
pub fn replay_hash(&self) -> u64
Return the current replay hash — a running fingerprint over all committed, deterministic mutations.
The replay hash is updated on every dispatch call
where the action is deterministic. Two engine instances that have
processed the same sequence of deterministic mutations will have
identical replay hashes, regardless of any non-deterministic commits
in between.
§Examples
use herdingcats::{Engine, Mutation, Action};
#[derive(Clone)]
enum CounterOp { Inc }
impl Mutation<i32> for CounterOp {
fn apply(&self, s: &mut i32) { *s += 1; }
fn undo(&self, s: &mut i32) { *s -= 1; }
fn hash_bytes(&self) -> Vec<u8> { vec![1] }
}
let mut engine: Engine<i32, CounterOp, (), u8> = Engine::new(0);
let hash_before = engine.replay_hash();
let mut tx = Action::new();
tx.mutations.push(CounterOp::Inc);
engine.dispatch((), tx);
assert_ne!(engine.replay_hash(), hash_before);Sourcepub fn add_behavior<B>(&mut self, behavior: B)where
B: Behavior<S, M, I, P> + 'static,
pub fn add_behavior<B>(&mut self, behavior: B)where
B: Behavior<S, M, I, P> + 'static,
Register a behavior with this engine.
The behavior is inserted into the sorted behavior list (sorted by
priority ascending). The behavior’s
active state is managed by the behavior itself via
is_active.
§Examples
use herdingcats::{Engine, Mutation, Behavior, Action};
#[derive(Clone)]
enum CounterOp { Inc }
impl Mutation<i32> for CounterOp {
fn apply(&self, s: &mut i32) { *s += 1; }
fn undo(&self, s: &mut i32) { *s -= 1; }
fn hash_bytes(&self) -> Vec<u8> { vec![1] }
}
struct NoRule;
impl Behavior<i32, CounterOp, (), u8> for NoRule {
fn id(&self) -> &'static str { "no_rule" }
fn priority(&self) -> u8 { 0 }
}
let mut engine: Engine<i32, CounterOp, (), u8> = Engine::new(0);
engine.add_behavior(NoRule);Sourcepub fn dispatch_preview(&mut self, event: I, tx: Action<M>)
pub fn dispatch_preview(&mut self, event: I, tx: Action<M>)
Run the full dispatch pipeline on event and tx without committing
any changes to state, replay hash, or behavior lifetimes.
dispatch_preview is a dry run: all active behaviors fire their before
and after hooks, mutations are applied, but everything is rolled back
at the end. This is useful for AI look-ahead, UI preview of pending
moves, or testing behavior interactions without side effects.
§Examples
use herdingcats::{Engine, Mutation, Behavior, Action};
#[derive(Clone)]
enum CounterOp { Inc }
impl Mutation<i32> for CounterOp {
fn apply(&self, s: &mut i32) { *s += 1; }
fn undo(&self, s: &mut i32) { *s -= 1; }
fn hash_bytes(&self) -> Vec<u8> { vec![1] }
}
struct NoRule;
impl Behavior<i32, CounterOp, (), u8> for NoRule {
fn id(&self) -> &'static str { "no_rule" }
fn priority(&self) -> u8 { 0 }
}
let mut engine: Engine<i32, CounterOp, (), u8> = Engine::new(0);
engine.add_behavior(NoRule);
let mut tx = Action::new();
tx.mutations.push(CounterOp::Inc);
engine.dispatch_preview((), tx);
// State is unchanged after preview
assert_eq!(engine.state, 0);Sourcepub fn dispatch(&mut self, event: I, tx: Action<M>)
pub fn dispatch(&mut self, event: I, tx: Action<M>)
Dispatch event through all active behaviors, apply the resulting
mutations, and push a CommitFrame onto the undo stack if the action
is reversible.
Behaviors fire in ascending priority order during before(), then
descending order during after(). If any behavior sets tx.cancelled = true, the mutations are not applied and no frame is committed.
If all mutations return is_reversible() == true, a CommitFrame is
pushed and the redo stack is cleared. If any mutation is irreversible,
both the undo and redo stacks are cleared (undo barrier). After a
successful commit, on_dispatch() is called on all behaviors.
§Examples
use herdingcats::{Engine, Mutation, Behavior, Action};
#[derive(Clone)]
enum CounterOp { Inc }
impl Mutation<i32> for CounterOp {
fn apply(&self, s: &mut i32) { *s += 1; }
fn undo(&self, s: &mut i32) { *s -= 1; }
fn hash_bytes(&self) -> Vec<u8> { vec![1] }
}
struct NoRule;
impl Behavior<i32, CounterOp, (), u8> for NoRule {
fn id(&self) -> &'static str { "no_rule" }
fn priority(&self) -> u8 { 0 }
}
let mut engine: Engine<i32, CounterOp, (), u8> = Engine::new(0);
engine.add_behavior(NoRule);
let mut tx = Action::new();
tx.mutations.push(CounterOp::Inc);
engine.dispatch((), tx);
assert_eq!(engine.state, 1);Sourcepub fn undo(&mut self)
pub fn undo(&mut self)
Reverse the most recent commit, restoring state to its value before
that commit, and calling on_undo() on all behaviors.
If the undo stack is empty, this is a no-op. The undone frame is moved
to the redo stack so redo can re-apply it.
§Examples
use herdingcats::{Engine, Mutation, Behavior, Action};
#[derive(Clone)]
enum CounterOp { Inc }
impl Mutation<i32> for CounterOp {
fn apply(&self, s: &mut i32) { *s += 1; }
fn undo(&self, s: &mut i32) { *s -= 1; }
fn hash_bytes(&self) -> Vec<u8> { vec![1] }
}
struct NoRule;
impl Behavior<i32, CounterOp, (), u8> for NoRule {
fn id(&self) -> &'static str { "no_rule" }
fn priority(&self) -> u8 { 0 }
}
let mut engine: Engine<i32, CounterOp, (), u8> = Engine::new(0);
engine.add_behavior(NoRule);
let mut tx = Action::new();
tx.mutations.push(CounterOp::Inc);
engine.dispatch((), tx);
assert_eq!(engine.state, 1);
engine.undo();
assert_eq!(engine.state, 0);Sourcepub fn redo(&mut self)
pub fn redo(&mut self)
Re-apply the most recently undone commit, advancing state forward again,
and calling on_dispatch() on all behaviors (redo is a forward operation).
If the redo stack is empty, this is a no-op. The redone frame is moved
back to the undo stack. Note that calling dispatch
clears the redo stack — once you commit a new action, the redo history
for the previous branch is discarded.
§Examples
use herdingcats::{Engine, Mutation, Behavior, Action};
#[derive(Clone)]
enum CounterOp { Inc }
impl Mutation<i32> for CounterOp {
fn apply(&self, s: &mut i32) { *s += 1; }
fn undo(&self, s: &mut i32) { *s -= 1; }
fn hash_bytes(&self) -> Vec<u8> { vec![1] }
}
struct NoRule;
impl Behavior<i32, CounterOp, (), u8> for NoRule {
fn id(&self) -> &'static str { "no_rule" }
fn priority(&self) -> u8 { 0 }
}
let mut engine: Engine<i32, CounterOp, (), u8> = Engine::new(0);
engine.add_behavior(NoRule);
let mut tx = Action::new();
tx.mutations.push(CounterOp::Inc);
engine.dispatch((), tx);
engine.undo();
assert_eq!(engine.state, 0);
engine.redo();
assert_eq!(engine.state, 1);Sourcepub fn read(&self) -> S
pub fn read(&self) -> S
Return a clone of the current state.
Use read when you need an owned snapshot rather than borrowing
engine.state directly. This is the idiomatic way to hand state to
code that needs ownership (e.g., serialization, AI evaluation).
§Examples
use herdingcats::{Engine, Mutation};
#[derive(Clone)]
enum CounterOp { Inc }
impl Mutation<i32> for CounterOp {
fn apply(&self, s: &mut i32) { *s += 1; }
fn undo(&self, s: &mut i32) { *s -= 1; }
fn hash_bytes(&self) -> Vec<u8> { vec![1] }
}
let engine: Engine<i32, CounterOp, (), u8> = Engine::new(42i32);
let snapshot = engine.read();
assert_eq!(snapshot, 42);Sourcepub fn write(&mut self, snapshot: S)
pub fn write(&mut self, snapshot: S)
Replace the engine’s state with snapshot and reset all history.
write clears both the undo and redo stacks and resets replay_hash
to its initial value. Use it for intentional state resets — loading a
saved game, starting a new round — where you want to discard all prior
history. Unlike direct mutation of engine.state, write guarantees
the stacks and hash are coherent with the new state.
§Examples
use herdingcats::{Engine, Mutation, Behavior, Action};
#[derive(Clone)]
enum CounterOp { Inc }
impl Mutation<i32> for CounterOp {
fn apply(&self, s: &mut i32) { *s += 1; }
fn undo(&self, s: &mut i32) { *s -= 1; }
fn hash_bytes(&self) -> Vec<u8> { vec![1] }
}
struct NoRule;
impl Behavior<i32, CounterOp, (), u8> for NoRule {
fn id(&self) -> &'static str { "no_rule" }
fn priority(&self) -> u8 { 0 }
}
let mut engine: Engine<i32, CounterOp, (), u8> = Engine::new(0);
engine.add_behavior(NoRule);
let mut tx = Action::new();
tx.mutations.push(CounterOp::Inc);
engine.dispatch((), tx);
assert_eq!(engine.state, 1);
// Reset to a fresh state — undo history is cleared
engine.write(100);
assert_eq!(engine.state, 100);
let hash_after_write = engine.replay_hash();
// replay_hash is back to its initial value
let fresh_engine: Engine<i32, CounterOp, (), u8> = Engine::new(0);
assert_eq!(hash_after_write, fresh_engine.replay_hash());Sourcepub fn can_undo(&self) -> bool
pub fn can_undo(&self) -> bool
Return true if there is at least one commit on the undo stack.
Returns false immediately after an irreversible commit (the undo
barrier), since dispatch clears the undo stack when the action
contains an irreversible mutation. Also returns false on a fresh
engine before any dispatches.
§Examples
use herdingcats::{Engine, Mutation, Behavior, Action};
#[derive(Clone)]
enum CounterOp { Inc }
impl Mutation<i32> for CounterOp {
fn apply(&self, s: &mut i32) { *s += 1; }
fn undo(&self, s: &mut i32) { *s -= 1; }
fn hash_bytes(&self) -> Vec<u8> { vec![1] }
fn is_reversible(&self) -> bool { false }
}
struct NoRule;
impl Behavior<i32, CounterOp, (), u8> for NoRule {
fn id(&self) -> &'static str { "no_rule" }
fn priority(&self) -> u8 { 0 }
fn before(&self, _s: &i32, _e: &mut (), tx: &mut Action<CounterOp>) {
tx.mutations.push(CounterOp::Inc);
}
}
let mut engine: Engine<i32, CounterOp, (), u8> = Engine::new(0);
engine.add_behavior(NoRule);
assert!(!engine.can_undo());
engine.dispatch((), Action::new());
// Irreversible commit — undo barrier clears the stack
assert!(!engine.can_undo());