开源 · 0.13.0 · 尚未到 1.0

参伍引擎

Canwu Engine

构建可重放的历史模拟,让每个玩家只看到自己角色知道的世界。

参伍是用 Rust 编写的无界面模拟引擎。你的代码向它提交命令,它负责推进时间、记录发生了什么以及原因,并跟踪每个角色知道什么。渲染、界面和历史内容仍由你的游戏、研究工具或智能体系统负责。

重放
相同输入,相同结果
知识
每个角色一份视图
渲染
任选引擎或界面
智能体
与玩家共用 API
starter 示例starter.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(())
}
在 GitHub 打开 starter.rs ↗

架构

参伍在你的应用中的位置

上层应用在最上层,只与 canwu-api 交互。领域插件提供资源、移动、法律等规则。下方的运行时持有实时世界状态,只在结算命令和调度工作时修改它。你可以逐层查看,也可以跟随一条命令,从提交一路看到重放。
  1. 对外 API 边界 · 这条线以上的代码只使用 canwu-api

  2. 引擎内部 · 只能经由 canwu-api 访问

上层应用

负责渲染、输入、现实时间、账号与历史内容。它依赖 canwu-api,注册需要的插件,提交命令并读取结果。

仓库中的参考客户端

  • canwu-debug基于对外 API 与参考整合包的桌面调试客户端(egui)

领域插件

模拟领域扩展以模拟插件的形式提供可复用规则,它们都建立在 canwu-api 之上。按需注册,也可以自己编写。参考内容包提供数据,参考整合包把这些部件组合成可以直接运行、也可以复制改造的小型世界。

模拟领域扩展(已发布到 crates.io)

参考内容包

参考整合包(仅在仓库中,未发布)

编写自己的插件 →

canwu-api

应用依赖的引擎 crate,扩展 crate 也建立在它之上。其中的 Canwu 类型负责创建和推进模拟运行、接收命令、返回独立快照和角色相对视图,并支持存档、派生分支与重放。下层 crate 中需要的类型都由它重新导出。

主要调用

接入参伍 →

canwu-sim

持有权威状态。它把收到的命令放入输入队列,运行调度工作,并以边界为单位结算到期工作:边界是某个模拟时间点上的一次整体结算,按 14 个固定阶段运行;任何一步失败,整个边界回滚。它还记录存档和重放所需的证据与哈希。应用只通过 canwu-api 访问它。

crate

  • canwu-sim运行时、调度、结算、插件、持久化、哈希与重放
边界如何结算 →

模型与机制

插件和应用经由 canwu-api 使用的数据类型:发生了什么以及原因、每个角色知道什么、待定的决策,以及路线与运输记录。运行时本身使用其中的事件、知识和决策类型。

crate

事件与因果 →

基础层

类型化 ID、确定性随机数、schema 元数据与模拟时间运算。其他所有 crate 都建立在这两个 crate 之上。

crate

  • canwu-core稳定 ID、确定性随机数与 schema 基础类型
  • canwu-time模拟时间与带溢出检查的时长运算
可重放的随机性 →
  1. 第 1 步,共 7 步

    构造命令

    客户端构造一个类型化命令。在 starter 示例中,参考世界把 MovementCommand 包装成命令信封。

    let envelope = order_movement(Issuer::Actor(commander), &command)?;
  2. 第 2 步,共 7 步

    排入队列

    enqueue_command 把请求按到期时间放入输入队列,并返回回执。原样重发同一请求会得到同一张回执;同一请求 ID 配不同内容会失败。此时世界状态还没有变化。

    canwu.enqueue_command(due_at, priority, CommandRequest::new(id, revision, envelope))?;
  3. 第 3 步,共 7 步

    推进时间

    advance_canonical 推进模拟时间。每一批到期的工作都在一个边界中结算。

    canwu.advance_canonical(SimDuration::hours(19))?;
  4. 第 4 步,共 7 步

    结算边界

    引擎检查签发者的权限、命令所基于的修订版本以及预期时间,被拒绝的命令连同原因一起记录。随后插件系统按 14 个固定阶段运行并提出变更,这些变更一起提交,或一起回滚。

    14 个结算阶段 →
  5. 第 5 步,共 7 步

    记录结果

    提交的变更生成带有因果与受众的事件。角色知识与报告随之更新,重放日志也多记录一个边界。

  6. 第 6 步,共 7 步

    读取结果

    玩家或智能体通过 viewer_for_actor 读取,只能看到该角色知道的内容。受信任的上层应用工具还可以用 events() 列出事件,用 explain() 查询原因。

    let viewer = canwu.viewer_for_actor(actor)?;
    安全读取状态 →
  7. 第 7 步,共 7 步

    存档与重放

    snapshot_json 保存模拟运行,replay_journal 返回它记录的全部输入。replay_from_journal 根据这些输入重建运行;检查点哈希相同,说明重放完全一致。

    let replayed = Canwu::replay_from_journal(&[&plugin], &canwu.replay_journal())?;
    assert_eq!(replayed.checkpoint_hash(), canwu.checkpoint_hash());
    存档、重放与派生分支 →
