This file spent a year making the catalogue narrower, and it did it while every individual entry in it was true. If you are about to propose something, read this section before the gap lists, because the gap lists are not where the pressure comes from.
The drift, measured on 2026-09-09 (92 games), comparing the 16 most recent against the 76 before them:
| earlier 76 | last 16 | |
|---|---|---|
multiplayer tag |
18 (24%) | 0 |
aiPowered |
7 in the whole catalogue | 0 |
competitive tag |
19 | 1 |
card + board + strategy + trivia categories |
16 | 0 |
logic tag |
10 (13%) | 5 (31%) |
| median page size | 38–42 KB | 117 KB |
Every one of the last sixteen is singleplayer, offline, keeps no shared state, ships no written
content, and is about three times the historical size. The site’s own premise is that language
models wrote these, a Cloudflare proxy for calling one is already wired up and used by 27 tools,
and no game has called a model since death_by_ai. That is not a run of coincidences. It is this
file.
Four things in here caused it. They are worth naming because each one still looks reasonable on its own:
The “Findings worth reusing” essays below are the file’s real instructions. They are two thirds of the curated text, and all six narrate the same method: find a structural gap, run an exhaustive search, ship the table. Nobody wrote “build abstract combinatorial puzzles” anywhere. Six worked examples of one shape did it instead.
The quality bar is denominated in a currency only one genre can pay. Every essay earns its authority with a number from enumerating a combinatorial space — 614,656 rulebooks, 1,023 vocabularies, 2,400 candidate holes. A game about timing, feel, humour, or another person cannot produce that number, so under this file it can never look rigorous, and it gets discarded at the proposal stage before anyone weighs whether it would be good. Verification should fit the claim, not the file. If the idea rests on a generator being varied, generate hundreds. If it rests on a joke landing, the verification is that someone laughed. If it rests on a game feeling good in the hand, the verification is playing it and showing the screenshots. An idea is not weaker because its risk cannot be enumerated; it is weaker when nobody checked the thing it actually depends on.
“Tried and rejected” is the same direction three times. Air hockey, Mini Metro, Into the Breach — every rejection on the list is of something physical, thematic, or genre-shaped. Each was turned down on its own merits and those judgements stand. Read as a block, which is how anyone skimming reads them, they say “non-abstract has been tried and it did not work here.” That is not what they say individually and it is not true.
The one force that would have corrected this was explicitly disarmed. The line “an empty
category is evidence about the catalogue, not about whether anyone wants to play the thing that
would fill it” is correct as written, and it is also what made multiplayer going 24% → 0%
unnoticeable for a year. Both halves are true: an empty category is not on its own a reason to
build. A category that emptied out is a different fact, and it is worth looking at.
Most recently the drift produced a proposal for a language-decipherment deduction game — glyphs, procedural grammar, exhaustive identifiability checking — pitched as filling a gap. It was the seventh abstract deduction puzzle in a row and it was pitched without noticing that. Set aside for that reason rather than on its merits; the idea is not on the rejected list.
The catalogue’s variety does not live in the categories field. It lives in axes that no field
records, and the recent entries have collapsed to one end of every one of them. When proposing,
say where the idea sits on these. Landing at the same end as the last five entries is not
disqualifying, but it should be a choice.
firebase.js
pages stay as they are; nothing new joins them. Pass-and-play on one device is the whole of this
axis now, and it needs no backend. Broken 2026-09-09 by
games/alibi.html; before it, the last game with a second player was artillery (2026-02-12),
and every hot-seat game here — checkers, ludo, reversi, artillery — is a perfect-information
board game. Alibi is the first with information hidden between the two players.death_by_ai and mystery_ai are among the most
distinctive things here and there are seven of them, ever. The interesting version is a model as
a character or a judge — something to persuade, something that rules on an answer no lookup
table could — not a model as a content faucet.suika,
stack_tower and downhill_dreamer are excellent and are none of that.ascii_camera, online_webcam_filters, overtone); games almost
never do.Checked against the catalogue on 2026-09-09 (92 games, 216 tools) and picked to fit the constraints the rest of the collection works under — one file, no assets, reproducible from a seed. Anything here may have been built since; the point is the shape of the gap, not the specific suggestion. These are suggestions, not a queue, and an idea that is on none of these lists is not worse for it.
Games — mechanism gaps
games/procession.html.cribwork covers the workbench side, not the play.Games — the axes above
games/alibi.html — two suspects answering the same questions in secret on one phone —
and games/blobby_volley.html on the hot-seat side. Both are two-player. The shape still missing is
many hands on one keyboard: the named candidate is Achtung, die Kurve! (Zatacka, 1995), up to six
players on two keys each, where the mechanism that keeps it from gridlocking is the periodic gap in
every player’s trail. It needs an honest answer for touch — six thumbs do not fit on a phone — which
is the part to settle before building, not after.games/hummingbird.html (microphone). Camera and gyroscope are still open on
the games side — push_mine is the only other game that touches a sensor. The camera was attempted
once and rejected; read the Curator entry under “Tried and rejected” before proposing another, since
what killed it was the shape of the game rather than anything about the camera.games/blind_crest.html, the jigsaw by games/jigsaw.html, tenpin
bowling by games/fresh_oil.html, Pac-Man by games/scatter.html, the shooting gallery by
games/moorhuhn.html, and alongside them minigolf by games/nine_holes.html and pinball by
games/escapement.html. Bingo is still open, and the platformer only partly — the climbers are
built, a level-based side-scroller is not. The crossword is not open — see the cryptic
crossword under “Tried and rejected”.interactive_buddy and doodling have no win state and are among the
most replayed pages here. There has been nothing like them in a long time.
Filled by games/strata.html (a falling-sand world whose materials you discover).
interactive_buddy was remade on 2026-10-09 as a crash-test lab (a standing dummy, ten tools, a wall that keeps the
highest launch) with its own tested core. Three things in it are worth reusing. A verlet ragdoll’s joint stops must be
distances, not rotations: the first build limited elbows, knees and the neck by rotating the limb back to the stop, and
a head wedged between wall and floor made the two fight until the whole body left at 100 m/s (a glove punch measured 1 m
on most seeds and 28 m on one). Replacing every stop with “these two ends may come no closer than d”, where
d = sqrt(a^2 + b^2 + 2ab cos(limit)), made 12 seeds of every tool land within a metre of each other. A standing ragdoll
is only balanced on paper: it buckles at the knees within three seconds whatever the iteration count, so the rest pose
is simply asleep (velocities zeroed, physics skipped) until a kick, a grab or a blast wakes it - a heap of black joint
balls was what four critic rounds kept calling “unreadable”, and a dummy at attention under a lamp fixed it in one
frame. Pay by what a hit did, then tune the whole economy with a person-paced bot, not by eye: the first payout
table let a bot own every tool in 86 seconds; scaling payouts, the chain multiplier (x3 cap) and the height bonus until the
same bot needed 16 minutes took five runs of node test/interactive_buddy.mjs bots.Tools
@keyframes.tools/easing_editor.html — drag the curve, a speed graph under it, and a preview the
browser animates with the real timing functions.tools/contrast_checker.html — ratio, nearest same-hue passing colour, and a palette matrix.overtone, which handles pitch and explicitly does not handle this.
Filled by tools/pocket.html — offset, spread, drift and per-beat rushing through the microphone,
with its own latency calibration.gasp
decide whether it is readable at all. Open next to sorts.overtone is monophonic by design and reports the common subharmonic of a
chord rather than guessing it is hearing one.Technique notes from things that shipped. These are notes on how a specific problem was solved, not a template for what to build or a model of how a proposal should read. Six of them describe exhaustive search because six abstract puzzles were built in a row; that is a fact about the last year, not a standard. Skim for the one that touches your problem and ignore the rest.
Filled since: an automation game, by games/shopfloor.html — a factory on a fixed screen, and
the first entry here where anything is automated (the six incrementals grow numbers; none has a
layout). It was chosen by the gate below rather than from a gap list, and three things generalise.
Ask first whether a stupid local heuristic reaches the ceiling. Before any interface existed, a model of the floor said: draw one sensible layout 1.20, then fiddle with it keeping every change that helps 2.00, then search properly accepting setbacks 3.00 — 1.50x headroom above the fiddling player, against 1.10x for the draw-a-structure idea that this killed. What matters is not the ratio but why it is there: hill-climbing gets stuck because reaching the good layout means tearing up a working one and being worse for a while. Look for that shape — interacting choices where fixing one thing breaks another — and be suspicious of anything where local improvement converges. Two supporting numbers: adding a third stage makes rebuilding the floor worth 1.50x over bolting the new stage onto what is there, which is what “keeps developing” looks like as a measurement; and the near-optimal layouts found were distinct from each other, so it is craft rather than one lookup-able answer.
Fix the instrument before believing it. The first version of that model returned a hard zero whenever routing failed, so the search was climbing a landscape made of cliffs and reported a headroom of exactly 1.00x — a clean, confident, wrong kill. The second version’s layout generators could not place more than nine machines on a hundred-cell floor, so nothing better was reachable. Both bugs produced plausible negative results. A negative result from an instrument you have not tried to break is not a result, and the tell in both cases was a suspiciously round number.
A rule that the author keeps getting wrong is too fiddly to ship. The first connection rule was symmetric and tidy: a belt pointing into a machine feeds it, a belt pointing out carries its work away, a belt passing by does neither. Every hand-built test layout I wrote under it was broken — repeatedly, in the same way, a machine with no valid output stub sitting idle with no symptom. It also produced a genuine deadlock: the bench’s output belt ran into the press’s supply line, so one engine parked at the head of a belt the press would never accept and jammed it permanently and silently. The fix that solved both was to make belts refuse anything their line cannot use — follow the belt to where it actually ends and ask whether that wants the item — after which the forgiving rule (a machine uses any belt beside it) became safe, and the same layout went from 0.47 to 0.67 finished units a second with every machine busy. Two lessons: trace where a conveyance actually goes rather than trusting local geometry, and if the person who wrote the rule keeps violating it, the players will too. What made it findable at all was running the whole chain and reading the per-machine idle times: a roller idle 107 of 120 seconds while the press it fed was starved 60 seconds is a contradiction, and contradictions in instrumentation are where the bugs are.
Filled since: a bowling game, by games/fresh_oil.html — a sports sim where the opponent is
the lane surface rather than the other bowlers. Six things in it generalise.
Calibrate against published numbers, not against how it feels. Tenpin is unusually well measured, so before writing the game I collected the targets the sim had to hit: a house shot breaks at board 8–10 at 39–41 ft, the pocket is the ball’s centre on board 17½ at 4–6°, carry rises to about 85–90% at 6° and falls off well below that, and on a proper strike the ball touches only pins 1, 3, 5 and 9 — the other six fall by scatter. Then I fitted the free constants (μ on fresh oil and on dry boards, and the exponent linking oil volume to friction) to all four lane targets at once, and the nine deck constants to the carry curve. The result: breakpoint 8.1 at 41 ft, entry 17.8 at 4.3°, 9.7 boards of hook, carry 58/77/87% at 2/4/6°, and the 1-3-5-9 signature on 95–100% of pocket hits. That last one was never fitted for; it is what told me the deck was right.
Model the mechanism and the known behaviour falls out for free — which is the only real test of a physical model. Three things appeared that I did not put in. Track flare peaks at a 45° drilling layout and vanishes at 0° and 90°, which is the actual pro-shop rule, and it emerges only because the ball carries an inertia tensor with a differential rather than a scalar moment. Its magnitude lands where real balls land: 0.15 in for a 0.005 differential, 6.6 in for 0.055. And on a burned lane, the correction that brings a high ball back to the pocket is feet two boards left, target one — “two and one”, the oldest adjustment in bowling. A parallel move (feet and target together) does almost nothing: it took a ball from 22.6 back to 21.6 where two-and-one took it to 18.8. None of that is in the code as a rule. If a model only reproduces what you tuned it to reproduce, you have fitted a curve, not built a mechanism.
A contact solver will politely launch your pins into orbit, and each cause looks like the last one. Five separate bugs produced the same symptom — pins at 28 m/s, sliding kilometres, flying back up the lane. In order: the kickback normal was inverted so pins passed through the side walls; several spheres of one pin each delivered a full restitution impulse, so a pin was kicked two or three times per collision; restitution was re-applied on every step a contact persisted, worth ten kicks in one collision; friction was applied along +t for body A when the tangent was defined from (v_B − v_A), so it accelerated every sliding contact instead of opposing it; and a pin’s polar moment is tiny, so an off-axis impulse spun it to 1600 rpm, at which speed a sphere sweeps further in one step than the contact tolerance and the penetrations explode. The general lesson: restitution belongs to the moment two bodies meet, one contact per body pair, de-penetration must be positional rather than impulsive, and every friction sign should be checked by asking whether a body sliding on a static floor slows down.
Sweep the control the player actually holds, and check the answer moves smoothly with it. Each of the four inputs was swept against where the ball ends up: one board at the arrows is 1.5–3.8 boards at the pins, one board of feet about 1.8, a 0.3 m/s speed step about 2.5, and the hand moves it 5–7 boards a notch while taking the entry angle from 0.9° to 6.7°. All monotonic, none twitchy. The same sweep is what exposed the design’s real problem: execution noise was only sd 0.45 boards, so the misses were systematic, not random — the lane was moving faster than the adjustment granularity could follow, which is why the target now moves in half boards.
A strategy is only worth building if you can show what ignoring it costs. Two bots played whole nights through the shipped game loop: one that never moves, and one that reads the entry board and applies two-and-one. Never moving averages ~106; chasing the lane averages 176 and peaks at 200 with seven strikes. Seventy pins is the game. Getting that number honestly took three tries — the first bot corrected on its spare ball (so it almost never corrected at all), and the second had the feet term backwards, which made it worse than doing nothing. A negative result from a controller you have not checked the sign of is not a result, and the fix also became the hint the game gives the player.
Two measurement bugs that a screenshot cannot show. Entry angle read −1.8° on a strike, because
the reading was taken one step after the headpin had already deflected the ball; it has to come from
the last clean state before any contact, projected forward to the pin row. And an empty full-screen
modal overlay at 90% opacity sat over the canvas for the entire first day of work, so every render I
looked at was a ninth of its real brightness — I retuned the lighting three times against it before
reading the framebuffer with readPixels and finding the render had been correct all along. When
a picture and an instrument disagree, believe the instrument, and find out what is between them.
Filled since: a racing game, by games/blind_crest.html — the largest genre hole in the
catalogue (97 games and not one with steering, a track or a lap; the backlog had listed it unbuilt
for years). It is a rally stage in failing light with a co-driver reading pacenotes aloud, and five
things in it generalise.
A feasibility check built from a backward speed pass is a tautology, and it looks exactly like rigour. The pass defines a feasible speed profile, so asking whether the road obeys it always answers yes: the first version cheerfully passed a fifty-metre run-up into a nine-metre hairpin. I wrote the same bug twice, once in a Python prototype and once in the shipped generator, and only caught it by feeding it a road that was obviously impossible. Any validator worth having must be shown rejecting something before it is trusted. The check with content was a different question entirely — not can this corner be taken but can you be told in time: is there room to brake between the moment the note is called and the corner arriving.
Satisfy a fairness constraint by construction rather than by rejection. Once that check worked it threw away 83% of stages, which is not a filter but a bias: the survivors were all long-straight layouts. Choosing each straight from the corner that follows it — long enough for the note to land, no longer — took rejection to 0% and gave back the whole range of straight lengths. Short straights now pair with fast corners, which is also how a real road works.
The whole game turned on one ratio, and it was measurable before any of the art was. Braking demand across 22,000 corners came out at p50 54 m and p90 117 m. Sight distance above about 120 m therefore makes a co-driver decorative, and the first build had 468 m. Sweeping it: at 240/150/96/72 metres of sight an eyes-only bot lapped in 80.1/80.1/80.1/80.2 s — sight is free until it drops under roughly 72 m, and only bites hard under 54. The shipped stages run 60 m down to 30 m.
The A/B nearly died of a bug in the A/B. Two bots, identical in steering, braking and throttle,
differing only in information. The notes bot lost 48 stages out of 48 — because it combined the two
sources as min(seen, noted), which lets a note slow you down but never lets it keep you flat. A
note is for the corner you cannot see; within sight, the eyes win. With that one line corrected the
same comparison flipped to notes winning 35/48, mean 2.9 s, and going off 0.00 times per stage
against 0.79. A negative result from an instrument you have not tried to break is not a result.
A pseudo-3D road anchored to the segment boundary jolts, and it is invisible in screenshots.
The projection took its camera height from segs[floor(pos/SEG)] and restarted the curvature
accumulation at that same boundary. Both are discrete in a quantity the car crosses continuously,
so at six metres a segment the whole scene snapped about seven times a second at speed — and every
still frame looked perfect, which is why it survived a full look pass and shipped. Measuring it
needs motion: step the car forward in even 25 cm increments and diff consecutive rendered frames.
Before the fix the road 30 m ahead moved 119 px sideways in one 25 cm step, 640x the median, with
a jolt every 24 samples; after integrating from the car’s exact position (interpolated camera
height, and a first partial span of 1-frac segments) the same probe reports zero steps above
4x median across three stages. The general form: anything sampled per-segment while the camera
moves per-metre must be interpolated, and a still frame cannot show you the difference. The same
mistake in a second place made the roadside banks start five segments out, giving an angled edge
that slid away as you approached and read as walls appearing and vanishing; the fix is to run the
strip to the camera and clamp the exploding near vertices off-screen rather than dropping them.
A car that is moved sideways is not a car that is driven. The original model took the input as lateral acceleration: the road curved underneath and you slid across it, never pointing the car anywhere. It plays as lane-shifting and reads to a player as the game steering for them. Giving the car a heading of its own — steering turns the wheels, the car follows where it points, the road turns underneath at its own rate, all in road-relative coordinates — is maybe thirty lines and is the difference between a driving game and a dodging game. Two traps inside it:
Clamping the steering angle after applying it silently makes steering binary. Capping delta to
what grip allows looks equivalent to limiting the turn and is not: above about a tenth of lock,
every input at speed clamps to the same angle, so the whole upper range of the control does
nothing. Take the input as a fraction of available grip and convert that to an angle instead —
then half a thumb is always half a turn, at any speed.
Yaw rate is proportional to speed, which is true and unplayable. A car crawling at walking pace
genuinely cannot be pointed, so a driver who has slowed right down gets stuck facing the wrong way.
A small constant added to the speed term ((v+3)) costs nothing above 30 km/h and removes the trap.
Constant off-road drag larger than your acceleration is a permanent trap, and only a long run
finds it. The verge applied a flat 13.5 m/s² while full throttle from a standstill gives 10.4, so
a car that stopped on the grass could never move again — stranded, with a stage restart the only
escape. It survived every short test and only showed up as one seed in eight sitting at zero for
712 seconds in a long bot run; the tell in the logs was a run whose time was the step cap.
Make verge drag a rolling resistance (2.0 + 0.30v), and add a backstop that recovers anything
stationary off-road for a couple of seconds. Any “stuck” state a physics model can reach, a
player will reach.
Stability and the reason to listen are the same dial, so sweep them together. Damping the car’s heading deviation makes it far more forgiving — offs at a human reaction lag went 0.94 → 0.11 → 0.00 as damping rose — but a car stable enough survives arriving too fast, and the value of being told about the corner collapsed with it (median gain 5.4s → 5.1s → 1.5s). Neither number alone picks a value; measuring both against the same parameter picks 1.3 immediately. Worth doing whenever a comfort fix touches the thing the game is about.
Periodic shimmer is invisible in a screenshot and easy to measure. Two surfaces alternated colour per-segment — the tarmac every four segments and the kerbs every one — which at 150 km/h is a 1.7 Hz and a 7 Hz pulse: slow enough to see, fast enough to irritate, and completely absent from any still. The probe is four lines: drive at a fixed speed, take the mean brightness of a few horizontal bands each frame, and report the standard deviation and the zero-crossing rate. The mid band read sd 7.7 at 1.2 Hz; replacing the tarmac alternation with per-segment noise (high frequency reads as surface, not throb) and sub-striping the kerbs inside each segment at ~1.5 m took it to sd 2.7, and the near band from 3.2 to 1.7. Animated screen-space “mist” bands sweeping vertically were the other offender and simply came out.
One number decides whether a pseudo-3D road reads as a lane or a motorway. The road’s
on-screen half-width at the bottom of the frame is ROADW*(H-HOR)/(CAM_D*CAM_H). At the
camera height and focal length this shipped with, that came to 2.3x the whole screen width, so
the lower half of every frame was featureless grey tarmac and the car looked like it was on a
dual carriageway. Raising the camera from 1.35 m to 2.6 m and the focal length from 0.86 to 1.15
brings it to about 0.9x, and nothing else about the scene had to change. Worth computing before
tuning a camera by eye.
Timing canvas2d calls measures nothing. An in-page performance.now() around render()
reported 0.4 ms a frame while the page was actually managing 20, because the 2D context queues
work and the rasteriser bills it later. Forcing synchronous renders in a tight loop gave 50 ms a
frame instead. The second trap is the sandbox: this headless Chromium rasterises on the CPU, so
cost scales with raw pixel count (1.0 Mpx 17 ms, 3.9 Mpx 50 ms) and removing even the largest
fills changed nothing. Real-device frame rate is not measurable from here — the useful thing
the profile still gave was the relative cost of each phase, by re-running with one removed.
Binary steering is why a driving game feels bad on a phone. Tapping a screen third gave full
lock in about a tenth of a second, so every correction was an over-correction. Making lock
proportional to how far out the thumb sits, and splitting a middle band into lift and brake so
there is a throttle at all, brought the touch input model level with the keyboard: driven through
the real update() at matched reaction lag, thumb and keys now score the same offs and times at
150, 250 and 400 ms. Worth doing before reaching for a wider road — widening would have bought the
same comfort by weakening the thing the game is about.
Art can silently delete a mechanic, so re-run the measurement after a look pass. The stage was later given biomes, parallax ridgelines, banks and roadside props. Every one of those is a way to reveal a corner earlier than the road does — scenery lining a bend, a hillside that follows the route — which would have quietly returned the game to the state where the co-driver was decorative, and it would have looked like a pure improvement while doing it. The rule that kept it honest: anything that correlates with the road’s local direction fogs out at exactly the sight distance, and anything that does not (distant ridgelines, sky) may be seen. Re-running the same notes-versus-eyes A/B afterwards returned 35/48 and 0.00 offs against 0.79, unchanged to three figures, which is the evidence that the look pass cost nothing. Also worth keeping: per-segment quads for a hillside read as detached glass panels, because each facet is separately fogged; one continuous path per side with a gradient anchored to its own near and far ends reads as ground. And props lifted onto a bank they are geometrically standing on will look like they are flying if you never draw the plateau under them — put them on the plane you actually render.
Information has no value where mistakes are cheap. Even with sight cut, arriving thirty km/h too hot cost almost nothing, because the run-off was generous and grass barely slowed the car — so knowing the corner in advance bought nothing. Tightening what going off costs is what made the knowledge worth having. Related, and the same shape as the Into-the-Breach finding above: measure the stupid baseline. A reaction-lag sweep put a human-plausible 150–250 ms at zero offs, 400 ms at one, 600 ms at nearly four — the curve a difficulty ought to have.
Filled since: hidden-rule deduction, by games/glyphgate.html; cooperating with recordings of your
own past, by games/selfsame.html; pinball, by games/escapement.html — swept-circle continuous
collision so nothing tunnels at any speed, flippers you can cradle on, habitrails, a slingshot that
throws along its own rubber, and a mission stack that bolts new parts onto the playfield as you go.
The physics core in it is reusable, though the two candidates once listed here for it are both rejected
below — a reusable core is a reason a build is cheap, never a reason the result is worth playing.
Filled since: classical cryptanalysis, by tools/cribwork.html — cipher identification from the index of
coincidence, solvers for the Caesar/affine/substitution/Vigenère/transposition families, and a cryptogram
worksheet. Nothing in the catalogue had touched ciphers beyond the toy converters (1337, morse_code_translator,
base64_encoder_decoder), which is the shape of gap worth looking for: a whole subject the collection only
gestures at. The neighbouring one still open is cipher-based puzzle games (nothing here is a word game
built on deduction from letter statistics).
Filled since: Worms, by games/worms.html — the three axes “Read this first” measured going to zero
(multiplayer 24%→0, competitive 19→1, strategy 16→0) in one entry, and it is the known thing done well
case rather than a novel mechanism: Worms Armageddon is an externally fixed target, so how close this lands is a
reading you can take. 2–4 teams, each human or CPU in any mix, hot-seat on one device, no backend. Four things
worth reusing. The terrain is a pixel mask, not polygons — a Uint8Array the size of the world plus a matching
offscreen canvas, with craters punching both and only the affected rectangle repainted. That single decision is
what makes a crater the exact shape its explosion had, and it costs a 2.2 MB array and a median 16.6 ms frame
through twelve simultaneous 70 px blasts. The WA constants are worth copying literally: a worm is a 9×16
“coffin” that climbs 8 px without jumping, and a test that walks it at 4/8/12 px steps (climbs, climbs, blocked)
pins the feel more usefully than any amount of playing. Wind is the balancing asymmetry, not decoration — it is
a constant horizontal acceleration applied to rockets and withheld from thrown things, measured here at 243 px of
spread on an 82-frame bazooka arc and exactly 0 on a grenade; the first cut had it ten times too weak and the shot
simply did not read as a Worms shot. And two bugs generalise past this build. Fall damage never fired, because
a fast landing always took the bounce branch and the damage lived in the landing branch — the slow falls that did
land were all below the threshold, so the whole system looked fine and was dead. Separately the first wind test
measured 2 px of spread and looked like a broken constant; the shot was hitting a cliff face, and the constant was
right. Both were instrument failures, not game failures, and both read as the opposite. A physics value is only
measured when the measurement runs somewhere the value is the thing deciding the outcome.
Filled since: fonts, by tools/sorts.html — the structural gap in the tool catalogue: 215 tools, a great
deal of text and typography work among them, and not one that opened a font file. file_type_identifier sniffed
magic bytes and stopped; character_map_explorer browsed Unicode knowing nothing about any font. Sorts parses the
sfnt container itself — cmap, name, OS/2, post, fvar, GSUB/GPOS — and leads with the question people actually have:
paste your text and it names every character the font cannot draw. Three things in it are worth reusing. The parser
was checked field by field against fontTools over 147 real fonts and matches exactly, cmap coverage included, which
is the only reason the coverage and language claims can be stated flatly rather than hedged. Two cheaper approaches
were measured and thrown away first: detecting coverage by canvas width comparison scored 79.8% against the true
cmap, and pixel-exact render comparison did worse at 59.4% with 4,442 false positives — both depend on the reader’s
installed fallback fonts, so neither can be trusted to describe a file. And the feature list measures rather than
asserts: each OpenType feature is rendered on and off and the advance widths compared, so the ones that genuinely
change your text are marked and the ones that do nothing to it are not oversold.
Still open on this side: WOFF2 cannot be read byte by byte in a browser. Its tables live in one Brotli stream and
DecompressionStream offers gzip and deflate but not br (checked, Chrome 141). A dictionary-free Brotli decoder would
have been small enough to ship, but 274 of 276 real WOFF2 files sampled from Google Fonts use the static dictionary,
so that shortcut is closed — full support means carrying the ~120KB dictionary. Sorts reads the WOFF2 table directory,
which is uncompressed, and renders the font, and says plainly that the rest is out of reach. The neighbouring gap still
open is hinting and rasterisation: nothing here shows what a font does at 11px, where hinting and gasp decide
whether it is readable at all.
Filled since: pitch, by tools/overtone.html — the audio-side version of the same gap, where a dozen
audio tools existed and not one showed you pitch. It is framed as an instrument rather than a tuner: a
cent-accurate pitch ribbon over time, a log-frequency spectrogram, a harmonic ladder that measures how far
each partial sits from a whole-number multiple (enough to show piano-string inharmonicity), and per-note
vibrato rate and depth, attack settling, sway and drift, plus a profile accumulated across held notes.
Two things in it are reusable: a YIN detector whose clarity gate was tuned by measurement (0.88 removed
all 729 octave errors across 5,760 synthetic frames while keeping every clean one), and the observation
that a pitch track is a moving average of the analysis window, so vibrato depth is attenuated by
sinc(pi·fv·W/sr) and can be corrected back out. Still open on the audio side: polyphonic pitch (rhythm and timing,
listed here before, is filled by tools/pocket.html) — Overtone is monophonic and
reports the common subharmonic of a chord rather than trying to guess that it is hearing one. An attempt
to detect chords from the spectrum was removed after it turned out to fire on hummed low notes: a
microphone that rolls off the fundamental leaves a spectrum shaped much like a chord’s, and every
statistic that separated the two also mistook quiet fundamentals for chords.
Filled since: programming games, by games/everywhere.html — the structural gap on the game side, where
91 games contained nothing in which the player writes something that then runs without them. The first framing
was the obvious one, a procedurally generated RoboZZle: one board, find the short looping program. It was
measured and thrown away, and the measurement is the reusable part. Generating levels by sampling a program and
carving the path it walked gave boards that were 56% solvable in three instructions or fewer, and across 107
levels 44 had the same answer — walk forward, repeat. Rendering them as ASCII showed why: the colours were
noise, so either they meant nothing (a straight corridor with random paint) or the solution was an arbitrary
program nobody could deduce. Generating board-first — pick a path motif, then colour the cells by backtracking
search so an intended program template traverses it — fixed legibility completely, and made the problem worse:
colour became signal, and 51 of 60 boards then had exactly the template’s four-line answer. Every level in a
family is the same puzzle. The general lesson, which is the third time this list has recorded a version of it:
in a puzzle whose answer is a short program, the space of sensible answers is small, so procedural boards
repeat the answer even when they look different.
What survived was the inversion: the level is not a board, it is a set of boards. The player writes one rulebook and it must solve every maze the stage can generate, including the hundreds it never sees; the game answers a failure by showing the specific maze that beat it with the robot’s path drawn on it. That framing has the property the single-board version lacked, and it was checked by exhaustive enumeration rather than assumed. Of all 614,656 rulebooks three lines long, exactly twelve solve every perfect maze — six phrasings of the right-hand rule and six of the left — and nothing of two lines works at all. Near-misses are dense and instructive: the same three instructions in the two other orders fail on all 200 mazes, because a turn on line one changes what line two is looking at. The slot budget turns out to be the difficulty dial: of the rulebooks that solve the first maze, 100% are universal at four lines, 48% at six, and 5% at ten, with the median failure on the third board — so a tight budget forbids overfitting and a generous one guarantees the counterexample loop has something to show you.
Two results are worth keeping past this game. The first is a negative one. The intended final stage was mazes with loops knocked through, where wall-following provably breaks, with chalk marks on the floor as the fix. No small rulebook for it was found: exhaustive search over three lines, 51,840 programs formed by splicing one or two chalk instructions into both wall-followers (none scoring above 298/300), seven hand-written Trémaux-flavoured strategies (best 70/200), and seeded hill-climbing up to eight lines in two independent implementations (best 96/120 at 7×7, decaying to 28/60 at 10×10 — the signature of a rule that is tuned rather than general). The stage was cut. A finite rulebook of this kind that solves braided mazes is still open, and probably needs edge-marks or a turn counter rather than one bit per tile.
The second is the finding that came out of chasing it, and it contradicts the premise the stage was built on: loops are not what beats wall-following. Sweeping loop density with the goal in a corner, the right-hand rule solved 200 of 200 at every density tried, from 0.02 to 0.20. The same rule on the same boards with the goal moved to the middle fell from 190 to 57 of 200. Wall-following fails when the goal is not on the wall you are holding; the loops were never the problem. The replacement stage came from asking what else breaks it and verifying before building: take the sideways senses away and leave only “wall ahead”, and a universal answer still exists at four lines — at least five of the 6,561, all leaning on a trick the earlier stages never need, the same line twice in a row. Verify a stage has an answer before designing around it; two of the six shipped here changed shape because the search said no.
A later pass asked whether it had room to grow, and the measuring is the useful part, because almost everything obvious turned out to be a dead end. Harder mazes do nothing: the wall-following rule is 200 of 200 at 25×25 and 200 of 200 with a random start cell and random facing, so difficulty cannot come from the boards. An efficiency score is dead too — all 90 universal rulebooks of three and four lines average between 38.6 and 38.8 steps to the goal, a spread of nothing, against a shortest route of 23.7 that a robot with no memory cannot reach. And new worlds mostly have no answers at all: one-way doors break the rule hard (12 of 200) but have no rulebook up to five lines even when given a “the way back is shut” sense; ice tiles (173 of 200) and spinner tiles (6 of 200) the same. That is three more stage ideas killed by search, on top of the loops stage killed earlier.
What does work is the opposite of adding: take tools away. Keep the mazes and shrink the vocabulary, and answers keep existing at longer and longer lengths — full vocabulary 3 lines, no side senses 4, only wall-senses and no “open ahead” 5, no left turns 6, no side senses and no left turns 6. That generalises, which is the actual find: a stage is just a subset of the vocabulary, there are 1,023 of them, and solvability is monotone, so a subset’s answer bounds every superset’s. Processing subsets smallest-first and inheriting those bounds turns an impossible enumeration into 19 seconds of work, and yields the whole table: 163 of the 1,023 vocabularies can solve a maze at all, with shortest rulebooks of 3 (38 of them), 4 (31), 5 (9) and 6 (85) lines. Shipping that table is what lets the endless mode hand out a handicap and promise it can be beaten, and say in how many lines, without shipping any answers.
Two mechanical notes worth reusing. The table only became affordable after the machine was reimplemented allocation-free — boards flattened to a Uint8Array of passable directions, and cycle detection against one reusable stamped buffer instead of a Set per run — which was about thirty times faster and, checked against the shipped engine over 480,000 runs, agrees on every one. The check earned its keep: the first version of it disagreed on 0.17% of runs because the condition indices were mapped in the wrong order, which no amount of staring would have caught. And the promise is verified against the boards the game actually generates, not the ones the table was built on — every one of 200 Deep rounds re-checked on its own 200 mazes.
Filled since: minigolf, by games/nine_holes.html. Worth reusing are three results, all of which
contradicted what the design assumed before it was measured.
The cup’s capture speed, not the level generator, decided whether open holes could exist at all. A forgiving cup swallows any weight, so on a hole where you can see the flag every power drops and the acceptance filter throws it out as a barn door — silently banning open holes and leaving a course of nothing but blind doglegs. Over 2,400 candidates: at capture 9, 3% of accepted holes had line of sight and 27% were rejected as barn doors; at capture 4, 31% and 4%. A single physics constant was producing what read as a level-design problem.
“Every hole can be aced” was true and hollow. The coarse angular sweep only proves an ace exists near some angle; refining it showed 10% of accepted holes had an ace window under one degree. The judge now measures the real angular tolerance and requires 1.25 degrees, so the median hole gives you 2.25 and the promise survives contact with a human hand.
Par cannot come from the optimum when every hole is aceable by construction — the optimum is always 1. It has to come from how a plausible player actually fares, which makes the player model a tuned parameter rather than a detail: at 2 degrees of aim error 95% of holes came out par 2 and difficulty stopped showing, at 6 degrees par 5 became common, and 1.5 degrees gives 59% par 2 and 32% par 3 with mean par rising across the tiers. The round is then ordered by measured par rather than by the nominal difficulty tier, because the tiers turned out not to be monotone — tightening corridors made some holes easier. Mean par by position is now 2, 2, 2, 2, 2.25, 2.5, 2.88, 3.5, 4.
A presentation pass afterwards produced two findings worth reusing for any canvas page here. Splitting the scene into a baked layer and a live one is what makes detail affordable: everything that cannot move — surround planting, table, felt, sand, slopes, rails, ambient occlusion — is drawn once into an offscreen canvas per hole, and only the ball, trail, water, flag and particles are drawn per frame. The rail itself is not tiles but an offset outline: dilate the play mask, subtract it, and the band that falls out follows the hole’s real shape at even width, where drawing it cell by cell wandered between one and two cells thick and read as chunky. And the profiler contradicted the obvious guess about cost — the blur and the dilation were 10ms and 22ms, while building those alpha buffers at full device resolution cost about two seconds a hole. They are deliberately blurred, so resolution buys nothing there: building them at 0.55 scale and scaling up on composite took a bake from 3.1s to 0.38s with no visible difference. A full-screen vignette gradient filled per frame also cost more than the entire rest of the loop; baking it took frames from 20ms to 16.7ms.
A later pass replaced the bitmap smoothing entirely, and the replacement is the reusable part. Terrain lives on a grid, but a grid-shaped bunker looks like a spreadsheet, and smoothing the mask as a bitmap gave soft mushy edges for a bake of nearly two seconds. Tracing the cell region to a polygon instead — walk the boundary between filled and empty cells emitting each edge with the filled side on its left, then round it with two passes of Chaikin corner cutting — gives crisp curved edges, exact clipping, and a bake of 20ms rather than 1780ms. Two traps came with it, both found by looking at screenshots rather than by reasoning. Corner cutting eats into convex corners, so a hazard smoothed naively ends up smaller than the cells it stands for and can catch a ball on a square that did not look like a hazard; hazards are grown outward first so the painted region is always a superset of the real one. And offsetting a polygon by moving each vertex along its normal self-intersects wherever a concave inlet is narrower than twice the offset, leaving an inverted loop that punched a hole clean through the rail on about one hole in three; stroking the outline at twice the width and clipping to the far side has no such failure mode. Unrelated but found the same way: the generator could wall off little pockets of green that no ball can reach, which are not playable but do collect the whole of the edge shading and render as a black blob, so a flood fill from the tee now turns anything unreachable back into rail.
The general lesson, which is the same one the rejected tactics prototype produced: the fraction of an action space that is “good” measures nothing, because the space is mostly nonsense. Ask instead whether a deliberately stupid player reaches the good play.
Filled since: the maze chase, by games/scatter.html — the first entry built deliberately against the
drift described at the top of this file, and the first “well-made known thing” since the drift began. The
useful part is that its central premise turned out to be wrong, and the measurement that killed it is
worth more than the game.
The pitch was that the original’s four ghost personalities — Blinky chasing, Pinky’s four-ahead cut with the up-direction overflow bug, Inky’s vector doubled through Blinky, Clyde losing his nerve inside eight tiles — are not decoration but the entire difficulty, and that the naive all-chase every clone ships cannot catch anyone. A Python prototype said exactly that: same evading player, same maze, same speeds, 21 deaths against the authentic AI and 0 against naive chase, with the naive ghosts clumped into one blob 35.6% of the time against 15.3%. On that basis the game was approved and built.
Measured again in the finished game, with the same bot driving both builds, it reverses: 0 deaths authentic, 6 naive, over 8 runs of 45 seconds each; ghost spread identical at 9.10 against 9.13 tiles; naive pincers slightly more often, 12.7% against 10.7%. Naive chase is the harder game, and obviously so in hindsight — the real ghosts spend 25% of level 1 in scatter, walking deliberately away from you, and a naive chaser never lets up.
Two lessons, and the first is the general one. The prototype omitted the ghost house. All four ghosts started outside and on top of each other, so “bunching” measured the spawn arrangement rather than the targeting, and the effect vanished once ghosts were released on the real dot counters (Pinky at 0, Inky at 30, Clyde at 60). The prototype was faithful about the thing being tested and careless about the surrounding machinery, which is the failure mode to watch for: it is not the model of the mechanism that is usually wrong, it is what was left out around it.
The second is about the original rather than the clone. The ghost AI is not a difficulty system, it is a legibility system. Four distinguishable characters and a quarter of the level where they let you go is what makes Pac-Man learnable, pattern-playable and fair; a relentless chaser is harder and much worse. So the fidelity is still worth having — just not for the reason claimed. The game now leads with a target overlay that draws each ghost’s chosen tile, Inky’s construction line through Blinky and Clyde’s eight-tile nerve radius, which is the honest version of the pitch: not “this is what makes it hard” but “this is what it is doing.”
A polish pass added a practice mode and then measured whether it does anything, which produced a more useful result than the feature. Against a bot that moves with intent but can only see ghosts two tiles away — the obvious model of a beginner — level 1 is cleared 8 of 8 with practice mode off and 8 of 8 with it on. The assist only starts to matter for a bot that also moves at random 55% of the time, and even there it is 0 of 8 against 1 of 8, with average score up 37%. So slowing the ghosts is close to irrelevant: the game is not beating people with speed, and an assist aimed at speed is aimed at the wrong thing. What beats a human is not knowing where four ghosts are going at once, which is an argument for the target overlay and for the new countdown strip to the next scatter, not for a difficulty slider. Practice mode stayed because it is harmless and clearly labelled, but it is not the answer and is not described as one.
Three mechanical notes worth keeping. The maze has a checksum: the original is 240 pellets plus 4 energizers, mirror-symmetric, with every pellet reachable and exactly one sealed region (the 6x3 ghost house). A hand-transcribed maze that hits all of those on the first try is almost certainly right, and it caught that Pac-Man’s start tile had been walled in — walls and empty floor both score zero pellets, so the count alone would not have found it. Tile-centre movement has a trap: checking the wall ahead only on arrival at a tile centre lets an actor parked on a centre walk straight into the wall on the next frame; the check has to run before each move, not after. That one shipped as Pac-Man strolling through the maze walls and wrapping across a row with no tunnel in it. And a 13-check spec suite run against the shipped page rather than against the prototype — the up-overflow firing only when facing up, Inky inheriting it through his own two-ahead tile, Clyde flipping at exactly 8 tiles, the door passing eyes but not Pac-Man, the level-1 speed and wave tables — is what makes it safe to say “faithful” in the description flatly instead of hedging.
Filled since: the microphone, by games/hummingbird.html — you hum, and a bird follows your
voice along a melody line. Four results are worth keeping.
A pitch detector tuned for accuracy is the wrong instrument for a controller, and the difference is one number. Overtone’s YIN was re-measured against 120,000 synthetic frames (3 timbres x 37 pitches x 5 room conditions, including a laptop-mic low-end rolloff) purely on controller terms — latency, dropped frames, octave jumps. A 1024-sample window is the answer: 98-100% of frames pass the 0.88 clarity gate with zero octave errors and 0.3 cents of error on a clean tone. 512 samples (12ms) collapses — it drops a third of all frames and throws 1-2% octave errors, which for a controller means the avatar teleports an octave. 2048 and 4096 pass just as cleanly and buy nothing but latency, so Overtone’s default is 23ms of lag this game does not need. False firing turned out to be a non-problem: 0 of 239 frames on digital silence, room tone, loud white noise, pink noise, typing clicks and 50Hz mains hum.
Then measure the same thing through the actual page, because it will disagree. Chromium in a
container exposes no audio input device at all, so the fake-mic flags are useless; handing the page a
MediaStreamAudioDestinationNode playing a synthesised singer instead exercises the whole real chain
— getUserMedia, AnalyserNode, YIN, smoothing. It reported 100% detection and zero octave errors over
23 notes across A2-A4, and 82ms to land a note against the 23ms the offline test promised. The gap
was the smoothing. One time constant cannot both iron out the wobble in a held note and get out of
the way when someone deliberately leaps, so the glide is now chosen by distance — jumps over a
semitone are followed almost at once — which took it to 65ms, close to the floor of window plus frame.
The melody generator was measured, was bad, and the fix is the nine_holes shape. The obvious generator — key, chord progression, phrase repetition, step-versus-leap weighting — produced, over 400 lines, leaps of up to 28 semitones, ranges to 26 semitones (nobody can sing two octaves), and a difficulty dial that ran backwards, with difficulty 0 leaping more than difficulty 0.5. “400/400 distinct melodies” was true and measured nothing. Growing a line and then judging it against range, maximum leap and a minimum proportion of stepwise motion gives a ladder that climbs: median max leap 2 -> 4 -> 5 -> 7 semitones, stepwise motion 100% -> 85% -> 78% -> 75%, at a rejection cost of a few milliseconds. The judge also caught a bug no amount of staring would have: a singer whose comfortable range is eight semitones was being handed fourteen-semitone tunes, and the placement code quietly shoved half of each one under their floor. Asking for no more range than the singer has fixed it, and 0% of notes now fall outside the voice across 8,400 songs at three voice widths.
The sloppy-player model is the tuned parameter, and the first one was nonsense. Testing the difficulty curve with a bot whose pitch was jittered every frame said ±0.9 semitones scored 100% at every meadow — because white noise is exactly what the game’s own smoothing removes. Real amateur singing is slow drift, a per-note bias and a late reaction to a leap. Modelled that way the meadows separate properly: a ±0.9-semitone singer goes 100% -> 96% -> 57% -> 43% across the journey while an accurate one stays at 100% throughout and a random one never breaks 2%. This is the third time this file has recorded a version of the same lesson — what is wrong is usually the machinery around the thing being tested, not the model of the thing itself.
One mechanical note, and it is the nine_holes finding again: painting a full-screen sky gradient, three filled hill paths and a handful of clouds every frame cost 39ms a frame at desktop size — 25fps — against 16.7ms on a phone, because none of it moved. Baking the backdrop once per meadow and capping the backing store by pixel budget rather than by device ratio alone took it to 16.7ms on both.
A polish pass afterwards produced one note worth reusing for any drawn character on a canvas. The bird’s wings were stacked behind her body, which is invisible at game size and reads as a broken sprite the moment anyone actually looks — a bird with no near wing at all. A character seen side-on needs both: a far wing behind the body and a near wing in front of it, beating together. The way to see this, and the reason it survived a whole build unnoticed, is to render the character on a contact sheet at 4x through a full animation cycle and in each of its states. At that size three more faults were obvious that no test would ever have failed on — a beak longer than her body, a tail like a fish fin, and a downstroke that swung the wing across her chest like a leaf, all of which came from numbers that looked reasonable in code.
Once believed, now false, and corrected 2026-09-09: an offline environment cannot verify a
model-in-the-loop game at all. It depends entirely on the sandbox. A later session reached
https://chatgpt.tobiasmue91.workers.dev/ fine through its egress proxy and ran ~700 scored calls
against it. Two mechanical notes for whoever tries next: Python’s urllib got a flat 403 from that
proxy where curl succeeded, so shell out to curl rather than concluding the host is blocked; and
the worker passes response_format: {"type":"json_object"} straight through, which is what makes a
scored game practical. Check before assuming you cannot measure it.
What that session measured, for the still-open “a model you have to talk round” gap. The idea was a council you argue in front of, and the finding is worth keeping whatever gets built there: asking a model to play a stubborn character makes it immovable in both directions. Under a “be hard to move by flattery” system prompt, five characters over 300 calls dropped 2.17 points on an argument that threatened what they privately wanted and correctly ignored flattery (-0.18) — but went up on the argument that guaranteed it exactly 0 times out of 60. Replacing the character acting with an explicit numeric rubric, and letting the model judge relevance rather than perform stubbornness, separates cleanly: aligned +2.75 (20/20 up), threatening -3.00 (20/20 down), flattery 0.00 (20/20 unchanged). Scoring a whole five-member room in one call per turn agreed with hand labels 94.7% of the time over 320 judgements with zero false negatives, and the disagreements were mostly the model catching side-effects the labels missed. Broken promises land too: contradict an earlier promise and the member relying on it drops 3, 8/8. One defect to design around — told “earlier promises: none”, it still invented a broken promise and scored it, 8/8, so a breach has to name which promise it broke and be checked against the real list. Two things did not survive: characters leak a “private” concern in 45-75% of replies even under pure flattery, so hidden levers do not work and the concerns may as well be public; and the setting matters more than the machinery — a town planning meeting was correctly rejected as a page nobody would open.
Filled since: two players on one phone, by games/alibi.html — two suspects corroborating a
night out under interrogation, answering in secret and passing the phone. Four findings, and the
first is the one that generalises.
A coordination game needs a rule the pair can both reason from, or it is a coin flip in costume. The first prototype was 4 arbitrary options per question, and the measurement that mattered was not variety — 500 sessions gave 500 unique transcripts on the first try — but what a briefed pair could do. Simulated pairs who had agreed a story still diverged 74.7% of the time, which is exactly 1-in-4 arithmetic: there was nothing to reason from. What fixed it was making the cost of an answer public and relative to the crime: every answer carries tags (unaccounted for, money, a vehicle, a witness, late, nothing to back it up), each crime is hot on two of them and says so up front, and the cheapest answer is a schelling point both players can find alone. Cracks fell to 21%. Half the option sets are built to have a single cheapest answer and half a tie, so the game alternates between a rule you can follow and a genuine guess about the other person.
Three bots, not one, and the interesting one is the worst player. Careless (uniform), briefed (cheapest, ties random) and telepathic (cheapest, ties always agreed) bracket the human band — suspicion 56 / 11 / 3, cracks 70% / 21% / 0%. The careless bot found the real defect: with an early-out at a suspicion cap it was cut off at question 8 100% of the time, so the pair who least understood the game never saw the written statement at the end, which is the entire payoff. The interview now always runs its full length and only the verdict varies. A bot that plays badly is worth more than a bot that plays well.
Content lumpiness is measurable and worth measuring. The corpus looked varied and wasn’t: 151 of ~390 authored answers carried a single identical tag, four combinations covered 77% of everything, and the risk landscape was bimodal (66% of options free, 33% at maximum, almost nothing between). Retagging every item took an hour and moved the cheapest-answer ties, the spread, and the crack rate all at once. The same check caught the venue question being 60% pure coin flip — four places, all free — which is now 0.2%.
Not everything could be measured, and it should be said which. Whether two humans actually reach the same answer when the cheapest is tied is a question about people, and one person with a headless browser cannot answer it. The telepathic bot is an upper bound nobody can hit and the briefed bot a lower one; real pairs sit somewhere between and only a real pair can say where.
Filled since: the deck-builder, by games/procession.html — the card games here were all
fixed-rule classics and nothing built a deck across a run. Nine fights, one card after each, and a
deck that is never shuffled: at end of turn the cards you played drop to the bottom in the order
you played them and the cards you held follow behind, so the hand you draw three turns from now is
one you dealt yourself. Nine results, and the first four are about how to tell whether a game of
choices actually has any.
Additive cards make drafting irrelevant, and the win rates say so. The first pool was ordinary — more damage, more block, more poison — and random drafting scored 38% against focused drafting’s 42–56%. Any nine cards were about as good as any other nine, so the draft was decoration. What fixed it was making each archetype superlinear: one card apiece whose value is a function of what you already built (damage = 5 x your Strength; damage = your current Block; double the enemy’s Poison; 3 damage per card already played this turn). That is also the answer to the Curator’s failure in this file’s rejected list — it is the difference between a decision that changes the outcome and one that does not.
Payoff cards look terrible in an unconditioned measurement, and deleting them would be the mistake. Ranked by win-rate lift under random drafting, Catalyst sits at -2.6 and Retribution at -3.7: bottom of the table, apparently dead weight. Conditioned on whether the deck drafted 3+ of the matching archetype they roughly double — Catalyst 13.5% -> 31.0%, Flurry 16.8% -> 32.5%, Retribution 16.7% -> 31.6%, Bastion 34.6% -> 54.8%. A conditional card is supposed to be bad in a deck that cannot use it. Measure card strength against the decks that would actually play it, or the metric will quietly ask you to remove the only cards that make building matter.
A naive heuristic that beats random is the bar; one that loses to it is the finding. Final table over 150 runs each: skipping every card 0.0% (dies on floor 3 every time), taking the biggest damage number 20.7%, drafting at random 24.7%, and the four archetypes drafted competently 34.0–38.7% with a generalist at 41.3%. Greedy-damage being worse than random is the useful number, not the top of the table: it means the game punishes the obvious read, because that policy never takes any defence. This is the third time this file has recorded a version of the same test and the first time it was run before the page was written.
Watch the gap, not the win rate — every buff to a weak card raises the floor. Three cards measured as traps (lift -8 to -10) and were given small bodies. That took random drafting from 24% to 32% and compressed the skill gap, which had to be paid back with a difficulty retune. Card balance and difficulty are not separable: the policy table has to be re-run after every card change, and doing it the other way round cost two rounds of work.
A run that is one long attrition sum has a cliff instead of a curve. With a single HP pool across nine fights, the outcome reduces to one inequality — total damage taken against starting HP plus heals — so everybody crosses the threshold at the same moment. Win rates swung 38% to 2% on a 5% parameter change, twice, before this was diagnosed rather than tuned around. Splitting the run into three acts with a full heal between them turns one long inequality into three short ones and makes the whole thing tunable.
Diagnose from the curve, not the outcome. Aggro sat at 12% and looked like a broken archetype. Printing HP floor by floor showed it was not losing fights at all: it reached floor 8 or 9 almost every run and ran out of health, bleeding ~20 HP a fight with no way to get any back. The fix was burst plus the only sustain card in the game, not more damage. The same printout showed act 1 was free — the starter deck beat every act-1 enemy for a total of 7 HP — meaning the first three drafts could not matter, which no win rate would ever have said.
The mechanic’s own worst property was invisible in code and obvious in a screenshot. Because the deck keeps its order, clumps are permanent and self-reinforcing: cards played together come back together, forever. The starting deck was five Strikes then five Defends, so turn one was five Strikes and turn two five Defends — the opening had no decisions in it at all, and the queue read as a column of identical cards. Interleaving the starter fixed the opening, and the clumping itself turned out to be the actual skill of the game rather than a bug: spacing your plays is how you avoid dealing yourself a dead hand later. It is now the thing the rules screen leads with.
A balance table describes whatever engine produced it, so check the shipped one. The whole tuning was done in a Python simulator; the page is a separate implementation. Running 60 scripted scenarios through both and comparing every field each turn gives 3,181 comparisons and 0 mismatches at the shipped constants, which is the only reason the numbers above can be stated about the page rather than about a script. The first run of that check reported 351 mismatches and every one was the instrument — the harness read the deck after the next hand had been drawn, and read health after the victory heal had been applied. Budget for the fact that a cross-check needs debugging before it can find anything.
A presentation pass afterwards, prompted by the note that it looked like a web interface rather than a card game, produced one reusable measurement. The fix was mostly obvious — parchment card faces on a dark table, a coloured archetype band, a cost gem, a per-archetype sigil, a fanned overlapping hand, a deck drawn as a stack with thickness, and energy as diamonds rather than “3 / 3” — but the fan broke the phone. Five readable cards do not fit across 390px. At 104px wide with a 26px overlap each card shows only ~66px, and the rules text is cut off mid-sentence, which is fatal in a game where you choose by reading. The check is one line — for each card, the distance to the next card’s left edge — and it turns an aesthetic argument into a number: the mobile hand now scrolls at a 14px overlap and every card shows 91–111px of its 104px width. Two other collisions (the End turn button under the fan, the Begin button under the title fan) were found the same way, by asserting the button’s top sits below the lowest card rather than by looking. And the whole pass was checked against the engine cross-check afterwards — still 3,181 comparisons and 0 mismatches, so none of the balance work was disturbed by the reskin.
A second pass before handing it over found the worst bug in the game, and it was a dead mechanic rather than a wrong number. The Hexer announced “preparing a hex” and then did nothing at all — the status it applied was written in the simulator and never read, and did not exist in the page version at all. In a game whose entire premise is that the enemy tells you its exact next move, a move that lies is the one unacceptable defect, and no balance run would ever surface it because the bots do not read intents. The check that finds this class of bug is one grep: for every status effect, confirm it is both written and read. A field that is only ever assigned is a promise the game does not keep. Implementing it properly cost nothing in balance — skip-everything still 0%, random 29.2%, the four archetypes 36.7–46.7% — and it made the naive attack-only bot notably worse (greedy-damage 20.7% -> 10.0%), which is the right direction.
Its sibling: the hex said “weak 2” and lasted one turn, because it was applied and expired inside the same end-of-turn block. Any duration counter decremented in the same phase that can apply it is off by one, and it only shows in a step-by-step trace — asserting the sequence goes 2, 1, 0 across three turns took one line and would not have been noticed by playing.
And a mechanical lesson about editing: a find-and-replace that matches nothing fails silently. The damage-pop and screen-shake CSS was written against a rule that an earlier stylesheet rewrite had already deleted, so the entire block vanished while the JS went on adding class names that styled nothing. The damage number still appeared — unstyled, laid out as a flex child 94% across the panel — which read as a positioning bug and sent the investigation the wrong way for a while. Asserting the anchor exists before substituting, and grepping for every marker afterwards, turns a silent no-op into an error. That audit is what caught it here.
Three things the interface was missing that the mechanic needs: the cards already played this turn, in order, since they are about to become the bottom of the deck and that is the whole game; whether Block is being kept into the next turn; and that you are currently weakened. All three were state the engine tracked and the screen never showed.
A third pass, prompted by “the leftmost card is cut off on mobile”, found a CSS trap worth
writing down because it is silent, general, and easy to reintroduce. A centred flex row that
overflows makes its leading items unreachable. justify-content:center puts equal overflow on both
sides, and scrollLeft cannot go below zero, so whatever spills off the left of a scrolling row can
never be brought into view — 12px of the first card at 390px, 30px of the first queue slip at 320px.
The fix is one keyword, justify-content: safe center, which centres while the content fits and
falls back to start the instant it does not; declare plain center first as the fallback. The same
pattern was present in three places and only one of them had been noticed.
Two things made the diagnosis longer than it should have been, and both are worth knowing.
Two rules for the same selector inside one media query: a later .hand{padding:22px 0 4px}
silently reset the padding an earlier .hand{padding:26px 18px 14px} had just added, so the first
fix appeared to do nothing. And a plausible wrong theory: transform-origin:50% 130% really does
swing a rotated card sideways, and a transform really does not contribute scrollable overflow, so the
fan looked like the culprit. Probing the element — layout box versus visual box, and the computed
justify-content and padding-left rather than the ones in the file — settled it in one call and
showed the transform contributed nothing.
The check that catches this whole class, and now runs across eight viewport widths: for every
scrollable row, assert the first child’s left edge is not left of the container’s, and that with the
container scrolled fully right the last child’s right edge is not past it. Watch out for two false
positives it will hand you — a non-scrolling box whose scrollWidth merely exceeds its clientWidth
is not clipping anything (the desktop fan is supposed to hang out past its box), and an item outside
the viewport inside a scrollable strip is reachable by swiping. Both looked like defects until the
probe was taught the difference.
One thing deliberately not measured: whether a person enjoys it. The bots establish that the decisions have consequences and that several ways of building all work; they say nothing about whether the ten minutes are worth spending, and that stays a question for a human.
Filled since: the material sandbox, by games/strata.html — the “toy, not a game” gap, and the
first thing here with no win state since interactive_buddy. 26 materials, five to start, the rest
discovered by putting things together. Five results, and four of them are the same lesson in
different clothes: the prototype was faithful about the mechanism and careless about everything
around it.
The stupid-player test is the whole verification, and it must run against the shipped page. A Python prototype of the reaction network said a bot that drops a blob of a random known material in a random place, 70 times, ends up knowing 13.4 of 23. The finished engine agreed on the total — 13.6 — and disagreed completely on the composition: soil, ore and salt were found 0/10 times. The bot pours from the sky and never digs, so the entire buried layer, and the metal, rust and acid gated behind it, was unreachable by anything except an action the game never suggested. Digging now discovers what it cuts through and the spade is called Dig, not Erase.
A hub with one improbable recipe is a dead tree. Lava gates obsidian, metal, acid and the main route to glass, and its only recipe was fire sitting on stone at p=0.006 — which never happens, because fire without fuel dies in under a second. It came out 0/10. Two fixes, both more legible than the constant they replaced: heat accumulates in stone and sand under sustained fire, so building a bonfire on a stone floor genuinely melts it; and sealed magma chambers sit at the bottom of the world, so digging far enough down also works. Lava is now found by 7 of 12 random bots and the final curve is 5 -> mean 18.1 of 26 (min 13, max 25) over 12 runs of 80 drops, with a reliable early ladder (plant, salt, ore, oil, wood all 12/12), a middle that half of players reach (steam, snow, ice, charcoal, lava 7-9/12) and a real tail still out there (obsidian 2/12, rust 1/12, acid 0/12 — reachable, since a deliberate player gets 23/26, just not by accident).
A simulation will hand the player discoveries nobody earned, and it will do it out of sight. The generated magma reacted with the ore and salt it happened to touch, so 8 of 8 untouched worlds announced Lava and Metal before anyone clicked anything. Sealing the magma in rock did not work either — it melts its own cage in about a second and then discovers itself. The fix generalises past this game: player influence is a per-cell property that propagates with the matter. Painting or digging marks a cell; a reaction marks both sides if either was marked; movement carries the mark along; and nothing is ever discovered in an unmarked cell. Deep magma can now do whatever it likes in the dark. The check that catches this class of bug is one line — open the world, touch nothing for nine seconds, assert the count has not moved — and it is worth writing for anything with an autonomous simulation behind it.
A world that shows you its own map has nothing to discover. The strata rendered in full from the first frame, so the salt seam was a white stripe you simply dug to. Ground is now opaque until an air pocket the player has reached touches it, with light creeping through connected air one cell per frame. The first attempt at this still leaked, and only a screenshot showed why: unexplored caverns were drawn as cave-dark rather than rock, so every cavity in the world was a legible black silhouette against the grey. Both the fault and the fix were invisible in code and obvious in a picture.
A later pass, driven by playtest feedback, produced two bugs worth keeping and one method note.
“It crashed” was the world being deleted, and the cause was in the brush, not the simulation. The report was that dragging fire around to clear the board broke the game. Reproducing it rather than adding the requested Clear button found the real fault: painting a material overwrote whatever occupied the cell, so a fire stroke replaced terrain with fire, which then burnt out and left nothing. Measured over a scripted eight-stroke sweep, occupied cells fell from 23,263 to 570 — bare bedrock. Placing now only fills empty space and Dig is the only thing that removes matter, which holds the same sweep at ~17,900. The general point is that a destructive brush is indistinguishable from a crash to the person holding it, and the bug report will describe the symptom in the simulation’s terms even though nothing in the simulation is wrong.
Anything that converts its neighbours into copies of itself needs a brake, and it will not show up in a short test. Lava’s own heat melted the rock around it, so a single pool the player touched turned the world molten without limit — 296 cells to 2,098 and still climbing fifteen seconds later with no fire anywhere. It never appeared in any earlier measurement because those ran for a few seconds and the growth is slow. The fix is physical rather than a fudged constant: molten rock chills against cold rock, so lava contributes no heat to melting stone (only real fire does), and a flow with few hot neighbours crusts over. Lava now recedes, 296 to 97 over the same run. Worth running any autonomous simulation for a minute with nobody touching it and plotting each element’s population, not just checking it looks right.
Adding materials moved the tail, not the middle. Eight more (mud, clay, ceramic, coal, diamond, steel, tar, gas) took the set from 26 to 34, extending the burial chain to plant -> oil and charcoal -> coal -> diamond. The random-bot curve barely moved — mean 18.1 of 26 became 18.2 of 34 — because every new material needs burial, sustained heat, or waiting, and none of that happens by accident. That is the right shape for a toy: the same generous early ladder, and sixteen rather than eight things left that only a deliberate player will reach. It does mean the headline “found by flailing” number stops being a useful summary once a collection grows a deliberate tier, and the reachability test for a knowledgeable player becomes the measurement that matters.
A palette can be measured instead of eyeballed. The request was a high-contrast mode for telling visually similar materials apart. Rather than judging swatches by eye, computing CIE76 distance between all pairs names the actual offenders and scores the fix: the normal palette’s closest pair is dE 2.1 (soil/mud), with 15 pairs under dE 10; the high-contrast palette went to dE 7.2, and tuning the six worst pairs it named took it to dE 13.0 with nothing under 10. Three rounds of this took a few minutes and are far more reliable than looking at a contact sheet, because the eye adapts to a picture and a number does not.
Two mechanical notes. Iterating occupied cells instead of scanning the grid was about 30x, and
is the only reason the headless experiments were affordable at all. And sizing the world from the
viewport against a fixed cell budget — h = sqrt(33000/aspect), clamped — rather than a fixed
grid gives 237x140 on a desktop and 132x250 on a phone, both filling the screen with no letterbox
and both landing at 16.7ms median with ~18k occupied cells. Fixing the grid and letterboxing it,
which was the first attempt, wasted about a third of a phone screen.
Filled since: the jigsaw, by games/jigsaw.html — a photograph you drop in becomes a real
interlocking puzzle, and pieces that snap stay snapped and move as one lump. A stub of this had been
sitting in the repo since 2026-09-09 (square tiles, mouse only, a hardcoded 800x600 canvas) and was
in none of games.json, sidebar.html or sitemap.xml, so it had never appeared on the site.
Seven results are worth keeping, and the first two are the same lesson from opposite ends.
Exactness came free from the construction, and the check was worth more than the result. Every internal edge is generated once and walked by both neighbours in opposite directions, so there is only one curve between two pieces and nothing to disagree about. Sampling random points and counting how many piece paths contain each one gives 0 gaps and 0 overlaps in 15,000 samples across 12 to 540 pieces. That number means nothing on its own, so four deliberate defects were injected first – bottom edge not reversed, wrong row index, left edge not reversed, every piece rolling its own tabs – and the check reports 145 to 307 gaps on each. One of those four came back clean at first and looked like a hole in the test; it was the defect injection that was a no-op, still writing into the shared edge array. Budget for the fact that the negative control needs debugging too, or it will quietly certify a validator that cannot fail.
The tab shape was wrong twice while every number was green, and only a contact sheet showed it.
First version: tall narrow heads on pinched necks, reading as keyholes on stalks. Second: heads with
flat tops, reading as buckets. Both tiled perfectly. The cause of the second was curvature – the
shoulder control points and the head control points were not tangent-continuous, so the outline
kinked at the join. What fixes it is two rules rather than better numbers: mirror the control
points instead of re-deriving them (symmetry by construction) and share one tangent direction
either side of each shoulder join (smoothness by construction), which leaves lean and head height
as the only two things to taste. Render the piece isolated at 4x, at real size, and at the size the
540-piece cut uses, all on one sheet; at game size all three versions looked equally fine.
Canvas shadows are not a decoration, they are the frame budget. A per-group shadowBlur cost
158ms a frame at 192 pieces on a desktop and 353ms on a phone – three frames a second – and it
is completely invisible in a screenshot, which is how it survived a look pass. Baking the shadow into
each tile once at cut time and keeping a live shadow only for the group under the cursor takes the
same 540-piece puzzle to 2.07ms a frame on a phone. Sizing the working image by the piece grid
rather than the picture’s diagonal took tile memory from 49MB to 33MB at the same time.
Whether a picture makes a good jigsaw is measurable, and eyeballing it is not enough. Downsample every piece-sized cell to 6x6 RGB and ask two questions: how flat is each cell, and how far is it from its nearest other cell. Two of the four procedural pictures scored 86% flat / 100% confusable and 79% / 82% – regions where a piece carries no information at all, which is not difficulty but a dead end – and both looked perfectly attractive as pictures. Rebuilt against the metric they are 0% and 0%. The separate trap is repetition: a patchwork of regular patterns produced genuinely identical pieces (twins 23.8%, nearest-neighbour distance at the 10th percentile of exactly 0), fixed not by an overlay but by giving every repeat of a pattern its own colour at the source (9.2%, p10 26.4). A low-frequency hue drift across the whole picture is the other half of it, and it is what lets you tell the top-left of a puzzle from the bottom-right – the thing a real photograph gives you for nothing.
Predicting the layout was wrong in both directions; measuring it was right. The camera’s “fit everything” was first computed from the scatter ring’s padding. Too tight and 13 pieces sat off the edge of a phone screen where they could never be reached; padded to fix that, the puzzle used 54% of the display. Computing the actual bounding box of the pieces that exist gives no clipping at any of five viewports and ~70% of the screen. Related, and worth stating separately: the strewn area should be shaped like the screen, not like the picture. A landscape picture ringed by a picture-shaped margin on a portrait phone wasted the top and bottom of the display and left the pieces small in the middle of it.
One finger has to mean two things, and the split is what makes a phone jigsaw work. A finger that lands on a piece drags it; a finger that lands on the felt pans; two fingers pinch. Measured through real touch events: dragging a piece moves the camera by 0, panning moves it by exactly the gesture, a pinch scales x1.89 against an expected x1.9, and adding a second finger while already holding a piece releases the piece rather than dragging it to the pinch. Also worth measuring rather than eyeballing: on a 390x844 phone the setup panel was 984px tall, so the one button that starts the game was below the fold on every phone size tested.
Two of the three times the game looked broken, the instrument was broken. A drag test that solved
0 of 12 pieces was aiming at grab points off the edge of the viewport; a rotation mode that “never
merged” was a harness computing a piece’s centre without rotating the half-cell offset. Both times the
engine was correct and a two-line probe – print whether pointerdown actually picked anything up –
settled it faster than reading the snapping code. When a mechanism fails a whole-system test, check
that the test can see the system before changing the mechanism.
One thing deliberately not measured: whether it is pleasant. A jigsaw’s difficulty dial is honestly just the piece count, so it grows rather than develops, and that is a real limitation rather than something the measurements above answer.
Filled since: a crossword tile game, by games/rack.html — the Scrabble line in the backlog, and
the first board game with a computer opponent since the drift above was measured.
Check the generator against brute force before trusting any number it produces. The move generator (Appel–Jacobson over a first-child/next-sibling trie in typed arrays: 593k nodes, built in about 130 ms) was checked two ways. Every move it produced in whole bot games was re-scored by an independent validator, the same one the human’s moves go through, and matched exactly. Separately, every one- and two-tile placement on mid-game boards was tried by brute force through that validator and compared set-for-set with what the generator found. The first check proves it never lies; only the second proves it never misses anything, and a generator that silently misses moves still produces bots that look reasonable.
The word list decided how the opponent felt, not how strong it was. The first medium bot averaged about 420 a game playing USQUE, SUNKET and NONRATED — all valid, all in a 270,000-word list, and exactly what makes a casual player feel cheated rather than outplayed. Restricting the easy and medium bots to SCOWL’s everyday tiers (about 39,000 words, levels 10–35) fixed the feel and set the ladder at the same time: roughly 190 / 330 / 520 points a game for easy / medium / hard, with the human still refereed by the full list. It was visible in the first screenshot of a played game and in no statistic.
The coach is what makes it hold up. Every human turn the worker finds the best available move, and the log says “best was QUEENIE for 102” under your 13. The end screen totals it into the share of available points you took. That number moves as you learn, which is the reason to play a second game.
Filled since: an incremental built to escalate, by games/bubble_break.html — pop bubble
wrap on a coffee break, quit for calluses, move from a desk to a warehouse to a factory and a city,
pop the world and then the rest of the universe. Chosen from three playable prototypes (a Katamari snowball and a firefly jar lost); the
verb was then chosen on a test page of three variants, and the one that made the player slow
down lost although it made sense. Balanced by a human-limited bot playing the whole career in
test/bubble_break.mjs. Four things generalise.
Diminishing returns are not a cap. Bonus time worth less the more you already have still grew like a square root: at warehouse scale, with hundreds of espresso bubbles a sheet, a break ran the full 400 seconds the bot allowed. What held was a jar — bonus time fills three quarters of the base break and no more.
A share of the last cycle is a lump sum worth the whole tree. Earnings inside a cycle grow exponentially, so “start with 8 % of what you earned” was most of the last break and bought 73 of 80 tree levels at once. The replacement pays a multiplier on the first break instead: it helps the climb and cannot skip it. The check that caught it is general: no new cycle may hand over more than half its tree after one run.
Specials are counted, not sprinkled. At half a million bubbles a 1.5 % gold rate is 8,400 gold bubbles, none of which a player can go and get. Counts are capped per sheet, and the cap grows with the upgrade, or its later levels buy nothing (“8 → 8”).
Make automation structurally unable to replace the player. Machines leave gold, espresso and giant bubbles alone, and a sheet only ends when every bubble is gone, so a machine alone earns about a tenth of a player however fast it is. Let it take gold and giants and the same machine earns 87 %.
The second version, after the first player’s notes, added three more.
When a stage runs long, add a scale, not more of the same. The first version’s factory took 95 of its 157 minutes: the numbers kept growing but nothing new happened. A fourth workplace between the factory and the world, with a verb whose yield grows with area rather than length (a drawn loop pops everything inside it), made the late game shorter and more eventful: the world now comes at about 140 minutes, and the city pops some forty times faster than the factory.
Name a tree by what it costs and when it goes. The first player could not tell what a tab called “Break tree” was for. “Upgrades” beside “Career”, with one line under the tabs saying what buys them and whether they last, answered it.
A count and a multiplier must not both wear the ×. The combo showed “57×” in the middle of the screen and “×1.5” in the corner, and it lapsed after half a second without a pop, so it read as broken. It now says “57 in a row”, shows its multiplier beside it, and draws the time it has left.
The third version answered the first player’s “harder wrap, maybe of other materials”: one sheet in four comes as a special delivery, a new material at each workplace from the warehouse on, each with a mastery that lasts. Four more things generalise.
A stroke is one touch. Jumbo wrap squashes a bubble on the first touch and pops it on the next, but a swipe is dozens of overlapping circles, so as first written it squashed and popped every bubble it crossed and the rule never showed. A bubble squashed by the stroke still going on cannot be popped by that stroke: a second swipe, or a tap, sets it off.
Precision does not scale with the tool, so bring the tool back. A print you must pop around needs gaps wider than the thumb, and from the factory on the thumb is the size of a car. Fragile wrap is instead a small sheet popped by fingertip, with the machines off: a desk-sized interlude in the middle of the hydraulic presses, worth what an ordinary sheet is.
Print on a hex grid in strokes, not pixels. A glass symbol drawn one bubble per pixel fell apart into scattered dots, because every other row is offset by half a bubble. The print is now a few strokes in a 10 × 10 box; a bubble is printed when its centre lies under the ink. Drawing those same strokes at full width was not enough, though: an ink band one bubble wide half covers the free bubbles beside it, the first player took them for print, left them, and the sheet never ended. The ink now sits on the printed domes only, joined by a thin line, so every bubble is either red or free.
Show the last bubbles. A sheet ends only when every bubble is gone, and the last few are easy to lose: on glass over a white vase, or as single pixels on a sheet of half a million. After a moment without a pop, a ring pulses round each of the last few; after a longer one, round the last tenth. On a sheet of millions the rings go round the patches of screen that still hold a bubble, once there are few such patches, and round every special one left. Two things had made city sheets hard to finish on top: thick bubbles were not drawn once they were smaller than five pixels, and a Cordon tape loop, which promises that everything inside it pops, left the thick ones. Both were rules nobody could see.
Measure the stickers too. The first player to reach the end had three stickers left and called it a skill issue. A bot career said otherwise: a thousand sheets and four stars on all three deliveries each took hours past the end, and in the city two deliveries in three were shrink wrap, so the others hardly came. Saving nine tenths from the heat gun takes more than your own pace, by construction. Now four fifths saved counts towards the mastery (nine tenths still doubles the bonus), the expert sticker asks for three stars, the sheet sticker for 400, and once the world is open the delivery with the fewest stars comes next. A bot gets all three within half an hour of the end. The pacing test’s world window also moved: a six-seed run of the unchanged game spread over 66 to 76 minutes, wider than its two seeds had shown.
Tie sound to time, not to frames. Pops were played once per frame, a handful at a time, and the voices were freed by timers. At the factory a slow computer draws fifteen frames a second instead of sixty, so it heard a quarter of the pops, and late timers kept the voices busy: the big sounds came through and the small bubbles went quiet. What a frame popped is now spread over the time that frame stood for, and the voices are counted on the audio clock.
Below a pixel, draw the material, not the bubbles. In the city a bubble is a third of a pixel, and the wrap was a flat milky veil; next to it the popped wrap read as dark holes. Fresh wrap is now a fine grain of little domes with a glare, long folds and the seams between rolls, taped down at its edges, and the mask keeps the shine only where bubbles are left, so popped wrap is clear, crinkled film. The city under it stands up off the ground: roofs over south walls, shadows to the lower right like every bubble’s, towers downtown, houses at the edge, and some buildings already wrapped for moving day.
Pace a pursuer by the player, not by the sheet. The heat gun crossing shrink wrap takes four fifths of the time this player would need at their pace so far in the break (or the last one at that workplace), so it presses a beginner and a veteran alike instead of being trivial for one and impossible for the other.
Price a side sheet in seconds, not in sheets. Jumbo and fragile wrap first paid what an ordinary sheet of that workplace was worth. In the city an ordinary sheet outlasts a whole break, so a small special sheet cleared in fifteen seconds paid six times the player’s usual rate. Each is now worth a number of seconds of the player’s own pace on ordinary wrap, and that pace is measured on ordinary wrap only, or the special sheets drag it down themselves. The first player also found jumbo wrap hard to tell apart and fragile wrap too slow: jumbo bubbles are now the same giant size at every workplace, and a small sheet carries three gold bubbles, not the twelve its rate asked for.
The fourth version added what the first player asked for next: lightning, and one more workplace, the country, where a sheet holds billions. Four more things generalise.
A power that takes from the player must pay what the player would have. Lightning pops wrap the player was about to pop anyway, and its strikes first paid the plain rate while the player’s own pops carried a combo of up to ×6: the bot earned less with lightning bought than without it. Strikes now pay the combo and keep it alive, and the test checks that a bolt at a high combo pays more than at none.
A new stage needs room of its own, or it eats the one before. The country’s promotion was first set between the city’s and the world’s, and the city shrank from half an hour to six minutes on one seed: the new workplace came out of the old one’s time. Promoting to the country at the calluses that used to open the world kept the city as it was and put the country on top.
Scale the cell, not the count. A sheet of three billion bubbles cannot be a bitset of three billion. In the country one cell stands for a hundred bubbles and every count and payout is multiplied, so the grid, the geometry and the frame budget are the city’s.
Most of a frame is pixels. At the factory, with the CPU slowed to half, a frame at twice the pixel density took a median 150 ms against 17 ms at one. Part of it was a pattern refilled on every pop at a scale that is never quite 1, which costs a resample of each pixel; laid out once, it halved. The rest is simply four times the pixels, so the sharpness is an option now, and on Auto it steps down, for good, when most frames of three seconds in a break are slower than a 30 fps screen gives. And in a game about bubbles, a round white cloud with a highlight is a bubble: the country’s clouds are one soft body each, flat below.
A bigger scale needs something known in it. The first player could hardly tell the country from the city: both were wrap over a map, with the same grain, the same tape and two machines the same size. Now the city from the last job lies down there by the river, a grey patch under its own sheet, the grain is half as coarse, the tape half as long, and the combines are a fleet of specks where the steamrollers were a few big machines. And an effect that fires on nothing looks broken: a bolt now goes only where a strike can still pop something, keeps its charge otherwise, and stays away from fragile wrap, whose print it cracked.
Built or prototyped, then deliberately not shipped. These are not open gaps — do not re-propose them, and do not treat the empty space they leave in the catalogue as an opportunity. The catalogue being thin somewhere is not on its own a reason to build there. Two of the entries below were recommended as gaps for exactly that bad reason before being turned down; an empty category is evidence about the catalogue, not about whether anyone wants to play the thing that would fill it.
One caveat on reading this list, added 2026-09-09. Three of the rejections below are of something physical, thematic or genre-shaped, and the taste judgements in them stand exactly as written. But three entries pointing the same way is a coincidence of what happened to get prototyped, not a ruling on that whole direction, and treating it as one is part of how the catalogue narrowed. These reject air hockey, line-routing sims and grid tactics. They do not reject making something physical. The fourth, the Curator, points the other way entirely — it rejects a camera-and-model game, and it is the only entry here that carries technical findings worth reusing rather than only a judgement.
Toast (a best man speech: gather material at the reception, then deliver it to eight guests who each have a private limit and turn on you when you cross it). Built, measured, played, and turned down — by the site’s owner, on the honest ground that filling a gap is not the same as being worth playing. It is the voice gap above, and that gap is now closed on purpose.
Why it was rejected, and this is the part that generalises. The system renewed and the writing did not, and the writing was the entire reason to be there. Run-to-run variety measured beautifully — 2,952 distinct rooms in 3,000 seeds, five of eight guests changing sensitivities between any two — but that is variety of constraints, and nobody arrives for the constraints. The number that mattered was never taken until afterwards: of the ~7.6 notes in your notebook, the count never seen before runs 7.6, 5.9, 4.3, 3.6, 2.6, 2.1 over successive runs. By the fourth run — about twelve minutes, exactly the horizon that matters — more than half of what you read is a joke you have already read, and a joke you have read is worth a fraction of a new one. Anything whose appeal is hand-written content has a hard ceiling you can compute before you write a word of it: divide the material by how much a run consumes. Measure the renewal of the part people come for, not the part that is easy to measure.
A second lesson about this file: it was picked because the gap list named it, which is the exact failure “Read this first” warns about, quoted in the proposal and then done anyway. An empty category still is not a reason.
Five things from the build survive it, and the first three are one lesson — a design’s central tension is a property of its structure, and you can measure whether you actually built it.
Offence as a tax is a discount on a laugh; offence as a threshold is a game. The premise was “the funniest line is the one that costs you”, priced first as a cost subtracted from the laugh. Over 300 runs at five settings of how strongly funniness correlates with risk, “always play the funniest thing” won at every setting (79 against 58 for careful play and 19 for silence) and never once blew up. A tension you pay for linearly is not a tension. Making a guest accumulate offence and turn at a private limit flipped the ordering with no renumbering at all: reckless fell to 50 and failed 43% of the time, reading the room rose to 79. If a sweep says the obvious strategy wins everywhere, the fix is structural — retuning constants only moves where it wins.
Press your luck needs a score that can go down. Expected score still rose monotonically with
speech length (60 at four beats, 108 at twenty-two), because a bad beat was floored at zero, so
talking more could only add. Deleting one Math.max — a beat that dies subtracts — produced a
real interior maximum at 10–12 beats. A “know when to stop” mechanic is decoration until
stopping too late costs, and the sweep that proves it is the sweep that exposed the fake.
A risky option that is merely survivable is a trap, not a gamble. Recklessness scored 86 when it worked against careful play’s 100 — strictly dominated, so the dangerous material was dead weight. Letting the room enjoy the danger separated the ceilings properly: reckless and careful both topped out near 155 while never-take-a-risk capped at 119. Compare the ceiling of each strategy, not only its mean. Lower mean with a higher ceiling is a real choice; lower on both is a trap.
Per-run state written onto shared content objects is invisible in one run and fatal in two.
Marking a note taken set the flag on objects in the module-level content array that every run
shares. The first playthrough was perfect and every later one had an empty reception. A manual
playthrough cannot see this by construction; the loop that caught it was measuring gather yield
across 200 runs and reported 0.1 notes of a possible 10. Run the content pipeline a few
hundred times even when it is not what you are testing — the second run is a different program.
Bots measure whether good play is rewarded, never whether bad play is punished. A sincere
beat meant to take some heat out of the room subtracted a flat amount larger than anything a
guest typically held, so one line reset the whole table and two made the game unlosable. Every
bot ladder still looked healthy, because none of them was trying to break it. One hand played by
hand found it immediately. Also: a setTimeout that changes screens must be cancelled by every
navigation, and any text keyed to a number (margin notes, verdict bands) should be cut at
measured quantiles — the transcript read “the room goes” against every line because the bands
were guessed at 3 and 7 when beats actually ran -3 to 25 with a median of 13.
A structure you draw, loaded until it breaks (sketch a bridge or a truss, watch the stress,
watch it fail). Killed at verification, before any interface existed, and the measurements are
worth keeping because they close this off: a deformable-structure game is not an open gap,
even though nothing in the catalogue simulates one (physics_simulator, fling, escapement,
interactive_buddy and fresh_oil are all rigid-body).
A proper 2D pin-jointed truss solver — stiffness matrix, member forces, Euler buckling with the yield crossover calibrated to steel at slenderness ~89 rather than chosen — validated three ways before anything was read from it: it reproduces a two-bar hand calculation exactly (0.7071 per bar), it was shown rejecting things (collinear bars and an unbraced square are mechanisms; the same square with one diagonal stands), and truss depth came out with an interior optimum that was never fitted for. Then:
| the choice the player makes | what it is worth |
|---|---|
| topology — arch, suspension, warren, pratt, shallow, deep | 1.8x |
| sizing — members matched to their forces vs even thickness | 1.5–2.1x |
| all of it: best design well built vs worst built evenly | 2.4x |
| searching every node position vs the rule found in two attempts | 1.10x |
The whole game is reachable by a stupid local rule. “Triangulate it, then thicken whatever snapped” is what any player does on their second go, and a 220-step optimiser re-sizing at every step beat it by ten per cent. Everything dramatic lives in the binary between collapses and stands, and that binary is the Curator’s trying-versus-not-trying: cleared in one attempt, with nothing above it. The usual rescue — authored levels with tight budgets and awkward terrain — is the Toast content-exhaustion problem in a hard hat.
Worth promoting to the first question asked of any proposal: can a stupid local heuristic reach the ceiling? Toast passed it (the naive rule got 83% of an oracle, and the top scores needed real nerve); this failed it at 91%. It is cheap, it runs before any interface exists, and it is the single most useful gate found so far. Look for it where greedy improvement cannot converge — interacting, non-convex choices, where fixing one thing breaks another.
One modelling note that cost a full pass: a position-based/Verlet solver cannot be used to answer this kind of question at all. It enforces distances rather than forces, so stiffness is iteration-dependent and effectively infinite, and every design “held” an identical load including a flat line across the gap. Structural questions need statics. And twice the ground truth was mine and wrong while the solver was right — an A-frame’s legs go into compression under a downward load at the apex, and a slenderness constant off by a factor of four made every stocky member buckle and deep trusses come out weaker than shallow ones, which is backwards.
A cryptic crossword that shows its working (procedurally cut grid, every clue carrying a machine-readable parse, wordplay devices introduced one at a time so the page teaches you to solve cryptics). Killed at the verification stage, before anything was built, and both measurements are worth keeping because they close the crossword gap in the list above rather than leaving it open.
Procedurally generated cryptic clues are sound and unreadable. 6,300 clues across seven devices (anagram, hidden, charade, container, reversal, deletion, double definition), built over a WordNet lexicon so the wordplay provably yields the answer. The wordplay was fine and the surfaces were word salad — “Loose madison domain”, “Stamp some of because alan” — because a cryptic clue’s surface is a sentence that has to mean something, and nothing in the construction is trying to make it mean anything. Double definitions were the one exception and are genuinely usable (“Duration distance” = LENGTH, “Proceeds outlet” = ISSUE), which is a thin base for a whole grid.
So the clues have to be authored, and an authored bank cannot fill a grid. Backtracking fill with minimum-remaining-values over random 180-degree-symmetric patterns: 0 of 40 7x7 grids filled from a bank of 300, 500 or 1,000 answers, and 6 of 40 at 2,000. A hand-written bank is a few hundred clues at the outside, so the two halves of the idea are mutually exclusive — generate enough answers to fill a grid and the clues are unreadable; write clues worth reading and there are not enough of them to fill anything.
A third route, having a model rewrite each generated clue’s surface while the wordplay is held fixed
and checked programmatically, was not testable: this sandbox’s egress policy refused
chatgpt.tobiasmue91.workers.dev outright (403 on CONNECT), unlike the session recorded above that
reached it fine. Sandbox network access is per-session and cannot be assumed either way — check
it before designing around a model, and check it before concluding you cannot.
A parser text adventure (a hand-authored world, dense implementation, a generous parser so the genre’s guess-the-verb misery never bites). Killed at the verification stage, before any prose was written. Worth keeping because it closes the interactive fiction gap — zero of 99 games and 216 tools is a parser game, and that stays true on purpose.
Three phrasing corpora, each written from the player’s side before seeing the parser’s synonym
tables, for the same four-room scene; each scored cold, then the parser was fixed against it, then
the next was written. Corpus 1, command-shaped (x mirror, take script), 239 phrasings: 68.6%.
Corpus 2, held out and more verbose, 119: 58.0%. Corpus 3, natural rambling (chuck the script,
i'm stuck, what does the noticeboard say), 99: 39.4%.
The sweep runs the wrong way, which is what killed it. The parameter the player controls is how naturally they phrase things, and sweeping it from command-shaped to conversational moves comprehension 68.6% → 39.4%. The interface gets worse exactly as the player relaxes into it. Fitting did not transfer either: every round bought ~30 points on the corpus in front of me and the next cold corpus started lower.
Three findings generalise past this idea.
Structure is worth a lot and vocabulary is worth almost nothing. Six structural rules — a
preposition of place dominates the verb (search behind X = look behind X); a light verb plus a
particle takes its meaning from the particle (get the drawer open = open drawer); question forms;
bare prepositional phrases; ditransitives; pull/push on an openable — took corpus 2 from 58.0% to
81.5% with zero regressions and no new words. A large synonym pass then bought only ten more
points. The residual is unbounded English and no amount of synonyms closes it.
The forgiving default is the dangerous one. Defaulting an unrecognised noun phrase to examine,
added to be kind, is what converts honest confusion into confident wrong action: on corpus 3, 37 of
60 failures did something silently wrong rather than admitting they were lost — chuck the script
read it, throw open the french window examined it, walk out onto the stage left by the wrong door.
Any page that takes free text should fail loudly; guessing is worse than admitting.
Write the test corpus before the thing it tests. Scoring a parser against phrasings invented after the synonym table exists measures the table, not the player.
Air hockey. Rejected. Not because the physics duplicates pong — a free 2D mallet with real
momentum transfer is a genuinely different control space from a paddle on a rail — but because
nothing about that survives to the catalogue card, where it reads as two paddles and a bouncing
thing, and the momentum transfer that makes it interesting is already the whole of 8-ball. A worse
pong next to a better 8-ball.
Line-routing sims (Mini Metro’s shape: growing demand, limited track, procedural maps). Rejected on taste. The genre as a whole, not this framing of it.
Turn-based tactics on a grid (Into the Breach’s shape: small grid, perfect information, enemies
that telegraph next turn’s move). Rejected on taste, not on mechanics — it was tried and was not
interesting enough to put on the page. strategy being the thinnest category does not resurrect it.
Nor do its variants: the version where the player has no weapon at all and every kill comes from
shoving enemies into each other, into walls, or into their own crossfire, is the same idea and is
also rejected.
Worth keeping from the prototyping, because it generalises past this idea: exhaustive enumeration showed that under 1% of legal turns were both safe and productive, which looked like difficulty and was not. A deliberately myopic greedy player — deflect unit by unit, no lookahead — survived 97% of solvable boards. Density of good plays in an action space measures nothing; the space is mostly pointless wandering. Measure instead whether a stupid heuristic reaches the good play.
Headline sub-editing (a newspaper game: headlines arrive too long for the column, you delete
words to make them fit, and the cut changes what the paper is saying — MAYOR DENIES TAKING BRIBES
FROM CITY CONTRACTOR cut down to MAYOR TAKING BRIBES). Killed at the prototype stage, before
anything was built, and the measurement is the interesting part. Tagging four headlines by
grammatical role and enumerating every cut that fits, then sweeping the column width from 8 to 38
characters: whether a cut could satisfy both the editor and the lawyer turned out to be
completely independent of the width. Two headlines were a forced compromise at every width, two
at none. The moral squeeze — the entire point — is decided when the headline is written, not by
anything the player does, so the player’s verb is not a lever on the thing the game is about. That
is the Curator fault in different clothes. Two lesser faults confirmed it: only three to five
distinct readings per headline no matter how many words it had, and every headline is spent the
moment you have found its good cut. Worth keeping generally: when an idea rests on a player
choice mattering, sweep the parameter the player controls and check the outcome actually moves
with it. The flip from DENIES TAKING to TAKING is still a lovely moment and belongs in
something; it does not carry a game on its own.
The Curator (a camera game judged by a model). Built, measured heavily, and not shipped. A curator of dubious credentials asked for eight exhibits — “something that holds liquid”, “something worn out by use”, “the least loved object in the room” — and you answered each with a photograph of whatever was in the room plus a one-line label. It ruled on the picture rather than the word, and a refusal was answered with another photograph rather than an argument.
Why it was rejected, and this is the part that generalises. The novelty in it belongs to the model, not to the player. The interesting moment is “it can see my mug”, which is a capability demo wearing a game’s clothes, and it is over the second time you see it work. The player’s action — walk to an object, point a camera — is never the clever part; all the cleverness sits on the judging side, which is backwards. And the obstacle is illegible: you cannot build a mental model of a model’s judgement, so you cannot form a theory of it, plan against it, or get better at it. That is the exact inverse of the Scatter finding, where four distinguishable ghosts and a quarter of the level spent walking away from you are what make Pac-Man learnable. Anything whose difficulty comes from a language model’s judgement is illegible by construction, and illegibility forecloses mastery. Worth weighing before building anything for the “a model you have to talk round” gap.
The measurements agreed and were misread at the time. A careless bot scoring 38% against a deliberate one’s 81% looked like the game rewarding skill; “deliberate” only meant picked a sensible object, which every player does on their first attempt, and there was no skill above that floor to climb. Measuring the difference between trying and not trying is not evidence of depth. A second structural fault, found by arithmetic after the fact: with 13/6/3 demands in the three tiers and 3/3/2 drawn per run, two of the three interpretive demands appeared in every single game, so the most distinctive part of it repeated almost exactly each time.
Five technical findings survive it, and they apply to any page here that wants to show a model a picture.
Vision through the shared worker works, several images per message included, at about 3,073
prompt tokens per call at detail:"low" — roughly half a cent for a whole eight-round session.
Nothing in the collection had ever done this: 14 pages open the camera and 27 call the model, and
the two had never met. gptranslator is the near miss, sending camera frames to Tesseract and only
the text onward.
A vision model told what to look for will report seeing it. Shown a photograph in which the
phone is a 25-pixel smudge, with the label “my cracked phone”, it answered “a cracked phone screen”
3 times out of 3 and accepted; across distant-bluff cases it accepted 9 of 12. The fix is ordering
and it is free: make it describe the frame cold, as if the label did not exist, and only then read
the label and rule. That took distant bluffs to 0 of 12 while honest, genuinely-visible answers
held at 16 of 16 and a 44-case grounding set stayed at 44/44. It is not a resolution problem —
detail:"high" scored 8 of 16 against low’s 9 of 16 at 2.8x the tokens.
The same bug was found once and not looked for twice. It was caught in the second-look call, fixed there, and the primary ruling assumed safe because it had passed 44/44 on true/false cases — which it only passed because every photograph in that set either plainly showed the thing or plainly did not. Nothing in it was too small to tell. The input class that exposes a bug has to be built on purpose.
A spoken appeal cannot be made to work. Scoring an appeal on grounded / relevant / new reproduces the council result exactly — flattery is immovable, 0 of 16 flips, scoring 0.0 on every axis — but a genuinely good grounded argument also flipped only 6%. Unwinnable in both directions. Moving the decision out of the model and onto the page fixes that (evidence would flip 16/16, flattery still 0/16), except that outright lies score “grounded” 50% of the time. Photographs are immune where words are not: a closer shot of the same thing 100%, a different object that genuinely answers 100%, an irrelevant object 0%.
A demand is only a demand if a photograph can contradict it. The interpretive tier was drafted as the interesting one and measured as the weakest: “something you kept for no reason” accepted 14 of 14 arbitrary objects. Every candidate also had to accept a genuinely correct answer, which cut “something pretending to be a material it is not” at 44% — you cannot tell laminate from oak in a photograph — and rescued another with one word, “older than you are” at 72% becoming “looks older than you are” at 94%.
Two notes on method. Twice the ground truth was wrong and the model was right — a mug base that
does carry writing, and a celadon ewer filed under kintsugi that is not a repair — and both times the
first instinct was to call it a model error. And this sandbox’s headless browser cannot reach the
network at all (every fetch resets, example.com included) while curl through the egress proxy
works, so the page was driven by intercepting its own worker request in Playwright and forwarding it
through curl: real page, real prompts, real rulings, only the socket substituted.
Reconciled against the catalogue on 2026-09-15 (109 games). built names the page that
covers the entry; rejected means it is on the “Tried and rejected” list above and should
not be re-proposed. Matching was done against titles and descriptions, not ids — three
of these were missed on an earlier pass because scatter, nine_holes and escapement
give no hint in their filenames.
games/ball_bouncing.html is a keepie-uppie and does not cover it.games/scatter.html)games/who_wants_to_be_a_millionaire.html, with games/family_feud.html
alongside)games/jigsaw.html)games/solitaire.html for Klondike, games/freecell.html for FreeCell)games/escapement.html)games/moorhuhn.html — a panorama you pan under a clock;
games/duck_hunt.html covers the fixed-screen NES shape)games/rack.html)games/checkers.html)games/backgammon.html)games/blind_crest.html)games/icy_tower.html and games/jump.html are vertical
climbers and games/void_runner.html an endless runner; a level-based side-scroller
with enemies is still open.games/escape_the_maze.html)games/8-ball.html)games/mini_tower_defense.html)games/mastermind.html)games/pulseforge.html)games/peggle.html; games/suika.html covers the merge variant)games/fresh_oil.html)games/mind_meter.html)Still open and not rejected: War, Snakes and Ladders, a bubble shooter, Roulette, Scrabble, Bingo, a side-scrolling platformer, Archery, Ice Hockey, Basketball, Dragon Slayer, Kung Fu Fighting, Treasure Hunt. Several of those are thin as written — “Dragon Slayer” and “Treasure Hunt” name no mechanism at all — so treat the list as a prompt, not a queue.