Every playthrough showed the exact same fixed 30 letters in the exact
same order — no randomization anywhere in the codebase. Added 15 new
letters (3 per day, l31-l45), bringing each day's pool to 9, and
changed the engine to draw PER_DAY_COUNT (6) at random from that pool
per game instead of always using the same fixed set.
- engine.ts: buildRoster() shuffles each day's pool and takes the
first 6; GameState now carries a `roster` (letter ids in day order)
generated once at createState() and persisted with the rest of the
save. lettersForDay/currentLetter read through the roster instead
of the full LETTERS array directly.
- Save key bumped to v3 — old saves have no roster field, and
backfilling one wouldn't match already-recorded decisions, so old
saves are simply ignored rather than partially migrated.
- Tests rewritten: several previously asserted specific letter ids at
specific positions, which no longer holds under randomization.
Split into buildRoster() property tests (right count, no dupes,
every id resolves, different seeds differ) and engine tests that
either check general correctness or construct an explicit roster
when a specific letter's behavior needs locking in (l22, l04).
Verified: 11/11 vitest, tsc clean, and a real 5-fresh-games browser
test confirming 5 different first-letter senders instead of the same
one every time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously reused gameover.webp for both win and loss states since it
was deliberately mood-neutral. Now has a dedicated photo: same desk,
chair pushed back in rather than empty, stamp/ink/letters back in
place, a fresh unopened stack waiting — continuation, not triumph,
matching the ending copy's own restraint. .end-screen now carries a
victory/failure class the same way .end-card already did, so CSS can
target the right background per outcome.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same treatment as the intro: generated desk photo (empty chair, a
single closed box where the stamp used to be, one dark pigeonhole
slot) converted PNG -> WebP, 1.7MB -> 45KB. Used for both win and
loss endings, since the image itself is mood-neutral rather than
victory- or failure-specific — the audit-stamp overlay (TILLSVIDARE
in green, OMPLACERAD/AVFÖRD in red) still carries the actual outcome.
.end-card switched from an opaque panel to a translucent one with a
backdrop blur so the photo is actually visible around and through the
card instead of fully hidden behind it — same fix .seal already
needed on the intro screen.
Verified by injecting all three real end states (clean win,
composure-driven loss, suspicion-maxed loss) and screenshotting each.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A generated desk/lamp/stamp photo (converted PNG -> WebP, 1.7MB ->
65KB, negligible quality loss at the size it's actually displayed)
now sits behind the intro screen, with the existing red "R" seal
stamped on top as an overlay — the seal already existed as a plain
CSS circle, just needed a photo worth stamping over. Gave it a subtle
dark backdrop + slightly stronger glow so it stays legible against
the busier background.
Raw source PNG gitignored — only the optimized derivative that
actually ships belongs in the repo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Letters l01-l20 only ever hit the generic default consequence lines
(engine.ts's DEFAULT_WRONG_CONSEQUENCES / a generic correct-route
line) — l21-l30 already had the good stuff. Gave all 20 their own
correctConsequence and wrongConsequences, matching the dry,
institutional dread already established, tied to each letter's own
specific details (the cardigan, the wrong key, the room removed from
the blueprint, Avdelning Noll's negative floors, the future self
warning about 16:42). Bumped a few penalty overrides (l04, l06, l08,
l09, l13, l15, l18) where the wrong choice is narratively worse than
the default 1/1 cost, same pattern already used on l22/l26.
Also split the single win/loss ending into four tiers based on final
suspicion: a clean TILLSVIDARE vs. a "TILLSVIDARE, VILLKORLIGT" win,
and an OMPLACERAD (composure-driven) vs. AVFÖRD (suspicion-maxed)
loss — same stamp-and-personnel-file frame, different outcome text,
so a second playthrough has something new to see even on the same
ending type.
Verified: tsc clean, 8/8 vitest (added a data-integrity check that
every letter has a correctConsequence, plus a test locking in l04's
new penalty), a real browser playthrough of l01's new consequence
text, and all four ending variants rendered by injecting finished
game states directly rather than scripting a fragile 30-letter
keyboard sequence.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>