160 lines
5.6 KiB
TypeScript
160 lines
5.6 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',
|
|
'lockout-warn',
|
|
'lockout-live',
|
|
'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);
|
|
}
|
|
});
|
|
});
|