TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1import { randomBytes, randomInt } from "node:crypto";23/**4 * RNG abstraction. Production uses `CryptoRng` (buffered `crypto.randomBytes`,5 * unbiased integer sampling). `Math.random` is never used for outcomes.6 */7export interface Rng {8 /** Uniform integer in [0, max). */9 int(max: number): number;10 /** Uniform float in [0, 1). */11 float(): number;12 /** Bernoulli trial. */13 chance(p: number): boolean;14 /** Weighted index sampling. */15 weighted(weights: number[] | Float64Array): number;16 /** Opaque description for audit. */17 reference(): string;18}1920/** Cryptographically secure RNG with an internal buffer for throughput. */21export class CryptoRng implements Rng {22 private buf = randomBytes(65536);23 private pos = 0;24 private drawn = 0;25 private readonly id: string;2627 constructor() {28 this.id = randomBytes(6).toString("hex");29 }3031 private next32(): number {32 if (this.pos + 4 > this.buf.length) {33 this.buf = randomBytes(65536);34 this.pos = 0;35 }36 const v = this.buf.readUInt32LE(this.pos);37 this.pos += 4;38 this.drawn++;39 return v;40 }4142 int(max: number): number {43 if (max <= 0) throw new Error("rng.int max must be > 0");44 if (max === 1) return 0;45 // Rejection sampling to avoid modulo bias.46 const limit = 0x1_0000_0000 - (0x1_0000_0000 % max);47 let v = this.next32();48 while (v >= limit) v = this.next32();49 return v % max;50 }5152 float(): number {53 return this.next32() / 0x1_0000_0000;54 }5556 chance(p: number): boolean {57 if (p <= 0) return false;58 if (p >= 1) return true;59 return this.float() < p;60 }6162 weighted(weights: number[] | Float64Array): number {63 let total = 0;64 for (let i = 0; i < weights.length; i++) total += weights[i];65 if (total <= 0) throw new Error("weighted: total weight must be > 0");66 let r = this.float() * total;67 for (let i = 0; i < weights.length; i++) {68 r -= weights[i];69 if (r < 0) return i;70 }71 return weights.length - 1;72 }7374 reference(): string {75 return `crypto:${this.id}:${this.drawn}`;76 }77}7879/** Slow but simplest secure sampler, used by tests to cross-check CryptoRng. */80export class DirectCryptoRng extends CryptoRng {81 override int(max: number): number {82 return randomInt(max);83 }84}8586/**87 * Deterministic xoshiro128** PRNG — ONLY for reproducible unit tests and88 * simulator regression fixtures. Never used for player outcomes.89 */90export class SeededRng implements Rng {91 private s: Uint32Array;92 private drawn = 0;93 constructor(seed: number) {94 const s = new Uint32Array(4);95 let x = seed >>> 0 || 0x9e3779b9;96 for (let i = 0; i < 4; i++) {97 x ^= x << 13;98 x ^= x >>> 17;99 x ^= x << 5;100 s[i] = x >>> 0;101 }102 this.s = s;103 }104 private next32(): number {105 const s = this.s;106 const result = (Math.imul(rotl(Math.imul(s[1], 5) >>> 0, 7), 9) >>> 0) >>> 0;107 const t = (s[1] << 9) >>> 0;108 s[2] ^= s[0];109 s[3] ^= s[1];110 s[1] ^= s[2];111 s[0] ^= s[3];112 s[2] ^= t;113 s[3] = rotl(s[3], 11);114 this.drawn++;115 return result;116 }117 int(max: number): number {118 if (max <= 1) return 0;119 const limit = 0x1_0000_0000 - (0x1_0000_0000 % max);120 let v = this.next32();121 while (v >= limit) v = this.next32();122 return v % max;123 }124 float(): number {125 return this.next32() / 0x1_0000_0000;126 }127 chance(p: number): boolean {128 return p > 0 && (p >= 1 || this.float() < p);129 }130 weighted(weights: number[] | Float64Array): number {131 let total = 0;132 for (let i = 0; i < weights.length; i++) total += weights[i];133 let r = this.float() * total;134 for (let i = 0; i < weights.length; i++) {135 r -= weights[i];136 if (r < 0) return i;137 }138 return weights.length - 1;139 }140 reference(): string {141 return `seeded:${this.drawn}`;142 }143}144145function rotl(x: number, k: number): number {146 return ((x << k) | (x >>> (32 - k))) >>> 0;147}148