Diagram: a 136px jump against 29px of headroom under a ceiling at y=80

Players could fly over our platformer. The puff mechanic that was supposed to rescue a jump had become a way to cruise above the whole stage, so we did two things about it: made holding the puff progressively more expensive, and added a ceiling.

The ceiling was one line. const ROOF = 80; It applied to all vertical motion.

A week later, six of the seven levels could not be finished.

The arithmetic nobody did

A full jump in this game rises JUMP_V² / (2 × GRAV), which is 790² / 4600, which is 136 pixels. The tallest platform in any level sits at y=147, and the character is 38 pixels tall, so standing on it his head is at y=109.

109 minus 80 is 29.

So a jump taken from the upper half of any stage was cut to a fifth of its height and landed short. Not for bots. For anybody. The ceiling was doing to the player exactly what it was meant to do to the exploit, and nobody had subtracted the two numbers.

It did not present as a ceiling bug. It presented as the levels are too hard, which is the kind of complaint you answer by moving platforms around. We moved platforms around three times before measuring.

The part that actually matters

We had a gate for this. A script called reach.mjs whose entire job was to answer “can every gap be crossed at all”, run on every change, pulling the movement constants straight out of the game so it could never disagree with them.

It said every gap was crossable. It said that the whole time.

Because it computed the jump as a parabola under constant gravity, took off, peaked, landed. Which is what the game did until the day it did not. The gate modelled open sky. It was computing the flight of a game we had stopped running, and it was doing it with the real GRAV and the real JUMP_V, which is what made it so convincing.

The second harness was worse. A difficulty bot drove the actual game code in a sandbox, and its sandbox carried ROOF: 80 as a hardcoded literal. So when the game's value changed, the bot went on scoring a game nobody was playing, and reported a baseline it met.

Teaching the gate about the ceiling

The fix to reachFor() is eight lines: rise freely until the head reaches the ceiling, lose the rest of the climb, then fall.

const freeRise = (v * v) / (2 * GRAV);
const headroom = (fromPlatY - NUG_H) - ROOF;
if (freeRise <= headroom) return MOVE * ((v + Math.sqrt(disc)) / GRAV);
const vAtRoof = Math.sqrt(Math.max(0, v * v - 2 * GRAV * headroom));
const tUp = (v - vAtRoof) / GRAV;
const drop = headroom + dy;
if (drop < 0) return -1;
return MOVE * (tUp + Math.sqrt((2 * drop) / GRAV));

Then we put the broken value back to see whether the gate would now fail. It named four impassable gaps, at x=730, x=1130, x=1550 and x=1130 on four different levels. One of those matched, to the pixel bucket, where the bot had been dying forty times out of forty.

That last step is the one worth stealing. A gate you have only ever seen pass is not a gate yet. It is a script that has never been asked a hard question. Break the thing on purpose, watch the check go red, put it back.

Which of the two fixes was even doing anything

Since we had changed two things at once, we swept them independently against the same bot:

ceiling onceiling off
expensive puff0 wins6
flat puff0 wins40

The cost alone took the exploit from 40 wins to 6, an 85% cut, with no ceiling in play at all. A full meter now buys about 1.07 seconds of float and carries roughly 350 pixels of a 2,700-pixel level. That is a rescue, not a flight.

So the ceiling was never load-bearing. It was a second fix for a problem the first fix had already solved, and it was breaking the game to do it. It is now set above the highest point any jump can reach, where it can still catch something that throws the player off-screen and can never touch an ordinary jump.

What we took from it

Three things, in the order they cost us time.

A test that re-implements behaviour will eventually test something else. Ours pulled the real constants, which felt rigorous, and that is exactly why nobody suspected it — it was wrong in its model, not in its inputs. Where you can, drive the real code path. Where you cannot, write down which behaviours you are approximating, because that list is the list of ways the test can quietly stop being true.

A duplicated constant is a future disagreement with a date on it. Slice it out of the source or do not have it.

Change one thing. Had we shipped the puff cost by itself, the measurement would have said 40 to 6 immediately, the ceiling would never have been written, and none of this would have happened.

We build games and tools, and we write up the bugs that taught us something. Rebel Studios.