慢慢

游戏制作 · 2026-07-17

不让随机游戏靠运气正确,确定性、快照与后置条件

固定 seed 只能让问题有机会复现。要证明随机游戏没有碰巧成功,还得隔离 RNG 流、保存下一步会用到的状态,并用恢复后的真实结果做后置条件。

不让随机游戏靠运气正确,确定性、快照与后置条件

《万法归一》的抽卡、敌人生成、事件和战斗 proc 都要用随机数。只要其中一处多抽一次、候选顺序变了,或者恢复时少存一个计数,同一个 seed 后面的结果就会分叉。画面可能暂时一样,几秒后才出现不同的卡牌、敌人或攻击。

最容易想到的办法是固定 seed,然后重复跑一遍。它只能让入口一致,不能保证各系统消费随机数的方式一致。一个共享 RNG 会让敌人生成多抽一次,顺手改掉下一次技能 Draft;只保存画面和当前血量,也不能保证恢复后的下一次抽取相同。画面没变,未来仍可能分叉。

我先把 RNG 拆成 draft、spawn、combat proc 等独立流,每条流保存自己的 PRNG state lanes、logical draw index 和 raw step count。需要固定抽取数量的系统再使用 transaction ledger,候选按稳定 ID 排序,不能靠容器插入顺序或节点创建顺序。Snapshot 也不只收集可见状态,还要按 provider 稳定排序,记录版本与哈希。确定性、Snapshot 与当前 schema 回归

随机回归常常来自所有权、消费顺序或恢复边界漂移。当前 checkout 正好留下一个很直白的例子,敌人生成 provider 已升到 schema 6,快照 codec 和旧用例仍停在 schema 5。测试稳定失败,这次版本漂移不能被当成一份可兼容存档静默读入。

测试设计不只比较一个画面。它会让 spawn 流前进 100 次,再确认 draft 的下一次结果没有变化;也会在保存前记录未中断路径的下一次抽取,恢复后重新抽一次,要求 value、draw index 和 state lanes 对齐。跨 provider 恢复则先全部 prepare,任何一个失败都撤销暂存,不改 live state。

现在我写随机系统的验收,不再问同一个 seed 看起来像不像,而是问恢复后下一步是否完全相同。错误输入还要有后置条件,没有消耗 draw,没有改 live state,没有留下半次 transaction。只有这些都成立,失败才是安全的失败。