import { describe, expect, it } from 'vitest'; import { buildRoster, createState, currentReport, reportsForDay, nextShift, routeReport, startGame, PER_DAY_COUNT, LAST_DAY } from './engine.ts'; import { REPORTS } from './data/content.ts'; // A fixed sequence, not a real PRNG — deterministic and enough entropy to // exercise the shuffle without pulling in a dependency for tests only. function fakeRng(seed: number): () => number { let s = seed; return () => { s = (s * 9301 + 49297) % 233280; return s / 233280; }; } function stateWithRoster(roster: string[]) { return { ...startGame(createState()), roster }; } describe('buildRoster', () => { it('draws exactly PER_DAY_COUNT reports per day from that day\'s pool', () => { const roster = buildRoster(fakeRng(1)); expect(roster).toHaveLength(PER_DAY_COUNT * LAST_DAY); for (let day = 1; day <= LAST_DAY; day++) { const ids = roster.slice((day - 1) * PER_DAY_COUNT, day * PER_DAY_COUNT); expect(ids).toHaveLength(PER_DAY_COUNT); expect(new Set(ids).size).toBe(PER_DAY_COUNT); // no duplicates within a day for (const id of ids) { const report = REPORTS.find((r) => r.id === id); expect(report, `roster id ${id} has no matching report`).toBeTruthy(); expect(report!.day).toBe(day); } } }); it('each day\'s pool has more reports than get drawn, so variety is possible', () => { for (let day = 1; day <= LAST_DAY; day++) { const poolSize = REPORTS.filter((r) => r.day === day).length; expect(poolSize).toBeGreaterThan(PER_DAY_COUNT); } }); it('different seeds produce different rosters', () => { const a = buildRoster(fakeRng(1)); const b = buildRoster(fakeRng(99)); expect(a).not.toEqual(b); }); }); describe('routing engine', () => { it('starts on a real report from shift one', () => { const state = startGame(createState()); const report = currentReport(state); expect(state.phase).toBe('playing'); expect(report).toBeTruthy(); expect(report!.day).toBe(1); }); it('advances and records a correct route', () => { const before = startGame(createState()); const report = currentReport(before)!; const state = routeReport(before, report.correctRoute); expect(state.reportIndex).toBe(1); expect(state.decisions[0].correct).toBe(true); expect(state.trust).toBe(3); }); it('penalizes an incorrect route', () => { const before = startGame(createState()); const report = currentReport(before)!; const wrongRoute = (['avfarda', 'arkivera', 'utred', 'eskalera'] as const).find((r) => r !== report.correctRoute)!; const state = routeReport(before, wrongRoute); expect(state.decisions[0].correct).toBe(false); expect(state.trust).toBeLessThan(3); expect(state.insight).toBeGreaterThan(0); }); it('escalating costs insight even when it is the correct route (d04, hand-picked into the roster)', () => { const roster = ['d01', 'd02', 'd03', 'd04', 'd05', ...buildRoster(fakeRng(2)).slice(PER_DAY_COUNT)]; let state = stateWithRoster(roster); for (const route of ['avfarda', 'arkivera', 'utred'] as const) state = routeReport(state, route); expect(currentReport(state)?.id).toBe('d04'); state = routeReport(state, 'eskalera'); expect(state.lastDecision?.correct).toBe(true); expect(state.lastDecision?.trustCost).toBe(0); expect(state.lastDecision?.insightCost).toBe(1); }); it('escalating a wrong report stacks the escalation cost on top of the wrong-route cost', () => { const roster = ['d01', 'd02', 'd03', 'd04', 'd05', ...buildRoster(fakeRng(3)).slice(PER_DAY_COUNT)]; let state = stateWithRoster(roster); // d01's correct route is avfarda; eskalera is wrong here. state = routeReport(state, 'eskalera'); expect(state.lastDecision?.correct).toBe(false); expect(state.lastDecision?.insightCost).toBe(2); // 1 for wrong + 1 for escalating }); it('ends a shift and restores one trust point for the next', () => { let state = startGame(createState()); const routes = reportsForDay(state, 1).map((report) => report.correctRoute); for (const route of routes) state = routeReport(state, route); expect(state.phase).toBe('shift-end'); state = nextShift(state); expect(state.day).toBe(2); expect(state.trust).toBe(3); }); it('has PER_DAY_COUNT reports available in every shift', () => { const state = startGame(createState()); for (let day = 1; day <= LAST_DAY; day += 1) expect(reportsForDay(state, day)).toHaveLength(PER_DAY_COUNT); }); it('every report has its own correct-route consequence', () => { for (const report of REPORTS) { expect(report.correctConsequence, `${report.id} is missing a correctConsequence`).toBeTruthy(); } }); it('loses on insight reaching the cap even with trust intact', () => { let state = startGame(createState()); // Escalate every report regardless of correctness to ramp insight fast. for (let i = 0; i < 10 && state.phase === 'playing'; i++) state = routeReport(state, 'eskalera'); expect(state.phase).toBe('lost'); expect(state.insight).toBeGreaterThanOrEqual(5); }); });