import { describe, expect, it } from 'vitest'; import { adoptCastIdentity, buildRegularCast, REGULAR_CAST_SIZE } from '../src/patrons/regularCast'; import { disguisedFromMemory, recordDenialBy } from '../src/patrons/memory'; import { freshGameState } from '../src/core/save'; import { dollPlan } from '../src/patrons/dollPlan'; import { SeededRNG } from '../src/core/SeededRNG'; import { generatePatron, _resetPatronSerial } from '../src/patrons/generator'; const NIGHT = new Date('2026-07-16T00:00:00'); describe('the regular cast (design ยง4.1)', () => { it('is stable per run seed โ€” same six faces, every night, every venue', () => { const a = buildRegularCast(4207, NIGHT); const b = buildRegularCast(4207, NIGHT); expect(a).toHaveLength(REGULAR_CAST_SIZE); expect(a.map((p) => p.dollSeed)).toEqual(b.map((p) => p.dollSeed)); expect(a.map((p) => p.idCard.name)).toEqual(b.map((p) => p.idCard.name)); expect(new Set(a.map((p) => p.flags.regularId)).size).toBe(REGULAR_CAST_SIZE); }); it('different runs meet different regulars', () => { const a = buildRegularCast(1, NIGHT).map((p) => p.dollSeed); const b = buildRegularCast(2, NIGHT).map((p) => p.dollSeed); expect(a).not.toEqual(b); }); it('cast members carry real, boring licences โ€” regulars are not ID puzzles', () => { for (const p of buildRegularCast(4207, NIGHT)) { expect(p.idCard.fake).toBeUndefined(); expect(p.age).toBeGreaterThanOrEqual(18); } }); it('adoption keeps tonight\'s roll but wears the cast identity', () => { _resetPatronSerial(); const rolled = generatePatron({ rng: new SeededRNG(9), nightDate: NIGHT }, 120, 'regular'); const member = buildRegularCast(4207, NIGHT)[0]!; const adopted = adoptCastIdentity(rolled, member, 1, false); expect(adopted.dollSeed).toBe(member.dollSeed); expect(adopted.idCard.name).toBe(member.idCard.name); expect(adopted.outfit).toBe(rolled.outfit); // tonight's clothes expect(adopted.intoxication).toBe(rolled.intoxication); // tonight's drinks expect(adopted.flags.regularId).toBe(member.flags.regularId); expect(adopted.flags.timesDeniedBefore).toBe(1); expect(adopted.flags.disguised).toBeUndefined(); }); it('two denials on the run save โ†’ the third appearance is disguised', () => { const run = freshGameState(4207); recordDenialBy(run, 'reg2', 0); expect(disguisedFromMemory(run.regulars['reg2'])).toBe(false); recordDenialBy(run, 'reg2', 1); expect(disguisedFromMemory(run.regulars['reg2'])).toBe(true); expect(run.regulars['reg2']!.timesDenied).toBe(2); }); it('the disguise is on the man, never on his card photo', () => { const member = buildRegularCast(4207, NIGHT)[3]!; const plain = { ...member, flags: { ...member.flags } }; const disguised = { ...member, flags: { ...member.flags, disguised: true } }; expect(dollPlan(disguised, 'queue').rects.length).toBeGreaterThan( dollPlan(plain, 'queue').rects.length, ); // The idPhoto stays clean-shaven: the card shows Thursday's bloke. expect(dollPlan(disguised, 'idPhoto')).toEqual(dollPlan(plain, 'idPhoto')); }); });