Finding
Rng documents its internal state as uint32, but the state grows unbounded between saves:
src/sim/rng.ts:11-15 stores state and truncates only the constructor seed with seed | 0.
src/sim/rng.ts:20 advances with this.state += 0x6d2b79f5 but does not assign a truncated/wrapped value back to this.state.
src/sim/rng.ts:37-38 returns the raw internal value from getState().
src/sim/types.ts documents rngState as the Mulberry32 uint32 state, while src/platform/save.ts validates only that it is a finite integer.
A short repro is enough: create new Rng(42), call nextU32() several times, and assert getState() remains a canonical 32-bit Mulberry32 state. Today it exceeds the 32-bit range almost immediately.
Impact
Replay may still appear stable today because new Rng(world.rngState) truncates on construction, but snapshot state is non-canonical and violates the save/replay contract. Long-lived Rng instances also drift toward JavaScript precision-risk territory instead of staying in the intended uint32 ring.
Suggested direction
Wrap this.state on every advance, and make save validation enforce the canonical range/sign contract we actually want. Add a regression test that checks getState() after repeated draws and a save/load replay equivalence check around a nontrivial draw count.
Review provenance
Confirmed by an independent fresh-context review agent during the 2026-05-28 adversarial codebase review of origin/main (16e4201).
Finding
Rngdocuments its internal state as uint32, but the state grows unbounded between saves:src/sim/rng.ts:11-15storesstateand truncates only the constructor seed withseed | 0.src/sim/rng.ts:20advances withthis.state += 0x6d2b79f5but does not assign a truncated/wrapped value back tothis.state.src/sim/rng.ts:37-38returns the raw internal value fromgetState().src/sim/types.tsdocumentsrngStateas the Mulberry32 uint32 state, whilesrc/platform/save.tsvalidates only that it is a finite integer.A short repro is enough: create
new Rng(42), callnextU32()several times, and assertgetState()remains a canonical 32-bit Mulberry32 state. Today it exceeds the 32-bit range almost immediately.Impact
Replay may still appear stable today because
new Rng(world.rngState)truncates on construction, but snapshot state is non-canonical and violates the save/replay contract. Long-livedRnginstances also drift toward JavaScript precision-risk territory instead of staying in the intended uint32 ring.Suggested direction
Wrap
this.stateon every advance, and make save validation enforce the canonical range/sign contract we actually want. Add a regression test that checksgetState()after repeated draws and a save/load replay equivalence check around a nontrivial draw count.Review provenance
Confirmed by an independent fresh-context review agent during the 2026-05-28 adversarial codebase review of
origin/main(16e4201).