BUILDLOG — the 20-genre Build-Measure-Learn campaign

hayao.js v0.1 proved its thesis at N=1 (Sokoban — the friendliest possible genre). This campaign stress-tests the engine across the 20 most popular 2D indie genres, one game per genre, each a small blend of beloved indie titles. Every build must challenge genre-typical engineering (perf, collision, pathfinding, lighting…) and genre-typical design (game feel, pacing, readability, addictiveness). The engine is upgraded after every game; lessons land here and in LESSONS.md.

The loop (per game)

  1. Spec — genre, title blend, 3–5 stress goals (engineering + design), and the fun target: what must feel good for a human.
  2. Build — engine gaps first (new src/ modules, tested), then the game under examples/<slug>/ per CONVENTIONS.md.
  3. Measurenpm run check + npm test + npm run verify green; a per-example verify.ts proving content (winnability / critical path / balance sim / perf budget — whatever the genre’s truth is); a headless screenshot for looks.
  4. Learn — a BUILDLOG entry below: what the engine lacked, what was upgraded, what convention changed, what transfers to other genres. Docs updated the moment a lesson lands (docs are prompts).

Roster

#GenreBlend inspirationPrimary stressStatus
1Grid puzzleSokobansolver proof, determinism✅ v0.1
2Precision platformerCeleste × TowerFalltilemap AABB collision; full platforming kit (coyote, buffer, peak gravity, corner correction, dash, lift momentum)
3MetroidvaniaHollow Knight-liteworld graph, ability gating, reachability proof
4Top-down action-adventureZelda × Hyper Light Driftercombat feel/juice kit, rooms, enemy AI
5StealthMark of the Ninja × Gunpointvision cones, noise propagation, guard FSM
6Twin-stick hordeNuclear Throne × Vampire Survivors100s of entities, spatial hash, upgrade economy
7Bullet hellTouhou × Jamestown1000+ bullets perf ceiling, pattern DSL
8Tower defenseKingdom Rush × Bloonspath following, wave balance sim, counter system
9RTS-lite(mass units)flow fields, 300–500 units, counters, HUD density
10Traditional roguelikeBrogue × Shattered Pixelprocgen + connectivity proof, FOV, turn scheduler
11Roguelike deckbuilderSlay the Spire-litecard DSL, balance bot, addictive loop
12Turn-based tacticsInto the Breach-litetelegraphed intents, push chains, fairness proof
13Match-3Puzzle Quest-ishcascade choreography vs deterministic sim
14Incremental/idleUniversal Paperclips-litebig numbers, offline time, pacing curves
15Farming/life simStardew-litecalendar clock, save/load, gentle pacing
16Survival horrorDarkwood-liteshadowcast lighting, spatial audio, dread pacing
17City/colony builderIslanders × Mini Motorwaysplacement scoring, growth sim, minimal viz
18RhythmNecroDancer-liteaudio clock vs sim clock determinism
19Physics arcadeBreakout roguelite / Pegglecontinuous collision, deterministic FP physics
20Top-down racingMicro Machines-litecar handling feel, racing-line AI
21Narrative decisionsReigns-litecontent DSL, long-arc balance sim
22Physics demolitionAngry Birds × Crush the CastleRIGID-BODY DYNAMICS: stacks, joints, CCD, sleep, contact events
23Pinballbrass-parlor tablekinematic motor paddles, extreme-speed CCD in tight geometry, no-dead-pocket proof
24Slide-and-merge puzzle2048 × stone blocksreduce an endless-arcade genre to a solver-provable finite puzzle; merge-run resolution across partitions

Order of battle: waves grouped by shared engine needs — movement/collision (2–5), mass/perf (6–9), turn/UI (10–14), atmosphere/sim (15–21). Order may be re-shuffled as lessons emerge.

Entries

24 · Emberfold — slide-and-merge puzzle (2048 × stone blocks) ✅

The gap it closes. 2048 is one of the most-cloned 2D games there is, but it resists this repo’s core claim: endless random-spawn arcade play can’t be machine-proven winnable. Emberfold takes the merge, drops the parts that fight proof, and adds a twist that puts them back as difficulty: a finite, dealt board with no spawns — slide the whole grid, equal embers fuse to their double — plus immovable stone blocks that partition every row and column into independent merge-runs. The result is a pure Puzzle<Grid, Move> the BFS solver bites on exactly like Sokoban: 40 boards, every one proven fusable to its target, the minimum-slide par proven honest, the curve proven to ramp 2→11.

No engine gap — it’s a reduction. Zero src/ changes. The whole game rides existing primitives: composeCampaign + generateLevels deal candidate boards (stones, then a 4, then the 2s) from a seeded Rng and keep only the in-band, solver-proven ones; the campaign ships as LevelRecord[] (seed + deal recipe + proven depth), re-derived identically everywhere and asserted equal to a fresh compose so it can’t drift. Art is code-as-art: embers are warm gradient tiles that glow hotter as they climb the heat ramp; stones are cold slate; the forge breathes a radial glow — all cosmetic, so only the dealt grid enters world.hash(). Covers all six proof channels (100% eval coverage).

