/**
 * Seedable pseudo-random number generator.
 *
 * The old app mixed `Math.random()` into game rolls, which made results
 * untestable and non-reproducible. Everything that rolls dice goes through an
 * `Rng` instance instead, so callers can pass a seed for deterministic tests
 * and reproducible "shared seed" sessions.
 */
export interface Rng {
  /** Float in [0, 1). */
  next(): number;
  /** Integer in [min, max] inclusive. */
  int(min: number, max: number): number;
}

/** Hash a string seed into a 32-bit integer (xfnv1a). */
function hashSeed(str: string): number {
  let h = 2166136261 >>> 0;
  for (let i = 0; i < str.length; i++) {
    h = Math.imul(h ^ str.charCodeAt(i), 16777619);
  }
  return h >>> 0;
}

/** mulberry32 — small, fast, well-distributed PRNG. */
function mulberry32(seed: number): () => number {
  let a = seed >>> 0;
  return () => {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

/**
 * Create a deterministic RNG from a seed. With no seed, derives one from
 * crypto for genuine unpredictability while keeping the same interface.
 */
export function createRng(seed?: string | number): Rng {
  let intSeed: number;
  if (seed === undefined) {
    intSeed = (globalThis.crypto?.getRandomValues(new Uint32Array(1))[0] ?? Date.now()) >>> 0;
  } else if (typeof seed === 'number') {
    intSeed = seed >>> 0;
  } else {
    intSeed = hashSeed(seed);
  }
  const gen = mulberry32(intSeed);
  return {
    next: gen,
    int(min: number, max: number): number {
      if (max < min) [min, max] = [max, min];
      return min + Math.floor(gen() * (max - min + 1));
    },
  };
}
