diff --git a/src/data/content.ts b/src/data/content.ts index c6df9c1..539dea9 100644 --- a/src/data/content.ts +++ b/src/data/content.ts @@ -28,101 +28,228 @@ export const LETTERS: Letter[] = [ id: 'l01', day: 1, sender: 'Britta Nilsson', recipient: 'Ture Andersson', address: 'Bokgatan 14, Malmö', postmark: '2026-07-21', body: 'Ture, din kofta ligger fortfarande här. Jag skickar den inte igen. Hämta den innan torsdag.', marks: ['Porto betalt'], correctRoute: 'deliver', explanation: 'Fullständig adress och inget annat anmärkningsvärt. Det är nästan misstänkt normalt.', + correctConsequence: 'Koftan levererades. Ture svarade inte, men tvättstugan bokades ändå i hans namn till torsdag.', + wrongConsequences: { + return: 'Koftan återsändes till Britta. Den luktade svagt av någon annans tvättmedel.', + archive: 'Arkiv 9 noterade den som "personligt föremål, obestämd ålder" och behöll den.', + quarantine: 'Karantänskåpet vägrade stänga helt. En ärm stack ut hela natten.', + }, }, { id: 'l02', day: 1, sender: 'K. Lund', recipient: 'Den som bor ovanför vinden', address: 'Huset med den gröna dörren', postmark: '2026-07-20', body: 'Ni har fått fel nyckel. Använd den inte i källardörren.', marks: ['Adress saknas'], correctRoute: 'return', explanation: 'Adressen är ogiltig och avsändaren finns. Återsänd. Nyckeln nämns inte i föreskrifterna.', + correctConsequence: 'Nyckeln återsändes. K. Lund svarade samma dag: "Fel nyckel igen. Sluta försöka."', + wrongConsequences: { + deliver: 'Nyckeln levererades till vinden. Källardörren rapporterades olåst nästa morgon.', + archive: 'Arkiv 9 tog inte emot föremål utan bekräftad ålder. Den kom tillbaka i ett brunt kuvert utan avsändare — ställt till dig.', + quarantine: 'Nyckeln låstes in. Källardörren gick upp av sig själv strax efter midnatt.', + }, }, { id: 'l03', day: 1, sender: 'Ester Malm', recipient: 'Oskar Malm', address: 'Södra Förstadsgatan 81', postmark: '1912-11-03', body: 'Käre Oskar. De knackar under golvet igen. Far säger att huset sätter sig.', marks: ['BLEKINGE BATALJON', 'Öppnat av censuren'], correctRoute: 'archive', explanation: 'Poststämpeln är äldre än femtio år. Arkiv 9 hanterar resten.', + correctConsequence: 'Arkiverat utan incident. Ett knackande hördes kort från arkivkorridoren, sedan tystnad.', + wrongConsequences: { + deliver: 'Levererat till en adress som revs 1974. Posten kom fram ändå.', + return: 'Avsändaren fanns inte längre. Brevet kom tillbaka öppnat, med källarens lukt kvar i papperet.', + quarantine: 'Skåpet knackade tillbaka, tre gånger, i samma rytm som brevet beskrev.', + }, }, { id: 'l04', day: 1, sender: 'Fastighetskontoret', recipient: 'Nattvakt 3', address: 'Förvaltningen, internfack 12', postmark: '2026-07-21', body: 'ÖPPNA INTE I BELYST RUM. Förvaras svalt. Om innehållet börjar nynna, kontakta ingen.', marks: ['ÖPPNA EJ'], correctRoute: 'quarantine', explanation: 'Brevet säger uttryckligen att det inte skall öppnas. Det var generöst av det.', + correctConsequence: 'Kartongen låstes in. Ingen nynnade mer den natten, vilket kändes värre än om den gjort det.', + wrongConsequences: { + deliver: 'Nattvakt 3 tackade för leveransen över internradion. Nattvakt 3 har inte hörts av sedan.', + return: 'Fastighetskontoret nekade retur. De hävdade att de aldrig skickat något.', + archive: 'Arkiv 9 öppnade kartongen för registrering. Nynnandet hörs fortfarande därifrån.', + }, + penalty: { deliver: { suspicion: 2 } }, }, { id: 'l05', day: 2, sender: 'Malmö Stad', recipient: 'Signe Persson (1888–1961)', address: 'Föreningsgatan 32', postmark: '2026-07-22', body: 'Påminnelse om utebliven närvaro vid den årliga medborgardialogen.', marks: ['ANDRA PÅMINNELSEN'], correctRoute: 'archive', explanation: 'Mottagaren är registrerad som avliden. Historiskt ärende, trots kommunens optimism.', + correctConsequence: 'Arkiverat som avslutat ärende. Kommunen skickade en tredje påminnelse veckan efter.', + wrongConsequences: { + deliver: 'Levererat enligt adress. Grannarna hörde något svara genom brevlådan samma kväll.', + return: 'Malmö Stad nekade retur. "Adressaten bor kvar," svarade handläggaren, utan att tveka.', + quarantine: 'Skåpet fylldes i onödan. Det var bara en påminnelse — trodde du.', + }, }, { id: 'l06', day: 2, sender: 'Signe Persson (1888–1961)', recipient: 'Malmö Stad', address: 'August Palms plats 1', postmark: '2026-07-22', body: 'Jag har fått era båda påminnelser. Jag kommer inte. Vänligen uppdatera era register.', marks: ['JORD UNDER FRIMÄRKET'], correctRoute: 'quarantine', explanation: 'Den avlidne är avsändare. Den senare föreskriften §4.7 har företräde.', + correctConsequence: 'Spärrat. Malmö Stad fick aldrig sitt svar och skickade en fjärde påminnelse ändå.', + wrongConsequences: { + deliver: 'Levererat till Malmö Stad. Registret uppdaterades: mottagaren "ej längre aktuell, se bilaga".', + return: 'Returnerat till Föreningsgatan 32. Brevbäraren la det i en brevlåda som inte längre satt där.', + archive: 'Arkiv 9 tog emot det som historia. Jorden under frimärket smulade sig över hela disken.', + }, + penalty: { deliver: { suspicion: 2 } }, }, { id: 'l07', day: 2, sender: 'Lars Dahl', recipient: 'Lars Dahl', address: 'Regementsgatan 19', postmark: '2026-07-22', body: 'Till mig själv: glöm inte att köpa mjölk. Och svara inte när telefonen ringer 03:14.', marks: ['EGENPOST'], correctRoute: 'deliver', explanation: 'Självkorrespondens är beklaglig men inte förbjuden.', + correctConsequence: 'Levererat till sig själv. Telefonen ringde 03:14 ändå. Ingen vet om han svarade.', + wrongConsequences: { + return: 'Returnerat till Lars Dahl, som redan hade brevet. Han skickade det igen samma eftermiddag.', + archive: 'Arkiv 9 vägrade arkivera pågående korrespondens. Formuläret kom tillbaka med en handskriven anteckning: "Fråga honom själv."', + quarantine: 'Egenpost är beklaglig men inte förbjuden, påminde tillsynen skriftligt. Skåpet öppnades igen inom timmen.', + }, }, { id: 'l08', day: 2, sender: '— SAKNAS —', recipient: 'Föreståndaren', address: '', postmark: '2026-07-22', body: 'Det finns inget returkontor på plan fyra. Sluta skicka personal dit.', marks: ['AVSÄNDARE SAKNAS'], correctRoute: 'quarantine', explanation: 'Både giltig avsändare och adress saknas. §4.9 tar över returregeln.', + correctConsequence: '§4.9 tillämpades. Ingen personal skickades till plan fyra igen den veckan.', + wrongConsequences: { + deliver: 'Utan adress levererades brevet ändå, till plan fyra. Den som bar det dit kom inte tillbaka i tid för lunch.', + return: 'Utan avsändare gick returen ingenstans. Den låg kvar på bordet till morgonen efter, nu adresserad till dig.', + archive: 'Arkiv 9 accepterade det trots avsaknaden, och frågade sedan varför plan fyra inte fanns på deras karta heller.', + }, + penalty: { deliver: { composure: 2, suspicion: 1 } }, }, { id: 'l09', day: 3, sender: 'Asta Berg', recipient: 'Hjalmar Berg', address: 'Kyrkogatan -4, Malmö', postmark: '2026-07-23', body: 'Trappan fortsätter fyra våningar nedanför källaren. Ta med lampan den här gången.', marks: ['FELVÄNT FRIMÄRKE'], correctRoute: 'return', explanation: 'Negativa gatunummer är ogiltiga. Var trappan leder är inte ert ansvarsområde.', + correctConsequence: 'Returnerat till Kyrkogatan. Asta bekräftade mottagandet med en enda rad: "Han tog med lampan."', + wrongConsequences: { + deliver: 'Levererat till Kyrkogatan -4. Hjalmar har inte återvänt uppför trapporna sedan.', + archive: 'Arkiv 9 accepterade adressen som historisk, trots dagens datum. Källartrappan förlängdes med ett våningsplan till.', + quarantine: 'Skåpet vägrade stänga över trappans riktning. Ljudet av steg fortsatte nedåt hela natten.', + }, + penalty: { deliver: { composure: 2 } }, }, { id: 'l10', day: 3, sender: 'Avdelning Noll', recipient: 'Arkivarien', address: 'Korridor -2, dörr 0', postmark: '2026-07-23', body: 'Inventeringen är slutförd. Vi saknar fortfarande en anställd. Namnet är ditt.', marks: ['INTERNPOST', '0'], correctRoute: 'deliver', explanation: 'Internpost från Avdelning Noll går alltid vidare, även till negativa adresser.', + correctConsequence: 'Vidarebefordrat enligt §5.9. Arkivarien läste ditt namn högt en gång och sa inget mer om saken.', + wrongConsequences: { + return: 'Avdelning Noll accepterar inga returer. Brevet kom tillbaka från en korridor som inte finns på ritningen.', + archive: 'Arkiv 9 registrerade inventeringen. Din personalakt fick ett nytt, odaterat tillägg.', + quarantine: 'Skåpet vägrade stänga om internpost från Avdelning Noll. Det stod olåst till morgonen.', + }, }, { id: 'l11', day: 3, sender: 'PostNord Kundservice', recipient: 'PostNord Kundservice', address: 'Ej utdelningsbart', postmark: '2026-07-23', body: 'Vi beklagar att ditt brev inte kunde levereras. Ditt ärende är viktigt för oss.', marks: ['AUTOMATISKT SVAR'], correctRoute: 'return', explanation: 'Ogiltig adress, giltig avsändare. Kretsloppet är inte ert problem.', + correctConsequence: 'Returnerat enligt rutin. Ett automatiskt kvitto bekräftade mottagandet av kvittot.', + wrongConsequences: { + deliver: 'Levererat till PostNord Kundservice, som svarade med ett identiskt brev, adresserat till dig.', + archive: 'Arkiv 9 accepterade inte ett autosvar som historia. Det kom tillbaka med tre nya stämplar och ingen förklaring.', + quarantine: 'Skåpet fylldes med ett brev utan fara, bara artighetsfraser i oändlig loop.', + }, }, { id: 'l12', day: 3, sender: 'Avdelning Noll', recipient: 'Ingen särskild', address: '', postmark: '2026-07-23', body: 'Vidarebefordra detta. Det behöver inte komma fram.', marks: ['INTERNPOST', '0'], correctRoute: 'deliver', explanation: '§5.9 säger alltid. Myndigheten har fetmarkerat ordet i originalet.', + correctConsequence: '§5.9 tillämpades igen. Brevet levererades till ingen särskild, precis som instruerat.', + wrongConsequences: { + return: 'Returen nekades. Avdelning Noll finns inte, så det fanns ingen adress att skicka den till.', + archive: 'Arkiv 9 tog emot det ändå. Fetstilen i originalet blev bara tjockare av registreringen.', + quarantine: 'Skåpet accepterade inte post som inte behövde komma fram. Det försvann därifrån ändå, till okänt mål.', + }, }, { id: 'l13', day: 4, sender: 'M. Ek', recipient: 'M. Ek', address: 'Amiralsgatan 7', postmark: '2026-07-24', body: 'Första läsningen: Lägg brevet i Arkiv 9. Andra läsningen: Du gjorde inte det.', marks: ['TEXTEN HAR ÄNDRATS'], correctRoute: 'quarantine', explanation: 'Föränderlig text omfattas av §6.6. Följ inte brevets egen sorteringsinstruktion.', + correctConsequence: 'Texten slutade ändra sig efter att brevet spärrades. Den sista versionen löd: "Bra val."', + wrongConsequences: { + deliver: 'Levererat enligt första läsningen. Andra läsningen visade att det redan låg i Arkiv 9 ändå.', + return: 'Returnerat till M. Ek, som skickade tillbaka samma brev med en tredje, ännu oläst mening.', + archive: 'Arkiv 9 tog emot brevet enligt dess första instruktion. Den andra läsningen bekräftade att det var fel val, för sent.', + }, + penalty: { archive: { suspicion: 2 } }, }, { id: 'l14', day: 4, sender: 'Riksarkivet', recipient: 'Lokalkontoret', address: 'Arkivgatan 9', postmark: '1948-02-09', body: 'Överföring av handlingar rörande det rum som avlägsnades ur ritningen.', marks: ['SVART STÄMPEL', 'SVART STÄMPEL', 'SVART STÄMPEL'], correctRoute: 'archive', explanation: 'Både ålder och tre svarta stämplar pekar mot Arkiv 9. För en gångs skull är reglerna överens.', + correctConsequence: 'Arkiverat utan incident. Rummet som avlägsnades ur ritningen finns kvar i registret, om inte på plan fyra.', + wrongConsequences: { + deliver: 'Levererat till lokalkontoret, som inte kunde hitta ritningen brevet syftade på — eller kontoret.', + return: 'Returnerat till Riksarkivet 1948. Det kom tillbaka 2026, poststämplat samma dag som avsändes.', + quarantine: 'Skåpet accepterade det trots åldern. Det knackade en gång, i samma takt som rummet det beskrev.', + }, }, { id: 'l15', day: 4, sender: 'Någon bakom dig', recipient: 'Du', address: 'Sorteringsbordet', postmark: '2026-07-24', body: 'Vänd dig inte om. Texten kommer vara annorlunda när du tittar tillbaka.', marks: ['SVART STÄMPEL', 'SVART STÄMPEL', 'SVART STÄMPEL', 'TEXTEN HAR ÄNDRATS'], correctRoute: 'quarantine', explanation: '§6.6 undantar brevet från trestämpelregeln. Du vände dig väl inte om?', + correctConsequence: '§6.6 tillämpades utan att du vände dig om. Bordet bakom dig var tomt när du till slut tittade.', + wrongConsequences: { + deliver: 'Levererat till "Du". Det kom tillbaka samma eftermiddag, adresserat till någon annan, i din handstil.', + return: 'Returnerat till avsändaren, som stod bakom dig hela tiden. Ingen retur behövdes egentligen.', + archive: 'Arkiv 9 tog emot det som historiskt, trots dagens datum. Tre nya svarta stämplar syntes redan på kuvertet.', + }, + penalty: { deliver: { composure: 2, suspicion: 2 } }, }, { id: 'l16', day: 4, sender: 'Kammarkollegiet', recipient: 'Vederbörande', address: 'Stora Nygatan 1', postmark: '2026-07-24', body: 'Härmed översändes blankett 14-C. Blanketten bekräftar att blankett 14-C saknas.', marks: ['DIARIEFÖRD'], correctRoute: 'deliver', explanation: 'Fullständig adress, nutida mottagare, vanlig svensk förvaltning.', + correctConsequence: 'Levererat enligt vanlig förvaltning. Blankett 14-C bekräftades fortfarande saknas, nu officiellt.', + wrongConsequences: { + return: 'Returnerat i onödan. Kammarkollegiet skickade tillbaka samma blankett med en tredje bekräftelse av dess frånvaro.', + archive: 'Arkiv 9 arkiverade ett pågående ärende. Det räknas nu som avslutat, vilket det inte var.', + quarantine: 'Spärrat utan anledning. Blankett 14-C existerar fortfarande inte, nu i karantän också.', + }, }, { id: 'l17', day: 5, sender: 'Ture Andersson', recipient: 'Britta Nilsson', address: 'Bokgatan 12, Malmö', postmark: '2037-07-25', body: 'Skicka inte koftan. Det är så den hittar hit.', marks: ['PORTO GÄLLER FRÅN 2037'], correctRoute: 'return', explanation: 'Framtida post återsänds. Familjens koftproblem får lösas kronologiskt.', + correctConsequence: 'Returnerat till 2037. Britta har elva år kvar att inte skicka koftan i.', + wrongConsequences: { + deliver: 'Levererat till Bokgatan 12, en adress som inte finns än. Huset byggs först 2029.', + archive: 'Arkiv 9 vägrade acceptera framtida post som historia. Det kom tillbaka poststämplat två gånger.', + quarantine: 'Spärrat utan grund. Koftan hittade ändå vägen dit den brukar, elva år för tidigt.', + }, }, { id: 'l18', day: 5, sender: 'Du', recipient: 'Du', address: 'Avdelningen för obeställbara försändelser', postmark: '2026-07-26', body: 'Klockan är 16:42. Du har två brev kvar. Lägg inte detta i returhögen. Det är där handen finns.', marks: ['IMORGON', 'TEXTEN HAR ÄNDRATS'], correctRoute: 'quarantine', explanation: 'Framtida post som beskriver den pågående arbetsdagen omfattas av §7.4.', + correctConsequence: '§7.4 tillämpades. Klockan blev 16:42 utan att hissen rörde sig. Två brev återstod, precis som brevet sa.', + wrongConsequences: { + deliver: 'Levererat till dig själv. Du läste det en andra gång innan du förstod vad du just hade gjort.', + return: 'Returnerat till avsändaren — dig, imorgon. Ingen kvittens kom tillbaka.', + archive: 'Arkiv 9 tog emot dagens post som redan avslutad. Klockan visade fortfarande 16:42 efteråt.', + }, + penalty: { return: { suspicion: 2 } }, }, { id: 'l19', day: 5, sender: 'Avdelning Noll', recipient: 'Föreståndaren', address: 'Kontor 4', postmark: '2031-01-01', body: 'Gratulerar till fem års prickfri tjänst. Beklagar resten.', marks: ['INTERNPOST', '0', 'FRAMTIDA'], correctRoute: 'deliver', explanation: '§5.9 säger alltid. Avdelning Noll har företräde även framför framtiden.', + correctConsequence: '§5.9 gäller före framtiden också. Gratulationen levererades femton år för tidigt, till rätt person.', + wrongConsequences: { + return: 'Avdelning Noll accepterar inga returer, inte ens från framtiden. Brevet kom tillbaka från ett kontor som inte finns förrän 2031.', + archive: 'Arkiv 9 arkiverade en gratulation som ännu inte inträffat. Fem år drogs av från någons tjänstetid, ospecificerat vems.', + quarantine: 'Spärrat i onödan. Firandet skedde ändå, tyst, i ett rum utan fönster.', + }, }, { id: 'l20', day: 5, sender: 'Föreståndaren', recipient: 'Ny handläggare', address: 'Sorteringsbordet', postmark: '1956-07-25', 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.', + correctConsequence: 'Arkiverat. Bordet fick sin nya handläggare samma dag brevet skrevs, sjuttio år tidigare.', + wrongConsequences: { + deliver: 'Levererat till "Ny handläggare" — dig. Du la brevet i din egen ficka utan att minnas att du gjort det.', + return: 'Returnerat till Föreståndaren, 1956. Ingen sådan person svarade, då eller nu.', + quarantine: 'Spärrat trots åldern. Personalakten öppnades ändå, av någon som redan gått hem för dagen.', + }, }, { id: 'l21', day: 1, sender: 'MKB Fastighets AB', recipient: 'Boende, lgh 1302', address: 'Grönegatan 8, Malmö', postmark: '2026-07-21', diff --git a/src/engine.test.ts b/src/engine.test.ts index e2ab30c..6518429 100644 --- a/src/engine.test.ts +++ b/src/engine.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { createState, currentLetter, lettersForDay, nextDay, routeLetter, startGame } from './engine.ts'; +import { LETTERS } from './data/content.ts'; describe('sorting engine', () => { it('starts on the first letter of day one', () => { @@ -49,4 +50,21 @@ describe('sorting engine', () => { it('has six authored letters in every shift', () => { for (let day = 1; day <= 5; day += 1) expect(lettersForDay(day)).toHaveLength(6); }); + + it('every letter has its own correct-route consequence, not just the generic fallback', () => { + for (const letter of LETTERS) { + expect(letter.correctConsequence, `${letter.id} is missing a correctConsequence`).toBeTruthy(); + } + }); + + it('applies a letter-specific penalty override on an early, low-stakes-looking letter', () => { + // l04 (Fastighetskontoret's "don't open" crate) escalates the deliver + // penalty to 2 suspicion instead of the default 1 — added for punch, + // this locks the number in. + let state = startGame(createState()); + for (const route of ['deliver', 'return', 'archive'] as const) state = routeLetter(state, route); + expect(currentLetter(state)?.id).toBe('l04'); + state = routeLetter(state, 'deliver'); + expect(state.lastDecision?.suspicionCost).toBe(2); + }); }); diff --git a/src/main.ts b/src/main.ts index 22beed2..742ab2f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import './style.css'; import { DAY_BRIEFINGS, REGULATIONS, ROUTES } from './data/content.ts'; -import { clearState, createState, currentLetter, lettersForDay, loadState, nextDay, routeLetter, saveState, startGame } from './engine.ts'; +import { MAX_SUSPICION, clearState, createState, currentLetter, lettersForDay, loadState, nextDay, routeLetter, saveState, startGame } from './engine.ts'; import type { Route } from './types.ts'; let state = loadState() ?? createState(); @@ -156,11 +156,31 @@ function renderDayEnd(): void { function renderEnding(): void { const won = state.phase === 'won'; + const firedBySuspicion = !won && state.suspicion >= MAX_SUSPICION; + + const stamp = won ? (state.suspicion <= 1 ? 'TILLSVIDARE' : 'TILLSVIDARE, VILLKORLIGT') : firedBySuspicion ? 'AVFÖRD' : 'OMPLACERAD'; + + const title = won + ? state.suspicion <= 1 + ? 'Du får behålla bordet.' + : 'Du får behålla bordet. För tillfället.' + : firedBySuspicion + ? 'Man slutade fråga vad du gjorde där nere.' + : 'Din stol behövs av någon annan.'; + + const body = won + ? state.suspicion <= 1 + ? 'Fem arbetsdagar avslutade utan en enda anmärkning. Föreståndaren lämnar en nyckel till plan fyra utan att kommentera den. Hissen har fortfarande bara tre knappar.' + : 'Fem arbetsdagar avslutade, men personalakten är tjockare än den borde vara. Föreståndaren lämnar en nyckel till plan fyra och tittar på dig en sekund för länge innan hen går.' + : firedBySuspicion + ? 'Misstanken nådde taket innan avvikelserna gjorde det. Ingen kartong står vid din plats — bara ett formulär om att du aldrig anställdes. Det är redan ifyllt.' + : 'För många avvikelser. En kartong står redan vid din plats. Den är adresserad till din efterträdare, med ditt namn överstruket en gång.'; + shell(`
SLUTLIG PERSONALAKT
-
${won ? 'TILLSVIDARE' : 'OMPLACERAD'}
-

${won ? 'Du får behålla bordet.' : 'Din stol behövs av någon annan.'}

-

${won ? 'Fem arbetsdagar avslutade. Föreståndaren lämnar en nyckel till plan fyra. Hissen har fortfarande bara tre knappar.' : 'För många avvikelser. En kartong står redan vid din plats. Den är adresserad till din efterträdare.'}

+
${stamp}
+

${title}

+

${body}

Noggrannhet
${state.accuracy}%
Misstanke
${state.suspicion}/5
Försändelser
${state.decisions.length}
`);