160 lines
13 KiB
Markdown
160 lines
13 KiB
Markdown
# 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-space→screen-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.
|