import { randomBytes, randomInt } from "node:crypto"; /** * RNG abstraction. Production uses `CryptoRng` (buffered `crypto.randomBytes`, * unbiased integer sampling). `Math.random` is never used for outcomes. */ export interface Rng { /** Uniform integer in [0, max). */ int(max: number): number; /** Uniform float in [0, 1). */ float(): number; /** Bernoulli trial. */ chance(p: number): boolean; /** Weighted index sampling. */ weighted(weights: number[] | Float64Array): number; /** Opaque description for audit. */ reference(): string; } /** Cryptographically secure RNG with an internal buffer for throughput. */ export class CryptoRng implements Rng { private buf = randomBytes(65536); private pos = 0; private drawn = 0; private readonly id: string; constructor() { this.id = randomBytes(6).toString("hex"); } private next32(): number { if (this.pos + 4 > this.buf.length) { this.buf = randomBytes(65536); this.pos = 0; } const v = this.buf.readUInt32LE(this.pos); this.pos += 4; this.drawn++; return v; } int(max: number): number { if (max <= 0) throw new Error("rng.int max must be > 0"); if (max === 1) return 0; // Rejection sampling to avoid modulo bias. const limit = 0x1_0000_0000 - (0x1_0000_0000 % max); let v = this.next32(); while (v >= limit) v = this.next32(); return v % max; } float(): number { return this.next32() / 0x1_0000_0000; } chance(p: number): boolean { if (p <= 0) return false; if (p >= 1) return true; return this.float() < p; } weighted(weights: number[] | Float64Array): number { let total = 0; for (let i = 0; i < weights.length; i++) total += weights[i]; if (total <= 0) throw new Error("weighted: total weight must be > 0"); let r = this.float() * total; for (let i = 0; i < weights.length; i++) { r -= weights[i]; if (r < 0) return i; } return weights.length - 1; } reference(): string { return `crypto:${this.id}:${this.drawn}`; } } /** Slow but simplest secure sampler, used by tests to cross-check CryptoRng. */ export class DirectCryptoRng extends CryptoRng { override int(max: number): number { return randomInt(max); } } /** * Deterministic xoshiro128** PRNG — ONLY for reproducible unit tests and * simulator regression fixtures. Never used for player outcomes. */ export class SeededRng implements Rng { private s: Uint32Array; private drawn = 0; constructor(seed: number) { const s = new Uint32Array(4); let x = seed >>> 0 || 0x9e3779b9; for (let i = 0; i < 4; i++) { x ^= x << 13; x ^= x >>> 17; x ^= x << 5; s[i] = x >>> 0; } this.s = s; } private next32(): number { const s = this.s; const result = (Math.imul(rotl(Math.imul(s[1], 5) >>> 0, 7), 9) >>> 0) >>> 0; const t = (s[1] << 9) >>> 0; s[2] ^= s[0]; s[3] ^= s[1]; s[1] ^= s[2]; s[0] ^= s[3]; s[2] ^= t; s[3] = rotl(s[3], 11); this.drawn++; return result; } int(max: number): number { if (max <= 1) return 0; const limit = 0x1_0000_0000 - (0x1_0000_0000 % max); let v = this.next32(); while (v >= limit) v = this.next32(); return v % max; } float(): number { return this.next32() / 0x1_0000_0000; } chance(p: number): boolean { return p > 0 && (p >= 1 || this.float() < p); } weighted(weights: number[] | Float64Array): number { let total = 0; for (let i = 0; i < weights.length; i++) total += weights[i]; let r = this.float() * total; for (let i = 0; i < weights.length; i++) { r -= weights[i]; if (r < 0) return i; } return weights.length - 1; } reference(): string { return `seeded:${this.drawn}`; } } function rotl(x: number, k: number): number { return ((x << k) | (x >>> (32 - k))) >>> 0; }