Expand letters and decision consequences

This commit is contained in:
Fredrik Johansson
2026-07-20 19:56:58 +02:00
parent b69131c540
commit 61e6b256d5
6 changed files with 153 additions and 14 deletions

View File

@@ -124,6 +124,94 @@ export const LETTERS: Letter[] = [
body: 'Om du läser detta har jag fått gå hem. Det händer sällan här. Bordet behöver någon.', marks: ['PERSONALAKT', 'SVART STÄMPEL', 'SVART STÄMPEL', 'SVART STÄMPEL'], body: 'Om du läser detta har jag fått gå hem. Det händer sällan här. Bordet behöver någon.', marks: ['PERSONALAKT', 'SVART STÄMPEL', 'SVART STÄMPEL', 'SVART STÄMPEL'],
correctRoute: 'archive', explanation: 'Äldre än femtio år och tre svarta stämplar. Arkiv 9 tar hand om företrädaren. Förmodligen.', correctRoute: 'archive', explanation: 'Äldre än femtio år och tre svarta stämplar. Arkiv 9 tar hand om företrädaren. Förmodligen.',
}, },
{
id: 'l21', day: 1, sender: 'MKB Fastighets AB', recipient: 'Boende, lgh 1302', address: 'Grönegatan 8, Malmö', postmark: '2026-07-21',
body: 'Planerat vattenavbrott torsdag 09:0015:00. Knackningar i ledningarna under övrig tid ingår inte i felanmälan.', marks: ['SAMHÄLLSINFORMATION'],
correctRoute: 'deliver', explanation: 'Fullständig adress och nutida mottagare. Fastighetens rörsystem ligger utanför uppdraget.',
correctConsequence: 'Meddelandet gick vidare. Klockan 09:01 på torsdag blev hela huset tyst.',
},
{
id: 'l22', day: 1, sender: 'Havet', recipient: 'Den som öppnade sist', address: '', postmark: '2026-07-21',
body: 'ÖPPNA INTE. Jag har redan kommit in genom kuvertets hörn.', marks: ['SALTFLÄCKAR', 'ÖPPNA EJ'],
correctRoute: 'quarantine', explanation: 'Brevet ber uttryckligen att inte öppnas. §3.1 gäller oavsett avsändarens geografiska omfång.',
correctConsequence: 'Karantänskåpets nedersta låda började droppa. Golvbrunnen tog hand om det mesta.',
wrongConsequences: {
deliver: 'Brevbäraren kom tillbaka genomblöt och utan skugga. Händelsen registrerades som arbetsrelaterad.',
return: 'Kustbevakningen hittade retursäcken flytande utanför Limhamn. Den var varm.',
archive: 'Arkiv 9 stängde för sanering. Din signatur fanns på insidan av samtliga kuvert.',
},
penalty: { deliver: { composure: 2, suspicion: 2 }, return: { suspicion: 2 }, archive: { composure: 2 } },
},
{
id: 'l23', day: 2, sender: 'Folkbokföringen', recipient: 'Karl-Gustav Holm (19011974)', address: 'Rönneholmsvägen 44', postmark: '2026-07-22',
body: 'Bekräftelse av adressändring. Ny folkbokföringsadress: samma plats, cirka två meter längre ned.', marks: ['PERSONBEVIS'],
correctRoute: 'archive', explanation: 'Mottagaren är avliden. Formulärets exakta djup förändrar inte §4.3.',
correctConsequence: 'Arkiv 9 lade handlingen i en mapp märkt FAST BOENDE. Mappen var redan full.',
},
{
id: 'l24', day: 2, sender: 'Karl-Gustav Holm (19011974)', recipient: 'Folkbokföringen', address: 'Kattsundsgatan 7', postmark: '2026-07-22',
body: 'Adressändringen godkänns inte. Det är trångt här nere och grannen spelar radio hela natten.', marks: ['LUKT AV FUKT', 'HANDSKRIVET'],
correctRoute: 'quarantine', explanation: 'En avliden person står som avsändare. §4.7 är den specifika och senare föreskriften.',
correctConsequence: 'Brevet låstes in. Radion hördes ändå svagt från ventilationsgallret resten av dagen.',
wrongConsequences: {
deliver: 'Folkbokföringen registrerade Karl-Gustav som levande. Systemet registrerade samtidigt dig som avliden.',
return: 'Kuvertet återkom med tillägget NEKAS: MOTTAGAREN EJ HEMMA, skrivet med din handstil.',
archive: 'Arkiv 9 vägrade ta emot avsändaren två gånger. Den tredje gången tog de emot budet i stället.',
},
penalty: { deliver: { composure: 1, suspicion: 2 }, return: { composure: 2 }, archive: { suspicion: 2 } },
},
{
id: 'l25', day: 3, sender: 'Avdelning Noll', recipient: 'Vaktmästeriet', address: 'Källarplan -6', postmark: '2026-07-23',
body: 'Byt lampan i korridoren. Den nuvarande belyser sådant som inte längre är anställt.', marks: ['INTERNPOST', '0', 'BRÅDSKANDE'],
correctRoute: 'deliver', explanation: 'Avdelning Nolls internpost skall alltid vidare enligt §5.9, trots det negativa våningsplanet.',
correctConsequence: 'Vaktmästaren tog med en stege och kom tillbaka utan den. Lampan fungerar nu.',
},
{
id: 'l26', day: 3, sender: 'Bostadsrättsföreningen Eken', recipient: 'Innehavaren', address: 'Östra Rönneholmsvägen -1', postmark: '2026-07-23',
body: 'Kallelse till extra föreningsstämma angående den nya lägenheten under tvättstugan.', marks: ['REKOMMENDERAT'],
correctRoute: 'return', explanation: 'Negativa gatunummer är ogiltiga enligt §5.2. Det gäller även bostadsrättsföreningar med expansionsplaner.',
correctConsequence: 'Föreningen strök lägenheten ur röstlängden. Avgiften drogs fortfarande via autogiro.',
wrongConsequences: {
deliver: 'En ny namnskylt dök upp under hissens nedersta knapp. Föreningen kräver nu att du betalar medlemsavgift.',
archive: 'Arkiv 9 registrerade fastigheten. Kommunens karta fick ett hål där kvarteret brukade ligga.',
quarantine: 'Karantänskåpet kallades till föreningsstämman och fick två röster.',
},
penalty: { deliver: { suspicion: 2 }, archive: { composure: 2 }, quarantine: { suspicion: 2 } },
},
{
id: 'l27', day: 4, sender: 'Arkiv 9', recipient: 'Sorteringen', address: 'Internfack 9', postmark: '2026-07-24',
body: 'Den här försändelsen har tre svarta stämplar. Den här försändelsen har fyra svarta stämplar.', marks: ['SVART STÄMPEL', 'SVART STÄMPEL', 'SVART STÄMPEL', 'TEXTEN HAR ÄNDRATS'],
correctRoute: 'quarantine', explanation: 'Textförändring enligt §6.6 har uttryckligt företräde framför trestämpelregeln.',
correctConsequence: 'Skåpet svalde brevet utan att öppnas. En fjärde svart stämpel syntes kort på din handrygg.',
wrongConsequences: {
archive: 'Arkiv 9 skickade tillbaka brevet tillsammans med ett fotografi av dig när du fattade beslutet.',
},
penalty: { archive: { composure: 2, suspicion: 2 } },
},
{
id: 'l28', day: 4, sender: 'Länsstyrelsen', recipient: 'Arkiv 9', address: 'Internleverans', postmark: '2026-07-24',
body: 'Slutrapport rörande tre försvunna brunnar, två felplacerade vittnen och ett godkänt avsteg.', marks: ['SVART STÄMPEL', 'SVART STÄMPEL', 'SVART STÄMPEL'],
correctRoute: 'archive', explanation: 'Tre svarta stämplar innebär Arkiv 9. Texten har inte förändrats, hur mycket man än önskar det.',
correctConsequence: 'Rapporten arkiverades. En av brunnarna ströks från bilagan och från verkligheten.',
},
{
id: 'l29', day: 5, sender: 'Skatteverket 2041', recipient: 'Skatteverket 2026', address: 'Kungsgatan 15', postmark: '2041-03-18',
body: 'Förhandsbesked: dödsboet efter Staten Sverige saknar behörig firmatecknare.', marks: ['FRAMTIDA', 'TJÄNSTEPOST'],
correctRoute: 'return', explanation: 'Framtida post skall återsändas enligt §7.1. Innehållets skattemässiga betydelse är inte ert ansvar.',
correctConsequence: 'Returen accepterades femton år i förväg. Ett automatiskt dröjsmålsbrev skrevs ut omedelbart.',
},
{
id: 'l30', day: 5, sender: 'Föreståndaren, måndag', recipient: 'Du, fredag', address: 'Ditt sorteringsbord', postmark: '2026-07-27',
body: 'Du läser det sista brevet nu. När du har stämplat hör du hissen stanna på plan fyra. Ta inte med nyckeln.', marks: ['NÄSTA VECKA', 'PERSONLIGT'],
correctRoute: 'quarantine', explanation: 'Framtida post som beskriver den pågående arbetsdagen omfattas av §7.4. Det räcker att beskrivningen är korrekt.',
correctConsequence: 'Brevet låstes in. Hissen stannade ändå på plan fyra. Någon därute provade din dörr tre gånger.',
wrongConsequences: {
deliver: 'Du vidarebefordrade brevet till dig själv. Hissen öppnades och nästa arbetsvecka började genast.',
return: 'Föreståndaren tog emot returen i måndags och skrev ditt namn på fredagens frånvarolista.',
archive: 'Arkiv 9 registrerade dagen som redan avslutad. Alla klockor i huset hoppade till 17:01.',
},
penalty: { deliver: { composure: 2, suspicion: 2 }, return: { suspicion: 2 }, archive: { composure: 2 } },
},
]; ];
export const DAY_BRIEFINGS = [ export const DAY_BRIEFINGS = [

View File

@@ -32,4 +32,21 @@ describe('sorting engine', () => {
expect(state.day).toBe(2); expect(state.day).toBe(2);
expect(state.composure).toBe(3); expect(state.composure).toBe(3);
}); });
it('applies a letter-specific high-risk consequence', () => {
let state = startGame(createState());
const letters = lettersForDay(1);
for (const letter of letters.slice(0, 5)) state = routeLetter(state, letter.correctRoute);
expect(currentLetter(state)?.id).toBe('l22');
state = routeLetter(state, 'deliver');
expect(state.lastDecision?.correct).toBe(false);
expect(state.lastDecision?.composureCost).toBe(2);
expect(state.lastDecision?.suspicionCost).toBe(2);
expect(state.lastDecision?.consequence).toContain('utan skugga');
});
it('has six authored letters in every shift', () => {
for (let day = 1; day <= 5; day += 1) expect(lettersForDay(day)).toHaveLength(6);
});
}); });

