// Seeded, serializable random number generator. // // Every draw the game makes comes from here, so a run is fully reproducible // from its seed plus the player's choice sequence. That makes tests // deterministic and playtest reports replayable: the seed and the generator's // position both live in the save file and the feedback log. /** Hash an arbitrary string seed down to a uint32. */ function hashSeed(seed) { let h = 1779033703 ^ String(seed).length; for (let i = 0; i < String(seed).length; i++) { h = Math.imul(h ^ String(seed).charCodeAt(i), 3432918353); h = (h << 13) | (h >>> 19); } h = Math.imul(h ^ (h >>> 16), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909); return (h ^ (h >>> 16)) >>> 0; } /** * mulberry32: one uint32 of state, which is the whole point — the generator's * position round-trips through JSON with no special handling. */ export function createRng(seed, position) { let state = position === undefined ? hashSeed(seed) : position >>> 0; const next = () => { state = (state + 0x6d2b79f5) >>> 0; let t = state; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; return { seed, /** Float in [0, 1). */ next, /** Integer in [0, n). */ int: (n) => Math.floor(next() * n), /** Uniform pick from a non-empty array. */ pick(items) { if (!items.length) throw new RangeError('cannot pick from an empty array'); return items[Math.floor(next() * items.length)]; }, /** * Weighted pick. `weightOf` must return a non-negative number; entries * weighing zero are never drawn. Returns undefined if nothing has weight. */ weighted(items, weightOf) { let total = 0; for (const item of items) total += Math.max(0, weightOf(item)); if (total <= 0) return undefined; let roll = next() * total; for (const item of items) { roll -= Math.max(0, weightOf(item)); if (roll < 0) return item; } return items[items.length - 1]; }, /** Current position, for saving mid-run. */ getPosition: () => state, }; }