not-tonight/tests/dazzaSchedule.test.ts
type-two 595371f684 Phase 1 review: merge door/floor/juice, fold CCRs into contracts, integration fixes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:26:35 +10:00

158 lines
5.5 KiB
TypeScript

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> = {}): DazzaWorld => ({
vibe: 50,
aggro: 0,
queueLength: 0,
insideCount: 50,
fired: new Set<string>(),
...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<DazzaWorld>; no: Partial<DazzaWorld> }[] = [
{ 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<DazzaWorld>[] = [
{ 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<string>();
const deliveredAt = new Map<string, number>();
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);
}
});
});