Skip to content

feat(prng): order-independent derived streams and distribution helpers - #38

Open
cmaycumber wants to merge 1 commit into
elemarin:mainfrom
cmaycumber:feat/prng-streams
Open

cmaycumber wants to merge 1 commit into
elemarin:mainfrom
cmaycumber:feat/prng-streams

Conversation

@cmaycumber

Copy link
Copy Markdown

fork(label) consumes a draw from the parent, so a child stream depends on how many values were drawn before it. That's right for per-entity streams created in a fixed order, and fork is unchanged. Procedural generation needs the opposite: adding a draw to one generation step must not reshuffle every other step.

  • derive(label): a child that is a pure function of the stream's seed identity and the label. It never advances the parent, and nested labels compose. The identity is carried in exportState/restoreState, and older states without it still restore.
  • Helpers: range, pick, weighted, gaussian, shuffle, and fn() for libraries that take a random function (noise generators).
  • gaussian sums twelve uniforms (Irwin–Hall) instead of Box–Muller. Math.log/Math.cos are not bit-identical across JS engines, which would break lockstep determinism. The result is bounded to ±6σ.
  • Diagnostics: RND_0501–0504 for empty picks and invalid weights.
  • Compatibility: the base PCG32 sequence and fork are byte-identical to before.

Verification: typecheck, build, 242/242 tests.

🤖 Generated with Claude Code

`fork(label)` consumes a draw from the parent, so a child stream depends on how many values
were drawn before it. That is right for per-entity streams created in a fixed order, and it is
left exactly as it was. Procedural generation needs the opposite: adding a draw to one step
(placing a new kind of landmark, say) must not reshuffle every other step.

- `derive(label)`: a child that is a pure function of this stream's seed identity and the
  label. It never advances the parent and ignores prior draws, and nested labels compose.
  The identity is carried in exportState/restoreState; states exported before this change
  (no `origin` field) still restore.
- Helpers used constantly by procedural code: `range(min, max)`, `pick(items)`,
  `weighted(items, weights)`, `gaussian(mean, stdDev)`, `shuffle(items)` and `fn()` for
  libraries that take a random function, such as noise generators.
- `gaussian` sums twelve uniforms rather than using Box-Muller, whose Math.log and Math.cos
  are not bit-identical across JavaScript engines; a world generated with it could differ
  between two browsers in a lockstep session. Bounded to six standard deviations.
- Diagnostics RND_0501 to RND_0504 for empty picks and invalid weights.
- The base PCG32 sequence and `fork` are byte-identical to before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant