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

10 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

Wave 0 complete 2026-07-30 — the gate is passed. node tools/verifyAngryBirds.js51 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 / 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              -> 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_TIME expires
  • 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=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.