Problem
SimRandom (js/model.js:38-69) is a global singleton shared across all SimEngine instances. During async Monte Carlo (runMonteCarloAsync), trials yield to the event loop via setTimeout(0). If a user steps the live simulation during this window, both paths draw from the same RNG — interleaving produces garbage results and breaks seeded reproducibility.
Every randomness source flows through SimRandom:
sampleDist() — distribution sampling
rollDice() — dice notation
sampleCustomVar() — custom variables
evalFormula via _formulaRandomScope() — formula randomness
- Engine core — chance gates, trigger chances, pull-any random picks
Plan
Convert SimRandom from a singleton to a factory that creates independent RNG instances. Each SimEngine gets this.rng. Thread the engine's RNG through model functions that use randomness.
Step 1 — Make SimRandom instantiable
function createRNG() {
return {
_fn: null, _a: 0,
random() { return this._fn ? this._fn() : Math.random(); },
seed(s) { /* ... */ },
getState() { return this._fn ? this._a : null; },
setState(state) { /* ... */ },
};
}
// Keep global export for backward compat:
const SimRandom = createRNG();
Step 2 — Give SimEngine its own RNG
Add this._rng = createRNG() in SimEngine constructor. Replace all SimRandom.xxx() calls in engine.js with this._rng.xxx().
Step 3 — Thread RNG through model functions
Functions that draw randomness (sampleDist, rollDice, sampleCustomVar, evalFormula, _formulaRandomScope) accept an optional rng parameter. When called from SimEngine, pass this._rng. When called standalone (tests, CLI, loop detector), fall back to the global SimRandom.
Step 4 — Update Monte Carlo
Monte Carlo trials create new SimEngine instances with their own rng seeded from ${seed}#${runIndex}. The finally block's SimRandom.seed(null) after MC is no longer needed — each engine manages its own state.
Effort
~4 hours. Touches js/model.js, js/engine.js, js/loops.js, js/app-analysis.js, cli.js, test/run.js, js/codegen.js.
Problem
SimRandom(js/model.js:38-69) is a global singleton shared across allSimEngineinstances. During async Monte Carlo (runMonteCarloAsync), trials yield to the event loop viasetTimeout(0). If a user steps the live simulation during this window, both paths draw from the same RNG — interleaving produces garbage results and breaks seeded reproducibility.Every randomness source flows through
SimRandom:sampleDist()— distribution samplingrollDice()— dice notationsampleCustomVar()— custom variablesevalFormulavia_formulaRandomScope()— formula randomnessPlan
Convert
SimRandomfrom a singleton to a factory that creates independent RNG instances. EachSimEnginegetsthis.rng. Thread the engine's RNG through model functions that use randomness.Step 1 — Make SimRandom instantiable
Step 2 — Give SimEngine its own RNG
Add
this._rng = createRNG()inSimEngineconstructor. Replace allSimRandom.xxx()calls inengine.jswiththis._rng.xxx().Step 3 — Thread RNG through model functions
Functions that draw randomness (
sampleDist,rollDice,sampleCustomVar,evalFormula,_formulaRandomScope) accept an optionalrngparameter. When called fromSimEngine, passthis._rng. When called standalone (tests, CLI, loop detector), fall back to the globalSimRandom.Step 4 — Update Monte Carlo
Monte Carlo trials create new
SimEngineinstances with their ownrngseeded from${seed}#${runIndex}. Thefinallyblock'sSimRandom.seed(null)after MC is no longer needed — each engine manages its own state.Effort
~4 hours. Touches
js/model.js,js/engine.js,js/loops.js,js/app-analysis.js,cli.js,test/run.js,js/codegen.js.