View File

@@ -5,6 +5,13 @@ export const MAX_COMPOSURE = 3;
export const MAX_SUSPICION = 5; export const MAX_SUSPICION = 5;
export const LAST_DAY = Math.max(...LETTERS.map((letter) => letter.day)); export const LAST_DAY = Math.max(...LETTERS.map((letter) => letter.day));
const DEFAULT_WRONG_CONSEQUENCES: Record<Route, string> = {
deliver: 'Försändelsen lämnade byggnaden. Någon ringde senare, men samtalet innehöll bara ditt eget andetag.',
return: 'Returtransporten kom tillbaka tom. Chauffören ville inte säga var han hade varit.',
archive: 'Arkiv 9 kvitterade försändelsen och registrerade samtidigt en anmärkning i din personalakt.',
quarantine: 'Karantänskåpet tog emot brevet. Något annat därinne slutade knacka.',
};
export function createState(): GameState { export function createState(): GameState {
return { return {
phase: 'intro', day: 1, letterIndex: 0, accuracy: 100, phase: 'intro', day: 1, letterIndex: 0, accuracy: 100,
@@ -30,11 +37,18 @@ export function routeLetter(state: GameState, route: Route): GameState {
if (!letter) return state; if (!letter) return state;
const correct = letter.correctRoute === route; const correct = letter.correctRoute === route;
const decisions = [...state.decisions, { letterId: letter.id, route, correct }]; const configuredPenalty = letter.penalty?.[route];
const composureCost = correct ? 0 : configuredPenalty?.composure ?? 1;
const suspicionCost = correct ? 0 : configuredPenalty?.suspicion ?? 1;
const consequence = correct
? letter.correctConsequence ?? 'Försändelsen försvann ned i rätt fack. Ingen protesterade högt nog för att räknas.'
: letter.wrongConsequences?.[route] ?? DEFAULT_WRONG_CONSEQUENCES[route];
const decision = { letterId: letter.id, route, correct, consequence, composureCost, suspicionCost };
const decisions = [...state.decisions, decision];
const correctCount = decisions.filter((decision) => decision.correct).length; const correctCount = decisions.filter((decision) => decision.correct).length;
const accuracy = Math.round((correctCount / decisions.length) * 100); const accuracy = Math.round((correctCount / decisions.length) * 100);
const composure = Math.max(0, state.composure - (correct ? 0 : 1)); const composure = Math.max(0, state.composure - composureCost);
const suspicion = Math.min(MAX_SUSPICION, state.suspicion + (correct ? 0 : 1)); const suspicion = Math.min(MAX_SUSPICION, state.suspicion + suspicionCost);
const dayLetters = lettersForDay(state.day); const dayLetters = lettersForDay(state.day);
const isLastLetter = state.letterIndex >= dayLetters.length - 1; const isLastLetter = state.letterIndex >= dayLetters.length - 1;
@@ -50,7 +64,7 @@ export function routeLetter(state: GameState, route: Route): GameState {
composure, composure,
suspicion, suspicion,
decisions, decisions,
lastDecision: { letterId: letter.id, route, correct }, lastDecision: decision,
}; };
} }
@@ -67,12 +81,12 @@ export function nextDay(state: GameState): GameState {
} }
export function saveState(state: GameState): void { export function saveState(state: GameState): void {
localStorage.setItem('retur-save-v1', JSON.stringify(state)); localStorage.setItem('retur-save-v2', JSON.stringify(state));
} }
export function loadState(): GameState | null { export function loadState(): GameState | null {
try { try {
const value = localStorage.getItem('retur-save-v1'); const value = localStorage.getItem('retur-save-v2');
return value ? JSON.parse(value) as GameState : null; return value ? JSON.parse(value) as GameState : null;
} catch { } catch {
return null; return null;
@@ -80,5 +94,5 @@ export function loadState(): GameState | null {
} }
export function clearState(): void { export function clearState(): void {
localStorage.removeItem('retur-save-v1'); localStorage.removeItem('retur-save-v2');
} }

View File

@@ -6,6 +6,7 @@ import type { Route } from './types.ts';
let state = loadState() ?? createState(); let state = loadState() ?? createState();
let selectedRoute: Route | null = null; let selectedRoute: Route | null = null;
let showingResult = false; let showingResult = false;
let resultAdvanced = false;
const app = document.getElementById('app')!; const app = document.getElementById('app')!;
@@ -16,6 +17,7 @@ function escapeHtml(value: string): string {
function render(): void { function render(): void {
document.documentElement.style.setProperty('--suspicion', String(state.suspicion / 5)); document.documentElement.style.setProperty('--suspicion', String(state.suspicion / 5));
if (state.phase === 'intro') return renderIntro(); if (state.phase === 'intro') return renderIntro();
if (showingResult) return renderDesk();
if (state.phase === 'day-end') return renderDayEnd(); if (state.phase === 'day-end') return renderDayEnd();
if (state.phase === 'won' || state.phase === 'lost') return renderEnding(); if (state.phase === 'won' || state.phase === 'lost') return renderEnding();
renderDesk(); renderDesk();
@@ -63,9 +65,13 @@ function renderDesk(): void {
? `<div class="result ${state.lastDecision.correct ? 'correct' : 'wrong'}"> ? `<div class="result ${state.lastDecision.correct ? 'correct' : 'wrong'}">
<strong>${state.lastDecision.correct ? 'KORREKT SORTERAD' : 'ANMÄRKNING REGISTRERAD'}</strong> <strong>${state.lastDecision.correct ? 'KORREKT SORTERAD' : 'ANMÄRKNING REGISTRERAD'}</strong>
<p>${escapeHtml(letter.explanation)}</p> <p>${escapeHtml(letter.explanation)}</p>
<div class="consequence"><span>FÖLJD</span>${escapeHtml(state.lastDecision.consequence)}</div>
${state.lastDecision.correct ? '' : `<div class="penalty">${state.lastDecision.composureCost} lugn · +${state.lastDecision.suspicionCost} misstanke</div>`}
<button id="continue-button">NÄSTA FÖRSÄNDELSE →</button> <button id="continue-button">NÄSTA FÖRSÄNDELSE →</button>
</div>` : ''; </div>` : '';
const recentDecisions = state.decisions.slice(-3).reverse();
shell(`<main class="desk"> shell(`<main class="desk">
<aside class="panel status-panel"> <aside class="panel status-panel">
<div class="panel-title">Arbetsdag</div> <div class="panel-title">Arbetsdag</div>
@@ -74,6 +80,7 @@ function renderDesk(): void {
<div class="meter"><span>Lugn</span><b>${'●'.repeat(state.composure)}${'○'.repeat(3 - state.composure)}</b></div> <div class="meter"><span>Lugn</span><b>${'●'.repeat(state.composure)}${'○'.repeat(3 - state.composure)}</b></div>
<div class="meter suspicion"><span>Misstanke</span><b>${state.suspicion} / 5</b><i style="width:${state.suspicion * 20}%"></i></div> <div class="meter suspicion"><span>Misstanke</span><b>${state.suspicion} / 5</b><i style="width:${state.suspicion * 20}%"></i></div>
<div class="meter"><span>Noggrannhet</span><b>${state.accuracy}%</b></div> <div class="meter"><span>Noggrannhet</span><b>${state.accuracy}%</b></div>
${recentDecisions.length ? `<div class="panel-title incidents-title">Senaste följder</div><ul class="incident-log">${recentDecisions.map((decision) => `<li class="${decision.correct ? 'ok' : 'bad'}"><b>${decision.letterId.toUpperCase()}</b>${escapeHtml(decision.consequence)}</li>`).join('')}</ul>` : ''}
<div class="panel-title regulations-title">Gällande föreskrifter</div> <div class="panel-title regulations-title">Gällande föreskrifter</div>
<ol class="regulations">${regulations.map((rule) => `<li class="${rule.fromDay === state.day && state.day > 1 ? 'new-rule' : ''}"><b>§${rule.number}</b>${escapeHtml(rule.text)}${rule.note ? `<em>${escapeHtml(rule.note)}</em>` : ''}</li>`).join('')}</ol> <ol class="regulations">${regulations.map((rule) => `<li class="${rule.fromDay === state.day && state.day > 1 ? 'new-rule' : ''}"><b>§${rule.number}</b>${escapeHtml(rule.text)}${rule.note ? `<em>${escapeHtml(rule.note)}</em>` : ''}</li>`).join('')}</ol>
</aside> </aside>
@@ -114,21 +121,20 @@ function stamp(): void {
const before = currentLetter(state); const before = currentLetter(state);
state = routeLetter(state, selectedRoute); state = routeLetter(state, selectedRoute);
saveState(state); saveState(state);
if (state.phase === 'playing' && before?.id !== currentLetter(state)?.id) { resultAdvanced = before?.id !== currentLetter(state)?.id;
if (resultAdvanced) {
// The engine has advanced; temporarily step the view back to show why the previous decision scored as it did. // The engine has advanced; temporarily step the view back to show why the previous decision scored as it did.
state = { ...state, letterIndex: state.letterIndex - 1 }; state = { ...state, letterIndex: state.letterIndex - 1 };
showingResult = true;
} else if (state.phase === 'day-end' || state.phase === 'won' || state.phase === 'lost') {
render();
return;
} }
showingResult = true;
render(); render();
} }
function advanceAfterResult(): void { function advanceAfterResult(): void {
if (!showingResult) return; if (!showingResult) return;
state = { ...state, letterIndex: state.letterIndex + 1 }; if (resultAdvanced) state = { ...state, letterIndex: state.letterIndex + 1 };
showingResult = false; showingResult = false;
resultAdvanced = false;
selectedRoute = null; selectedRoute = null;
saveState(state); saveState(state);
render(); render();
@@ -158,7 +164,7 @@ function renderEnding(): void {
<dl class="final-stats"><div><dt>Noggrannhet</dt><dd>${state.accuracy}%</dd></div><div><dt>Misstanke</dt><dd>${state.suspicion}/5</dd></div><div><dt>Försändelser</dt><dd>${state.decisions.length}</dd></div></dl> <dl class="final-stats"><div><dt>Noggrannhet</dt><dd>${state.accuracy}%</dd></div><div><dt>Misstanke</dt><dd>${state.suspicion}/5</dd></div><div><dt>Försändelser</dt><dd>${state.decisions.length}</dd></div></dl>
<button class="primary" id="restart-button">NY PERSONALAKT</button> <button class="primary" id="restart-button">NY PERSONALAKT</button>
</div></main>`); </div></main>`);
document.getElementById('restart-button')?.addEventListener('click', () => { clearState(); state = createState(); selectedRoute = null; showingResult = false; render(); }); document.getElementById('restart-button')?.addEventListener('click', () => { clearState(); state = createState(); selectedRoute = null; showingResult = false; resultAdvanced = false; render(); });
} }
function bindShell(): void { function bindShell(): void {

View File

@@ -28,6 +28,11 @@ button { font: inherit; }
.meter.suspicion::after, .meter.suspicion i { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: var(--border); } .meter.suspicion::after, .meter.suspicion i { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: var(--border); }
.meter.suspicion i { right: auto; background: var(--danger); z-index: 1; transition: width .25s; } .meter.suspicion i { right: auto; background: var(--danger); z-index: 1; transition: width .25s; }
.regulations-title { margin-top: 24px; } .regulations-title { margin-top: 24px; }
.incidents-title { margin-top: 24px; }
.incident-log { margin: 0; padding: 0; list-style: none; display: grid; gap: 7px; }
.incident-log li { padding-left: 10px; border-left: 2px solid var(--safe); color: var(--dim); font-size: 9px; line-height: 1.45; }
.incident-log li.bad { border-left-color: var(--danger); }
.incident-log b { display: block; color: var(--text); font-size: 8px; }
.regulations { padding: 0; margin: 0; list-style: none; display: grid; gap: 9px; } .regulations { padding: 0; margin: 0; list-style: none; display: grid; gap: 9px; }
.regulations li { padding-left: 36px; color: #aaaacc; font-size: 10px; position: relative; } .regulations li { padding-left: 36px; color: #aaaacc; font-size: 10px; position: relative; }
.regulations b { position: absolute; left: 0; color: var(--accent); } .regulations b { position: absolute; left: 0; color: var(--accent); }
@@ -64,6 +69,9 @@ button { font: inherit; }
.result strong { font-size: clamp(22px, 4vw, 38px); letter-spacing: .12em; } .result strong { font-size: clamp(22px, 4vw, 38px); letter-spacing: .12em; }
.result.correct strong { color: var(--safe); } .result.wrong strong { color: var(--danger); } .result.correct strong { color: var(--safe); } .result.wrong strong { color: var(--danger); }
.result p { max-width: 540px; color: var(--dim); } .result p { max-width: 540px; color: var(--dim); }
.consequence { max-width: 540px; margin: 10px auto 14px; padding: 12px; border: 1px solid var(--border); color: var(--text); font-size: 11px; }
.consequence span { display: block; margin-bottom: 4px; color: var(--archive); font-size: 8px; letter-spacing: .12em; }
.penalty { margin-bottom: 12px; color: var(--danger); font-size: 10px; text-transform: uppercase; }
.result button { border: 0; background: none; color: var(--text); cursor: pointer; } .result button { border: 0; background: none; color: var(--text); cursor: pointer; }
.intro-screen, .end-screen { min-height: calc(100vh - 62px); display: grid; place-items: center; padding: 30px; background: radial-gradient(circle, #20192c 0, var(--bg) 66%); } .intro-screen, .end-screen { min-height: calc(100vh - 62px); display: grid; place-items: center; padding: 30px; background: radial-gradient(circle, #20192c 0, var(--bg) 66%); }
.intro-screen { grid-template-columns: auto minmax(280px, 540px); gap: clamp(30px, 7vw, 90px); } .intro-screen { grid-template-columns: auto minmax(280px, 540px); gap: clamp(30px, 7vw, 90px); }

View File

@@ -11,6 +11,9 @@ export interface Letter {
marks: string[]; marks: string[];
correctRoute: Route; correctRoute: Route;
explanation: string; explanation: string;
correctConsequence?: string;
wrongConsequences?: Partial<Record<Route, string>>;
penalty?: Partial<Record<Route, { composure?: number; suspicion?: number }>>;
} }
export interface Regulation { export interface Regulation {
@@ -25,6 +28,9 @@ export interface Decision {
letterId: string; letterId: string;
route: Route; route: Route;
correct: boolean; correct: boolean;
consequence: string;
composureCost: number;
suspicionCost: number;
} }
export interface GameState { export interface GameState {