SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
3.8 KB · 148 lines typescript
Raw Blame History
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