export type Rng = () => number; /** Small deterministic generator, so a daily challenge is the same all day. */ export function seeded(seed: number | string): Rng { let h = typeof seed === 'number' ? seed >>> 0 : 2166136261; if (typeof seed === 'string') { for (let i = 0; i < seed.length; i++) h = Math.imul(h ^ seed.charCodeAt(i), 16777619); } return () => { h = (h + 0x6d2b79f5) >>> 0; let t = h; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } export const int = (rng: Rng, min: number, max: number) => min + Math.floor(rng() * (max - min + 1)); export const pick = (rng: Rng, list: readonly T[]): T => list[Math.floor(rng() * list.length)]; export function shuffle(rng: Rng, list: readonly T[]): T[] { const copy = [...list]; for (let i = copy.length - 1; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); [copy[i], copy[j]] = [copy[j], copy[i]]; } return copy; } /** Up to `count` distinct values from `list` that differ from `exclude` once normalized. */ export function sample( rng: Rng, list: readonly T[], count: number, key: (value: T) => string = String, exclude: string[] = [] ): T[] { const seen = new Set(exclude.map((value) => value.toLowerCase())); const out: T[] = []; for (const value of shuffle(rng, list)) { if (out.length >= count) break; const k = key(value).toLowerCase(); if (seen.has(k)) continue; seen.add(k); out.push(value); } return out; }