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

96 lines
3.5 KiB
TypeScript

import { beforeEach, describe, expect, it } from 'vitest';
import { SeededRNG } from '../src/core/SeededRNG';
import { ageOn } from '../src/core/GameClock';
import { generatePatron, _resetPatronSerial, type GeneratorContext } from '../src/patrons/generator';
import type { Patron } from '../src/data/types';
const NIGHT = new Date('2026-07-18T00:00:00');
const ctx = (seed: number): GeneratorContext => ({ rng: new SeededRNG(seed), nightDate: NIGHT });
const batch = (seed: number, n: number, clockMin = 0): Patron[] => {
_resetPatronSerial();
const c = ctx(seed);
return Array.from({ length: n }, () => generatePatron(c, clockMin));
};
beforeEach(() => _resetPatronSerial());
describe('generatePatron', () => {
it('is deterministic: same seed → identical batch', () => {
expect(batch(42, 50)).toEqual(batch(42, 50));
});
it('different seeds → different batches', () => {
const a = batch(1, 20).map((p) => p.dollSeed);
const b = batch(2, 20).map((p) => p.dollSeed);
expect(a).not.toEqual(b);
});
it('real IDs never carry fake tells and their DOB matches true age', () => {
for (const p of batch(7, 300)) {
if (!p.idCard.fake) {
expect(ageOn(p.idCard.dob, NIGHT)).toBe(p.age);
expect(p.age).toBeGreaterThanOrEqual(18);
}
}
});
it('every fake ID has at least one detectable tell', () => {
const fakes = batch(11, 600).filter((p) => p.idCard.fake);
expect(fakes.length).toBeGreaterThan(0);
for (const p of fakes) {
const f = p.idCard.fake!;
const expired = new Date(`${p.idCard.expiry}T00:00:00`) < NIGHT;
const hasTell =
!!f.peelingLaminate || !!f.wrongHologram || !!f.jokeName || !!f.photoMismatch || expired;
expect(hasTell).toBe(true);
}
});
it('almost18s are 17 with fake IDs claiming 18+; fresh18s are genuinely 18', () => {
_resetPatronSerial();
const c = ctx(13);
for (let i = 0; i < 40; i++) {
const kid = generatePatron(c, 0, 'almost18');
expect(kid.age).toBe(17);
expect(kid.idCard.fake).toBeDefined();
expect(ageOn(kid.idCard.dob, NIGHT)).toBeGreaterThanOrEqual(18);
const fresh = generatePatron(c, 0, 'fresh18');
expect(fresh.age).toBe(18);
expect(fresh.idCard.fake).toBeUndefined();
}
});
it('photoMismatch is the only case where photoSeed differs from dollSeed', () => {
for (const p of batch(17, 600)) {
if (p.idCard.fake?.photoMismatch) expect(p.idCard.photoSeed).not.toBe(p.dollSeed);
else expect(p.idCard.photoSeed).toBe(p.dollSeed);
}
});
it('late crowd arrives drunker on average', () => {
const early = batch(23, 200, 0);
const late = batch(23, 200, 300);
const avg = (ps: Patron[]) => ps.reduce((s, p) => s + p.intoxication, 0) / ps.length;
expect(avg(late)).toBeGreaterThan(avg(early) + 0.05);
});
it('outfits fill every slot and intoxication/tolerance stay in 0..1', () => {
for (const p of batch(29, 100)) {
expect(p.outfit.map((l) => l.slot).sort()).toEqual(
['accessory', 'hair', 'legs', 'outer', 'shoes', 'top'],
);
expect(p.intoxication).toBeGreaterThanOrEqual(0);
expect(p.intoxication).toBeLessThanOrEqual(1);
expect(p.tolerance).toBeGreaterThanOrEqual(0);
expect(p.tolerance).toBeLessThanOrEqual(1);
}
});
it('guest-list truth implies the claim (liars exist, honest listers do not hide)', () => {
for (const p of batch(31, 400)) {
if (p.flags.onGuestList) expect(p.flags.claimsGuestList).toBe(true);
}
});
});