open source · 0.13.0 · pre-1.0

Canwu Engine

参伍引擎

Build replayable historical simulations where each player sees only what their character knows.

Canwu is a headless simulation engine written in Rust. Your code sends it commands; it advances time, records what happened and why, and tracks what each character knows. Rendering, UI, and historical content stay in your game, research tool, or agent system.

Replay
Same inputs, same result
Knowledge
A separate view per character
Rendering
Use any engine or UI
Agents
Same API as players
The starter examplestarter.rs
use canwu_api::{Canwu, CommandRequest, CommandRequestId, EntityRef, Issuer, SimDuration};
use canwu_reference_world::{
    MovementCommand, ReferenceWorldPlugin, demo_scenario, order_movement,
    snapshot as reference_snapshot,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (scenario, ids) = demo_scenario()?;
    let plugin = ReferenceWorldPlugin;
    let mut canwu = Canwu::new_with_plugins(35, scenario, &[&plugin])?;

    let envelope = order_movement(
        Issuer::Actor(ids.commander),
        &MovementCommand {
            subject: EntityRef::Army(ids.army),
            destination: ids.eastern_territory,
            cargo: Vec::new(),
        },
    )?
    .at_time(canwu.time());
    canwu.enqueue_command(
        canwu.time(),
        0,
        CommandRequest::new(CommandRequestId::new(1), canwu.revision(), envelope),
    )?;
    canwu.advance_canonical(SimDuration::hours(19))?;

    let saved = canwu.snapshot_json()?;
    let loaded = Canwu::from_snapshot_json_with_plugins(&saved, &[&plugin])?;
    let fork = loaded.fork();
    let journal = canwu.replay_journal();
    let replayed = Canwu::replay_from_journal(&[&plugin], &journal)?;

    assert_eq!(loaded.checkpoint_hash(), canwu.checkpoint_hash());
    assert_eq!(fork.checkpoint_hash(), canwu.checkpoint_hash());
    assert_eq!(replayed.checkpoint_hash(), canwu.checkpoint_hash());
    println!(
        "army_location={} checkpoint={}",
        reference_snapshot(&replayed)?
            .army(ids.army)
            .expect("demo army exists")
            .location,
        replayed.checkpoint_hash()
    );
    Ok(())
}
Open starter.rs on GitHub ↗

Architecture

How Canwu fits into your application

Your application sits on top and talks only to canwu-api. Domain plugins add rules such as resources, movement, or law. The runtime underneath holds the live world and changes it only while settling commands and scheduled work. Select a layer, or follow one command from submission to replay.
  1. Public API boundary · code above this line uses only canwu-api

  2. Engine internals · reached only through canwu-api

Your application

Owns rendering, input, the real-time clock, accounts, and historical content. It depends on canwu-api, registers the plugins it needs, submits commands, and reads the results.

Reference client in this repository

  • canwu-debugDesktop debug client (egui) built on the public API and the reference integrations

Domain plugins

Domain extensions add reusable rules as simulation plugins built on canwu-api. Register only the ones you need, or write your own. Reference content packs supply data, and reference integrations combine the pieces into small worlds you can run and copy.

Domain extensions (published on crates.io)

Reference content packs

Reference integrations (repository only, not published)

Write your own plugin →

canwu-api

The engine crate your application depends on; extension crates build on it too. Its Canwu type creates and advances a run, accepts commands, returns detached snapshots and actor-relative views, and saves, forks, and replays runs. It re-exports the types you need from the crates below.

Main calls

Integrate Canwu →

canwu-sim

Holds the authoritative state. It queues incoming commands, runs scheduled work, and settles due work in boundaries: one all-or-nothing pass at one simulation time, run in 14 fixed phases. If anything fails, the whole boundary rolls back. It also records the evidence and hashes used for saves and replay. Applications reach it only through canwu-api.

Crate

  • canwu-simRuntime, scheduling, settlement, plugins, persistence, hashing, and replay
How a boundary is settled →

Models and mechanisms

Data types that plugins and applications reach through canwu-api: what happened and why, what each actor knows, pending decisions, and route and transport records. The runtime itself uses the event, knowledge, and decision types.

Crates

Events and causality →

Foundation

Typed IDs, deterministic random numbers, schema metadata, and simulation time arithmetic. Every other crate builds on these two.

Crates

  • canwu-coreStable IDs, deterministic random numbers, and schema primitives
  • canwu-timeSimulation time and checked duration arithmetic
