My magnets were decoration
Magnet Break is a Breakout game where the paddle never moves. You hold the left or right half of the screen to switch on a magnet and bend the ball in flight. After the MVP was done I ran a bot that never touches the magnets at all. It cleared 4 of the 12 stages.
What the game was supposed to say
The rules are short. A 96-wide paddle sits fixed at the bottom centre and does not move, ever. Instead, holding the left half of the screen switches on the left magnet and applies a horizontal acceleration of 520 px/s² to the ball; the right side is symmetric. A magnetic energy gauge caps at 100, drains at 34 per second while a magnet is on, and refills at 22 per second while it is off. Hit zero and you get a 1.2-second lockout — which is what turns "hold it down forever" into the real question, when do you pull.
There is one more layer. The ball always carries a polarity, and the left magnet makes it N while the right magnet makes it S. Blue n blocks only break against an N ball, red s blocks only against an S ball. So the central tension of the whole game is one sentence: the direction you want to travel and the polarity you need are in conflict. If an s gate guards a target on your left, switching on the left magnet turns the ball N and the gate stays shut.
All of that exists to support a single claim: the paddle does not move, so if you never switch on a magnet, nothing happens.
The kind of bug you cannot find by playing
I finished the MVP, played all twelve stages by hand, thought "yes, that works", and moved on. The problem is that the person who built the game uses the mechanic. I never once tried playing without the magnets, because the magnets are the entire game.
So I wrote scripts/noinput-probe.mjs. It does something almost insultingly simple: it feeds zero magnet input and simulates each stage for 60 seconds against the real deterministic physics. Sixty seconds is comfortably longer than any stage's time limit — if it does not fall in that window, it does not fall.
| Measurement | Result |
|---|---|
| Stages cleared with zero magnet input | 4 / 12 |
| Which ones | 5 · 7 · 8 · 10 |
A third of the game did not need the thing the game is about. That is not "too easy". That is a different game hiding inside mine.
The fix I had already shipped, and why it was wrong
The part that stung is that I had met this bug once before, during implementation, and had already "fixed" it.
Originally the ball launched straight up (vx=0). But the paddle's bounce angle is determined purely by the offset of the contact point — dead centre bounces vertical, the edges bounce at 45 degrees. A ball fired vertically from dead centre therefore oscillates up and down the centre column forever. And because I had placed every stage's guaranteed clear corridor down that same centre column, the target blocks broke on their own while nobody touched the controls. I found it in a test screenshot.
My fix at the time was a diagonal launch: vx=±90, vy=−√(250²−90²), alternating left and right on each spawn so the speed |v| = v0 = 250 stays exactly the same and only the angle changes. That does stop the ball from getting stuck in the middle. I wrote a comment calling it a required correction and moved on.
The no-input bot, run against that version, still reported 4 / 12.
The reason is obvious in hindsight. A diagonal launch just sends the ball ricocheting freely around the whole field, and over 60 seconds a free-ranging ball eventually finds the targets. The diagonal launch converted a guaranteed no-input clear into a probabilistic one — it did not remove it.
That is the sentence I actually took away from this project: a defect you reduced to a probability is a defect you did not fix. Without the measurement I would have shipped on the strength of my own comment.
The second fix: geometry instead of odds
The real fix runs in the opposite direction. Instead of adding randomness, I removed it and made the layout carry the guarantee.
- Fire the ball perfectly vertical again (
vx=0). Because the paddle is fixed and the bounce angle depends only on the contact offset, a ball launched vertically from dead centre can never leave the centre column unless a magnet moves it. That is geometry, not probability. - Never place a target (
*) in the centre column. Targets live in the outer columns only (0–2, 6–8).
Put those two lines together and the claim stops being a design statement and becomes an enforced property of the code. Without a magnet the ball cannot reach the outer columns; the targets are only in the outer columns. A no-input clear is not rare — it is impossible.
Four level-design principles fell out of that: (1) never put a target in the centre column, (2) targets go in the outer columns, (3) the column below a target must stay open, and (4) polarity gates sit directly beneath their target.
Measure both directions, or you will break the other one
Verification runs both ways. Look at one side only and a level set tips very easily into either "nobody can clear this" or "anybody can clear this".
| Measurement | Before redesign | After redesign |
|---|---|---|
No-input clears — noinput-probe.mjs | 4 / 12 | 0 / 12 |
Bot clears — npm run verify:clear | 12 / 12 | 12 / 12 |
The clearing bot in verify-clear.js had to be rewritten to handle polarity gates. The old one was a naive heuristic that aimed at the column of the nearest target, and it simply could not solve stages where polarity and direction disagree. The new one uses the same solution a human uses: build speed with the magnet on your side of travel, then burst the opposite magnet briefly to flip polarity only, and coast in on momentum. That the bot has to use that solution to pass is, incidentally, evidence that the solution exists.
Two things broke while I drove the number to zero
Measuring both directions immediately earned its keep — clearability collapsed twice during the redesign, and the clearing bot caught both.
- I sealed the approach below a target. Chasing the "target hidden behind a wall" idea in stages 11 and 12, I laid decorative blocks under the gate. The approach path disappeared and bot clears fell from 12/12 to 10/12. I restored it by restricting decoration below a gate to the middle three columns (3–5). That is where principle (3) came from.
- I put a reinforced block directly above a target. In stage 11 the ball would break the target and then bounce repeatedly off the reinforced block (
H, two hits) above it until a life was gone. Not unclearable, but from the player's chair it reads as "I won and then died". MovingHout to the outer columns (0 and 8) fixed it.
One side effect: with targets pushed outward the field looked too empty to read as Breakout at all, so I raised density with ordinary # blocks. Ordinary blocks are optional — only target blocks gate the clear — so they cost nothing in clearability.
Nailing it down with regressions
This class of bug does not stay fixed. Edit one row of a level grid and it walks back in. So test/game.spec.js now carries two "level design invariant" cases. Touch the level strings and those fail first.
The redesign also broke two existing tests, in an interesting way. Both polarity tests hit their gate from above; once the redesign parked a target block directly above the gate, the ball hit the target first and the assertion failed. I rewrote the fixtures to strike from below and flipped the assertions to match. The rule itself did not change. All 24 cases pass now.
What I keep from it
"Is my core mechanic actually required?" looks like a balance question. It is not — it is binary, and binary questions can be answered by a bot. The no-input probe is under 30 lines.
So the question I now ask before shipping is not just "can this be won?" but, first, "can this be won without the rule the game is about?" The first question asks whether the game works. The second asks whether it is the game I think I made.
Try it
Other making-of notes
- The board was dying in silence — 27% of rows were already unfixable
- The morning was harsher than the peak — a band-width paradox
- I shipped two games nobody could win
Every number here comes from §9 of the repository's SPEC.md and the output of the measurement scripts themselves. If you spot something inaccurate, tell us through contact.