Fairway is our mini golf game. Every hole is generated, so every hole has to be proven clearable at or under its stated par before a player ever sees it.
It shipped with four verification seams: __verify, __verifyCurrent,
replayWitness and __prove. Four. That is more verification than most of our
games have.
Every one of them replayed the shot through runShot(), while the player's
ball runs through shoot() and advance(). Nothing anywhere asserted that
those two paths produce the same result.
The sabotage matrix
We took a copy of the shipped file and broke things in it, one at a time, then ran both the replay seam and a harness driving the real input path.
| Sabotage | Replay seam | Live path |
|---|---|---|
| Witness angle +0.05 rad | 0/9 red | 0/9 red |
| Near-miss counts as sunk | 8/9 red | 2/9 red |
Delete strokes++ from shoot() |
9/9 green | 0/9 red |
Extra drag in advance(), live only |
9/9 green | 0/9 red |
The two that stayed green are the two that matter.
Deleting the stroke increment makes the game unloseable — you can never exceed par, because your strokes never go up. Adding drag only to the live path makes par unreachable — the ball no longer travels as far as the proof assumed.
Both are catastrophic. Both were completely invisible to four verification seams, because all four verified a version of the game the player never touches.
The rule
A prover has to be driven through the code the player's input reaches, not through a helper that shares its physics constants.
Sharing the constants is what makes this so convincing. runShot() and
advance() used the same gravity, the same friction, the same restitution. They
agreed on everything except the parts that had drifted — and the parts that
drift are, by definition, the parts nobody is checking.
The fix was a __playWitness() that plays the proof's own witness shot through
the real input path and reports what the scoreboard actually said afterwards.
A sabotage staying green is not automatically a hole
Worth stating, because the opposite belief leads to chasing ghosts.
We enlarged the cup 10x. Both seams stayed green, and that was correct: the
generator rejects every layout the search can ace, so a bigger cup does not
produce a one-shot hole. The MIN_PAR floor was already doing that work.
We only accepted it after verifying it independently — an 864-shot lattice across the hole, 0 aces, nearest rest 3.03 units from the cup. A green sabotage is a question, not an answer.
"Proven minimum" was an overclaim
While we were in there, we found the copy was wrong in a subtler way.
A bounded search returns a proven upper bound, not a minimum. It proves the hole is clearable in N. It does not prove N is the fewest. A dense sweep — 1440 angles by 30 powers — found one-shot lines on a hole we had shipped as par 2.
The copy now says "at or under a proven par", which is exactly what the search establishes. Say what you actually proved, not the nearest impressive thing.
And a prover that runs on a budget must not leave the world half-swapped
Fairway generates in the background with a time budget. It placed the ball on the current generation attempt while the prover kept re-rolling layouts — and when it finished, it swapped in the final layout and never moved the ball.
Result: the ball standing inside a wall, on a hole it did not belong to.
We reproduced it by setting the budget to 0ms. The worst real proving time we measured was 293ms, so shipping it would have needed only a phone about three times slower than our test device. Which is to say: a phone.
Play it: Fairway · how to play
Related reading: three ways a suite passes with the rule deleted · five wrong ways to prove a racing game is fair