makepad/handoff.md
Admin 449f51be7a gamemaker prerequisites: makepad-tts library, TTS/whisper model downloader, aigame design docs
libs/tts was never tracked despite being a build dependency of the gamemaker
example. tools/download_tts.sh fetches the public upstream weights (HuggingFace
Kokoro-82M + whisper.cpp) and converts them locally with the in-repo stdlib-only
converter; model artifacts are gitignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:29:22 +02:00

233 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# handoff — the Makepad AI Game Maker (godot-like livecoding agent)
State as of 2026-07-10. Everything below is in the working tree, **nothing committed**.
Written as the handoff for `examples/gamemaker`: a voice-driven game maker for kids
where Claude Code live-edits a single splash script and the game hot-reloads in-process
— the replacement for the Godot-backed `examples/godot` (which still exists, untouched,
as the reference implementation).
## 1. What it is
```
kid holds F1, talks (Whisper) ──► Claude Code (via makepad_ai::ClaudeCodeAgent)
▲ │ edits ~/games/<name>/game.splash
│ TTS speaks replies ▼
Game Maker app ── chat pane │ GameView pane: splash isolate + fixed-60Hz world,
│ re-evals on every edit (aichat-style streaming),
│ broken edits never replace the running world
```
- One process. No engine restarts, no focus stealing. A clean edit is visible mid-turn
("the world grows while the AI talks"); a broken edit keeps the last-good world
running and returns line-numbered errors to the agent.
- The whole game is ONE file, `game.splash`, written in a curated ~45-verb `game.*`
DSL (deliberately smaller than GDScript — easier to prompt, verify, and keep stable).
- The agent self-verifies through `tools/ag`: screenshots, deterministic input-tape
playtests with numeric probes, error/log round-trip.
## 2. Engine architecture (examples/gamemaker)
**Host** (`src/main.rs`, cloned from the godot example): chat/voice/TTS/session shell,
per-game state in `<game>/.gamemaker/` (chat log, Claude session id, model), project
dropdown over `~/games/*/game.splash` (`GAMEMAKER_HOME` overrides), harness re-stamped
into each game on switch (`tools/`, `CLAUDE.md` — app-owned, kid files never touched).
System prompt teaches kid-talk rules + the DSL + the always-check-errors workflow.
Permission policy: `Edit/Write(./**)` + `Bash(./tools/ag:*)` only.
**GameView** (`src/game_view.rs`, ~2k lines — splitting into modules was repeatedly
deferred, do it before it grows again):
- **Script hosting**: dedicated splash isolate (`cx.alloc_splash_vm_with_network`),
incremental re-eval via `eval_with_append_source` parser checkpoints (the same
machinery aichat uses to stream `runsplash` blocks), instruction limits (2M per
eval / 500k per tick), pointer-stable ScriptMod keying. Reload triggers: 250ms mtime
watch + immediately on the agent's Edit/Write ToolRequest completing.
- **Last-good semantics**: eval snapshots the world (entities, parts, labels, terrain,
sky, HUD), rebuilds from scratch, rolls back wholesale on failure. Errors go to
`.agent/game.log` + `.agent/last_error.txt` (cleared-to-empty on success BY DESIGN —
"empty = your edit is live") via a platform addition: `ScriptVm::take_errors` +
`captured_errors` sink (streaming evals otherwise silence errors).
- **Dispatch design**: one native `game` handle over `Rc<RefCell<GameWorld>>`; every
`game.verb(...)` is a match arm in `game_dispatch` — synchronous mutation, no async
widget trampoline, deterministic ordering. Adding a verb = one arm + one doc row.
- **Physics**: deliberately tiny — gravity + axis-separated AABB sweeps, kinematic
carry, sensors, mover ground detection, terrain height-lookup collision with 0.55
step-up. **The physics body NEVER rotates — only the visual model yaws.** This
matches Godot exactly (their CharacterBody3D does the same); it is the reason a
mini-engine reaches parity. box3d swap remains a marked seam (see §5).
- **Renderer**: offscreen 3D pass composited into the pane. Shaders (script-DSL,
JIT-compiled on headless): `DrawGameCube` (instanced lit boxes + emission + fog),
`DrawGameAlpha` (translucent pass: sensors, water, blob shadows), `DrawGameSky`
(gradient dome), `DrawGameTerrain` (triangulated heightfield, per-vertex color).
Near plane **1.0** (Godot's CAM_NEAR) so lens-overlapping creatures clip open
instead of filling the screen. Billboard labels project into a 2D overlay with
automatic 4-copy dark outlines; HUD slots + crosshair are plain overlay draws.
- **Input**: ActionMap — keyboard (WASD/arrows, Space, F=shoot, G=grab) + gamepad
(`cx.game_input_states`: stick analog w/ deadzone, dpad, A=jump/X=shoot/B=grab,
edge-detected) merged at read time via `PadState` (never into the key-held set, so
devices can't cancel each other). Input snapshot per tick: held/pressed booleans,
raw `axis_x/axis_z`, **camera-relative `move_x/move_z`** (the canonical walk input;
the axes rotated by effective camera yaw — derived from the camera basis, and the
fix for "controls don't match the camera": the first documented recipe rotated by
yaw). Tapes zero the pad and pin yaw+pitch → byte-identical replays.
- **Audio** (`src/synth.rs`): polyphonic synth (osc + percussive envelope, 24-voice
cap) mixed into the app's audio callback under the TTS voice; mute button silences
both. Named bank (18: jump shoot zap grab angry calm rescue shove board coin hurt
win lose squeak roar bark moo clank whip) + `game.beep{}` + `game.jingle("C5 E5")`.
The Godot corpus AI hand-built a synth in GDScript when it had no audio API —
that's why this is an engine service.
- **Agent harness** (`tools/ag` + file RPC through `.agent/`): `peek` (4 live frames
via `Cx::capture_next_frame_to_file` — a platform addition riding the studio
screenshot pipeline — + entity state), `test N tape` (restart, frame-indexed tape
through the ActionMap, captures + `probe.txt`, `sheet.py` contact sheet), `errors`,
`logs`. Tape format is the Godot harness's JSON unchanged. Errors are also
PUSHED into the agent chat (GameViewAction → auto fix wake-up on post-turn broken
evals and idle runtime errors; 2-wake-up guard, reset by a kid message;
GAMEMAKER_NO_AGENT=1 disables the agent for token-safe headless tests). Engine
registration errors surface in the status bar, never in the kid chat.
**DSL surface** — the authoritative, always-in-sync docs:
- **repo-root `splashgame.md`** — THE agent-facing API contract, loaded into the
system prompt at runtime (fs read + include_str fallback), exactly the pattern
aichat uses for `splash.md`. Adding a verb = dispatch arm + a row here.
- `resources/template/CLAUDE.md` — per-game workflow only (ag ritual, house style,
gotchas); points at the system prompt for the API.
- `resources/aigame-dsl.md` — developer guide (execution model, extension points).
Spawning: `box/mover/spawn/terrain/part/label`. Driving: `on_tick(dt, input)`, `walk/
jump/on_floor/pos/vel/set_pos/set_vel/face/yaw`, `find/tag/distance`, timers `after`,
`on_touch`. Look: `set_color/glow/scale/move_part/sky/camera` (orbit / `side:` /
`third_person:` with occlusion pull-in ignoring `"scenery"`). Systems: `attach`
(seat + `mode:"ride"` w/ spin)/`detach`, `speed_mult`, `beam`, `ground_y/ground_peak`,
`rand/rand_range` (seeded per eval — tape-deterministic, better than the corpus's
`randomize()`), HUD `text(slot,…)/crosshair`, labels w/ ids + `label_text`, sounds.
Terrain noise is shaped engine-side (`freq/offset/amp/step/min/max/plaza`, ≤384
cells, `bands:` height-color thresholds — snow bands are what make hills read as
mountains) so big worlds cost no script instruction budget.
## 3. How parity was reached (method matters as much as the result)
1. **Corpus first**: the AI-generated Godot game (`~/games/my-game`, grew to ~44
creatures) was inventoried exhaustively (`aigame_port_inventory.md`), and every
engine feature exists because the corpus used it — nothing speculative. Confirmed
non-needs (no tweens/particles/navmesh/shaders in 2600+ generated lines) were
deliberately NOT built.
2. **Port as acceptance test**: the game was ported to
`resources/fixtures/sandbox3d.splash` (932 lines, 90 entities — ~2100 lines of
GDScript; the compression is the argument the DSL is at the right altitude).
Each port round produced findings (`aigame_port_findings.md`); findings became
engine verbs; the fixture re-ported until clean. Gap analysis lives in
`aigame_parity_gap.md`; the original plan in `aigame.md`.
3. **Verification discipline**: headless eval cleanliness + numeric probes against
Godot ground truth (plaza floor exactly 7.9, walk exactly 6.0, jump apex JUMP²/2G,
ride-debuff exactly 3.0) + determinism (two tape runs byte-identical). Pixels on
GPU were checked by rik; pure-headless pixels are blocked on §5.5.
The fixture runs the CURRENT game: Giant DogDay guardian (intercept-charge, beam
bonk-arc), 3 headcrabs (leap→latch→speed debuff→jump to shake off), the Prototype
weeping-angel (LOS dot vs camera), Baba Chops (glow-ramp fire eyes, ram), Nightmare
Huggy (arm-reach via move_part), CatNap sleep-curl (scale), Kissy bodyguard, heal
quest, 10 farm animals (per-kind sfx pitch), trucks + passengers, grapple hand on G
(terrain yank / creature haul, beam cable), 257×257 smooth terrain at Godot's exact
constants with banded snow mountains, sky/fog/blob shadows, crosshair/hint/flash HUD.
Installed for rik as `~/games/dogday-world`.
## 4. Infrastructure built along the way (reusable beyond gamemaker)
- **Headless render/test pipeline**: `MAKEPAD=headless` builds render frames to PNG
via CPU raster + JIT-compiled Rust shaders (`--draws=N`,
`MAKEPAD_HEADLESS_OUT_DIR`); warm cache at `examples/splash/target`. gamemaker has
its own `build.rs` mirroring the platform's env→cfg wiring.
- **Three JIT shader-compiler bugs fixed** (platform/script + headless preamble):
scalar casts `u32(x)``as` casts; `Mat4f*Mat4f` missing in the runtime preamble;
heterogeneous constructors splat-padding because `ShaderType::Id` args weren't
scope-resolved. Regression stages 4k/4l in `platform/script/test/src/main.rs`.
- **`ScriptVm::take_errors` + `captured_errors` sink** (platform/script/src/vm.rs) —
the error round-trip primitive; should be upstreamed into the `Splash` widget too
(aichat runsplash blocks still render blank on error).
- **`Cx::capture_next_frame_to_file`** (platform/src/os/cx_shared.rs + metal path) —
in-app window capture without the studio bridge.
- **xr on box3d**: the whole `makepad-xr` crate ported off Rapier3D (raycast vehicle
reimplemented in `xr/src/scene/raycast_vehicle.rs`; 4 pre-existing test failures
match the Rapier baseline exactly). box3d `body_set_mass_data` stale
world-inverse-inertia bug found + fixed with a regression test.
- The **godot example harness** got the no-focus-steal treatment first (open -g,
unfocusable offscreen capture window, last-good relaunch policy) — kept as-is.
## 5. Remaining work for FULL parity (ranked)
1. **Caves / overhangs** — the only dropped world feature. Heightfields can't carve.
Options: engine `game.tunnel(a, b, r)` laying rock-slab roofs (matches the Godot
`_carve_caves` approach — it lays roof slabs too, it does NOT boolean-carve), or
accept boxes-as-caves authored by the AI.
2. **`game.raycast(from, dir, {mask})`** — the grapple currently probes terrain
height only; it can't catch trees/boxes/creatures mid-flight, and ledge-probe AI
(the corpus's no-navmesh pathing trick) can't be written. One verb unlocks both.
3. **Real shadow maps** — blob shadows ground creatures but the 21:54 Godot capture
has directional shadows. A single-cascade sun map over the play area is enough.
4. **Camera-overlap creature fade** — near-clip 1.0 fixed the giant-polygon fill;
inside a crowd you can still sit within a body. Fade entities whose AABB
intersects a small camera sphere (Godot mitigates via its spring-arm feel).
5. **Headless offscreen-pass compositing** (task #9) — box3d example renders nothing
headless; gamemaker's pane is blank in pure-headless frames (GPU capture path is
fine). Blocks CI-grade visual verification of `ag test` sheets. Look at
`platform/src/os/headless/event_loop.rs` pass scheduling + overlay draw lists +
NextFrame-only apps never drawing.
6. **box3d under the verbs** — the mini-AABB physics is the deliberate seam
(`TODO(aigame)` in game_view.rs). Swap when games need slopes-with-momentum,
stacking, ragdolls, vehicles-with-suspension. box3d is deterministic and already
in-tree; keep the tape guarantees.
7. **The 2D side-scroller** (`main.tscn`) was never ported — `side:` camera + boxes
cover it in principle; port it as a second fixture to harden 2D ergonomics
(AnimatableBody movers + Camera2D limits analogues).
8. **Engine polish debt**: terrain band colors interact with auto-shade (bands win —
fine, but no slope shading within a band); `game.terrain` columns mode still
spawns per-cell entities (instanced draw + height collision would retire it);
part transforms have no rot lerp shortest-path handling; gamepad stick vertical
sign untested on hardware (one-line flip if inverted).
9. **Perf headroom**: 90 entities × parts ≈ fine; the tick budget (500k instructions)
fits the fixture's ~15 actors of AI — a 100-creature brawl will need either budget
raise or engine-side steering helpers (`walk_towards`, `flee`) which the corpus's
shared `_drive()` suggests anyway.
10. **Future (from aigame.md phase 3)**: host the scene layer on `XrNode` so the same
game.splash runs on Quest (xr is on box3d now, and has hands/multiplayer);
upstream the error round-trip to `Splash`; batch corpus regression harness
(splash_preview-style: run recorded kid asks through the real CLI against the
DSL guide, eval + tape-smoke each result).
## 6. Run / verify cheatsheet
```bash
# the app (from the makepad repo root; Whisper model resolves from CWD)
cargo run -p makepad-example-gamemaker --release
# rik's install: ~/games/dogday-world (set as .last), full fixture
# headless smoke of any game dir
MAKEPAD=headless CARGO_TARGET_DIR=examples/splash/target \
MAKEPAD_HEADLESS_OUT_DIR=/tmp/frames GAMEMAKER_HOME=<games-root> \
cargo run -p makepad-example-gamemaker --release -- --draws=6
# then: <game>/.agent/game.log ("eval #N: ok, E entities"), last_error.txt empty
# AND count startup script errors: ... 2>&1 | grep -cE "^\[E\]" must be 0
# (shader/widget REGISTRATION errors print as [E] lines but do not fail the run —
# a missed [E] once shipped a build with the terrain shader dead)
# agent-side playtest, in a game dir (app running)
./tools/ag errors | logs | peek | test 200 tools/tapes/<tape>.json
# sheet: .agent/sheet.png probes: .agent/probe.txt (byte-identical across runs)
# engine tests touched by this work
cargo run -p makepad-script-test --release # shader codegen stages 4k/4l
cargo test -p makepad-box3d --release # incl. mass-data regression
cargo test -p makepad-xr --release # 127 pass / 4 pre-existing
```
## 7. Document index
| doc | what |
|---|---|
| `aigame.md` | the original migration plan (Godot → splash), phases + rationale |
| `aigame_port_inventory.md` | exhaustive Godot API usage of the generated corpus |
| `aigame_port_findings.md` | port findings v1+v2: API cleanups, fidelity ledgers, open gaps |
| `aigame_parity_gap.md` | the final gap matrix (current game vs engine) + fork specs |
| `examples/gamemaker/resources/aigame-dsl.md` | developer guide to the DSL + engine internals |
| `splashgame.md` (repo root) | THE agent-facing API contract, loaded into the system prompt (keep in sync!) |
| `examples/gamemaker/resources/template/CLAUDE.md` | per-game workflow stamped into each project |
| this file | orientation + what's left |