Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
import { describe, expect, it } from 'vitest';
|
Add real replay variety: 15 new reports, random per-day roster
Same fix as Retur (which this engine is descended from): every
playthrough showed the same fixed 25 reports in the same order, with
zero randomization anywhere. Added 15 new reports (3 per day,
d26-d40) citing more of Djupet's event pool (the bridge's impossible
vibration frequency, the investigator in the grey suit whose shadow
didn't follow, a mirror showing an older version of the room),
bringing each day's pool to 8, and changed the engine to draw
PER_DAY_COUNT (5) at random per game instead of a fixed set.
Same shape as Retur's fix: buildRoster() shuffles and slices each
day's pool, GameState carries a `roster` generated once at
createState() and persisted, reportsForDay/currentReport read through
it. Save key bumped to v2 for the same reason — no backfill for old
saves, just start fresh.
Tests rewritten the same way: buildRoster() property tests plus
engine tests that either check general correctness or construct an
explicit roster when a specific report's behavior needs locking in
(d04's escalation-even-when-correct case).
Verified: 12/12 vitest, tsc clean, real browser test confirming 4
distinct first-report sources across 5 fresh games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 21:12:32 +02:00
|
|
|
import { buildRoster, createState, currentReport, reportsForDay, nextShift, routeReport, startGame, PER_DAY_COUNT, LAST_DAY } from './engine.ts';
|
Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
import { REPORTS } from './data/content.ts';
|
|
|
|
|
|
Add real replay variety: 15 new reports, random per-day roster
Same fix as Retur (which this engine is descended from): every
playthrough showed the same fixed 25 reports in the same order, with
zero randomization anywhere. Added 15 new reports (3 per day,
d26-d40) citing more of Djupet's event pool (the bridge's impossible
vibration frequency, the investigator in the grey suit whose shadow
didn't follow, a mirror showing an older version of the room),
bringing each day's pool to 8, and changed the engine to draw
PER_DAY_COUNT (5) at random per game instead of a fixed set.
Same shape as Retur's fix: buildRoster() shuffles and slices each
day's pool, GameState carries a `roster` generated once at
createState() and persisted, reportsForDay/currentReport read through
it. Save key bumped to v2 for the same reason — no backfill for old
saves, just start fresh.
Tests rewritten the same way: buildRoster() property tests plus
engine tests that either check general correctness or construct an
explicit roster when a specific report's behavior needs locking in
(d04's escalation-even-when-correct case).
Verified: 12/12 vitest, tsc clean, real browser test confirming 4
distinct first-report sources across 5 fresh games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 21:12:32 +02:00
|
|
|
// 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);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
describe('routing engine', () => {
|
Add real replay variety: 15 new reports, random per-day roster
Same fix as Retur (which this engine is descended from): every
playthrough showed the same fixed 25 reports in the same order, with
zero randomization anywhere. Added 15 new reports (3 per day,
d26-d40) citing more of Djupet's event pool (the bridge's impossible
vibration frequency, the investigator in the grey suit whose shadow
didn't follow, a mirror showing an older version of the room),
bringing each day's pool to 8, and changed the engine to draw
PER_DAY_COUNT (5) at random per game instead of a fixed set.
Same shape as Retur's fix: buildRoster() shuffles and slices each
day's pool, GameState carries a `roster` generated once at
createState() and persisted, reportsForDay/currentReport read through
it. Save key bumped to v2 for the same reason — no backfill for old
saves, just start fresh.
Tests rewritten the same way: buildRoster() property tests plus
engine tests that either check general correctness or construct an
explicit roster when a specific report's behavior needs locking in
(d04's escalation-even-when-correct case).
Verified: 12/12 vitest, tsc clean, real browser test confirming 4
distinct first-report sources across 5 fresh games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 21:12:32 +02:00
|
|
|
it('starts on a real report from shift one', () => {
|
Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
const state = startGame(createState());
|
Add real replay variety: 15 new reports, random per-day roster
Same fix as Retur (which this engine is descended from): every
playthrough showed the same fixed 25 reports in the same order, with
zero randomization anywhere. Added 15 new reports (3 per day,
d26-d40) citing more of Djupet's event pool (the bridge's impossible
vibration frequency, the investigator in the grey suit whose shadow
didn't follow, a mirror showing an older version of the room),
bringing each day's pool to 8, and changed the engine to draw
PER_DAY_COUNT (5) at random per game instead of a fixed set.
Same shape as Retur's fix: buildRoster() shuffles and slices each
day's pool, GameState carries a `roster` generated once at
createState() and persisted, reportsForDay/currentReport read through
it. Save key bumped to v2 for the same reason — no backfill for old
saves, just start fresh.
Tests rewritten the same way: buildRoster() property tests plus
engine tests that either check general correctness or construct an
explicit roster when a specific report's behavior needs locking in
(d04's escalation-even-when-correct case).
Verified: 12/12 vitest, tsc clean, real browser test confirming 4
distinct first-report sources across 5 fresh games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 21:12:32 +02:00
|
|
|
const report = currentReport(state);
|
Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
expect(state.phase).toBe('playing');
|
Add real replay variety: 15 new reports, random per-day roster
Same fix as Retur (which this engine is descended from): every
playthrough showed the same fixed 25 reports in the same order, with
zero randomization anywhere. Added 15 new reports (3 per day,
d26-d40) citing more of Djupet's event pool (the bridge's impossible
vibration frequency, the investigator in the grey suit whose shadow
didn't follow, a mirror showing an older version of the room),
bringing each day's pool to 8, and changed the engine to draw
PER_DAY_COUNT (5) at random per game instead of a fixed set.
Same shape as Retur's fix: buildRoster() shuffles and slices each
day's pool, GameState carries a `roster` generated once at
createState() and persisted, reportsForDay/currentReport read through
it. Save key bumped to v2 for the same reason — no backfill for old
saves, just start fresh.
Tests rewritten the same way: buildRoster() property tests plus
engine tests that either check general correctness or construct an
explicit roster when a specific report's behavior needs locking in
(d04's escalation-even-when-correct case).
Verified: 12/12 vitest, tsc clean, real browser test confirming 4
distinct first-report sources across 5 fresh games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 21:12:32 +02:00
|
|
|
expect(report).toBeTruthy();
|
|
|
|
|
expect(report!.day).toBe(1);
|
Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('advances and records a correct route', () => {
|
Add real replay variety: 15 new reports, random per-day roster
Same fix as Retur (which this engine is descended from): every
playthrough showed the same fixed 25 reports in the same order, with
zero randomization anywhere. Added 15 new reports (3 per day,
d26-d40) citing more of Djupet's event pool (the bridge's impossible
vibration frequency, the investigator in the grey suit whose shadow
didn't follow, a mirror showing an older version of the room),
bringing each day's pool to 8, and changed the engine to draw
PER_DAY_COUNT (5) at random per game instead of a fixed set.
Same shape as Retur's fix: buildRoster() shuffles and slices each
day's pool, GameState carries a `roster` generated once at
createState() and persisted, reportsForDay/currentReport read through
it. Save key bumped to v2 for the same reason — no backfill for old
saves, just start fresh.
Tests rewritten the same way: buildRoster() property tests plus
engine tests that either check general correctness or construct an
explicit roster when a specific report's behavior needs locking in
(d04's escalation-even-when-correct case).
Verified: 12/12 vitest, tsc clean, real browser test confirming 4
distinct first-report sources across 5 fresh games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 21:12:32 +02:00
|
|
|
const before = startGame(createState());
|
|
|
|
|
const report = currentReport(before)!;
|
|
|
|
|
const state = routeReport(before, report.correctRoute);
|
Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
expect(state.reportIndex).toBe(1);
|
|
|
|
|
expect(state.decisions[0].correct).toBe(true);
|
|
|
|
|
expect(state.trust).toBe(3);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('penalizes an incorrect route', () => {
|
Add real replay variety: 15 new reports, random per-day roster
Same fix as Retur (which this engine is descended from): every
playthrough showed the same fixed 25 reports in the same order, with
zero randomization anywhere. Added 15 new reports (3 per day,
d26-d40) citing more of Djupet's event pool (the bridge's impossible
vibration frequency, the investigator in the grey suit whose shadow
didn't follow, a mirror showing an older version of the room),
bringing each day's pool to 8, and changed the engine to draw
PER_DAY_COUNT (5) at random per game instead of a fixed set.
Same shape as Retur's fix: buildRoster() shuffles and slices each
day's pool, GameState carries a `roster` generated once at
createState() and persisted, reportsForDay/currentReport read through
it. Save key bumped to v2 for the same reason — no backfill for old
saves, just start fresh.
Tests rewritten the same way: buildRoster() property tests plus
engine tests that either check general correctness or construct an
explicit roster when a specific report's behavior needs locking in
(d04's escalation-even-when-correct case).
Verified: 12/12 vitest, tsc clean, real browser test confirming 4
distinct first-report sources across 5 fresh games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 21:12:32 +02:00
|
|
|
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);
|
Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
expect(state.decisions[0].correct).toBe(false);
|
Add real replay variety: 15 new reports, random per-day roster
Same fix as Retur (which this engine is descended from): every
playthrough showed the same fixed 25 reports in the same order, with
zero randomization anywhere. Added 15 new reports (3 per day,
d26-d40) citing more of Djupet's event pool (the bridge's impossible
vibration frequency, the investigator in the grey suit whose shadow
didn't follow, a mirror showing an older version of the room),
bringing each day's pool to 8, and changed the engine to draw
PER_DAY_COUNT (5) at random per game instead of a fixed set.
Same shape as Retur's fix: buildRoster() shuffles and slices each
day's pool, GameState carries a `roster` generated once at
createState() and persisted, reportsForDay/currentReport read through
it. Save key bumped to v2 for the same reason — no backfill for old
saves, just start fresh.
Tests rewritten the same way: buildRoster() property tests plus
engine tests that either check general correctness or construct an
explicit roster when a specific report's behavior needs locking in
(d04's escalation-even-when-correct case).
Verified: 12/12 vitest, tsc clean, real browser test confirming 4
distinct first-report sources across 5 fresh games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 21:12:32 +02:00
|
|
|
expect(state.trust).toBeLessThan(3);
|
|
|
|
|
expect(state.insight).toBeGreaterThan(0);
|
Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
});
|
|
|
|
|
|
Add real replay variety: 15 new reports, random per-day roster
Same fix as Retur (which this engine is descended from): every
playthrough showed the same fixed 25 reports in the same order, with
zero randomization anywhere. Added 15 new reports (3 per day,
d26-d40) citing more of Djupet's event pool (the bridge's impossible
vibration frequency, the investigator in the grey suit whose shadow
didn't follow, a mirror showing an older version of the room),
bringing each day's pool to 8, and changed the engine to draw
PER_DAY_COUNT (5) at random per game instead of a fixed set.
Same shape as Retur's fix: buildRoster() shuffles and slices each
day's pool, GameState carries a `roster` generated once at
createState() and persisted, reportsForDay/currentReport read through
it. Save key bumped to v2 for the same reason — no backfill for old
saves, just start fresh.
Tests rewritten the same way: buildRoster() property tests plus
engine tests that either check general correctness or construct an
explicit roster when a specific report's behavior needs locking in
(d04's escalation-even-when-correct case).
Verified: 12/12 vitest, tsc clean, real browser test confirming 4
distinct first-report sources across 5 fresh games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 21:12:32 +02:00
|
|
|
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);
|
Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
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', () => {
|
Add real replay variety: 15 new reports, random per-day roster
Same fix as Retur (which this engine is descended from): every
playthrough showed the same fixed 25 reports in the same order, with
zero randomization anywhere. Added 15 new reports (3 per day,
d26-d40) citing more of Djupet's event pool (the bridge's impossible
vibration frequency, the investigator in the grey suit whose shadow
didn't follow, a mirror showing an older version of the room),
bringing each day's pool to 8, and changed the engine to draw
PER_DAY_COUNT (5) at random per game instead of a fixed set.
Same shape as Retur's fix: buildRoster() shuffles and slices each
day's pool, GameState carries a `roster` generated once at
createState() and persisted, reportsForDay/currentReport read through
it. Save key bumped to v2 for the same reason — no backfill for old
saves, just start fresh.
Tests rewritten the same way: buildRoster() property tests plus
engine tests that either check general correctness or construct an
explicit roster when a specific report's behavior needs locking in
(d04's escalation-even-when-correct case).
Verified: 12/12 vitest, tsc clean, real browser test confirming 4
distinct first-report sources across 5 fresh games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 21:12:32 +02:00
|
|
|
const roster = ['d01', 'd02', 'd03', 'd04', 'd05', ...buildRoster(fakeRng(3)).slice(PER_DAY_COUNT)];
|
|
|
|
|
let state = stateWithRoster(roster);
|
Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
// 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());
|
Add real replay variety: 15 new reports, random per-day roster
Same fix as Retur (which this engine is descended from): every
playthrough showed the same fixed 25 reports in the same order, with
zero randomization anywhere. Added 15 new reports (3 per day,
d26-d40) citing more of Djupet's event pool (the bridge's impossible
vibration frequency, the investigator in the grey suit whose shadow
didn't follow, a mirror showing an older version of the room),
bringing each day's pool to 8, and changed the engine to draw
PER_DAY_COUNT (5) at random per game instead of a fixed set.
Same shape as Retur's fix: buildRoster() shuffles and slices each
day's pool, GameState carries a `roster` generated once at
createState() and persisted, reportsForDay/currentReport read through
it. Save key bumped to v2 for the same reason — no backfill for old
saves, just start fresh.
Tests rewritten the same way: buildRoster() property tests plus
engine tests that either check general correctness or construct an
explicit roster when a specific report's behavior needs locking in
(d04's escalation-even-when-correct case).
Verified: 12/12 vitest, tsc clean, real browser test confirming 4
distinct first-report sources across 5 fresh games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 21:12:32 +02:00
|
|
|
const routes = reportsForDay(state, 1).map((report) => report.correctRoute);
|
|
|
|
|
for (const route of routes) state = routeReport(state, route);
|
Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
expect(state.phase).toBe('shift-end');
|
|
|
|
|
state = nextShift(state);
|
|
|
|
|
expect(state.day).toBe(2);
|
|
|
|
|
expect(state.trust).toBe(3);
|
|
|
|
|
});
|
|
|
|
|
|
Add real replay variety: 15 new reports, random per-day roster
Same fix as Retur (which this engine is descended from): every
playthrough showed the same fixed 25 reports in the same order, with
zero randomization anywhere. Added 15 new reports (3 per day,
d26-d40) citing more of Djupet's event pool (the bridge's impossible
vibration frequency, the investigator in the grey suit whose shadow
didn't follow, a mirror showing an older version of the room),
bringing each day's pool to 8, and changed the engine to draw
PER_DAY_COUNT (5) at random per game instead of a fixed set.
Same shape as Retur's fix: buildRoster() shuffles and slices each
day's pool, GameState carries a `roster` generated once at
createState() and persisted, reportsForDay/currentReport read through
it. Save key bumped to v2 for the same reason — no backfill for old
saves, just start fresh.
Tests rewritten the same way: buildRoster() property tests plus
engine tests that either check general correctness or construct an
explicit roster when a specific report's behavior needs locking in
(d04's escalation-even-when-correct case).
Verified: 12/12 vitest, tsc clean, real browser test confirming 4
distinct first-report sources across 5 fresh games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 21:12:32 +02:00
|
|
|
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);
|
Initial commit: Diariet
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>
2026-07-27 19:50:31 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
});
|