跳转到内容

随机性

参伍从带命名和版本的随机流中抽取随机数。每条随机流归声明它的机制所有,每次抽样都会被记录下来。因此,相同的运行输入总能得到相同的结果,重新加载存档也无法重掷已有结果。准备给插件加入随机性时,可以参考本页。

边界系统从已声明的随机流抽样;抽样被记录,随边界一起保留或回滚,并在重放时重新计算. 查看图表源码.
查看图表源码
flowchart LR
  System["边界系统<br/>声明随机流"] --> Draw["random_range 或<br/>random_sample_for_operation"]
  Draw --> Record["RandomDrawRecord<br/>流、地址、上界、<br/>结果、用途"]
  Record --> Commit{"边界<br/>提交?"}
  Commit -- "是" --> Saved["快照与<br/>边界证据"]
  Commit -- "否" --> Undo["抽样记录与流位置<br/>一起回滚"]
  Saved --> Replay["重放时逐次<br/>重算并比较"]

一个 RandomStreamKey 由 namespace、name 和 version 组成,例如:

canwu.core / knowledge-report-delay / 1

内核用运行的根种子和随机流的键派生出每条流的种子。每个机制从自己的流中抽样,所以给一个系统增加一次抽样,其他系统拿到的数值保持不变。流状态保存算法、派生出的种子、当前位置和生成器状态。

新建的随机流使用 SplitMix64V2,用无偏的拒绝采样把原始值缩小到请求的范围。记录为 SplitMix64V1 的流仍按取模方式缩小范围,旧日志因此能按原来的数值重放。算法属于存档格式的一部分:要改变某条流的抽样方式,需要发布新的流版本。

边界系统只能从 BoundarySystemContract::random_streams 中列出的流抽样。系统运行时,内核给它一个随机会话,里面是这些流的副本。从其他流抽样会以 UndeclaredRandomStream 失败。

每次抽样都要求:

  • 上界大于零;
  • purpose 非空,且首尾没有空白;
  • 对顺序抽样而言,流位置还能继续递增。

系统拿到的是一个有界整数,生成器状态留在内核内部。

顺序抽样 SimulationView::random_range 从流中取下一个值。系统总按固定顺序抽样时,这就够了。可能重试、回滚或跨阶段继续的机制,应改用 random_sample_for_operation(或只返回数值的 random_range_for_operation)。

操作定址抽样按它所服务的操作来定位。它的地址由产生抽样的插件、操作种类、应用的操作 ID、目标和抽样槽位组成。目标是一个 RandomOperationTarget:实体、精确的决策票据版本、精确的领域记录版本、知识持有人,或一个规范键。抽样还要引用引发它的证据。内核根据根种子、随机流、地址、上界和用途计算数值,所以无论之前抽过多少次,结果都相同。用相同的地址、证据、上界和用途再次请求,会得到相同的值;用同一地址配不同的证据、上界或用途,则以 RandomOperationConflict 失败。

随机决策策略也走这条路径:RandomOperationTarget::DecisionTicket 把抽样绑定到精确的票据版本,DecisionOptionWeight 把抽到的值映射到票据中已有的某个选项,ResolveDecisionRandomly 指令让内核为下一个边界生成一条普通决策。前置规则效用策略的随机平局决胜也使用同一路径,只在分数接近的最高选项之间抽样。

控制者(controller)的策略种类(Utility、Rule、Random、Human、External 或 Llm)在注册控制者时确定,用同一个控制者 ID 再次注册会失败。随机决策策略只在票据选项之间做选择;天气、故障、疫情等世界事件应由领域系统从自己的随机流中抽样。

每次抽样都记录一条 RandomDrawRecord:

字段 含义
stream 与 address 哪条版本化的流,以及顺序位置或操作地址
upper_exclusive 与 value 请求的上界和返回的值
purpose 机制为什么抽样
producer 执行抽样的边界系统或内核系统
outcome 这次抽样决定了什么,例如一次决策选择或一份报告送达
cause 与 correlation_id 因果来源和所属处理链

抽样记录和流状态都保存在快照和边界证据中,并计入哈希。边界失败时,它的流位置和抽样记录一起回滚,失败的尝试不会消耗随机数。

确定性重放消除了最简单的重掷方式:相同的权威状态、命令或决策输入、插件语义环境和模拟时间,得到相同的抽样结果。因此,从事件发生前的快照重新加载并重放相同的输入,事件结果不变。

要从同一快照尝试不同的命令,先用 fork() 派生分支,再在分支上提交。结果是一个平行现实:一段新的因果历史,与原运行的精确重放不同。玩家能使用手动存档、单写入存档还是研究用派生分支,由上层应用的存档策略决定。

第一方 crate 在哪里使用随机性,又在哪里刻意不用:

  • canwu-movement 不抽样。由应用系统判定路段失败、道路封闭或发生扣押(通常用应用自己的操作定址抽样),再通过 movement_incident_v1 输入报告结果,并引用一条精确的证据记录。
  • canwu-correspondence 在自己的随机流 canwu-correspondence / operation-resolution / 1 上抽样。对于截取、遇灾、承运人扣押等事故请求,应用提供 probability_per_mille,插件做一次操作定址抽样,决定事故是否发生。通信机会及其接收人也由同一条流选出。
  • 真伪判定背后的伪造是否被识破,由应用决定;canwu-information 记录判定结果。
  • 运力预订分配按固定顺序进行:优先级、窗口开始时间、平局决胜键、准入序号和预订标识。
  • 带表决单元的法律程序阶段,其平局处理是规则:取 For 与取 Against 的单元数相等时,status-quo 不增加单元,casting-seat:<seat> 则在该席位本人投 For 时增加一个 For 单元。
  1. 为每个独立机制设置一条稳定的命名随机流。
  2. 把抽样原因写进 purpose,让调试和重放输出能解释结果。
  3. 把随机流声明与插件版本、语义哈希放在一起审查。
  4. 不要用现实时间、线程顺序或容器遍历顺序作为种子或平局依据。
  5. 把未知事实、角色选择、离散事故和群体比率分别建模;不要用随机数补全缺失的事实。

精确重放要求随机流身份、算法版本、根种子、顺序位置、操作地址、每一次抽样和每个最终结果都一致。加载快照时,内核检查每条顺序流的状态是否等于“种子推进已记录的抽样次数之后”的状态,并重新计算每个操作定址抽样的值。边界重放还会比较每次抽样的生产者、用途、原因和相关标识。