Skip to content

Give each SimEngine its own RNG instance instead of sharing a singleton #50

Description

@zntznt

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions