kgames

KGames — free keyboard games for kids
git clone https://www.keyboard.games/code/kgames.git
Log | Files | Refs | README | LICENSE

CLAUDE.md (9189B)


      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. Keep it that way.
     11 
     12 ## Testing — run before every commit
     13 
     14 ```bash
     15 npm install          # one-time; downloads playwright (~112 MB Chromium)
     16 npm test             # unit tests + browser smoke tests; must all pass
     17 npm run test:unit    # fast, no browser (~0.2 s)
     18 npm run test:browser # headless Chromium (~10 s)
     19 ```
     20 
     21 **Never commit if `npm test` fails.** The browser tests catch broken
     22 rendering that syntax checks miss (see the projection-bug incident).
     23 
     24 ## Architecture
     25 
     26 | Path | Purpose |
     27 |---|---|
     28 | `index.html` + `css/style.css` + `js/main.js` | Portal — game tile grid |
     29 | `games/<slug>/index.html` | HTML shell: CDN scripts, HUD buttons, game container |
     30 | `games/<slug>/lib.js` | Pure logic (projection math, route gen) — dual-export for unit tests |
     31 | `games/<slug>/game.js` | Phaser scene — all graphics via `generateTexture`, audio via Web Audio / Tone.js |
     32 | `js/shared/game-shared.js` | Shared `KGames.*` helpers (AudioSystem, win screen, confetti, progress bar) |
     33 | `js/shared/touch-controls.js` | On-screen keyboard / D-pad for phones — see "Mobile / touch controls" |
     34 | `assets/thumbnails/<slug>.svg` | 160×130 viewBox, rx=18, stroke from palette, `#FFF5F5` fill |
     35 | `tests/unit/` | `node --test` unit tests — no browser, no Phaser |
     36 | `tests/browser/runner.js` | Playwright smoke tests — no JS errors + canvas renders |
     37 | `tests/serve.js` | Tiny Node HTTP server used by browser tests (and for local dev) |
     38 
     39 ## Top-down rendering (fire-truck)
     40 
     41 Fire-truck is a **top-down** driving game: the camera zooms 1/`SCALE` (SCALE=5)
     42 and follows the truck over a 50×50-cell island. `lib.js` `buildIsland()` lays
     43 out water/beach ring roads + BSP-subdivided interior blocks; the road graph
     44 drives the route and fire pathing.
     45 
     46 **Chunk bake pipeline** (`renderIsland()` in game.js). The static island is
     47 baked once into 16×16-cell chunk textures at `BAKE_CELL = 96` px/cell, drawn
     48 ×SCALE (zoom 1/SCALE) → **~1:1 on screen**, so ~96 px of detail per cell costs
     49 nothing per frame and Phaser culls offscreen chunks for free. Each chunk runs
     50 named passes into one `Graphics`, in order: `_bakeGround` → `_bakeSidewalks`
     51 (incl. road corner masks) → `_bakeRoadMarkings` → `_bakeBuildings` → `_bakeParks`
     52 → `_bakePalms`.
     53 
     54 - **1-cell apron rule** (highest visual-bug risk): every pass iterates a 1-cell
     55   margin around the chunk (clamped) at true local coords so overhanging art —
     56   SE drop shadows, palm fronds, block-spanning buildings — that crosses a chunk
     57   seam is re-drawn by the neighbour; `generateTexture` clips the overflow.
     58 - Buildings are drawn **per block, not per cell**: `FT.findBuildingBlocks(grid)`
     59   covers all building cells with disjoint rectangles (BSP guarantees rectangular
     60   road-bounded blocks) so each block reads as one building with one roof.
     61   `FT.assignParks()` tags ~15% of blocks `cell.park = true` (**type stays
     62   `building`**, so grid/route tests are untouched; parks are excluded as fire
     63   destinations).
     64 
     65 **Ocean underlay**: a screen-fixed (`scrollFactor 0`) `TileSprite` at depth −1
     66 drifts each frame. A world-sized TileSprite would allocate a multi-GB fill
     67 canvas — keep it viewport-sized.
     68 
     69 **Dual cameras** (game.js): `cameras.main` is the world; `uiCamera` is the HUD.
     70 Every new **world** object must be hidden from the HUD camera —
     71 `this._addWorld(obj, depth)` sets depth + `uiCamera.ignore`. HUD objects are
     72 added to `cameras.main.ignore(...)`.
     73 
     74 **Depth ordering** (exact values live in game.js): ocean < chunks <
     75 boats/buoys < peds < bikes < cars/buses < burning overlay < fire glow < fire <
     76 spray < smoke < cloud shadows < truck < birds < HUD.
     77 
     78 **Hose minigame** (`FireHoseScene`): when the truck reaches the fire, the
     79 driving scene enters state `minigame`, pauses itself, and launches a
     80 street-view scene — building facade with a window grid (`FT.buildFacade` in
     81 lib.js, pure + unit-tested), arrow keys move an aim reticle, SPACE sprays from
     82 the truck's rotating ladder. Quenched windows reveal waving residents/a cat;
     83 when all are out the scene resumes the driving scene and calls
     84 `_onMinigameComplete()`. **Headless-RAF rule**: gameplay logic lives in
     85 `_step()` driven from both `update()` and a wall-clock `setInterval` fallback
     86 (same trick as the driving scene's `fallbackTimer`), and all scene transitions
     87 use wall-clock `setTimeout` — never `this.time.delayedCall`, which starves in
     88 throttled/headless browsers and hangs the tests.
     89 
     90 **Shared cartoony art** (file-scope helpers in game.js): `bakePeopleTextures`
     91 (`kg-person-0..7`, racially diverse cartoon townspeople), `bakeSideTruckTexture`
     92 (`kg-truck-side`), `bakeLadderTexture`, `bakeCritterTextures` (dog/cat/parrot/
     93 iguana), `drawSidePalm`. Used by both `FireHoseScene` and `FireTruckEndScene` —
     94 reuse these rather than drawing new people/trucks.
     95 
     96 **Ambient life** (`_initAmbient` / `_updateAmbient`, driven from `update()` —
     97 never `step()`, which tests call directly): baked-texture Images for traffic
     98 (`FT.advanceCarPlan` graph walk), pedestrians, cloud shadows, boats, birds.
     99 Guard with `this.ambientReady`; clean up in `shutdown()`. All decor variety is
    100 deterministic via `FT.hashCell(x, y, salt)` so the island bakes identically
    101 every run.
    102 
    103 ## Mobile / touch controls
    104 
    105 Phones have no physical keyboard and a Phaser canvas can't summon the native
    106 one, so `js/shared/touch-controls.js` renders an on-screen control bar whose
    107 buttons dispatch **synthetic `KeyboardEvent`s to `window`**. Phaser's keyboard
    108 plugin already listens on `window`, so every game's existing key handling works
    109 unchanged — game logic never references this module.
    110 
    111 - Opt in from the HTML shell, after `game.js`:
    112   `<script>KGames.initTouchControls({ layout: 'alphabet' });</script>`
    113 - Layouts: `alphabet` (A–Z), `text` (A–Z + Space/Backspace/Enter, e.g. work),
    114   `drive` (arrows + Siren/Lights/Spray, e.g. fire-truck).
    115 - Shown only on touch / coarse-pointer devices. Force with `?kbd=1` / `?kbd=0`
    116   or `window.__FORCE_TOUCH_CONTROLS__` (tests use the latter).
    117 - The bar is a flex child below `#game-container`, so the canvas shrinks to fit
    118   above it. Phaser's `Scale.RESIZE` ignores a synthetic window `resize` when the
    119   window size is unchanged, so the module calls `game.scale.refresh()` directly.
    120   **This requires the game instance on `window.__KG_GAME__`** — assign it where
    121   you call `new Phaser.Game(...)`.
    122 
    123 ## Adding a new game
    124 
    125 1. Create `games/<slug>/index.html` (clone from `games/fire-truck/index.html`)
    126 2. Create `games/<slug>/lib.js` for pure (unit-testable) logic
    127 3. Create `games/<slug>/game.js` for the Phaser scene
    128 4. Add `render: { preserveDrawingBuffer: true }` to the `Phaser.Game` config
    129    (required for the Playwright pixel-reading tests to work), and assign the game
    130    to `window.__KG_GAME__` so touch controls can resize it
    131 5. Create `assets/thumbnails/<slug>.svg` (160×130, rx=18 rounded rect)
    132 6. Replace a `Coming Soon` placeholder in `js/main.js`
    133 7. Wire up touch controls: `KGames.initTouchControls({ layout: ... })` in the
    134    HTML shell (pick or add the layout that matches the game's keys)
    135 8. Add unit tests in `tests/unit/<slug>-*.test.js`
    136 9. Add browser test cases to `tests/browser/runner.js`
    137 10. Run `npm test` — all must pass before committing
    138 
    139 ## Hosting & deploy
    140 
    141 The project is self-hosted on NearlyFreeSpeech — there is no Codeberg/GitHub forge.
    142 
    143 - `origin` is `ssh://…@ssh.nyc1.nearlyfreespeech.net/home/private/kgames.git`
    144   (a bare repo outside the document root).
    145 - A **`pre-push` hook** runs `deploy.sh` on every push to `origin`, which rsyncs
    146   the site, refreshes the restic-backed mirror at
    147   `/media/bespin/kyle/backups/gits/kgames.git`, and publishes a static
    148   [stagit](https://codemadness.org/stagit.html) code browser + dumb-HTTP clone
    149   source to `https://www.keyboard.games/code/`. Skip with `KG_SKIP_DEPLOY=1`.
    150 - `deploy.sh` and the hook are **untracked**, so a fresh clone does not reproduce
    151   this setup — copies plus restore notes live in
    152   `/media/bespin/kyle/backups/gits/kgames-untracked/`.
    153 - The mirror is deliberately **never repacked** so restic dedups its objects.
    154 - `tools/stagit-style.css` is the code browser's stylesheet (site palette).
    155 
    156 ## Color palette
    157 
    158 `#E63946` red · `#1D7CF2` blue · `#FFD23F` yellow · `#2EC4B6` teal
    159 
    160 Tint equivalents: `0xE63946 · 0x1D7CF2 · 0xFFD23F · 0x2EC4B6`
    161 
    162 ## Key conventions
    163 
    164 - **No external image assets** — all textures via `make.graphics({ add: false }).generateTexture()`
    165 - **Audio**: Tone.js for looping music; Web Audio API oscillators for SFX
    166   (lazy-init on first user interaction to satisfy browser autoplay policy)
    167 - **Font**: Fredoka, 400 + 600 weight (Google Fonts)
    168 - **Scale**: `Phaser.Scale.RESIZE` + `applyLayout(W, H)` / `onResize` pattern
    169 - **No `console.log` in production code** — browser tests assert zero console errors
    170 - **`window.__FT_SCENE__ = this`** exposed in `create()` for browser test inspection