Carrom gets played on verandas all over Mauritius, so I built a version for the browser. Two or four players, simulated physics, spectators, and a rating table. It runs on Cloudflare, with each live match living in its own Durable Object.

What stayed with me was how often I was wrong about my own code.

I wrote the physics myself because I wanted the simulation to be deterministic. Given the same board and the same shot, every machine should produce the same result. That lets the server send three numbers for a shot, and every player and spectator replays it locally. It keeps the data sent to spectators small. Checking that a shot starts from a legal position remains the server’s job.

Except my first version was not actually deterministic.

JavaScript allows functions such as Math.hypot, Math.sin and Math.cos to produce slightly different results across engines. Tiny numerical differences can grow over repeated collisions, leaving a phone and the server with different boards.

I rewrote the simulation loop to avoid those functions, using basic arithmetic and Math.sqrt instead. That fixed the problem I had found. A replay test now checks that the final board matches the server’s board bit for bit.

Then I gave the computer opponent an idea of position, so it would consider the board it leaves behind as well as the pot it makes. It seemed like an obvious improvement. I measured it anyway, against a version identical in every way except that it ignored position.

It won just 34% of the games.

My positional score added up a side’s opportunities across all its remaining pieces. But potting a piece removes it from that total. The score was quietly penalising the exact thing it was meant to reward.

Measuring opportunity per piece instead of in total fixed the bug. After that, the result was roughly 51% over 120 games. No clear improvement in that run.

It does block, denying the opponent around 10% of their reply shots in the tests. But the pots it passes up appear to cost about as much as the blocking gains.

I kept it because it changes how the opponent feels to play. The README says neutral, because that is what the numbers support.

The best bug came from watching long games. One run found a deadlock: one piece each, the queen still on the board, 400 shots, and nothing changing.

Two faults were interacting. The computer opponent refused to go for the queen unless it had a piece to spare, even though the queen had to be settled before anyone could win. Separately, my rules allowed a potted but uncovered queen to stay off the board after the turn passed. A match could then end without the queen ever being settled.

Fixing both brought the average game in the tests down from 173 shots to 40.

Running the game thousands of times made those mistakes visible. It also gave me a reason to stop calling something an improvement until I had measured it.