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); } }); });