每一层只使用本层和下方各层。选择某一层可查看其中的 crate;切换到“跟随一条命令”后,可用箭头逐步查看。
阅读架构指南 →

设计保证

引擎提供的三项保证

引擎负责时间、状态与历史,插件负责特定时代的规则。对所有客户端,以及只通过声明契约读写状态、抽取随机数的插件,下面三点都成立。
replay

确定性重放

时间、调度工作和随机抽样都按固定顺序执行。根据重放日志可以重建一次模拟运行,检查点哈希用来确认结果完全一致。

knowledge

每个角色所知不同

世界的真实状态与角色知识分开保存。每条观察都记录来源、可信度和信息时效,因此角色可能依据迟到或有误的报告行事。

commands

所有客户端共用一套 API

客户端只通过提交命令改变世界,通过快照、事件或角色视图读取世界。游戏、研究笔记本和 AI 智能体调用的是同一组接口。

安装

添加 canwu-api 0.13.0

需要 Rust 1.88 或更新版本。在应用的 Cargo.toml 中添加 canwu-api。示例使用的参考世界位于仓库中,克隆仓库即可运行示例。使用 = 精确锁定版本,是因为存档只能由写入它的引擎版本加载。
Cargo.toml[dependencies] canwu-api = "=0.13.0"

五分钟上手

运行 starter 示例

starter 用一条类型化命令让军队移动,并推进 19 小时。随后它对这次运行做存档与读档、派生分支和重放,并检查三份副本的检查点哈希都与原运行一致。

克隆并运行

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

预期输出

army_location=3 checkpoint=dd9796a606983d02eafd3495df11b20bb8423cec33f96c0eaddc24e44e4e2de1
查看分步教程

AI 智能体

智能体与玩家使用同一套 API

智能体读取限定在其角色范围内的视图,提交与玩家相同的类型化命令,也只能看到该角色所知范围内的规则说明。每个领域插件定义自己的命令和视图。
viewer_for_actor(actor)viewer.query_knowledge(query)viewer.visible_changes_since(time)viewer.evaluation_traces(subject, after)enqueue_command(...)advance_canonical(...)
智能体循环actor-scoped
// 1. 读取该角色知道的内容
let viewer = canwu.viewer_for_actor(actor)?;
let knowledge = viewer.query_knowledge(&query)?;

// 2. 作出决定,提交类型化命令
canwu.enqueue_command(time, priority, request)?;
canwu.advance_canonical(duration)?;

// 3. 查看该角色能看到的变化
let changes = canwu.viewer_for_actor(actor)?.visible_changes_since(since);

应用场景

可以用它构建什么

查看集成模式 →

大战略游戏

在任意地图或渲染器下运行模拟。

历史研究

运行可重复的场景,派生平行现实并比较结果。

智能体环境

为多个智能体提供不同的知识、权限与命令。

教育与公众史学

在可重放的状态之上构建时间线、地图与课堂工具。

参与贡献

一起完善参伍

先阅读架构说明和贡献指南,再提交议题或范围明确的拉取请求。欢迎改进对外 API、文档、测试和参考场景。