not-tonight/tests/rng.test.ts
2026-07-19 16:20:53 +10:00

72 lines
2.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { SeededRNG } from '../src/core/SeededRNG';
describe('SeededRNG', () => {
it('same seed + stream name → identical sequence', () => {
const a = new SeededRNG(42).stream('patrons');
const b = new SeededRNG(42).stream('patrons');
for (let i = 0; i < 100; i++) expect(a.next()).toBe(b.next());
});
it('different seeds diverge', () => {
const a = new SeededRNG(1).stream('x');
const b = new SeededRNG(2).stream('x');
const seqA = Array.from({ length: 10 }, () => a.next());
const seqB = Array.from({ length: 10 }, () => b.next());
expect(seqA).not.toEqual(seqB);
});
it('streams are independent — draining one does not shift another', () => {
const rng1 = new SeededRNG(7);
const rng2 = new SeededRNG(7);
rng1.stream('a');
for (let i = 0; i < 500; i++) rng1.stream('a').next(); // drain a
const fromDrained = rng1.stream('b').next();
const fromFresh = rng2.stream('b').next();
expect(fromDrained).toBe(fromFresh);
});
it('stream() returns the same stream instance per name (stateful)', () => {
const rng = new SeededRNG(9);
const first = rng.stream('q').next();
const second = rng.stream('q').next();
expect(first).not.toBe(second); // continued, not restarted
});
it('int() covers bounds inclusively and stays in range', () => {
const s = new SeededRNG(3).stream('ints');
const seen = new Set<number>();
for (let i = 0; i < 2000; i++) {
const v = s.int(1, 6);
expect(v).toBeGreaterThanOrEqual(1);
expect(v).toBeLessThanOrEqual(6);
seen.add(v);
}
expect(seen.size).toBe(6);
});
it('chance() distribution sanity', () => {
const s = new SeededRNG(11).stream('coin');
let hits = 0;
for (let i = 0; i < 10_000; i++) if (s.chance(0.3)) hits++;
expect(hits / 10_000).toBeGreaterThan(0.27);
expect(hits / 10_000).toBeLessThan(0.33);
});
it('weighted() respects weights and never returns zero-weight entries', () => {
const s = new SeededRNG(13).stream('w');
const counts = { a: 0, b: 0, c: 0 };
for (let i = 0; i < 10_000; i++) {
counts[s.weighted([['a', 9], ['b', 1], ['c', 0]] as const)]++;
}
expect(counts.c).toBe(0);
expect(counts.a).toBeGreaterThan(counts.b * 5);
});
it('pick/weighted throw on empty/invalid input', () => {
const s = new SeededRNG(1).stream('e');
expect(() => s.pick([])).toThrow();
expect(() => s.weighted([])).toThrow();
});
});