Replayable randomness →
  1. Step 1 of 7

    Build a command

    Your client creates a typed command. In the starter example, the reference world wraps a MovementCommand into a command envelope.

    let envelope = order_movement(Issuer::Actor(commander), &command)?;
  2. Step 2 of 7

    Queue it

    enqueue_command puts the request in the ingress queue for its due time and returns a receipt. Resending the identical request returns the same receipt; reusing its ID with different content fails. The world has not changed yet.

    canwu.enqueue_command(due_at, priority, CommandRequest::new(id, revision, envelope))?;
  3. Step 3 of 7

    Advance time

    advance_canonical moves simulation time forward. Each batch of work that falls due is settled in one boundary.

    canwu.advance_canonical(SimDuration::hours(19))?;
  4. Step 4 of 7

    Settle the boundary

    The engine checks the issuer's authority, the revision the command was based on, and its expected time. A rejected command is recorded with its reason. Plugin systems then run in 14 fixed phases and propose changes, which commit together or roll back together.

    The 14 phases →
  5. Step 5 of 7

    Record what happened

    Committed changes produce events that carry their cause and audience. Characters' knowledge and reports are updated, and the replay journal gains one more boundary.

  6. Step 6 of 7

    Read it back

    A player or agent reads through viewer_for_actor and sees only what that actor knows. Trusted host tools can also list events() and ask explain() why something happened.

    let viewer = canwu.viewer_for_actor(actor)?;
    Reading state safely →
  7. Step 7 of 7

    Save and replay

    snapshot_json saves the run, and replay_journal returns its recorded inputs. replay_from_journal rebuilds the run from those inputs; equal checkpoint hashes show the replay is exact.

    let replayed = Canwu::replay_from_journal(&[&plugin], &canwu.replay_journal())?;
    assert_eq!(replayed.checkpoint_hash(), canwu.checkpoint_hash());
    Save, replay, and fork →
Each layer uses only its own layer and the layers below it. Select a layer to see its crates, or switch to Follow a command and use the arrows.
Read the architecture guide →

Guarantees

Three things the engine guarantees

The engine owns time, state, and history; your plugins own the rules of your period. The engine enforces these three properties for every client, and for every plugin that reads, writes, and draws randomness only through its declared contract.
replay

Deterministic replay

Time, scheduled work, and random draws run in a fixed order. Replaying a run's journal rebuilds it, and the checkpoint hash confirms the result is identical.

knowledge

Each character knows different things

The true state of the world is kept apart from what each character knows. Each observation records its source, confidence, and age, so a character can act on a report that is late or wrong.

commands

One API for every client

Clients change the world only by submitting commands, and read it through snapshots, events, or a character's view. A game, a research notebook, and an AI agent use the same calls.

Install

Add canwu-api 0.13.0

Requires Rust 1.88 or newer. Add canwu-api to your application's Cargo.toml. The reference world used by the examples lives in the repository; clone it to run the examples. The exact = pin matters because a save loads only in the engine version that wrote it.
Cargo.toml[dependencies] canwu-api = "=0.13.0"

Five-minute check

Run the starter example

The starter moves an army with a typed command and advances 19 hours. It then saves and reloads, forks, and replays the run, and checks that all three copies match the original's checkpoint hash.

Clone and run

git clone https://github.com/PeiyuanQi/canwu
cd canwu
cargo run -p canwu-reference-world --example starter

Expected output

army_location=3 checkpoint=dd9796a606983d02eafd3495df11b20bb8423cec33f96c0eaddc24e44e4e2de1
Step-by-step tutorial

AI agents

Agents use the same API as players

An agent reads a view limited to its character, submits the same typed commands a player would, and sees rule explanations only for what that character may know. Each domain plugin defines its own commands and views.
viewer_for_actor(actor)viewer.query_knowledge(query)viewer.visible_changes_since(time)viewer.evaluation_traces(subject, after)enqueue_command(...)advance_canonical(...)
Agent loopactor-scoped
// 1. Read what this character knows
let viewer = canwu.viewer_for_actor(actor)?;
let knowledge = viewer.query_knowledge(&query)?;

// 2. Decide, then submit a typed command
canwu.enqueue_command(time, priority, request)?;
canwu.advance_canonical(duration)?;

// 3. See what changed from this character's point of view
let changes = canwu.viewer_for_actor(actor)?.visible_changes_since(since);

Use cases

What you can build

See integration patterns →

Grand strategy games

Run the simulation behind any map or renderer.

Historical research

Run repeatable scenarios, branch alternative histories, and compare the results.

Agent environments

Give several agents different knowledge, authority, and commands.

Education and public history

Build timelines, maps, and classroom tools on replayable state.

Contribute

Help improve Canwu

Start with the architecture guide and the contribution guide, then open an issue or a focused pull request. Improvements to the public API, docs, tests, and reference scenarios are all welcome.