10 KiB
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
Wave 0 complete 2026-07-30 — the gate is passed. node tools/verifyAngryBirds.js → 51 checks green. A 10-box tower settles in 0.68s, sleeps, drifts <1px sideways, and tilts 6.1e-3 rad (0.35°, invisible). A 15-box pyramid settles too. The solver runs a 6-second, 41-body shot in 254ms headless.
Next: Wave 1, the playable core.
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. Zero imports. |
⬜ 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. | 🔨 Wave 0 |
assets/gamedata/angrybirds/ |
levels.json manifest + 63 level files. |
⬜ Wave 4 |
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
- Convex polygon + circle bodies, mass/inertia derived from geometry
- Uniform-grid AABB broadphase
- SAT narrowphase with reference-face clipping → 2-point manifolds
- Sequential-impulse solver with warm starting
- Coulomb friction clamped against the accumulated normal impulse
- Split-impulse position correction with penetration slop (not plain Baumgarte — see finding 1)
- Island-based sleeping
cloneWorld/hashWorldfor determinism tests- Verifier green — all four gate conditions pass
The gate — all four passed
- ✅ 10-box tower settles in 0.68s, every body asleep, drift 0.00px, tilt 6.1e-3 rad. A 15-box pyramid also settles.
- ✅ Box on a 15° slope with µ=0.9 does not creep; a µ=0.2 box on 40° does slide.
- ✅ Bit-identical replay; 1/60 frame == 4 substeps exactly; ragged frame pacing matches even pacing.
- ✅ 12 monkey seeds × 900 substeps: no NaN, no overspeed, nothing sinks.
Wave 0 findings
- 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. - Total stack sag is bounded by
SLOP × contactCount, by design. The verifier asserts against that budget rather than a flat pixel value. ShrinkingSLOPtightens stacks but reintroduces resting-contact jitter — 0.5px on a 40px block is the sweet spot. - 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. - 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. - 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.
- 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. - 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 forcedSUBSTEP_DTto 1/240 andMAX_SPEEDto 2400. No block may be thinner than 24px. - 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.
- Contacts cache direct body references, so
cloneWorldmust 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 -> 51 checks green
node tools/verifyAngryBirds.js --seeds=40 -> slower monkey soak, still green
Wave 1 — playable core — ⬜ NEXT
- Materials: wood / stone / ice with distinct density, friction, restitution, hp, damage threshold
- Damage as a health model — impulse above threshold subtracts hp; 2/3 and 1/3 crossings emit crack stages
- Pigs take damage from debris, not just direct hits
- Slingshot: drag, clamp to max draw, release
- Win/lose evaluated only once the world is settled or
MAX_SHOT_TIMEexpires stepSim(state, dt) → events[]contract- Phaser scene with sim-space→screen-space mapping, camera pan/zoom
- Trajectory memory (dotted trail of previous shots)
Wave 2 — the full roster, TNT, scoring — ⬜
- 8 birds, abilities fire on tap mid-flight
- TNT blocks detonate when damaged
- Scoring: 5,000/pig + material destruction points + 10,000 per unused bird
Wave 3 — editor and generator — ⬜
?angrybirds-editor=1entrance, authoring in play coordinates- Settle button so levels export already at rest
- Test Winnable button running the same gate as the verifier
AngryBirdsAutobeam search over (angle, power, ability timing)- Blob export of
level-NNN.json+ regeneratedlevels.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 inab-best-N/ab-stars-N - Match history — use
{ slug, score, opponentScores, result }. ⚠️ Goo Tower gets this wrong (GooTowerGame.js:879-881); copyRushHourGame.js:513-516instead. tutorial.md+hasTutorial: true- Paint
game-icons.pngframe 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.