not-tonight/tests/regularCast.test.ts
type-two f38ea0743c Cross-night regulars with disguises, venue crowd identities, retractions as texts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:43:40 +10:00

68 lines
3.1 KiB
TypeScript

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