/** * Deterministic pseudo-random generator. * * The whole rules core is reproducible from a seed: given the same seed and the * same sequence of actions, a match replays identically. That is what lets a bug * report be a single integer, and what keeps the soak tests meaningful. * * Algorithm is mulberry32 — small, fast, and good enough for shuffling cards. * Its entire state is one 32-bit integer, so it serialises into the save/state * without ceremony. */ export class Rng { private _state: number; constructor(seed: number) { // Force to uint32 so a negative or fractional seed still behaves. this._state = seed >>> 0; } /** Current internal state, for serialising a game in progress. */ get state(): number { return this._state; } /** Restores a generator to a previously captured state. */ static fromState(state: number): Rng { return new Rng(state); } /** Uniform float in [0, 1). */ next(): number { this._state = (this._state + 0x6d2b79f5) >>> 0; let t = this._state; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; } /** Uniform integer in [0, bound). `bound` must be positive. */ nextInt(bound: number): number { if (bound <= 0) { throw new RangeError(`Rng.nextInt bound must be positive, got ${bound}`); } return Math.floor(this.next() * bound); } /** Uniformly picks one element. Throws on an empty array. */ pick(items: readonly T[]): T { if (items.length === 0) { throw new RangeError('Rng.pick called on an empty array'); } return items[this.nextInt(items.length)]; } /** Fisher-Yates, in place. Returns the same array for convenience. */ shuffle(items: T[]): T[] { for (let i = items.length - 1; i > 0; i--) { const j = this.nextInt(i + 1); const tmp = items[i]; items[i] = items[j]; items[j] = tmp; } return items; } }