import { afterEach, describe, expect, it } from 'vitest'; import { ACTIVE_RULE_CAP, DRESS_CODE_RULES, SHUFFLE_EVERY_MIN, SHUFFLE_RULE_COUNT, activeRules, announcedRules, setDressCodeShuffle, shuffleWindow, violations, } from '../src/rules/dressCode'; import { generatePatron, _resetPatronSerial } from '../src/patrons/generator'; import { SeededRNG } from '../src/core/SeededRNG'; // Module-level mode: ALWAYS clear after each test or the whole suite goes to ROOM. afterEach(() => setDressCodeShuffle(null)); const ids = (clockMin: number): string[] => activeRules(clockMin).map((r) => r.id); describe('ROOM dress-code shuffle', () => { it('windows turn every SHUFFLE_EVERY_MIN', () => { expect(shuffleWindow(0)).toBe(0); expect(shuffleWindow(SHUFFLE_EVERY_MIN - 1)).toBe(0); expect(shuffleWindow(SHUFFLE_EVERY_MIN)).toBe(1); expect(shuffleWindow(SHUFFLE_EVERY_MIN * 4 + 3)).toBe(4); }); it('serves SHUFFLE_RULE_COUNT real rules, stable within a window', () => { setDressCodeShuffle(4207); const a = ids(0); expect(a).toHaveLength(SHUFFLE_RULE_COUNT); expect(SHUFFLE_RULE_COUNT).toBeLessThanOrEqual(ACTIVE_RULE_CAP); for (const id of a) expect(DRESS_CODE_RULES.some((r) => r.id === id)).toBe(true); // re-reading the board mid-window must not reroll it expect(ids(SHUFFLE_EVERY_MIN - 1)).toEqual(a); }); it('rewrites across windows and is deterministic per seed', () => { setDressCodeShuffle(4207); const windows = Array.from({ length: 8 }, (_, w) => ids(w * SHUFFLE_EVERY_MIN)); // Some consecutive pair must differ — a board that never changes isn't ROOM. expect(windows.some((w, i) => i > 0 && w.join() !== windows[i - 1]!.join())).toBe(true); // Same seed replays the same night. setDressCodeShuffle(4207); expect(ids(SHUFFLE_EVERY_MIN * 3)).toEqual(windows[3]); // A different seed is a different board. setDressCodeShuffle(999); const other = Array.from({ length: 8 }, (_, w) => ids(w * SHUFFLE_EVERY_MIN)); expect(other.join('|')).not.toBe(windows.join('|')); }); it('announcedRules mirrors the live set — nothing greyed, nothing historical', () => { setDressCodeShuffle(4207); expect(announcedRules(200).map((r) => r.id)).toEqual(ids(200)); }); it('clearing the mode restores the Dazza chronology', () => { setDressCodeShuffle(4207); setDressCodeShuffle(null); expect(activeRules(0)).toEqual(DRESS_CODE_RULES.filter((r) => r.activeFrom <= 0).slice(-ACTIVE_RULE_CAP)); }); it('violations() judges against the shuffled set', () => { _resetPatronSerial(); const rng = new SeededRNG(7); const ctx = { rng, nightDate: new Date('2026-07-16T00:00:00') }; setDressCodeShuffle(4207); for (let i = 0; i < 40; i++) { const p = generatePatron(ctx, 30); const live = new Set(ids(30)); for (const v of violations(p, 30)) expect(live.has(v.id)).toBe(true); } }); });