96 lines
3.5 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|