# Building Hallway 8 with Claude Code This is the method behind the game: how it was designed and built end to end through a conversation with an AI coding agent (Claude Code), how each feature was verified before it shipped, and the gotchas that cost time so you can skip them. If you want to reproduce *this* game, read the README. If you want to reproduce the *process* on a game of your own, read this. The whole thing was built by talking to the agent in plain language, one change at a time, and having it write the code, run it, screenshot it, test it, and commit it. There was no separate design doc up front; the design emerged in the first few messages and then got refined by playing. --- ## 0. Setup that made it work A few things were decided once, early, and paid off every turn: - **A tiny, no-build stack.** Python + Flask backend, vanilla HTML/CSS/JS frontend, JSON for content. No bundler, no framework, no npm. The agent can edit any file and immediately run the app, which keeps the feedback loop fast. - **A `CLAUDE.md` at the repo root.** This is the agent's standing memory: the stack, the core mechanic, the conventions (see below), and "keep it this way" notes next to load-bearing decisions. Every session reads it first, so you don't re-explain the project or re-litigate settled choices. - **Explicit conventions, stated once.** Commit authorship, a no-spoilers rule, no em-dashes, colour constraints. Written down, they get honoured for free. - **A test runner with zero dependencies** (`python tests/test_game.py`) and a **headless browser** (Chromium via Playwright) for UI verification. The agent can prove a change works instead of guessing. The single highest-leverage artifact is `CLAUDE.md`. Start it on day one and update it whenever a decision becomes load-bearing. --- ## 1. The design conversation The game started as a discussion, not a spec: *how do you make a "spot the change" loop that is about doubting your memory rather than hunting for a weird sentence?* That produced a small set of principles that became the mechanic: - Never describe everything; show only a subset of details each loop. - Change *meaning*, not wording. Draw each detail from a pool of interchangeable sentences so players can't diff two loops literally. - One quiet mutation per changed loop, never signposted. - Frame it as a memory test, not "find the anomaly." Only after the mechanic felt right did any code get written. Lesson: **spend the first messages on the core loop and its framing, not on architecture.** --- ## 2. The working loop Every feature, from the MVP to the audio, followed the same rhythm: 1. **Ask in plain language** for one change ("add an arc about taking the stairs", "the top bar is hidden on mobile", "make the crackle rarer"). 2. **The agent implements it** across whatever files it touches. 3. **It verifies before claiming success:** - runs `python tests/test_game.py`, - drives the real app in headless Chromium and screenshots the result, - for logic, plays the game against the server's own answer to prove win/lose paths. 4. **It commits** (authored as the owner) and **pushes**. 5. You look at the screenshot / play it, and the next message is the next change. Bugs and polish are just more turns. This is the important part: **the agent is expected to prove each change, not assert it.** Screenshots and a passing test suite in the same turn as the edit are what make it safe to move fast. --- ## 3. Architecture decisions (and why) - **Server owns the truth.** `correct = (choice == "back") == has_anomaly`. The answer lives only in server state and is stripped from every payload (keys starting with `_` are dropped). The browser literally cannot be inspected to cheat. Decide early what must never reach the client. - **Content is data, not code.** Each arc is one JSON file of properties, sentence pools, and story. New files appear on the menu automatically. This is what let "add an arc" be a 20-minute change with no engine edits. - **Session state keyed by a client-echoed id, not a cookie.** (See lessons.) - **Single process, in-memory state.** Fine for this game; it dictates single-worker deployment. Know this constraint before you scale. --- ## 4. Content model: arcs An arc is a skin + backstory over the shared mechanic. The schema and the design rules (every arc needs a strong diegetic hook, an NPC that can become the anomaly, a loop-aware NPC twist, and a non-physical `sense` layer) live in `docs/ARCS.md`, along with shared tuning and a queue of planned arcs. Keeping a living design doc meant new arcs stayed consistent and we never re-derived the rules. The difficulty ramp and the doubt-prompt curve are engine-wide and documented so every arc inherits them. --- ## 5. Procedural audio There are no audio files in the game. Ambience is synthesised live in the browser with the Web Audio API (`static/audio.js`): oscillators and filtered noise per arc (fluorescent buzz, subway roll, stairwell wind), plus a muted arpeggio for the landing. Rare events (a flicker, a passing train, a gust) are scheduled sparsely. This kept the repo tiny and every soundscape editable as code. For sharing, the same synth is rendered offline (`OfflineAudioContext`) into an OGG and a 1080p MP4 by `scripts/export_landing_media.py`, so the exported media is exactly what players hear. --- ## 5b. The win screen: identity, sharing, and a fleeting look back Late in the build the game got a critical review (kept in `docs/REVIEW.md`), and several of its fixes converged on one screen: the exit. Worth copying as a pattern: - **Reward the win, keep the punishment.** The reviewer wanted the reset softened *and* the win rewarded. We took only the second half: the reset stays hard (it is the whole point of a memory game), and the reward is a way to pass the place on plus an optional, fleeting "look back" at how many runs it took. Say no to the half of a suggestion that would defang the design. - **Make sharing look intentional, or don't ship it.** A share link with no Open Graph tags unfurls as a bare, broken-looking URL. The fix was a generated 1200x630 "8" card (rendered from the game's own look with the same headless Chromium used for screenshots) plus `og:*` / `twitter:*` meta, and a favicon so the browser tab is not the "invisible = broken" tell the reviewer flagged. The identity is the single number `8`, reused in the card and the icon. - **Mobile-first sharing.** Instead of five platform buttons, lead with the native share sheet where it exists (one tap reaches Instagram, Snapchat, X, Messages, everything) and fall back to explicit X / Facebook / LinkedIn only on desktops without the API. Less chrome, and it actually reaches the apps that have no web-link share. - **Fleeting by design.** The "look back" line surfaces on tap, lingers a few seconds, then fades and reclaims its space so the eye returns to sharing or replaying. A thing that is meant to be glanced at should remove itself. - **Copy is the owner's call; show options.** Voice ("invitation" vs "dare"), the exact per-arc share prompts, and the "look back" label were all previewed as rendered mockups and chosen by the owner before implementation. Render the choice, don't assume it. - **Localize idioms as meaning.** "Look back" is metaphoric; every language uses its own retrospective phrase, not the literal turn-around verb (see `docs/LOCALIZATION.md`). The eight share invitations and the per-arc run-count lines went through the same i18n pipeline as everything else. Two small UI fixes rode along, both from the same review: the emoji sound toggle became a drawn line icon, and the native `