import { describe, expect, it } from 'vitest'; import { ROLE_BEATS, beatsDue } from '../src/data/roleBeats'; import { ROLES } from '../src/data/roster'; // Thu/Fri/Sat. Not imported from NightScene: that module pulls in Phaser, // which needs a `window` this suite does not have. const TOTAL_NIGHTS = 3; describe('role story beats (design §6)', () => { it('every role on the board has a week of them', () => { for (const r of ROLES) { const beats = ROLE_BEATS[r.id]; expect(beats, `${r.id} has no beats`).toBeTruthy(); expect(beats.length).toBeGreaterThan(0); } }); it('one beat per night, and every night of the week is covered', () => { for (const r of ROLES) { const nights = ROLE_BEATS[r.id].map((b) => b.night).sort(); expect(nights, `${r.id} week`).toEqual([...Array(TOTAL_NIGHTS).keys()]); } }); it('they land mid-shift, not at the door-open or after last drinks', () => { // A beat at minute 0 is noise; one at 3 AM never arrives. for (const r of ROLES) { for (const b of ROLE_BEATS[r.id]) { expect(b.atMin, `${r.id} n${b.night}`).toBeGreaterThan(60); expect(b.atMin, `${r.id} n${b.night}`).toBeLessThan(300); } } }); it('fires each beat exactly once as the clock passes it', () => { const seen: string[] = []; let last = -1; for (let m = 0; m <= 360; m++) { for (const b of beatsDue('glassie', 0, m, last)) seen.push(b.text); last = m; } expect(seen.length).toBe(1); }); it('never fires another role\'s week, or another night of your own', () => { const all = (role: 'glassie' | 'hos', night: number): number => { let last = -1; let n = 0; for (let m = 0; m <= 360; m++) { n += beatsDue(role, night, m, last).length; last = m; } return n; }; expect(all('glassie', 0)).toBe(1); expect(all('hos', 0)).toBe(1); expect(ROLE_BEATS.glassie[0]!.text).not.toBe(ROLE_BEATS.hos[0]!.text); // A night index past the week is silent rather than throwing. expect(all('glassie', 9)).toBe(0); }); it('a clock that jumps several minutes does not swallow a beat', () => { // GameClock can advance more than a minute in one tick under load. const b = ROLE_BEATS.bartender.find((x) => x.night === 1)!; expect(beatsDue('bartender', 1, b.atMin + 5, b.atMin - 5)).toHaveLength(1); }); it('each role gets its OWN voice — no shared copy between shifts', () => { const texts = ROLES.flatMap((r) => ROLE_BEATS[r.id].map((b) => b.text)); expect(new Set(texts).size).toBe(texts.length); }); });