Skip to content

确定性随机数 ​

可复现性属于核心能力,不是附加功能。目标有二:

  1. 可复现:同配置 + 同 seed + 同版本 → 结果逐位一致;
  2. 可比随机路径(Common Random Numbers):修改一个参数前后,两次实验尽可能走相同的随机路径,让 A/B 差值反映参数效应而不是随机噪声。

实验标识 ​

一次运行由以下五元组共同确定,并写入每次输出:

text
seed · model_version · config_hash · scenario_hash · experiment_id

其中 config_hash 与 scenario_hash 基于规范化配置计算(键序无关,见配置与公式引擎)。

为什么"按系统分流"不够 ​

直觉方案是给每个系统一条独立流(combat / loot / economy / behavior / world)。但它达不到目标:

假设 combat 流与 loot 流分离。把攻击力从 100 改到 95,某场战斗从 3 回合变 4 回合,combat 流被多抽了一次;之后 combat 流里所有抽取全部错位——后续每场战斗的命中判定都换了随机数。A/B 差异里混入了大量"流错位"噪声,Common Random Numbers 名存实亡。

问题根源:流的推进依赖调用次数,而调用次数本身受参数影响。

方案:带键随机数 ​

随机数不来自"逐个推进的流",而是按键直接派生:

text
key = (seed, actor_id, day, event_index, purpose)
random_values = derive(key)   // 纯函数,与调用顺序无关
键分量含义
seed实验种子
actor_id玩家(或怪物等实体)标识
day仿真天
event_index该 actor 当天、该 purpose 下的第几次抽取(从 0 起)
purpose抽取用途:behavior / combat_hit / combat_damage / loot / churn / …

实现上是 counter-based 派生:把键哈希为初始状态(如 SplitMix64 / Philox 一类),再产出所需数量的随机值。

这个结构直接解决错位问题:攻击力变化导致第 3 场战斗多打一个回合,只是该场战斗消耗的 event_index 变多,第 4 场战斗仍然用自己的 (day, 战斗序号) 键,不受任何影响。抽取次数变化只影响事件内部,不污染后续事件。

无共享状态、无调用顺序依赖,还带来两个附带收益:

  • 天然并行安全:派生是纯函数,不需要锁,也不需要"哪个线程先抽"的约定;
  • 可局部重放:调试某个玩家的某一天,直接用键重放随机数,不必从头跑。

Common Random Numbers 的达成边界 ​

带键结构下,键里不含被改参数的抽取(典型如 behavior 流)在 A/B 两次运行中键完全相同 → 随机值相同 → 路径一致。

::: note 诚实边界 CRN 降低噪声,不消除噪声。若行为决策依赖状态(例如"等级 ≥ 10 才解锁某行为"),改参数间接改变了状态,后续行为路径会分叉——这是真实效应,应当保留。原则:参数直接影响的抽取(战斗判定)允许不同;参数不直接影响、仅因状态变化而分叉的抽取,分叉本身就是实验结果的一部分。 :::

并行确定性规则 ​

随机数派生确定只是必要条件,聚合与遍历也必须确定:

陷阱规则
Rust HashMap 迭代顺序随机实体存储与注册表用 BTreeMap 或 IndexMap,不用 HashMap
rayon 浮点归约顺序不固定固定归约树(按确定性键排序后成对归约),或先排序再串行聚合
浮点求和误差随顺序变化指标聚合优先用整数 / 定点(如金币以整数计),浮点求和走固定顺序

跨平台浮点提示:编译器优化(FMA、向量化重排)与 libm 差异可能使同一份代码在不同 CPU / 编译选项下浮点结果不同。因此:

  • 承诺:同版本 + 同平台 + 同 seed → 逐位一致(快照测试,见测试策略);
  • 尽力:跨平台一致是目标而非承诺,跨平台比对使用统计回归而非逐位比较。

native 与 wasm32 之间也属于"跨平台":加减乘除与 sqrt 遵循 IEEE-754,两端逐位一致;powf / ln / sin 这类超越函数走不同 libm 实现,可能有 ULP 级差异——凡是影响判定结果的阈值比较,优先用整数 / 定点表述,浮点只用于展示层(指标的整数聚合规则即为此)。

禁止事项 ​

  • 禁止把 rand::thread_rng() 或任何非确定性源引入仿真路径;
  • 禁止在 System 内自建"流式" RNG 实例并跨事件持有(会重新引入调用顺序依赖);
  • 所有随机消费必须通过带键派生,purpose 枚举集中注册,防止同名不同义。

以 Apache-2.0 许可证发布