A bureaucratic horror game — five shifts processing incoming tips and incident reports for a municipal office that officially handles nuisance complaints and unofficially exists to keep Djupet's cult off the front page. Route each report: dismiss, file as known, flag for follow-up, or escalate and lose visibility into what happens next. Guidelines accumulate and contradict across shifts, same shape as Retur's regulation stack. Directly descended from Retur's engine (routeLetter/nextDay -> routeReport/nextShift, same 4-route-per-item structure) but with one real mechanical difference: escalating always costs Insyn (insight/ notice), whether or not it was the correct call — being right doesn't make you invisible. Several reports cite real events from Djupet's own event pool (the bleeding IKEA LACK table, Göran added to the cult's Discord by accident, the Värnhemstorget pigeons, Turning Torso rotating), reframed as outside tips from residents who saw a fragment of something they don't understand. Same universe, a level further from the truth, and the player is explicitly never given the full picture — that's the design, not a limitation. Shares djupet/retur's exact :root palette but its own accent (cold institutional cyan instead of Retur's stamp-red or Djupet's eldritch purple). Intro/win/loss screens use real generated desk photos (rotary phone, corkboard map with red string, a ceiling water stain shaped like a coastline that grows between screens), converted PNG->WebP. Verified: tsc clean, 9/9 vitest (data-integrity check that every report has real consequence text, and a locked-in test for the escalation-always-costs-insight mechanic), a real browser playthrough confirming the wrong+escalate cost stacks correctly (-1 trust, +2 insight), and all three ending states screenshotted via injected game state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
73
src/engine.test.ts
Normal file
73
src/engine.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createState, currentReport, reportsForDay, nextShift, routeReport, startGame } from './engine.ts';
|
||||
import { REPORTS } from './data/content.ts';
|
||||
|
||||
describe('routing engine', () => {
|
||||
it('starts on the first report of shift one', () => {
|
||||
const state = startGame(createState());
|
||||
expect(state.phase).toBe('playing');
|
||||
expect(currentReport(state)?.id).toBe('d01');
|
||||
});
|
||||
|
||||
it('advances and records a correct route', () => {
|
||||
const state = routeReport(startGame(createState()), 'avfarda');
|
||||
expect(state.reportIndex).toBe(1);
|
||||
expect(state.decisions[0].correct).toBe(true);
|
||||
expect(state.trust).toBe(3);
|
||||
});
|
||||
|
||||
it('penalizes an incorrect route', () => {
|
||||
const state = routeReport(startGame(createState()), 'arkivera');
|
||||
expect(state.decisions[0].correct).toBe(false);
|
||||
expect(state.trust).toBe(2);
|
||||
expect(state.insight).toBe(1);
|
||||
});
|
||||
|
||||
it('escalating costs insight even when it is the correct route', () => {
|
||||
// d04 (Turning Torso rotating) is correctly routed eskalera.
|
||||
let state = startGame(createState());
|
||||
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', () => {
|
||||
let state = startGame(createState());
|
||||
// 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(1).map((report) => report.correctRoute);
|
||||
state = routeReport(state, 'arkivera'); // deliberately wrong on d01
|
||||
for (const route of routes.slice(1)) 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 five authored reports in every shift', () => {
|
||||
for (let day = 1; day <= 5; day += 1) expect(reportsForDay(day)).toHaveLength(5);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user