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

13 KiB
Raw Blame History

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.js114 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

  • 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 / hashWorld for determinism tests
  • 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

  • Materials: wood / stone / ice with distinct density, friction, restitution, hp, damage threshold
  • Damage as a health model — impact 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_TIME expires
  • stepSim(state, dt) → events[] contract
  • Phaser scene with sim-space→screen-space mapping
  • Trajectory memory (dotted trail of previous shots)
  • Level select, progress persistence, win/lose panel
  • 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)

  • 8 birds, abilities fire on tap mid-flight, one use per shot
  • TNT blocks detonate when damaged (removed before detonating, so they can't recurse into themselves)
  • 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.