# Design: enterprise-random-family ## Context `@suaveplan/types` defines `Rng = { nextUint32(): number; nextFloat(): number }` but ships zero implementations. The genesis `@suaveplan/testing` package contains an inline Mulberry32 `seededRng` for test fixtures — this inline copy stays (testing packages cannot take workspace deps), but the canonical implementations live here. The games codebase runs five different hand-rolled PRNG algorithms across bingo, baccarat, rummy, minesweeper, and fruit-machine — this change provides the single canonical set they all migrate to. Three packages with zero inter-dependency between them at the leaf level: `pseudo-random` has no workspace deps, `random` depends on `@suaveplan/types` and `@suaveplan/error`, and `noise` depends only on `@suaveplan/error`. ## Goals / Non-Goals **Goals:** - Ship `SeededRng` interface and four algorithm implementations (Mulberry32, SFC32, Xoshiro256++, CounterRng) with full offset/fork/checkpoint support - Ship the full high-level random utilities surface: strings, IDs, collections, objects, game primitives, and all statistical distributions with `DistributionConfig` serializable dispatch - Ship spatial noise functions (Perlin, Simplex, Value, Worley, fBm, Blue noise) - All three packages at 100% coverage (foundational layer) - Full enterprise checklist for each **Non-Goals:** - Cryptographic PRNGs (ChaCha20-DRBG, HMAC-DRBG) — belong in `@suaveplan/crypto-sodium`/`crypto-browser` - Gaussian Process regression, Monte Carlo integration, statistical hypothesis testing — future `@suaveplan/stats` - Geo random geometry (`randomPoint`, `randomPolygon`) — deferred to `@suaveplan/geo/random` - Changing `@suaveplan/testing`'s inline Mulberry32 — testing rule holds; it stays inline ## Decisions ### Package: `@suaveplan/pseudo-random` ```text src/ types/ seeded-rng.types.ts / seeded-rng.md # SeededRng interface + RngCheckpoint algorithms/ mulberry32.ts / mulberry32.test.ts / mulberry32.md sfc32.ts / sfc32.test.ts / sfc32.md xoshiro256.ts / xoshiro256.test.ts / xoshiro256.md counter-rng.ts / counter-rng.test.ts / counter-rng.md seed/ normalize.ts / normalize.test.ts / normalize.md # normalizeSeed(string|number): number utils/ shuffle.ts / shuffle.test.ts / shuffle.md # shuffle, shuffleInPlace sample.ts / sample.test.ts / sample.md # sample, weightedSample distributions.ts / distributions.test.ts / distributions.md # uniform int/float helpers errors/ codes.ts / pseudo-random-error.ts / index.ts constants/index.ts index.ts ``` **`SeededRng` interface:** ```ts interface SeededRng extends Rng { readonly seed: number readonly position: number skip(n: number): this reset(): this at(offset: number): SeededRng fork(id: number | string): SeededRng checkpoint(): RngCheckpoint restore(checkpoint: RngCheckpoint): this } ``` **Algorithm selection guide** (exported as docs, not enforced): - `Mulberry32Rng` — 32-bit state, ~2^32 period. Best for trivial use cases, minimal memory footprint. - `Sfc32Rng` — 128-bit state, period >2^127. Best default for games and simulations. - `Xoshiro256Rng` — 256-bit state, period 2^256-1, supports fixed-size jump (2^128 and 2^192 steps). Best when independent parallel streams are needed at scale. - `CounterRng` — splitmix64-based hash counter. O(1) seek to any position. Best for direct-address use cases like bingo ticket generation. **Seed normalization** — `normalizeSeed(seed: string | number): number`: - String seeds: splitmix64 mixing over char codes (replaces djb2/FNV-1a used in bingo) - Numeric seeds: splitmix64 single-round mixing to avalanche input bias - Rejects NaN, Infinity, non-safe-integers **`fork(id)`** — derives a child seed via `splitmix64(parentSeed ^ hash(id))`. Child streams are statistically independent from the parent and from each other. Critical for game use cases: ```ts const master = new Sfc32Rng(gameSeed) const weather = master.fork('weather') const enemies = master.fork('enemies') const loot = master.fork('loot') ``` **`at(offset)`** — for linear-burn algorithms (Mulberry32, SFC32): constructs a new instance at `seed` and burns `offset` values at construction time (O(offset), cost paid once). For CounterRng: O(1), directly sets the counter. **Offset validation** — no hard cap (the bingo 1,000,000 limit was app-specific). Throws `PRNG.OFFSET.OUT_OF_RANGE` only for negative values. ### Package: `@suaveplan/random` ```text src/ source/ crypto-rng.ts / crypto-rng.test.ts / crypto-rng.md # cryptoRng(): Rng default-rng.ts / default-rng.test.ts / default-rng.md # getDefaultRng / setDefaultRng bytes/ random-bytes.ts / random-bytes.test.ts / random-bytes.md strings/ random-string.ts / random-string.test.ts / random-string.md # presets + custom charset ids/ uuid.ts / uuid.test.ts / uuid.md # randomUUID (v4+v7), randomULID, randomNanoid numbers/ random-int.ts / random-int.test.ts / random-int.md random-float.ts / random-float.test.ts / random-float.md collections/ shuffle.ts / shuffle.test.ts / shuffle.md sample.ts / sample.test.ts / sample.md choice.ts / choice.test.ts / choice.md weighted.ts / weighted.test.ts / weighted.md # weightedChoice, weightedSample objects/ random-key.ts / random-key.test.ts / random-key.md random-value.ts / ... random-entry.ts / ... random-subset.ts / ... game/ dice.ts / dice.test.ts / dice.md # coin, dice, roll distributions/ continuous/ normal.ts / normal.test.ts / normal.md log-normal.ts / ... exponential.ts / ... gamma.ts / ... beta.ts / ... pareto.ts / ... weibull.ts / ... cauchy.ts / ... logistic.ts / ... discrete/ bernoulli.ts / bernoulli.test.ts / bernoulli.md geometric.ts / ... binomial.ts / ... poisson.ts / ... sampler.ts / sampler.test.ts / sampler.md # createSampler, sample, DistributionConfig errors/ codes.ts / random-error.ts / index.ts constants/index.ts index.ts ``` **`cryptoRng()` factory** — returns an object implementing `@suaveplan/types`'s `Rng` interface backed by `crypto.getRandomValues`: ```ts { nextUint32: () => { const buf = new Uint32Array(1); crypto.getRandomValues(buf); return buf[0] }, nextFloat: function() { return this.nextUint32() / 0x1_0000_0000 } } ``` This is the **only** callsite for `crypto.getRandomValues` in `@suaveplan/random`. Guarded by `tests/no-disallowed.test.ts`. **Default source** — module-level mutable (non-exported) singleton. `getDefaultRng()` returns it; `setDefaultRng(rng)` replaces it (test hook). Default is `cryptoRng()`. **String presets** (`randomString(length, { type, rng? })`): | type | charset | |---|---| | `hex` | `0-9a-f` | | `base64` | A-Z, a-z, 0-9, +, / | | `base64url` | A-Z, a-z, 0-9, -, _ | | `url-safe` | A-Z, a-z, 0-9, -, _, . | | `numeric` | `0-9` | | `alphanumeric` | A-Z, a-z, 0-9 | | `distinguishable` | `CDEHKMPRTUWXY012458` (no I/O/0/1 confusion) | | `ascii-printable` | all printable ASCII 0x20-0x7E | Custom charset via `{ characters: string }` option. **UUID v7** — time-ordered, 48-bit millisecond timestamp in high bits, 74 random bits. Monotonically increasing within the same millisecond via sequence counter. **`DistributionConfig` tagged union** — every distribution has a named-parameter config object. The `type` discriminant selects the algorithm; all other fields are the algorithm's parameters. `createSampler(config, rng?)` returns `() => number`. `sample(config, rng?)` is a one-shot convenience. Config objects are plain JSON-serializable data — `rng` is always external. **Implementation notes for distributions:** - Normal: Box-Muller transform - LogNormal: `exp(normal(mu, sigma))` - Gamma: Marsaglia-Tsang method (k ≥ 1); Ahrens-Dieter for k < 1 - Beta: ratio of two Gamma samples - Poisson: Knuth algorithm for λ ≤ 30; rejection method for larger λ - All discrete distributions use `nextUint32()` not `nextFloat()` for the base draw ### Package: `@suaveplan/noise` ```text src/ perlin/ perlin.ts / perlin.test.ts / perlin.md # perlin2, perlin3 with seed simplex/ simplex.ts / simplex.test.ts / simplex.md # simplex2, simplex3, simplex4 value/ value.ts / value.test.ts / value.md # value2, value3 worley/ worley.ts / worley.test.ts / worley.md # worley2, worley3, WorleyMode (F1|F2|F1-F2) fbm/ fbm.ts / fbm.test.ts / fbm.md # fbm compositor, FbmOptions blue/ blue.ts / blue.test.ts / blue.md # blueNoiseSample, Poisson disk sampling errors/ codes.ts / noise-error.ts / index.ts constants/index.ts index.ts ``` All noise functions accept a `seed: number` parameter and return values normalised to `[-1, 1]` (except Worley which returns `[0, 1]` distances). No dependency on `@suaveplan/pseudo-random` — noise uses its own internal seeded hash permutation tables, which is the standard noise implementation pattern. **fBm compositor:** ```ts fbm(noiseFn, x, y, { octaves: 6, lacunarity: 2.0, gain: 0.5, seed: 0 }): number ``` Composes any 2D noise function with octave summation. **Blue noise** — Bridson's Poisson disk sampling algorithm: ```ts blueNoiseSample(width, height, minDistance, { seed?, rng? }): [number, number][] ``` ## Risks / Trade-offs | Risk | Mitigation | |---|---| | `at(offset)` is O(offset) for Mulberry32/SFC32 | Document clearly; `CounterRng` is the O(1) alternative; no silent perf traps | | `setDefaultRng` is global mutable state | Documented as test-only; tests restore previous value with `afterEach`; not exported from subpath exports | | `fork` child streams not proven independent | Use `normalizeSeed` mixing + test with fast-check that `fork('a')` and `fork('b')` produce uncorrelated sequences across 10k samples | | Simplex noise patent (expired 2022) | US patent 6,867,776 expired April 2022 — safe to use | | Blue noise is O(n) output size | Cap at 100k points; throw `NOISE.BLUE.SAMPLE_LIMIT_EXCEEDED` above |