import { describe, expect, it } from 'vitest'; import { dazzaDue, nextDazza, type DazzaWorld } from '../src/scenes/door/dazzaSchedule'; import { DAZZA_TEXTS, type DazzaLine } from '../src/data/strings/dazza'; // Default world satisfies none of the mood gates, so rule-bearing timing can be // tested without a gate accidentally letting commentary through. const world = (over: Partial = {}): DazzaWorld => ({ vibe: 50, aggro: 0, queueLength: 0, insideCount: 50, fired: new Set(), ...over, }); const ids = (lines: readonly DazzaLine[]): string[] => lines.map((l) => l.id); const dueIds = (clockMin: number, w: DazzaWorld): string[] => ids(dazzaDue(clockMin, w)); const lineById = (id: string): DazzaLine => { const line = DAZZA_TEXTS.find((l) => l.id === id); if (!line) throw new Error(`unknown dazza line: ${id}`); return line; }; const RULE_LINES = DAZZA_TEXTS.filter((l) => l.ruleId); describe('dazzaDue', () => { it('says nothing at the top of the night', () => { expect(dazzaDue(0, world())).toEqual([]); }); it('delivers every rule-bearing text on its minute, never a minute early', () => { expect(RULE_LINES).toHaveLength(8); for (const line of RULE_LINES) { expect(dueIds(line.fromMin - 1, world())).not.toContain(line.id); expect(dueIds(line.fromMin, world())).toContain(line.id); } }); it('puts the law ahead of the philosophy when several land at once', () => { const due = dazzaDue(360, world({ vibe: 20, queueLength: 8, insideCount: 2 })); const ruleIdx = due.flatMap((l, i) => (l.ruleId ? [i] : [])); const moodIdx = due.flatMap((l, i) => (l.ruleId ? [] : [i])); expect(ruleIdx.length).toBeGreaterThan(0); expect(moodIdx.length).toBeGreaterThan(0); expect(Math.max(...ruleIdx)).toBeLessThan(Math.min(...moodIdx)); }); it('orders within each group by fromMin ascending', () => { const due = dazzaDue(360, world({ vibe: 20, queueLength: 8, insideCount: 2 })); expect(ids(due.filter((l) => l.ruleId))).toEqual([ 'rule-basics', 'rule-logos', 'rule-songRequest', 'rule-sunnies', 'rule-couples', 'rule-sneakers', 'rule-bucketHats', 'rule-suits', ]); expect(ids(due.filter((l) => !l.ruleId))).toEqual([ 'vibe-empty', 'vibe-mid', 'vibe-queue', 'kayden', 'owner-scare', 'philosophy-1', 'philosophy-2', ]); }); it('never repeats a line that has already been sent', () => { const w = world({ vibe: 20, queueLength: 8, insideCount: 2 }); const first = dazzaDue(360, w); expect(first.length).toBeGreaterThan(0); expect(dazzaDue(360, { ...w, fired: new Set(ids(first)) })).toEqual([]); const partial = world({ fired: new Set(['rule-basics']) }); expect(dueIds(90, partial)).not.toContain('rule-basics'); expect(dueIds(90, partial)).toContain('rule-logos'); }); }); describe('mood gates', () => { const cases: { id: string; yes: Partial; no: Partial }[] = [ { id: 'vibe-mid', yes: { vibe: 34 }, no: { vibe: 35 } }, { id: 'vibe-good', yes: { vibe: 73 }, no: { vibe: 72 } }, { id: 'vibe-queue', yes: { queueLength: 6 }, no: { queueLength: 5 } }, { id: 'vibe-empty', yes: { insideCount: 3 }, no: { insideCount: 4 } }, { id: 'kayden', yes: { queueLength: 3 }, no: { queueLength: 2 } }, ]; for (const c of cases) { it(`${c.id} fires only when the room justifies it`, () => { const at = lineById(c.id).fromMin; expect(dueIds(at, world(c.yes))).toContain(c.id); expect(dueIds(at, world(c.no))).not.toContain(c.id); // gate satisfied but too early is still silence expect(dueIds(at - 1, world(c.yes))).not.toContain(c.id); }); } it('ungated lines fire on time whatever the room is doing', () => { const rooms: Partial[] = [ { vibe: 0, aggro: 100, queueLength: 0, insideCount: 0 }, { vibe: 100, aggro: 0, queueLength: 40, insideCount: 400 }, ]; for (const id of ['owner-scare', 'philosophy-1', 'philosophy-2']) { const at = lineById(id).fromMin; for (const room of rooms) { expect(dueIds(at, world(room))).toContain(id); expect(dueIds(at - 1, world(room))).not.toContain(id); } } }); }); describe('nextDazza', () => { it('hands back the head of the queue, one text at a time', () => { const w = world({ vibe: 20, queueLength: 8, insideCount: 2 }); const due = dazzaDue(360, w); expect(nextDazza(360, w)).toBe(due[0]); expect(nextDazza(360, w)?.id).toBe('rule-basics'); }); it('returns undefined when he has nothing to say', () => { expect(nextDazza(0, world())).toBeUndefined(); expect(nextDazza(360, world({ fired: new Set(ids(DAZZA_TEXTS)) }))).toBeUndefined(); }); }); describe('a whole night, minute by minute', () => { it('delivers every line exactly once, rules on the minute they become law', () => { const fired = new Set(); const deliveredAt = new Map(); for (let m = 0; m <= 360; m++) { // vibe swings so both vibe-mid and vibe-good get a chance across the night const line = nextDazza(m, { vibe: m % 2 === 0 ? 20 : 90, aggro: 0, queueLength: 8, insideCount: 2, fired, }); if (!line) continue; expect(deliveredAt.has(line.id)).toBe(false); fired.add(line.id); deliveredAt.set(line.id, m); } expect([...deliveredAt.keys()].sort()).toEqual(ids(DAZZA_TEXTS).sort()); for (const line of RULE_LINES) { expect(deliveredAt.get(line.id)).toBe(line.fromMin); } }); });