CLAUDE.md (17239B)
1 # KGames — Claude Guide 2 3 ## What this project is 4 5 Free educational games for young kids. No ads, no signup, no backend. 6 Static HTML + CSS + JavaScript served from any web host. Phaser 3 and 7 Tone.js loaded from CDN. GPL-3.0. 8 9 No build tools — double-clicking `index.html` is a valid development 10 workflow for the games that exist today, and worth preserving for them. 11 New games are not bound by it; see "Constraints for new games". 12 13 ## Testing — run before every commit 14 15 ```bash 16 npm install # one-time; downloads playwright (~112 MB Chromium) 17 npm test # unit tests + browser smoke tests; must all pass 18 npm run test:unit # fast, no browser (~0.2 s) 19 npm run test:browser # headless Chromium (~10 s) 20 ``` 21 22 **Never commit if `npm test` fails.** The browser tests catch broken 23 rendering that syntax checks miss (see the projection-bug incident). 24 25 ## Architecture 26 27 | Path | Purpose | 28 |---|---| 29 | `index.html` + `css/style.css` + `js/main.js` | Portal — game tile grid | 30 | `games/<slug>/index.html` | HTML shell: CDN scripts, HUD buttons, game container | 31 | `games/<slug>/lib.js` | Pure logic (projection math, route gen) — dual-export for unit tests | 32 | `games/<slug>/game.js` | Phaser scene — all graphics via `generateTexture`, audio via Web Audio / Tone.js | 33 | `games/story-stamp/` | DOM-based phonics game (no Phaser) — see "Story Stamp" | 34 | `tools/story-stamp-assets.py` | Fetches + cuts Story Stamp's recorded audio and pictures, writes its manifest and credits | 35 | `tools/story-stamp-audit.py` | Checks the phoneme clips with independent methods (acoustics, cross-speaker match, Vosk recognition, spectrograms) | 36 | `js/shared/game-shared.js` | Shared `KGames.*` helpers (AudioSystem, win screen, confetti, progress bar) | 37 | `js/shared/touch-controls.js` | On-screen keyboard / D-pad for phones — see "Mobile / touch controls" | 38 | `assets/thumbnails/<slug>.svg` | 160×130 viewBox, rx=18, stroke from palette, `#FFF5F5` fill | 39 | `tests/unit/` | `node --test` unit tests — no browser, no Phaser | 40 | `tests/browser/runner.js` | Playwright smoke tests — no JS errors + canvas renders | 41 | `tests/serve.js` | Tiny Node HTTP server used by browser tests (and for local dev) | 42 43 ## Top-down rendering (fire-truck) 44 45 Fire-truck is a **top-down** driving game: the camera zooms 1/`SCALE` (SCALE=5) 46 and follows the truck over a 50×50-cell island. `lib.js` `buildIsland()` lays 47 out water/beach ring roads + BSP-subdivided interior blocks; the road graph 48 drives the route and fire pathing. 49 50 **Chunk bake pipeline** (`renderIsland()` in game.js). The static island is 51 baked once into 16×16-cell chunk textures at `BAKE_CELL = 96` px/cell, drawn 52 ×SCALE (zoom 1/SCALE) → **~1:1 on screen**, so ~96 px of detail per cell costs 53 nothing per frame and Phaser culls offscreen chunks for free. Each chunk runs 54 named passes into one `Graphics`, in order: `_bakeGround` → `_bakeSidewalks` 55 (incl. road corner masks) → `_bakeRoadMarkings` → `_bakeBuildings` → `_bakeParks` 56 → `_bakePalms`. 57 58 - **1-cell apron rule** (highest visual-bug risk): every pass iterates a 1-cell 59 margin around the chunk (clamped) at true local coords so overhanging art — 60 SE drop shadows, palm fronds, block-spanning buildings — that crosses a chunk 61 seam is re-drawn by the neighbour; `generateTexture` clips the overflow. 62 - Buildings are drawn **per block, not per cell**: `FT.findBuildingBlocks(grid)` 63 covers all building cells with disjoint rectangles (BSP guarantees rectangular 64 road-bounded blocks) so each block reads as one building with one roof. 65 `FT.assignParks()` tags ~15% of blocks `cell.park = true` (**type stays 66 `building`**, so grid/route tests are untouched; parks are excluded as fire 67 destinations). 68 69 **Ocean underlay**: a screen-fixed (`scrollFactor 0`) `TileSprite` at depth −1 70 drifts each frame. A world-sized TileSprite would allocate a multi-GB fill 71 canvas — keep it viewport-sized. 72 73 **Dual cameras** (game.js): `cameras.main` is the world; `uiCamera` is the HUD. 74 Every new **world** object must be hidden from the HUD camera — 75 `this._addWorld(obj, depth)` sets depth + `uiCamera.ignore`. HUD objects are 76 added to `cameras.main.ignore(...)`. 77 78 **Depth ordering** (exact values live in game.js): ocean < chunks < 79 boats/buoys < peds < bikes < cars/buses < burning overlay < fire glow < fire < 80 spray < smoke < cloud shadows < truck < birds < HUD. 81 82 **Hose minigame** (`FireHoseScene`): when the truck reaches the fire, the 83 driving scene enters state `minigame`, pauses itself, and launches a 84 street-view scene — building facade with a window grid (`FT.buildFacade` in 85 lib.js, pure + unit-tested), arrow keys move an aim reticle, SPACE sprays from 86 the truck's rotating ladder. Quenched windows reveal waving residents/a cat; 87 when all are out the scene resumes the driving scene and calls 88 `_onMinigameComplete()`. **Headless-RAF rule**: gameplay logic lives in 89 `_step()` driven from both `update()` and a wall-clock `setInterval` fallback 90 (same trick as the driving scene's `fallbackTimer`), and all scene transitions 91 use wall-clock `setTimeout` — never `this.time.delayedCall`, which starves in 92 throttled/headless browsers and hangs the tests. 93 94 **Shared cartoony art** (file-scope helpers in game.js): `bakePeopleTextures` 95 (`kg-person-0..7`, racially diverse cartoon townspeople), `bakeSideTruckTexture` 96 (`kg-truck-side`), `bakeLadderTexture`, `bakeCritterTextures` (dog/cat/parrot/ 97 iguana), `drawSidePalm`. Used by both `FireHoseScene` and `FireTruckEndScene` — 98 reuse these rather than drawing new people/trucks. 99 100 **Ambient life** (`_initAmbient` / `_updateAmbient`, driven from `update()` — 101 never `step()`, which tests call directly): baked-texture Images for traffic 102 (`FT.advanceCarPlan` graph walk), pedestrians, cloud shadows, boats, birds. 103 Guard with `this.ambientReady`; clean up in `shutdown()`. All decor variety is 104 deterministic via `FT.hashCell(x, y, salt)` so the island bakes identically 105 every run. 106 107 ## Story Stamp (DOM game, recorded audio) 108 109 Story Stamp teaches blending (synthetic phonics) and is the first game built 110 outside Phaser: plain DOM + CSS animations in `games/story-stamp/`, keyboard 111 only. It is the reference for "Constraints for new games" in practice. 112 113 The child runs the checkout desk of the island library. Patrons queue with a 114 topic sign (bugs, space, dinosaurs …); the child picks one, gets a request 115 slip with the title in sound boxes, stamps each letter (hearing its sound), 116 presses SPACE to sound it out, and picks the matching book from the cart. 117 **The game never says the word the child is decoding** — three blend passes 118 with shrinking gaps, then the picture choice is the proof. "Returns" (patron 119 says the word, child types its sounds) unlock after five mastered words. 120 121 - `lib.js` — pure content + logic: `PHONEMES` (with `stretch` for held 122 sounds and `home` for override sounds), `GRAPHEMES`/`BASE`/`SEQUENCE` (the 123 teaching order, s-a-t-p-i-n first, digraphs and vowel teams later), `TOPICS`, 124 `WORDS` (decodable words; `g` is the grapheme split, `art` the Fluent Emoji 125 asset, `t` topics, `p` overrides ambiguous sounds like book's `oo` or 126 robot's first `o` — override words are only offered once the child knows 127 the grapheme that teaches that sound), `PATRONS`, the mastery / 128 spaced-repetition scheduler (`recordOrder`, `pickNext`, `shouldIntroduce`), 129 `buildQueue` (distinct topics per patron), `blendTimeline` (no word event), 130 and the multi-child profile store. Unit-tested in 131 `tests/unit/story-stamp-*.test.js`. 132 - `audio.js` — Web Audio player for the recorded clips, speech synthesis for 133 everything else, oscillator SFX. **Phonemes are never synthesised** and 134 **never looped**: held sounds are ~1 s recordings and `playPhoneme(id, 135 {len})` cuts them with a fade. A missing clip is silent. `audio.log` 136 records every clip for the tests. 137 - `game.js` — screens + desk flow. Exposes `window.__SS_GAME__` (`state`, 138 `order`, `queue`, `profile`, `audio`) and `quickStart(name)` (lands on the 139 queue) for tests; honours `window.__SS_NOW__` (clock) and 140 `window.__SS_FORCE_MODE__` (`'return'`). 141 - **Assets are generated, not hand-edited.** `python3 tools/story-stamp-assets.py` 142 downloads Wikimedia Commons recordings and Fluent Emoji 3D PNGs and writes 143 `assets/manifest.js` + `CREDITS.md`. Continuants and short vowels are cut 144 from IPA recordings and held to ~1 s by WSOLA time-stretching of the steady 145 middle; stops/affricates are cut from the start of word recordings (burst + 146 aspiration, up to voicing); r-coloured vowels and diphthongs are whole word 147 recordings ("are", "or", "ow", "boy" minus the b …). Add a word to `WORDS` 148 with a Fluent asset name, rerun the script (`--offline` reuses `.cache/`), 149 commit the outputs. The assets unit test checks manifest ↔ lib ↔ credits. 150 - **Audit the clips after touching the pipeline:** `python3 151 tools/story-stamp-audit.py` (Vosk in `.cache/pylib`, model in 152 `.cache/vosk`; `--no-asr` without them). It checks acoustics per clip 153 (duration class, voicing, centroid, stutter = looping, level flatness), 154 matches each clip against sounds cut from a *different speaker's* word 155 recordings, runs Vosk over words assembled from the clips (about half 156 recognised at speech rate is normal for concatenated sounds; look for one 157 sound far below the rest), and writes a spectrogram sheet to 158 `.cache/audit/phonemes.png` to look at. 159 - Age rules the code enforces: no timers/lives/fail state (a wrong key 160 re-demonstrates the sound; after three wrong books the word is given and the 161 book pointed out), every instruction is spoken, six books make a "day", 162 meaning is checked with the book choice, progress is per-child in 163 `localStorage` (`kg-story-stamp-v1`). 164 165 ## Mobile / touch controls 166 167 Phones have no physical keyboard and a Phaser canvas can't summon the native 168 one, so `js/shared/touch-controls.js` renders an on-screen control bar whose 169 buttons dispatch **synthetic `KeyboardEvent`s to `window`**. Phaser's keyboard 170 plugin already listens on `window`, so every game's existing key handling works 171 unchanged — game logic never references this module. 172 173 - Opt in from the HTML shell, after `game.js`: 174 `<script>KGames.initTouchControls({ layout: 'alphabet' });</script>` 175 - Layouts: `alphabet` (A–Z), `text` (A–Z + Space/Backspace/Enter, e.g. work), 176 `text-nav` (text + arrow row, e.g. story-stamp), `drive` (arrows + 177 Music/Siren/Lights/Spray, e.g. fire-truck). 178 - Shown only on touch / coarse-pointer devices. Force with `?kbd=1` / `?kbd=0` 179 or `window.__FORCE_TOUCH_CONTROLS__` (tests use the latter). 180 - The bar is a flex child below `#game-container`, so the canvas shrinks to fit 181 above it. Phaser's `Scale.RESIZE` ignores a synthetic window `resize` when the 182 window size is unchanged, so the module calls `game.scale.refresh()` directly. 183 **This requires the game instance on `window.__KG_GAME__`** — assign it where 184 you call `new Phaser.Game(...)`. 185 186 ## Adding a new game 187 188 The checklist below is the **Phaser + no-build** recipe the current games 189 follow. It is a well-worn default, not a requirement — read "Constraints for 190 new games" first and deviate where the game calls for it. 191 192 1. Create `games/<slug>/index.html` (clone from `games/fire-truck/index.html`) 193 2. Create `games/<slug>/lib.js` for pure (unit-testable) logic 194 3. Create `games/<slug>/game.js` for the Phaser scene 195 4. Add `render: { preserveDrawingBuffer: true }` to the `Phaser.Game` config 196 (required for the Playwright pixel-reading tests to work), and assign the game 197 to `window.__KG_GAME__` so touch controls can resize it 198 5. Create `assets/thumbnails/<slug>.svg` (160×130, rx=18 rounded rect) 199 6. Replace a `Coming Soon` placeholder in `js/main.js` 200 7. Wire up touch controls: `KGames.initTouchControls({ layout: ... })` in the 201 HTML shell (pick or add the layout that matches the game's keys) 202 8. Add unit tests in `tests/unit/<slug>-*.test.js` 203 9. Add browser test cases to `tests/browser/runner.js` 204 10. Run `npm test` — all must pass before committing 205 206 ## Hosting & deploy 207 208 The project is self-hosted on NearlyFreeSpeech — there is no Codeberg/GitHub forge. 209 210 - `origin` is `ssh://…@ssh.nyc1.nearlyfreespeech.net/home/private/kgames.git` 211 (a bare repo outside the document root). 212 - A **`pre-push` hook** runs `deploy.sh` on every push to `origin`, which rsyncs 213 the site, refreshes the restic-backed mirror at 214 `/media/bespin/kyle/backups/gits/kgames.git`, and publishes a static 215 [stagit](https://codemadness.org/stagit.html) code browser + dumb-HTTP clone 216 source to `https://www.keyboard.games/code/`. Skip with `KG_SKIP_DEPLOY=1`. 217 - `deploy.sh` and the hook are **untracked**, so a fresh clone does not reproduce 218 this setup — copies plus restore notes live in 219 `/media/bespin/kyle/backups/gits/kgames-untracked/`. 220 - The mirror is deliberately **never repacked** so restic dedups its objects. 221 - `tools/stagit-style.css` is the code browser's stylesheet (site palette). 222 223 ## Asset policy 224 225 **Prefer real, openly licensed art made by human artists.** Procedural 226 generation and AI generation are not the house style and must not be the 227 default for a game's character and world art. 228 229 Priority order for any new art or audio: 230 231 1. Existing open-license work (CC0, CC-BY, CC-BY-SA, OFL, or otherwise 232 GPL-3.0-compatible) — OpenGameArt, Kenney, open itch.io packs, Wikimedia 233 Commons, Freesound, and similar. 234 2. Commissioned or hand-made original work contributed under a 235 GPL-3.0-compatible license. 236 3. Procedural generation — fine for incidental effects (particles, gradients, 237 simple shapes, UI chrome), not for characters, creatures, or environments. 238 4. AI-generated assets — avoid. 239 240 The same order applies to audio: real recorded voice and openly licensed music 241 and SFX beat synthesised equivalents, so use human recordings when they can be 242 found (Wikimedia Commons has thousands of CC-licensed word pronunciations and 243 IPA sound recordings — see `tools/story-stamp-assets.py`). **Synthesised speech 244 is acceptable** where no recording exists or the text is dynamic 245 (instructions, character chatter, a word with no clip): use the browser's Web 246 Speech API, which needs no server. Where a game depends on audio accuracy 247 (phoneme audio for phonics), recorded assets are still expected. 248 249 Record every third-party asset's source, author, and license in the repo, and 250 surface attribution wherever the license requires it. **GPL-3.0 compatibility 251 is a hard requirement**, never a preference. 252 253 ## Constraints for new games 254 255 The rules above under "Architecture" and "Key conventions" describe how the 256 *existing* games are built. They are not a cage for new ones. A new game may 257 use a different engine, a build step, bundled dependencies, and real image and 258 audio asset files — pick whatever serves the game. 259 260 What still holds for everything in this repo: 261 262 - **Open source, GPL-3.0**, with license-compatible dependencies and assets. 263 - **No ads, no signup, no backend, no tracking.** Static hosting only; any 264 saved progress stays on the player's device. 265 - **Tested** — new work ships with unit and browser coverage, and 266 `npm test` must pass before every commit. 267 - **Keyboard-first** — a game must be fully playable from the keyboard alone. 268 Pointer input may be a convenience; no mechanic may depend on it. Phones are 269 served by the on-screen keyboard in `js/shared/touch-controls.js`. 270 271 ## Color palette 272 273 `#E63946` red · `#1D7CF2` blue · `#FFD23F` yellow · `#2EC4B6` teal 274 275 Tint equivalents: `0xE63946 · 0x1D7CF2 · 0xFFD23F · 0x2EC4B6` 276 277 ## Key conventions 278 279 - **Existing games use no external image assets** — all textures via 280 `make.graphics({ add: false }).generateTexture()`. This is a description of how 281 the current games were built, **not a rule for new ones** — see "Asset policy". 282 - **Audio**: Tone.js for looping music; Web Audio API oscillators for SFX 283 (lazy-init on first user interaction to satisfy browser autoplay policy). 284 **Never start music/SFX straight from `Tone.start()` or a first keydown** — 285 on phones the first key comes from the touch bar's `pointerdown`, which does 286 not grant user activation, so contexts stay suspended (Safari never 287 auto-resumes). Hand every AudioContext to `KGames.installAudioUnlock(...)` 288 and do the "audio is up" work in its callback; the `audio:` browser tests 289 emulate the Safari policy and will catch regressions. 290 - **Music is opt-in.** Background music and the fire-truck siren start OFF and 291 are switched on by the player (`M` / `S`, mirrored in the settings panel and 292 the touch bar), so the first audio work a browser ever sees happens inside a 293 real keypress — the one moment every browser (Edge on macOS included) accepts. 294 Fire-truck also runs a once-a-second watchdog (`_audioWatchdog`) that re-nudges 295 a Tone transport that failed to start, and the on-canvas HUD line says whether 296 music is off, starting, or on. 297 - **Font**: Fredoka, 400 + 600 weight (Google Fonts) 298 - **Scale**: `Phaser.Scale.RESIZE` + `applyLayout(W, H)` / `onResize` pattern 299 - **No `console.log` in production code** — browser tests assert zero console errors 300 - **`window.__FT_SCENE__ = this`** exposed in `create()` for browser test inspection