明代财政案例
明代一个财政衙门开出 100 单位的应征额,授权征收其中 70,然后等待这 70 实际入库的证明。资源适配器报告征收完成后,财政扩展记下一张凭证,户部尚书随即收到一份估计:应征多少、已征多少、还欠多少。三个起点在不同的历史背景下跑同一套流程:洪武 1391(江南的登记土地、徭役和漕粮负担)、万历 1581(一条鞭法在四个地区处于不同阶段)、弘光 1644(南明的五个独立财政权责方)。
这个示例回答的问题是:法规随时期和地区变化的税制,怎样在游戏里建模,同时让账目和实际流动的粮食、白银分开?
法规、地区、时期和采纳阶段来自带出处的参考内容包。数量、人物和机构都是示意值,取自一个小型的合成参考世界。
示例涉及的引擎部件
Section titled “示例涉及的引擎部件”- 财政模拟扩展
canwu-fiscal(FiscalPlugin):财政法规及其地区采纳、核算、执行授权和凭证。资源余额和实际转移仍归资源或物流领域管理。 - 参考内容包
canwu-ming-fiscal:data/pack.json中的时期、地区、法规、改革转型和出处,以及作为 fixture 文件的三个起点。 - 参考整合包
canwu-ming-fiscal-reference:在canwu-reference-world上搭建每个起点,注册插件,运行示例流程并写出 trace。 - 以领域命令(
Command::Plugin)提交的财政行动,由FiscalAuthorityBinding校验权限。 - 财政执行凭证(
FiscalExecutionReceiptPacket):依据适配器写入的领域记录的精确版本结算。 - 户部尚书的报告:以持有人相对知识发布,每个总数都是一个区间。
cargo run -p canwu-ming-fiscal-reference --example ming_fiscal_starter -- hongwu-1391cargo run -p canwu-ming-fiscal-reference --example ming_fiscal_starter -- wanli-1581cargo run -p canwu-ming-fiscal-reference --example ming_fiscal_starter -- hongguang-1644第一个参数是 fixture ID,省略时运行 hongwu-1391。每次运行打印一行(trace 路径已删减):
fixture=hongwu-1391 checkpoint=b282b58b82d8fbe7646fdb1cfbae1e64e1e2187a4bd84d014826351cb7ae5c0c frames=6 trace_manifest=... trace_steps=...fixture=wanli-1581 checkpoint=1c79fd79784d86ac285ad81e80e0f54dd7b0389edce99a8351012f0555f3255b frames=6 trace_manifest=... trace_steps=...fixture=hongguang-1644 checkpoint=8aa76890d6bdf198a8d1b620ee5db3597157b642f27e757bc9f120a5e451df50 frames=6 trace_manifest=... trace_steps=...种子固定为 DEFAULT_SEED,所以同一 fixture 每次运行打印的检查点哈希都相同。frames=6 是结算过的边界数:每个财政行动两个,适配器的证据记录一个,凭证一个。trace 写到工作区的 artifacts/traces/ming-fiscal-reference/<fixture>/:manifest.json,外加 steps.jsonl 中每个边界一行 JSON。洪武最后一行里,fiscal.state.ledgers 下该范围的账册和 fiscal.projections 下尚书的报告记录了结果(已删减):
"totals":{...:{"assessed":100,"remission_granted":0,"collected":70,...,"outstanding":30}}"<cycle>:0000:0":{...,"assessed":{"minimum":100,"maximum":109},"collected":{"minimum":70,"maximum":71},"outstanding":{"minimum":30,"maximum":31}}旁边的 fiscal.counts 统计财政记录里有什么:账册、核算、请求、凭证,以及各条记录的最近结果。
每个起点核算的采纳记录不同:
| Fixture | 历史模式 | 核算所用法规与范围 | 单位 |
|---|---|---|---|
hongwu-1391 |
recorded_baseline |
scope.lower-yangzi.grain 上的 canal_grain_tribute |
shi_grain_equivalent |
wanli-1581 |
recorded_baseline |
scope.lower-yangzi.land 上的 single_whip_lower_yangzi |
liang_silver |
hongguang-1644 |
research_replay |
scope.southeast.salt 上的 southern_ming_salt_finance |
liang_silver |
选项写在 fixture ID 之后:
| 选项 | 作用 |
|---|---|
--days <N> |
在示例流程之后继续运行 N 个模拟日,每一步结算一个日历边界。 |
--cadence <marker> |
把每一步标记为 daily、monthly 或 annual。SimTime 以分钟计、不带日历,所以月度和年度步长分别取 30 和 365 个模拟日。 |
--step-days <N> |
改用别的步长。最后不足一步的部分按不带 cadence 标记的边界结算。 |
--trace-dir <path> / CANWU_TRACE_DIR |
改变 trace 根目录。 |
--open-viewer / --viewer-port <N> |
在示例流程开始前于 127.0.0.1 上启动 trace viewer 并打开默认浏览器。viewer 随 steps.jsonl 增长而加载新内容,进程一直提供服务,直到按下 Ctrl+C。端口默认为 0,即自动选择空闲端口。 |
步进方式参见连续时间游戏循环。viewer 依赖工作区中的 tools/trace-viewer/,在工作区外运行时需设置 CANWU_WORKSPACE_ROOT。
示例程序 ming_fiscal_starter.rs 调用参考整合包 lib.rs 中的 new_ming_fiscal_reference 和 run_ming_fiscal_sample_cycle_with_trace,并为每张边界回执写一帧 trace。
1. 为一个 fixture 编译目录
Section titled “1. 为一个 fixture 编译目录”ming_fiscal_reference_scenario 读取 fixture JSON,例如 wanli-1581.json。fixture 给出年份、历史模式、地区以及各条法规的采纳阶段。函数按这些地区编译内容包:
let catalog = compile_ming_fiscal(FiscalContentSelection { historical_year: fixture.historical_year, region_ids: fixture.region_ids.clone(), ..FiscalContentSelection::default()})?;默认选择的机制集合为空,即选中全部 11 种机制。编译后的目录保留所选地区的全部时期和全部法规;年份只决定当前活动时期,例如 1391 年对应 founding_reconstruction。
2. 构造初始财政状态
Section titled “2. 构造初始财政状态”同一个函数创建 FiscalState。reference_institutions 给每个起点一个朝廷机构(参考世界中的政府),由 authority.revenue-minister 负责;hongguang-1644 另外加入前线将领、地区财库、盐务衙门和商人信用机构。每个机构得到一个只有一名常设角色的权限绑定:
FiscalAuthorityBinding { id: institution.authority_id.to_owned(), institution: institution.entity.clone(), authorized_actor: Some(institution.actor), acting_actor: None, authority_basis: None, rebound_by: None,},rebound_by 一直是 None,直到运行中有 RebindAuthority 行动更换这个绑定的角色;这几个起点都不会这样做。
fixture 中的每条采纳记录变成一个 FiscalScopeBinding(把一个辖区和一种机制交给某个机构,粒度为 SimulationGranularity::Aggregate)和一条带 fixture 阶段的采纳状态;每个范围各有一条范围明细记录。观察者绑定 observer.revenue-minister 让户部尚书以 confidence_per_mille: 850 看到这些机构的总数;它把尚书写在 actor、把他本人写在 knowledge_holder,controlled_by: None 表示没有财政行动能改指它。granularity 和 cadence 都用默认值:每个范围一行,账册一变就发报告;观察者很多的世界会给每个观察者 Interval 节律、为朝廷选机构粒度(见报告如何发布)。随后 FiscalState::into_records 把状态拆分成场景携带的各条记录。
3. 注册插件
Section titled “3. 注册插件”let canwu = Canwu::new_with_plugins(seed, reference.scenario, &[&world, &adapter, &fiscal])?;validate_ming_fiscal_reference(&canwu)?;new_ming_fiscal_reference 注册参考世界、执行适配器和 ming_fiscal_plugin()。后者是一个只接受一种证据的 FiscalPlugin,这种证据就是适配器的 fiscal_execution_evidence 记录。validate_ming_fiscal_reference 是参考整合包的语义校验:把每张凭证与其证据重新核对一遍,并只用封存无法改变的状态检查每处引用,所以它接受 Canwu 或封存过的 CompactedCanwu,答案相同。
4. 开立核算并授权征收
Section titled “4. 开立核算并授权征收”sample_cycle_plan 按 ID 顺序取第一条满足条件的采纳记录:阶段可执行(implemented、audited 或 entrenched),且法规的法定年份范围包含当前年份。hongguang-1644 中太仓银库的法规止于 1643 年,所以流程改用盐务衙门。第一个行动:
action: FiscalAction::OpenAssessment { assessment_id: assessment_id.clone(), rule_id, scope_binding_id: scope_id, accounting_cycle_id: format!("{period_id}.{id_prefix}"), quantity: 100, unit: sample_unit(payment_form).to_owned(), payment_form, commutation_quote: None,},reference_batch 用 fiscal_target_revision 读出范围明细的修订号,并按它把行动放进一个只有一个条目的 FiscalActionBatch;submit_reference_action_with_trace 用 fiscal_action_command 包装这个批次,以绑定中的角色身份提交。第一个边界上,准入过程核对角色与绑定、绑定机构与范围;下一个边界把条目的 expected_revision 与这个边界开始时范围明细的修订号对照,结算行动,把结果记入范围明细的结果环,并发出 canwu.fiscal.action_settled.v1,辅助函数用 FiscalSettledAction::from_events 读回它。示例每次只提交一个行动;一个包含多个条目的批次,或按同一个修订号构造的多个批次,会在这一个边界里按准入顺序一起结算。核算周期 ID 由上层应用决定,与历史时期相互独立;某周期的第一笔核算为该范围开立这个周期的账册。第二个行动 AuthorizeExecution 允许把资源 ResourceId::new(1) 中的 70 从该角色征收给机构,资源余额保持不变。
5. 记录征收并结算凭证
Section titled “5. 记录征收并结算凭证”settle_sample_execution_with_trace 扮演资源一方。它通过 enqueue_reference_execution_result 发出一条 FiscalExecutionEvidence(70 单位、Fulfilled,带 external_operation_id)。第 5 个边界上,适配器按已授权的请求核对这条结果,并写成一条只可创建的领域记录。随后函数提交该记录的精确版本:
enqueue_execution_receipts( canwu, now, &[canwu_fiscal::FiscalExecutionReceiptPacket { receipt_id: format!("{id_prefix}.receipt"), scope_binding_id, request_id, external_evidence: [evidence_version].into_iter().collect(), }],)?;第 6 个边界上,FiscalPlugin 解码每条被引用的记录,检查记录种类,并逐项核对范围、请求、执行类型、支付形态、资源、来源、目标和单位是否与授权一致,再从证据推出凭证的数量(70)和处置结果(fulfilled)。插件不能接受的凭证会以 canwu.fiscal.ingress_rejected.v1 事件被拒绝,绝不会让边界失败。
6. 读取账册合计与报告
Section titled “6. 读取账册合计与报告”此时该范围的账册显示应征 100、已征 70、未结 30。只要某个边界改动了观察者能看到的账册,FiscalPlugin 就把这位观察者的报告发布为他持有的知识记录:一条摘要记录,加上各行有变化的分段,每段 16 个范围行。户部尚书的报告在这里只有一行,所以是一条摘要加一个分段。derive.rs 里的 estimate 按观察者的置信度把每个总数归入一个区间:
let precision_divisor = match confidence_per_mille { 900..=1_000 => 100, 750..=899 => 10, 500..=749 => 2, _ => 1,};let bucket_width = (magnitude / precision_divisor).max(2);let minimum = value / bucket_width * bucket_width;magnitude 是不超过该值的最大 10 的幂。置信度为 850‰ 时,100 的区间宽度为 10(100-109),70 取最小宽度 2(70-71)。trace 从持有人那里读回每位观察者的报告:trace.rs 中的 collect_projections 调用 fiscal_reports_of_holder(canwu, &binding.knowledge_holder),即 fiscal_reports(&viewer) 的受信任上层应用形式,由它把摘要和分段拼回每个观察者绑定的一份 FiscalReport:
let report = fiscal_reports_of_holder(canwu, &binding.knowledge_holder) .ok()? .remove(observer_id)?;Some((observer_id.clone(), report.projection))面向玩家的客户端改用 fiscal_reports(&canwu.viewer_for_actor(actor)?),它只读视图所属持有人自己的账本。
7. 关闭周期并归档账册
Section titled “7. 关闭周期并归档账册”要运行多年的游戏会关闭每个核算周期并归档它的账册,让财政记录保持小巧。close_ming_fiscal_sample_cycle 为示例范围和周期提交 CloseAccountingCycle,关闭后的账册只接受审计结论。archive_ming_fiscal_books(&mut canwu, &store) 随后对你提供的 FiscalArchiveStore 执行归档协议:prepare_fiscal_archive、enqueue_fiscal_archive、一个结算提交的边界,再 finalize_fiscal_archive_retention,直到没有已关闭的账册为止。archive_century 测试每年跑一次示例周期,跑一百年,每年关闭、每十年归档,财政记录始终只有几 KB;没有任何边界读取归档存储。
值得注意的地方
Section titled “值得注意的地方”- 已征数量只能经由适配器的证据记录进入财政状态,比授权晚两个边界。
- 凭证的数量和处置结果来自所引用的证据。本整合包的适配器为一个范围中的一个请求写一条证据,所以一个精确证据版本最多结算一张凭证;此外,与往常一样,一个证据种类与
external_operation_id的组合在该范围的常驻账册内最多结算一张。(一次操作服务几个范围时,参伍也接受汇总执行证据;本整合包不使用它,其适配器也拒绝这种证据。) - 每个行动只写明它改动的那条记录(这里是范围明细)的修订号,所以其他范围上的行动不会让它过期。
- 三个起点共用一套代码,由 fixture JSON 决定流程使用的法规、范围和机构。
- 户部尚书看到的是 100-109,权威状态中的值是 100。
wanli-1581的 trace 把adopt_single_whip_north和adopt_single_whip_southwest列为转型候选;在ApplyTransition行动结算之前,它们一直只是候选。
- 报告精度。 在
ming_fiscal_reference_scenario中把观察者的confidence_per_mille改为 600。除数变为 2,应征区间变成 100-149,已征区间变成 70-74。 - 部分征收。 在
settle_sample_execution_with_trace中把证据改为quantity: 40和disposition: FiscalReceiptDisposition::Partial。账册随之显示已征 40、未结 60。 - 实施改革。 在
wanli-1581中提交adopt_single_whip_north的ApplyTransition,把single_whip_north绑定到scope.north.land,写法可参照测试single_whip_transition_atomically_suspends_the_superseded_rule。华北的折银采纳变为suspended,出现一条implemented的single_whip_north采纳,华北的候选随之消失。
真正的游戏还要补上一层:上层应用的适配器在自己的资源、市场、生产或物流领域执行实际操作,再像第 5 步那样提交证据。
模型的其余部分见财政制度:覆盖单元及明代内容包 704 个单元的构成、拆分后的记录及其预算、行动批次与按记录的修订号、核算周期与归档、历史语境与改革候选、代理角色与审计人任命,以及各项上限。
内容包覆盖 1368 年至 1683 年的 8 个时期,引用 10 条出处,其中引用最多的是 Ray Huang(1974);每条出处都列出它不支持的推断,例如把登记户数当作实际人口。案例说明列出了各个时期,data/pack.json列出了全部法规和出处。
打开可运行示例
阅读参考整合测试
浏览参考内容包
阅读案例说明