fertig-classic-games/docs/angrybirds-build-plan.md

160 lines
13 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

# Angry Birds — build plan
Living plan doc. Survives context clears: **read this file first**, pick the next unchecked item, update the checkboxes and the Status line as work lands.
## Status
**Waves 0 and 1 complete 2026-07-30. Playable end to end, never browser-tested.** `node tools/verifyAngryBirds.js`**114 checks green**.
The solver holds: a 10-box tower settles in 0.68s, sleeps, drifts <1px sideways and tilts 0.35°; a 15-box pyramid settles too; a 6-second 41-body shot runs in 254ms headless. The rules layer is in: 3 materials + TNT, health-model damage with crack stages, all 8 birds, scoring/stars/win/lose, and a 6-level starter bank that a greedy aim sweep clears.
Registered and wired (slug `angrybirds`, iconFrame **92**) it appears under **Video Games** and is reachable from the menu.
Next: Wave 2 is largely done inside Wave 1 (all 8 birds and TNT landed early). The real remaining work is **Wave 3** (editor + generator + `AngryBirdsAuto`), **Wave 4** (grow 6 levels 63), and **Wave 5** (artwork JSON, tutorial, icon art).
## Decisions taken up front
| Question | Answer |
|---|---|
| Slug / category / iconFrame | `angrybirds` / `arcade-console-pc` ("Video Games") / **92** (next free after Excitebike's 91) |
| Physics | **Bespoke deterministic rigid-body solver.** Matter.js ships inside the CDN Phaser build and would work in-browser, but it is not importable in bare Node and not reproducible that forfeits the verifier, the generator's measured star thresholds, and the editor's winnability gate. |
| Visual treatment | **No CRT overlay, no scanlines, no pixel font.** It's a 2009 touch game, not an arcade cabinet. Clean cartoon look, app's normal fonts. |
| Campaign scale | **63 levels, 3 episodes of 21** Poached Eggs / Mighty Hoax / Danger Above. |
| Art | Procedural, with a `data/angrybirds-artwork.json` drop-in hook so real sprites can replace the fallbacks later with no code change. Procedural fallbacks must always work standalone. |
| Bird roster | All 8 Red, Chuck, Blue, Bomb, Matilda, Terence, Hal, Bubbles. |
| Modes | Single-player campaign only. `maxOpponents: 0`, so it skips `OpponentSelect` entirely. |
## Files
| File | Role | State |
|---|---|---|
| `src/games/angrybirds/AngryBirdsPhysics.js` | Rigid-body solver. Zero imports. | Wave 0 |
| `src/games/angrybirds/AngryBirdsLogic.js` | Materials, damage, birds, scoring, `stepSim`. Only imports Physics. | Wave 1 |
| `src/games/angrybirds/AngryBirdsGame.js` | Phaser scene, key `AngryBirdsGame`. | Wave 1 |
| `src/games/angrybirds/AngryBirdsArt.js` | Procedural textures + artwork-JSON resolution. | Wave 5 |
| `src/games/angrybirds/AngryBirdsEditor.js` | Level editor, key `AngryBirdsEditor`. | Wave 3 |
| `src/games/angrybirds/AngryBirdsAuto.js` | Reference bot / winnability gate. In `src/` so the editor button and the verifier run the same gate. | Wave 3 |
| `src/games/angrybirds/tutorial.md` | Tutorial copy. | Wave 5 |
| `tools/genAngryBirds.js` | Level-bank generator; **measures** star thresholds. | Wave 3 |
| `tools/verifyAngryBirds.js` | Node harness. | 114 checks |
| `assets/gamedata/angrybirds/` | `levels.json` manifest + level files. | 🔨 6 of 63 |
| `data/angrybirds-artwork.json` | Drop-in sprite map. | Wave 5 |
Wiring touchpoints: `src/data/gamesRegistry.js`, `src/main.js` (import + scene array, both scenes), `src/scenes/GameRoomScene.js:26` (`slugDispatch`), `src/data/assetManifest.js`, `src/scenes/PreloadScene.js` (manifest JSON + `?angrybirds-editor=1` entrance).
---
## Wave 0 — the rigid-body solver — ✅ DONE 2026-07-30
- [x] Convex polygon + circle bodies, mass/inertia derived from geometry
- [x] Uniform-grid AABB broadphase
- [x] SAT narrowphase with reference-face clipping 2-point manifolds
- [x] Sequential-impulse solver with warm starting
- [x] Coulomb friction clamped against the accumulated normal impulse
- [x] **Split-impulse** position correction with penetration slop (not plain Baumgarte see finding 1)
- [x] Island-based sleeping
- [x] `cloneWorld` / `hashWorld` for determinism tests
- [x] Verifier green all four gate conditions pass
### The gate — all four passed
1. 10-box tower settles in 0.68s, every body asleep, drift 0.00px, tilt 6.1e-3 rad. A 15-box pyramid also settles.
2. Box on a 15° slope with µ=0.9 does not creep; a µ=0.2 box on 40° does slide.
3. Bit-identical replay; 1/60 frame == 4 substeps exactly; ragged frame pacing matches even pacing.
4. 12 monkey seeds × 900 substeps: no NaN, no overspeed, nothing sinks.
### Wave 0 findings
1. **Plain Baumgarte is not good enough for a 10-high stack, and the failure looks like a bug when it isn't.** Feeding position error back as bias velocity makes a contact behave like a stiff spring, so penetration is proportional to load the bottom contact of a 10-box tower carries 9 boxes and sinks ~9× deeper than the top one. Measured sag was 12.85px. Switching to **split impulse** (a separate pseudo-velocity accumulator, added into the position integration then discarded) removes a fixed fraction of the excess per substep *regardless of load*, and never leaks the correction back as bounce energy. Sag dropped to 0.46px per contact, which is exactly `SLOP` i.e. the remaining sag is the slop band we deliberately allow, not solver error. **Do not "simplify" this back to a single bias term.**
2. **Total stack sag is bounded by `SLOP × contactCount`, by design.** The verifier asserts against that budget rather than a flat pixel value. Shrinking `SLOP` tightens stacks but reintroduces resting-contact jitter 0.5px on a 40px block is the sweet spot.
3. **Warm starting must key on a stable feature id, not array position.** Contact points are matched across substeps by `(refFace << 8) | incidentVert`, with a flip bit. Matching by index scrambles impulses the moment the clip order changes and the stack sags visibly.
4. **The reference-face choice needs hysteresis.** Preferring A unless B is deeper by a relative margin (`0.1 × |sepA| + 0.01`) keeps the choice stable frame to frame, which is what keeps feature ids and therefore warm starting stable for a resting contact.
5. **Sequential impulses are Gauss-Seidel, so solve order genuinely changes the answer.** Bit-identical results across different body *creation* orders are not achievable and were never the goal; the verifier asserts the dependence stays sub-pixel (measured 0.4px). What must be exact and is is that a *given* scene replays identically.
6. **Island sleeping, not per-body sleeping.** Union-find over contacts; an island sleeps only when its slowest member has been slow for `SLEEP_TIME`. Per-body sleeping freezes half a tower while the rest still moves. Sleeping is also how the rules layer knows a shot is over, so it is not just an optimization.
7. **Anti-tunnelling is a design constraint, not a runtime check.** `MAX_SPEED × SUBSTEP_DT < MIN_HALF_EXTENT` (10px < 12px) is asserted by the verifier, which is what forced `SUBSTEP_DT` to 1/240 and `MAX_SPEED` to 2400. No block may be thinner than 24px.
8. **String keys dominated the profile.** Numeric composite keys for the grid and contact maps, plus building the contact list in already-sorted pair order instead of re-sorting it four times per substep, took a 6-second shot from 368ms to 254ms with bit-identical output.
9. **Contacts cache direct body references, so `cloneWorld` must rewire them.** A shallow spread leaves the clone's contacts pointing at the original's bodies the clone then drives the world it was supposed to leave alone. The winnability bot and shot preview both depend on this; the verifier now asserts it explicitly.
### Baselines
```
node tools/verifyAngryBirds.js -> 114 checks green
node tools/verifyAngryBirds.js --seeds=40 -> slower monkey soak, still green
```
---
## Wave 1 — playable core — ✅ DONE 2026-07-30
- [x] Materials: wood / stone / ice with distinct density, friction, restitution, hp, damage threshold
- [x] Damage as a **health model** impact above threshold subtracts hp; 2/3 and 1/3 crossings emit crack stages
- [x] Pigs take damage from debris, not just direct hits
- [x] Slingshot: drag, clamp to max draw, release
- [x] Win/lose evaluated only once the world is settled or `MAX_SHOT_TIME` expires
- [x] `stepSim(state, dt) → events[]` contract
- [x] Phaser scene with sim-spacescreen-space mapping
- [x] Trajectory memory (dotted trail of previous shots)
- [x] Level select, progress persistence, win/lose panel
- [x] 6-level starter bank, all stable and sweep-clearable
- [ ] Camera pan/zoom deliberately deferred, see finding 4
### Wave 1 findings
1. **The damage signal must be approach velocity, not the solver's accumulated normal impulse — and this one is a trap.** The accumulated impulse includes the static load a contact carries, which grows with stack height. Measured: the base contact of a 10-block stone tower sits at **3.4e5**, which is *above* the threshold that should shatter wood (1.5e5) and level with stone's (3.2e5). Wiring damage to it makes tall towers quietly crush themselves while nothing is happening and it looks like a level-design bug, not a physics one. `Physics.prestep` now also publishes `contact.impactImpulse` = the momentum needed to stop the approach, which is zero at rest by construction. Resting dropped to **2.4e4**, real hits run 1.9e5 (feeble) to 2.7e6 (full draw). `verifyAngryBirds.js` pins both ends.
2. **Calibrate thresholds by measuring, never by guessing.** The whole material table was set from a one-off script that fired birds at 400/800/1200/1600/2000 px/s and printed peak impact impulses. Re-run it after any change to density, gravity or `SUBSTEP_DT` all three move the impulse scale.
3. **Levels must be authored already at rest.** `createState` drops blocks exactly where the file says; if the author's structure was mid-collapse when exported, the player sees a different level. The verifier asserts every level stands unaided for 4s with zero self-damage. Wave 3's editor gets a **Settle** button for this.
4. **No camera pan/zoom yet, on purpose.** The scene maps the 2400×1080 logical level to the canvas through fixed `sx()`/`sy()` helpers at scale 0.78 (the PeggleGame idiom) rather than zooming the camera, which keeps HUD text in plain screen coordinates. The whole level is visible at once. A follow-camera is the more faithful feel but is pure presentation and cannot be validated headlessly it belongs in Wave 5 alongside a real browser pass.
5. **Bubbles has to be rebuilt, not resized.** The solver derives mass and inertia at creation and assumes they never change, so inflating means swapping in a new, larger, lower-density body and transferring velocity. Mutating `radius` in place would leave mass properties describing the old bird.
6. **Spent birds are removed when a shot resolves.** Left in, a dead bird props structures up forever and the next shot plays against a level the author never built.
---
## Wave 2 — the full roster, TNT, scoring — ✅ DONE 2026-07-30 (landed inside Wave 1)
- [x] 8 birds, abilities fire on tap mid-flight, one use per shot
- [x] TNT blocks detonate when damaged (removed before detonating, so they can't recurse into themselves)
- [x] Scoring: 5,000/pig + material destruction points + 10,000 per unused bird
---
## Wave 3 — editor and generator — ⬜
- [ ] `?angrybirds-editor=1` entrance, authoring in play coordinates
- [ ] **Settle** button so levels export already at rest
- [ ] **Test Winnable** button running the same gate as the verifier
- [ ] `AngryBirdsAuto` beam search over (angle, power, ability timing)
- [ ] Blob export of `level-NNN.json` + regenerated `levels.json`
Gate is **one-directional**: if the bot wins, a human can. A bot failure is *not* proof a level is impossible.
---
## Wave 4 — the 63-level campaign — ⬜
- [ ] Poached Eggs (1-21) wood then stone; Red, Chuck, Blue
- [ ] Mighty Hoax (22-42) ice, TNT, taller structures; adds Bomb, Matilda
- [ ] Danger Above (43-63) mixed materials, elevated structures; adds Terence, Hal, Bubbles
Every level needs a **load-bearing** support whose removal collapses the structure. That, not bird variety, is what makes a shot feel clever.
---
## Wave 5 — art, audio, progress, polish — ⬜
- [ ] Procedural art + artwork-JSON drop-in
- [ ] Progress via `/puzzles/angrybirds/{progress,complete}`, per-level best/stars in `ab-best-N` / `ab-stars-N`
- [ ] Match history use `{ slug, score, opponentScores, result }`. Goo Tower gets this **wrong** (`GooTowerGame.js:879-881`); copy `RushHourGame.js:513-516` instead.
- [ ] `tutorial.md` + `hasTutorial: true`
- [ ] Paint `game-icons.png` frame 92 (row 6, col 2 44×44 at x=88, y=264)
---
## How difficulty is decided
`TUNING` in `AngryBirdsLogic.js` owns damage thresholds and material hp tune there, not in level files. Star thresholds are **measured** by `tools/genAngryBirds.js` from the bot's achieved score, never hand-authored (same discipline as Excitebike's `qualifyMs`).
## Sources for original-game behaviour
- Rovio *Angry Birds* (2009) material behaviour, bird abilities, 5,000/pig and 10,000/unused-bird scoring.
- Episode structure: Poached Eggs, Mighty Hoax, Danger Above 21 levels each.