Skip to main content

Engine

Struct Engine 

Source
pub struct Engine<S, M, I, P>
where S: Clone, M: Mutation<S>, P: Copy + Ord,
{ 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: S

The 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>
where S: Clone, M: Mutation<S>, P: Copy + Ord,

Source

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);
Source

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);
Source

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);
Source

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);
Source

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);
Source

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);
Source

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);
Source

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);
Source

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());
Source

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());
Source

pub fn can_redo(&self) -> bool

Return true if there is at least one commit on the redo stack.

Returns false on a fresh engine, after write, or after any new forward dispatch (which clears the redo stack).

Auto Trait Implementations§

§

impl<S, M, I, P> Freeze for Engine<S, M, I, P>
where S: Freeze,

§

impl<S, M, I, P> !RefUnwindSafe for Engine<S, M, I, P>

§

impl<S, M, I, P> !Send for Engine<S, M, I, P>

§

impl<S, M, I, P> !Sync for Engine<S, M, I, P>

§

impl<S, M, I, P> Unpin for Engine<S, M, I, P>
where S: Unpin, M: Unpin,

§

impl<S, M, I, P> UnsafeUnpin for Engine<S, M, I, P>
where S: UnsafeUnpin,

§

impl<S, M, I, P> !UnwindSafe for Engine<S, M, I, P>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.