What the probe taught. The solver counts slides, not merges — and one slide can cascade many fusions, so depths sit LOW (2–11) and cluster tight. Two knobs actually move difficulty: material (embers·2 + fours·4 must exceed the target with slack, or the board is unsolvable — an early A4 config dealt heat 28 for a target of 32 and every candidate was correctly rejected) and stones + board size (partitions force feeding runs in the right order → deeper search). Lesson, same as Lanternfold’s: set generator bands from the measured depth distribution, and let the generator loudly fail an impossible band rather than ship a thin act.

What transfers. The move that made it provable is the general one: to fit an “endless” or randomness-driven arcade genre into the solver’s frame, strip the nondeterminism and re-add the challenge as spatial structure. No-spawn 2048 is to 2048 what a Sokoban level is to a warehouse — the same verb, made finite and fair. The stone-as-partition trick (resolve each maximal non-stone run independently, then stitch) is a clean pattern for any grid-slide mechanic that wants obstacles.

B2 · Gravewell — benchmark reproduction of Black Hole Square (js13k 2021, #9) ✅

Second rung of the benchmark ladder. Target: a 6×6 tap-puzzle — X squares vanish, neutron stars collapse into black holes, arrows slide contiguous runs of debris, holes swallow whatever slides in; every level has a hard tap budget (par).

Shipped: pure tap rules (exact extraction from the original’s shiftPieces, including the subtlety that blocked pushes don’t consume budget), 5 original levels teaching tap → collapse → sweep → gap-cost → collapse-then-sweep combo finale, keyboard-native tap model (cursor + confirm; cursor position is canonical state), three distinct fail states surfaced (out of taps / not clean / stuck), piece-taxonomy view with on-screen legend. Verified: every level solver-proven within budget (3→3→3→5→6, finale deepest) AND proven UNsolvable in par−1; full campaign replayed through the cursor model with 0 taps to spare on every level; deterministic + golden; finale feel probes (monotone cleanup, ≥1 positioning tap, last tap lands at 0); filmstrip + taxonomy still judged readable.

Fidelity score (rubric in the retired BENCHMARK doc): mechanics 10/10 M-checks green · content parity: teaching arc ✓, volume 5 vs original’s ~40 puzzles (arc ✓, volume ✗) · feel/look ✓ · learning yield: no engine gap, two findings below.

Findings:

CO-OP · Kinfall — 2-player local co-op survival (surviv.io duo) ✅

The engine already had player-namespaced input (p1:* / p2:*, from Fernclash) and the horde stack (SpatialHash broad phase + quadratic spawn ramp, from Emberwake). Kinfall composes them into the first cooperative shared-world game: two players on ONE keyboard drive ONE world.state.kin, surviving a closing storm ring together. Controls are exactly as briefed — P1 on WASD with «,»/«y», P2 on the arrows with «.»/«-» — mapped straight into the hot-seat input map, zero engine changes.

Shipped: a 100-second night on a walled field; a stormcircle that shrinks through keyframed holds and bleeds anyone caught outside (the surviv gas); six loot crates that raise a player’s weapon tier (pistol → SMG → scatter → rifle); an auto-aiming gun so 8-way movement can still fight; a quadratic horde of swarmers + brutes chasing the nearest STANDING player; and the co-op soul — a down / bleed-out / hold-to-revive loop. Extraction (win) needs one survivor at t=100; a full team wipe (both dead) loses. Verified: the co-op pair-bot holds the ring to extraction on 6/6 seeds while an idle pair is wiped by ~39s (the skill delta); the storm proven load-bearing (a rim-camper is knocked out in 1.5s — no safe camp); revive proven BOTH ways (a partner in range hauls you to 5 hp; unattended you bleed out and die); deterministic + golden; feel probes (first kill after the loot grace, storm monotonic, kill-lull bounded, final ring peaks ≥ 40 enemies); layout + control-hint lints clean; salience feel-gate green.

Findings:

NET · Fernclash — deterministic multiplayer (lockstep + rollback netcode) ✅

The engine was always a lockstep core (step(inputs) pure, seeded rng, hash()); this build added the missing transport and proved it with a 2-player game. New src/net/: player-namespaced inputs (p1:left — merged frames are ordinary frames, the whole verify harness works on multiplayer unchanged), bus transports (BroadcastChannel tabs / zero-dep WebSocket relay in scripts/relay.mjs / deterministic LoopbackHub with latency+loss for tests), LockstepSession (input delay, redundancy windows, stall-not-desync), RollbackSession (snapshot-ring predict/rollback on the proven snapshot()/restore()), room layer (seed+roster handshake, mid-game join via snapshot shipped at an agreed future frame, leave cutoffs), periodic hash() exchange that freezes on desync and dumps a replayable input log.

Fernclash (examples/fernclash/): sumo duel on a fern ring — the same game runs hot-seat (pre-namespaced input map) and true netplay with zero branches in game code. Verified: bot duel won 3–0; golden replay; deterministic; two lockstep peers finish a full match over a lossy, laggy loopback and agree bit-for-bit; real-socket relay covered in src/net/relay.test.ts.

Findings:

B1 · Seamfold — benchmark reproduction of Edge Not Found (js13k 2020, #2) ✅

First rung of the benchmark ladder: reproduce a human-ranked game under the house discipline. Target: Sokoban on a twisted torus — no outside, seams that shift you along the other axis (xOff/yOff).

Shipped: pure twisted-torus rules (fixpoint wrap resolution — the original ships a commented known bug in its x→y→x ordering), 4 original levels teaching torus → yOff twist → xOff twist → both, 3×3 ghost-copy tiling honoring the twist (all cosmetic), undo/restart, win/next-level loop. Verified: every level solver-proven winnable (8→6→5→13 moves, finale deepest) AND proven UNsolvable in a no-wrap variant — the seam is machine-proven load-bearing; full campaign replayed through the scene view; deterministic + golden; timeline probes show exactly one visible box seam-jump in the level-1 solve; filmstrip + stills judged readable.

Fidelity score (rubric in the retired BENCHMARK doc): mechanics 8/8 M-checks green · content parity: teaching arc matches, volume 4 vs the original’s 20+ levels (arc ✓, volume ✗) · feel/look ✓ (3 timeline metrics + judged artifacts) · learning yield: no engine gap (see below) + one new lesson + one friction fix.

Findings:

Playtest wave 1 — the unmeasured layer (first human contact)

The first human playtest reported five defects; every one instantly read as “no human reviewed this”, and every one shares a root cause: it lived in the layer the verification philosophy exempted from measurement. “The cosmetic layer can be deleted without changing the game” had been treated as license to never verify it. Bots read probes, not pixels; they know the controls a priori and what every entity is — so buried HUDs, kissing labels, bracket soup, missing onboarding, indistinguishable pickups, unearned story payoffs, and edge-hiding cheese were invisible BY CONSTRUCTION.

Reported → root cause → systemic fix:

The meta-lesson joins the synthesis laws as #8: verify the human-contact layer with the same machinery — the display list is data, onboarding is a checkable contract, and “would a stranger understand this screen in 30 seconds” is a test, not a vibe. Remaining human-only judgement (taste, tone) routes through the shots/ artifacts: emitting key screens per verify run makes “a human looked at it” a pipeline step instead of an accident.

Campaign synthesis (all 20 genres complete)

The portfolio’s cross-genre laws, earned the hard way:

  1. Every genre has a mechanical truth, and it is provable. Puzzles have solvers; movement has bots; economies have pacing windows; counter systems have duels; procgen has connectivity; stealth has both-ways affordance proofs; rhythm has frame-exact windows; narrative has content lints. The verify suite IS the design document.
  2. Skill-delta proofs are the closest thing to a fun proof: drafting beats skipping (17/20 vs 9/20), greedy beats random (158 vs 82), braking beats flooring (26.2s vs 27.7s), judgement beats recklessness (19/20 vs 0/20), counters beat spam. If a null strategy competes with intended play, the game is broken — assert the delta.
  3. Derive constraints, don’t vibe them: movement envelopes before levels, season length ≥ growDays, turn radius vs corner radius, fuel arithmetic vs night length, spike damage vs block ceilings. Every “feels wrong” traced to a violated inequality.
  4. Null-strategy baselines are the cheapest scenario test: the undefended lane, the do-nothing tactics turn, the never-draft climb, the camping keeper. A threat that a null strategy survives isn’t a threat.
  5. Grace is a system, not polish: coyote/buffers, i-frames, hit-stop input buffering, wound-before-death, mercy clears, phase-transition clears — the same shape at every timescale, and each is unit-testable.
  6. The observer split held everywhere: cosmetic views (pooled sprites, spring choreography, particle bursts, spatial audio) were deleted-without- diff throughout; the instant-sim/animated-view split (Glimmerfall) is its purest form. The beat being sim time (Cadence) is its deepest consequence.
  7. Pure-data state pays compound interest: structuredClone-and-score powered tactics options, Peggle aim search, and deckbuilder pilots; hashing and goldens pinned 20 games; every sim doubles as its own planning model.

2 · Shard Ascent — precision platformer (Celeste × TowerFall) ✅

Shipped: 6 teaching-ramp levels (run/jump → coyote gaps → drop-through + shard fetch → apex dash → spike rhythm → moving-lift finale). Every level bot-proven beatable with 0 deaths; full-run determinism verified; spike lethality verified; 6.4 kB gzipped. Completion telemetry: 3.1s / 3.0s / 5.2s / 2.8s / 3.1s / 8.0s — the ramp shape falls out of the verify labels for free.

Game phase findings:

3 · Sproutveil — metroidvania (Hollow Knight-lite) ✅

Shipped: four connected rooms (Atrium / Shaft / Roots / Crown), two ability pickups (double-jump seed, dash boots), spike hazards, ability-gated progression to the Heart. Bot-proven full run in 27.1s with 0 deaths; both gates proven REAL; snapshot save/load round-trips; deterministic.

Findings:

4 · Gleamvale — top-down action-adventure (Zelda × Hyper Light Drifter) ✅

Shipped: four rooms, sword-arc combat with hit-stop/knockback/i-frames, three telegraphed enemy types (chaser, darter, sentry), key-locked vault, heart-container win. Combat bot wins in 31.6s, 0 deaths, floor of 2/3 hearts; door gate proven both ways; enemies containment-checked every frame; deterministic.

Findings:

5 · Veilstep — stealth (Mark of the Ninja × Gunpoint) ✅

Shipped: single-level heist across three patrol bands: vision cones with raycast LOS, a fill/drain detection meter, bush concealment, sprint noise that pulls guards to investigate, alarm-reset. Heist bot: idol stolen and exfiltrated in 33.3s, 0 alarms. All stealth affordances proven both ways (exposure punished in 1.3s; a bush inside a patrol lane conceals through a full loop; noise flips guards to investigate). Deterministic.

Findings:

6 · Emberwake — twin-stick horde survival (Nuclear Throne × Vampire Survivors) ✅

Shipped: 120-second survival night: auto-aim fire, quadratic spawn ramp, two enemy types with soft-separation flocking, kill-driven level-ups with pick-1-of-3 builds (sim pauses on choice, picks are input actions). Orbit bot survives with hp floor 5/8, peak horde 161, 569 kills; sim step averages 0.02ms at peak (100× under the 2ms budget); deterministic.

Findings:

7 · Duskveil — bullet hell (Touhou × Jamestown) ✅

Shipped: three-phase boss with a declarative pattern DSL (ring / fan / rain, spin + arc + cadence params), 5px hitbox, focus mode, grazing, mercy-clears on death and phase transitions. Dodge bot clears the full fight deathless in 143.6s at 487 peak live bullets; sim rounds to 0.00ms/step; deterministic.

Findings:

8 · Rootward — tower defense (Kingdom Rush × Bloons) ✅

Shipped: S-curve lane with waypoint interpolation, 12 build pads, three counter-typed towers (arrow / frost aura / splash cannon), ten composed waves with a bounty economy, cursor-driven building via input actions. The scripted mixed build survives 10/10 waves at 9 lives; an arrow-only build of a larger budget falls to the tank waves; the bare lane falls on wave 2; the pressure curve ramps with breathers; deterministic.

Findings:

9 · Bramblefall — RTS-lite (mass units + counters + HUD) ✅

Shipped: keep-vs-keep skirmish: BFS flow-field pathfinding (cached per goal tile), 260+ units steering with hash separation, spear→cavalry→archer counter triangle, per-unit order targets, reinforcement trickle, a pulsing enemy commander, cursor-command HUD with per-type army counts. Verified: every counter edge wins its 40v40 duel to extinction; the commander bot (turtle → counterpush) razes the keep in 82s with 120 standing; a walled-off unit routes around brambles in 3.7s; 0.80ms/step at peak; deterministic.

Findings:

10 · Hollowdeep — traditional roguelike (Brogue × Shattered Pixel) ✅

Shipped: three procgen floors (rooms + L-corridors), raycast FOV with lit/explored/unknown memory, turn scheduler (shades act every other turn), bump combat, potions + a blade upgrade, descent to the Pale Amulet. Verified: connectivity across 50 seeds × 3 floors (stairs + ALL loot reachable), seeded layouts reproduce exactly, a full-knowledge explorer bot wins seed 1 and 10/10 random seeds, turn log replays deterministically.

Findings:

11 · Thornspire — roguelike deckbuilder (Slay the Spire-lite) ✅

Shipped: data-driven card DSL (8 cards: dmg/hits/block/draw/vulnerable), 3-energy turns with reshuffling piles, deterministic enemy intent scripts (attack/block/charge-doubles-next), pick-1-of-3 drafts after every fight, an 8-node climb (fights, rests, an elite, the Spire Heart). Verified: a greedy pilot wins 17/20 seeds (target 11–19); a never-draft pilot wins 9/20 — progression proven; 30 turns of intents audited as exactly honest; seed-1 climb pinned as a golden replay hash.

Findings:

12 · Vantage — turn-based tactics (Into the Breach-lite) ✅

Shipped: 8×8 grid, three mechs (melee push / lobbing artillery / ranger), bugs with directional telegraphs resolved exactly as shown, push mechanics with bump damage and chain redirects, greenhouse protection over five turns, scripted spawns. Verified: a 1-ply greedy defender achieves a PERFECT defence; push-redirect, rim-bump and unit-bump each proven in isolation; a do-nothing defence loses everything (threat real); golden end-state.

Findings:

13 · Glimmerfall — match-3 (Puzzle Quest-ish) ✅

Shipped: 8×8 six-color board, grab-and-swap input, line matches, gravity, rng refills, cascade combos (×combo scoring), dead-board reshuffles, a 22-move / 1300-light goal. Verified: 100 fresh boards fair (no pre-matches, always a move), the resolve script accounts for every point, a greedy matcher hits the target on 13/20 seeds, scripted session golden-pinned.

Findings:

15 · Fernrow — farming/life sim (Stardew-lite) ✅

Shipped: a 16-day year over four seasons, energy-budgeted days (24 actions), till/plant/water/harvest on a 10×6 farm, three season-locked crops, overnight growth, unripe-crops-wither on season change, a 700-coin festival goal. Verified: a diligent bot wins on day 12 (30 harvests); no-water-no- growth; wither honesty; the energy bound; reinvestment compounds (740 vs 236 coins); golden year replay.

Findings:

16 · Palewood — survival horror (Darkwood-lite) ✅

Shipped: one 90-second night: a raycast-shadowed lantern (radius + LOS — trees cast real darkness), fuel that drains and cans that force fetch runs, Pales that stalk the dark and flinch from light, wound/grace grabs, panned + distance-attenuated growls (new engine audio.spatial, StereoPanner), a dread heartbeat that quickens. Verified: the keeper survives to dawn burning all 5 cans; fuel arithmetic proves camping impossible AND the night winnable; light-repels and darkness-kills each proven; deterministic + golden.

Findings:

17 · Tarnholm — city/colony builder (Islanders × Mini Motorways) ✅

Shipped: procgen island (water rim, forest patches, grassy heart), an 18-building queue, adjacency scoring (huts cluster, farms want open grass, sawmills forest, docks water, temples love huts and hate industry), live score preview under the cursor, a 150-renown target. Verified: 50 islands always fit the queue; greedy placement wins 20/20 at avg 158 (tight); greedy nearly doubles random placement (158 vs 82 — skill is real); scoring honesty audited; deterministic + golden.

Findings:

18 · Cadence Hollow — rhythm (Crypt of the NecroDancer-lite) ✅

Shipped: a beat-locked dungeon chamber: 120 BPM = exactly 30 fixed frames per beat, moves legal only inside a ±4-frame window (one per beat), foes act in lockstep on beat ticks, combos build on-beat and shatter off-beat, floor and metronome pulse read straight from the frame counter. Verified: a beat-perfect dancer clears the chamber; the window honest TO THE FRAME (+4 in, +5 out); one-action-per-beat; foes provably frozen between beats; the whole dance replays hash-identically.

Findings:

19 · Pinshine — physics arcade (Breakout roguelite / Peggle) ✅

Shipped: a Peggle-ish board: aim fan + trajectory preview, gravity flight, SWEPT circle-vs-circle collision (closed-form time-of-impact per substep, up to 3 impacts resolved per substep for corner rattles), restitution bounces, a patrolling refund bucket, 10 orange goals in 8 balls. Verified: an aim-searching sharpshooter (49 candidate aims simulated per shot on cloned states) clears the board in 7 balls; a 24,000px/s ball cannot tunnel and a 1px graze correctly misses; bounces never add energy; golden replay.

Findings:

20 · Vellgrove Rally — top-down racing (Micro Machines-lite) ✅

Shipped: whole-track fixed camera, arcade handling (thrust, hard lateral grip, high-speed UNDERSTEER, grass drag), a 12-waypoint circuit with ordered-checkpoint laps, two racing-line rivals (seek-ahead + brake-for-bend), countdown start, live positions. Verified: the line finishes 3 laps in 26.5s; braking beats flat-out (26.2 vs 27.7 — cornering is a real skill); infield cutting advances nothing; grass caps speed at 17%% of tarmac; the player-bot wins P1 through the input layer; golden grand prix.

Findings:

21 · Emberreign — narrative decisions (Reigns-lite) ✅

Shipped: four meters between two ditches, a 15-card data-driven deck (every choice double-edged), flag-chained story arcs (the plot you pay to learn of vs the one that springs), eight themed dooms, survive-12-years victory. Verified: a balanced regent survives 19/20 reigns; always-left wins 0/20 (avg 17 seasons) — judgement is the game; content lints clean (unique ids, bounded effects, all arc flags settable); every doom fires its OWN ending; the plot arc terminates both ways; deterministic + golden.

Findings:

Shipped: 5-tier exponential economy (lantern → dawn engine), forge clicking, unlock-by-lifetime-total reveals, DOM shop sidebar, pulsing-forge SVG view with orbiting fireflies. Balance sim–verified 10-minute arc: first buys at 3s / 71s / 236s / 402s / 607s, era gaps 69→164→166→205s.

Findings:

1 · Sokoban (v0.1 baseline)

Proved: pure Puzzle module + BFS solver + assertDeterministic + scripted playthrough. Weakness identified: everything about this engine was only ever exercised by a discrete grid puzzle. Hence this campaign.

22 · Rookspire — physics demolition (Angry Birds × Crush the Castle) ✅

Engine first: this game forced hayao’s biggest engine upgrade — a full deterministic 2D RIGID-BODY module (src/physics/rigid*.ts, exported via @hayao): circle/convex-poly bodies, SAT narrowphase with clipped 2-point manifolds, warm-started sequential impulses (restitution, Coulomb friction), SPLIT-IMPULSE penetration recovery (pseudo velocities — zero kinetic energy injected), distance + revolute joints with motors and limits, swept-circle bullet CCD, island-atomic sleeping, contact events with impulse magnitudes, ray/point queries. The whole world is PLAIN JSON — it lives in world.state and inherits hash/snapshot/replay/structuredClone-bots for free.

Shipped: three castles (tower / twin rooks + lintel / walled keep with a rope-hung idol), aim-arc slingshot, materials (wood/stone), idol destruction by contact impulse or earth-touch, rubble that stays. Verified: an aim-searching siege bot with a structural-displacement gradient proves every castle falls within its stone budget; a full-power flat shot proven to strike (CCD); settled castles proven to SLEEP (0 awake → 0.03ms/step); golden replay; feel probes (impact wakes the pile, dust settles in 4.3s).

Solver-class findings (each found by a failing gate, none by eyeballs):

23 · Brasswick — pinball (brass-parlor table) ✅

Shipped: felt-and-brass table, three bumper-bells with impulse kicks and a jackpot relight, kinematic flipper blades driven about their pivots, drain sensor, three deterministic serves, first-to-2000. Verified: a pulse-flip bot wins in 13s (peak ball speed 2445px/s); the ball proven table-bound for the whole game (CCD in tight geometry, 0 escaped frames); a searched flip proven to return a blade-rolling ball to the bumper field; every unflipped serve proven to drain (no dead pockets); golden metronome rally; feel probes (first bell 0.4s, ball lives 5.2s, max lull 4.7s).

Findings:

E1 · Engine primitives — FSM, weighted tables, graph search (js13k-mined) ✅

Not a game — the “engine gaps first” step run standalone, filling the cleanest pure-logic wins from JS13K-MINING (rows #4/#5/#6, and an assessment of #17). One new module dir, src/logic/, three files, 19 tests, all deterministic and hash-safe (no cosmetic concerns — pure logic only).

Gaps filled:

Decision — ECS-lite behavior hooks (#17) deferred, not built. norman’s Behaviour{onUpdate,onCollision,onDamage} mixin earns its keep in a codebase without a scene graph; Hayao already has one (Node.onProcess + Signal/ EventBus for collision/damage), so a parallel per-entity hook bus would be a second, competing update path — exactly the “keep the node tree lean” trap the row flagged. The composition it offers is already expressible as small Nodes or world.state behavior tags iterated in onProcess. Revisit only if a real game hits friction the node tree can’t absorb; until then it’s redundant surface. (Row #17 marked evaluated → declined in JS13K-MINING.)

Findings:

E2 · Procgen generators + color engine + ambient particles (js13k-mined) ✅

Second “engine gaps first” batch — JS13K-MINING rows #7, #8, #14. Unlike E1 (pure logic only), this batch spans the whole determinism spectrum, and the discipline was drawing the line per output: logical structure is hash-relevant and runs off world.rng; decoration and view are cosmetic and stay out of the hash. New src/procgen/ dir (5 files), art/palette.ts + scene/particles.ts extensions, one core/math helper. 31 new tests, all green; headless SVG screenshot verified.

Gaps filled:

Decision — no simplex/Perlin lib. Per the mining doc’s anti-recommendation, value-noise (bilinear-smoothed integer-cell hash) + integer scatter cover every sampled game’s real need at a fraction of the size; a heavyweight simplex import would be cosmetic weight for no gameplay dependency. Declined on purpose.

Findings:

E3 · Presentation / game-feel primitives (js13k-mined) ✅

Third “engine gaps first” batch — JS13K-MINING rows #9, #11, #12, #16. All four are view/juice: transitions, spring-smoothed values, damage popups, and UI frames. The whole batch is cosmetic-by-construction — nothing here enters world.hash() — and every effect runs off the fixed clock dt, never a variable rAF delta. New ui/transition.ts, render/nineSlice.ts, scene/floatingText.ts, scene/tween.ts + core/dmath.ts extensions. 28 new tests, all green; a headless filmstrip renders the wipe + panel + pops + spring.

Gaps filled:

Decision — floating-text jitter uses a private Rng, not world.rng. The mining row said “jitter via world.rng”, but the stronger in-repo invariant (particles.ts: cosmetic juice carries its own stream so it “can be deleted without changing any game outcome”) wins. A cosmetic node drawing from world.rng would perturb the hashed RNG state, so toggling popups off would diverge the sim — the precise anti-pattern the separate-stream rule exists to prevent. FloatingText seeds its own Rng like Particles/Shaker.

Findings:

E4 · Persistence & content-engine primitives (js13k-mined) ✅

Fourth “engine gaps first” batch — JS13K-MINING rows #1 (save/load — the single most-frequent gap, ~12/17 games), #13 (undo / record-replay), #15 (data-driven content DSL). The unifying theme is plain-data over the existing snapshot seam: save/load, undo, and the wave director all build on world.snapshot()/restore() and store their cursor state as JSON, so nothing here reinvents serialization or escapes the hash. New persist/storage.ts

Gaps filled:

Findings:

E5 · Art-from-code primitives — procedural sprites, bitmap text, autotiling (js13k-mined) ✅

Fifth “engine gaps first” batch — JS13K-MINING rows #2 (procedural sprite/texture generation, 6 games), #3 (bitmap/pixel font + rich text layout, 6 games), #10 (autotiling — bitmask Wang + marching squares). The unifying theme is view over data: every module is a pure function to a DrawCommand[], and the three scene nodes it ships (TextureSprite, BitmapText, and the autotile emitters) are the only stateful surface — each cosmetic = true in its constructor, so decoded pixels / rendered glyphs / tile art never enter world.hash(). New art/texture.ts, art/font5.ts, art/bitmapFont.ts, art/autotile.ts. 21 new tests, all green; npm run verify unchanged (no golden drift); headless SVG proof composes all three.

Gaps filled:

Findings:

E6 · Camera decoupled from the screen — Camera2D follow, scroll, screenToWorld

Sixth “engine gaps first” batch, prompted by a direct question: can the camera be decoupled from the screen for scrolling and zooming? It already could — the render root applies World.viewTransform() (the inverse of the active Camera2D, centred in design space and scaled by zoom) to the whole display list, so moving a Camera2D node scrolls the world and setting its zoom scales it. What was missing was the ergonomic + proof layer: nothing drove the camera, nothing converted screen→world, and no example exercised a world larger than the viewport. Filled that. New scene/cameraController.ts and two World methods; 10 new engine tests + a scrolling-camera demo. Full portfolio verify unchanged (no golden drift on the other 26 games).

Gaps filled:

What transfers:

E7 · Atmospheric render — gradient/glow paint + parallax depth (atmospheric demo) ✅

What the engine lacked. The Paint vocabulary was flat colour strings only: fill/stroke hex, nothing else. That is the ceiling between “flat woodblock” and luminous — no way to render a dawn sky, a lantern’s glow, water that reflects light, or fog. The prior games are all flat-fill; the art toolkit (shapes/texture/autotile) shapes silhouettes but can’t light them.

What was upgraded (all reusable, folded into src/render).

Convention / traps this cost. SVG url(#id) references are DOCUMENT-global, but the filmstrip composites N frames into ONE document — so gradient/shadow def ids MUST be salted per render. commandsToSVGInner(cmds, idPrefix) now takes a prefix; renderFilmstrip passes p{panel}. Without it, panel 3’s fill resolves to panel 0’s gradient. Two more: (1) the Sprite constructor hand-copies paint fields and silently dropped the new gradient/shadow until added — new Paint keys need a line there; (2) serializeProps must return a COPY of any mutable array (gathered.slice()) — returning the live reference lets a later mutation corrupt an already-taken snapshot, surfacing only as a snapshot-restore hash divergence, never in normal play.

What transfers. Every game gains gradients + glow for free — skies, water, health bars, lit projectiles, rim-light. Because paint is cosmetic data on cosmetic nodes it never enters world.hash(): the atmospheric demo recolours the sky every frame and the golden replay hash is unchanged. The “looks judge sees only a static SVG” gate still binds — all richness is static per frame (the day arc is a function of downstream progress, not wall-clock), so the filmstrip reads true.

Driver game — Driftlight (ambient art-runner). A paper lantern auto-drifts a night river to the dawn sea; steer across-stream (↑/↓) to thread rock gates and gather firefly-light that feeds a draining flame (win: reach the sea lit; lose: flame out). The course is a deterministic chain of gates with rocks derived around each gap and lights strung on the safe line, so tracking the path both survives and refuels — the winnability proof. Flame tuned to a nervous sawtooth (a perfect line dips to 44%, ends 81%); a human who misses lights dies. Sim state (px/py/flame/gathered/won/lost) is hashed; sky/water/glow/parallax are cosmetic. 5 tests + full verify green, golden pinned.


Wave — the content-volume unlock: solver-backed generation + a generated flagship

The campaign proved hayao could make one verified level reliably; the honest gap was volume — an agent hand-authoring forty balanced rooms is where quality falls apart. This wave closes it by making generation first-class, then proving it with a flagship built entirely from generated content.

Loop 0 — generate, don’t hand-author. Three engine modules:

Loop 2 — the flagship, Lanternfold. A Lights-Out lantern puzzle chosen because its taps are self-inverse, so any scramble from solved is guaranteed winnable and the solver’s minimum-tap count is a clean difficulty metric. 42 boards across four acts, generated + solver-proven, composed into a ramp that climbs 2→8 taps, shipped as levels.ts (verify asserts the committed data equals a fresh compose, so it can’t drift). Art is code-as-art: radial-gradient lantern glow on a dusk-sky gradient — never a placeholder square. Covers all six proof channels.

Depth ceiling, learned by probing. Plus-stencil Lights-Out saturates low (3×3/4×4 → min-depth ~7); a satisfying finale needed board-shape tuning (5×3 reaches 8 with cheap BFS) and monotone per-act bands. Lesson: set generator bands from the measured solution-depth distribution — the generator loudly fails an impossible band, which is the right failure.

Loop 1 — distribution for agents. create-hayao (npm create hayao) scaffolds a runnable project whose starter already generates a proven campaign; scripts/eval.ts (npm run eval) scores every game on the six proof channels + verified rate — the AI-first KPI. llms.txt / AGENTS.md now foreground “generate + prove”; docs/GALLERY.md frames the portfolio as proof-forward, not faith-based.

F1 · Kintsugi — the flagship metroidvania ✅

The gap it closes. Every prior example is a slice — one capability, one screen, sixty seconds. The roadmap’s honest admission was “an Ori-scale metroidvania is not yet in the box.” Kintsugi puts it in the box: a hand-authored, ability-gated, 30-room / 5-biome metroidvania with combat, four multi-phase bosses, adaptive music, a plotted arc, and a map — machine-proven completable and softlock-free.

The one engine gap, and why it’s the whole point. The metroidvania spine was proven in miniature already (sproutveil’s negative gate proofs; gleamvale’s combat with zero engine changes). The missing piece was a verifiable world model — and it collapses onto an existing primitive: progression IS a Puzzle. State = (region, pickups taken); a move is “traverse a gated edge” or “collect a pickup”. So the engine’s solve() BFS proves a valid 100% seam order EXISTS, and — because abilities are monotonic — enumerating the reachable state graph proves NO SOFTLOCK (the only remaining hazard once one-way drops enter). Shipped as src/content/worldgraph.ts (proveCompletable/proveFullCompletion/ findSoftlocks/reachableRegions/validateWorld) and merged first, on its own.

Geometry proven to honour the graph. Rooms are authored as data (a spec → 40×22 ASCII); a test asserts every room exit corresponds to a real graph edge and that seams are reciprocal (one-way drops exempted). The graph is the contract; the geometry can’t silently contradict it.

What “publishable” took, layer by layer. The Mender (procedural articulated figure, 7 poses, gold-seam repair motif). Combat (swept hitbox, hit-stop, i-frames, FSM enemies) — reused from the proven kit. Four guardians (one multi- phase FSM escalating by HP, sealing its arena until slain). A per-biome adaptive MusicDirector (key/mode/mood + threat layers) built on the audio bus so it no-ops headlessly. Story beats (prologue, area cards, ability & guardian lines). A fog-of-war map from the same graph. A looks pass against design/JUDGE.md — biome-specific midground (grove trees / cistern dripstone / ember forge-chimneys / sky clouds / heart roots) + ambient motes to kill the empty-void look.

Verified. Winnability is proven the metroidvania way (the graph solver, a stronger guarantee than any single bot run) + a deterministic tutorial romp with a pinned golden + first-screen layout-lint/control-hints + an opening filmstrip. Everything visual is cosmetic (world.state.kg is the only hashed truth). ~36 example tests; whole portfolio green.

What transfers. The lesson that carried the whole build: a flagship is a scaling problem, not an invention problem, once the verifiable spine exists — and the spine is a reduction to a primitive you already have. Progression graphs, lock-and-key dungeons, tech trees, and quest chains are all the same Puzzle.

This page rendersthe repo's markdowndirectly — edit it there, it changes here.