/** * Seedable RNG using the xoshiro128** algorithm. * * @module rng/seeded */ import type { RNG } from './types.js'; /** * A snapshot of {@link SeededRNG}'s internal state: a format version followed * by the four state words as unsigned 32-bit integers. Pass one back to the * constructor to resume the exact sequence it was taken from. * * Opaque and bound to the major version, the same contract seeds carry: the * words mean nothing outside the engine that produced them, and a release that * changes that engine bumps the leading version. A snapshot from a different * version is rejected with an `INCOMPATIBLE_RNG_STATE` error rather than * resumed under the wrong semantics — safe to hold in memory or serialize for * a save file, and loud rather than silent across an upgrade. * * @category RNG */ export type RngState = readonly [version: number, s0: number, s1: number, s2: number, s3: number]; /** * Seedable pseudo-random number generator using xoshiro128**. The default * randomness source — `roll(notation)` builds one per call, and * `roll(notation, { seed })` builds one from your seed. * * Period 2^128 - 1. Every seed is stringified and hashed with cyrb128 into * the full 128-bit state, so numeric seeds keep all 53 bits and unrelated * strings are overwhelmingly unlikely to share a state; an omitted seed hashes * `Date.now()` together with two `Math.random()` draws, roughly 100 bits of * width rather than 32 — wide enough that concurrently created generators are * very unlikely to collide, though unpredictability stays bounded by the host * engine's `Math.random()` seeding. The first 8 draws are discarded as a run-in. * * Stringifying means `42` and `'42'` are the same seed — the two forms share * one namespace, which matters when seeds arrive from a CLI flag or JSON. * * {@link state} snapshots the state words as a versioned {@link RngState}, and * passing one to the constructor resumes that exact sequence — restore copies * the words verbatim, skipping both the hash and the run-in. A snapshot from * another format version throws `INCOMPATIBLE_RNG_STATE`; within the current * version the type is the contract, and a hand-built tuple is coerced to 32 * bits per word rather than rejected. * * Reproducibility guarantee: the same seed and the same notation produce the * same dice for the lifetime of a major version, and `RngState` carries that * binding. The one exception is a genuine distribution bug — bias, faulty * rejection sampling — which may change the mapping in a minor release, never * silently in a patch, and always with a `BREAKING` changelog note. The * sequence is *not* cryptographically secure. To pin a roll beyond that, * persist the {@link RollResult} rather than re-deriving it from a seed. * * @example * ```typescript * import { SeededRNG, roll } from 'roll-parser'; * * // Same seed = same sequence * const a = new SeededRNG('test-seed'); * const b = new SeededRNG('test-seed'); * a.nextInt(1, 6) === b.nextInt(1, 6); // true * * // An injected instance keeps advancing across rolls; `{ seed }` restarts * // the stream on every call. * const rng = new SeededRNG('demo'); * roll('1d20', { rng }).total; // 1 * roll('1d20', { rng }).total; // 20 — the stream moved on * roll('1d20', { seed: 'demo' }).total; // 1, every single time * ``` * * @category RNG */ export declare class SeededRNG implements RNG { private s0; private s1; private s2; private s3; constructor(seed?: string | number | RngState); private initState; /** All-zero is a fixed point for xoshiro — at least one word must be non-zero. */ private guardZeroState; /** * Returns the current state as a format version followed by four unsigned * 32-bit words. Feeding the snapshot back to the constructor resumes this * exact sequence; the source instance is untouched, and the two then advance * independently. * * The words are {@link RngState} — opaque, restorable within the major * version that produced them, and rejected with `INCOMPATIBLE_RNG_STATE` * outside it. * * @returns A snapshot of the version and the four state words * * @example Replay a roll that was never seeded * ```typescript * import { SeededRNG, roll } from 'roll-parser'; * * const rng = new SeededRNG(); * const snapshot = rng.state(); * * const first = roll('1d20', { rng }); * const replay = roll('1d20', { rng: new SeededRNG(snapshot) }); * first.total === replay.total; // true * ``` * * Not a fork primitive. A restored generator replays the parent's stream, so * children taken at different points are the same sequence at an offset, not * independent substreams — derive a seed per entity instead. * * @example Per-entity streams — derive seeds, do not fork state * ```typescript * import { SeededRNG } from 'roll-parser'; * * const goblin = new SeededRNG('world:goblin'); * const orc = new SeededRNG('world:orc'); * ``` */ state(): RngState; /** * Normalizes the constructor seed into a string. Numbers are stringified * rather than coerced to uint32, so all 53 exact bits reach the hash. */ private toSeedString; /** cyrb128 — hashes the seed into all four state words at once. */ private hashSeed; private nextUint32; /** * Returns a float in `[0, 1)`, derived from one uint32 draw. Resolution is * 2^-32, not the full 2^-53 a double can hold. * * Not used by the evaluator — dice go through {@link nextInt}. * * @returns A float in `[0, 1)` */ next(): number; /** * Returns an integer in the inclusive range `[min, max]`, uniformly * distributed — rejection sampling removes the modulo bias a plain * `% range` would introduce. * * Bounds handling, in order: * - `min > max` is normalized by swapping, so `nextInt(6, 1)` behaves as * `nextInt(1, 6)`. (The mock RNG throws instead; see {@link RNG.nextInt}.) * - `min === max` returns that value without consuming a draw. * - Ranges wider than 2^32 use two draws composed into a 53-bit value. * - Ranges wider than 2^53 cannot be sampled exactly and throw a * `RangeError` rather than silently skewing. * * @param min - Lower bound, inclusive * @param max - Upper bound, inclusive * @returns An integer in `[min, max]` * @throws {RangeError} If `max - min + 1` exceeds 2^53 * * @example * ```typescript * import { SeededRNG } from 'roll-parser'; * * const rng = new SeededRNG('demo'); * rng.nextInt(1, 6); // 1..6 * rng.nextInt(3, 3); // 3, always * rng.nextInt(1, Number.MAX_SAFE_INTEGER); // fine — two-draw path * ``` */ nextInt(min: number, max: number): number; /** * Unbiased sampling in `[0, range)` for ranges above 2^32, built from two * uint32 draws combined into a 53-bit integer (the largest width JS numbers * represent exactly). Ranges beyond 2^53 cannot be sampled without bias — * throw instead of silently degrading. */ private nextBoundedWide; } //# sourceMappingURL=seeded.d.ts.map