A devlog by Neeraj Kotwani, who builds every game on Elipar.
Writing a sudoku generator sounds like it should take an afternoon. Fill a grid, take some numbers away, done. That is roughly the shape of it, and it is also how you produce a game that calls people wrong when they are right.
The failure is specific. If you remove too many clues, or remove the wrong ones, the grid stops having a single answer. It now has two, or five, or forty. The player works carefully through a chain of real deductions, arrives at a grid that genuinely satisfies every rule, and the game tells them a cell is wrong — because it is comparing against the one answer it happened to start from, not against the rules.
That is unarguably a broken puzzle, and from the player's side it is indistinguishable from the game having a bug in its checking. So the entire interesting part of this build was not generating puzzles. It was proving them.
Dig one square at a time, and put it back if it costs you
The generator fills a complete valid grid first, which is the easy half. Then it walks the 81 cells in random order and tries to empty each one. After every single removal it counts how many solutions the grid now has. If the answer is still exactly one, the cell stays empty. If it is anything else, the number goes straight back and the generator moves on to the next cell.
So the proof is not a check at the end. It is an invariant held across every step of construction: the puzzle is unique after the first removal, and after the second, and after the forty-fourth, because a removal that would have broken it was never kept. There is no way for a non-unique grid to reach a player, short of the counter itself being wrong.
Easy ships 44 clues, medium 34, hard 27. Those are targets rather than promises — the loop stops when it hits the target or runs out of cells it can legally empty, and it reports back how many clues it actually managed. In practice all three hit their number.
The counter has to be fast, or the idea does not work
Here is the problem with the approach above: it runs a full solve after every removal. Thirty-odd removals on a hard grid means thirty-odd solves, and a sudoku solver written the naive way — pick the first empty square, try 1 through 9, recurse — is slow on exactly the sparse grids this produces. Do that and the player taps NEW GAME and watches a spinner.
The fix is in which square the solver picks. Instead of the first empty one, it scans for the empty square with the fewest legal candidates remaining, and if it ever finds a square with exactly one, it takes it immediately and stops looking. Rows, columns and boxes are each held as a nine-bit mask, so "what can go here" is one bitwise operation and "how many things can go here" is a lookup.
Two things fall out of that. Contradictions surface almost immediately — if any empty square has zero candidates the branch dies right there rather than after a long descent. And the search naturally follows the forced moves first, which is the same thing a human does.
The counter also stops at a limit rather than enumerating everything. It is asked for at most two solutions, because the only question being asked is "is this exactly one?" — finding a second answer is enough to reject the removal, and counting the other eighteen would be wasted work.
Measured across eight grids at all three difficulties, the worst case is 2ms. There is no loading state in this game because there is nothing to load.
Tapping a number you already placed clears it
This is standard in sudoku apps and I implemented it without thinking hard about it. It then cost me an afternoon, in a way that is worth writing down because the lesson is about tests rather than sudoku.
I had a test that solved a whole board automatically to check the win condition. It walked every cell and placed the correct digit. Three of those cells already contained the correct digit, placed by a hint — so for those three, placing "the right answer" toggled them empty. The board ended one move from finished, the win never fired, and the failure looked exactly like a broken win condition.
The tell was that the same code passed on a fresh board and failed only after hints had been used. The game was right throughout; my helper was un-solving cells. I fixed the helper, and then wrote a separate test for the tap-to-clear rule itself, because it had been load-bearing behaviour that nothing actually asserted.
Scoring that cannot be farmed
Score banks every time a 3×3 box is completed rather than only on a finished grid, because a sudoku takes minutes and most sessions stop partway — a score that only arrives at the end records nothing for almost everybody.
Because of that I checked the formula against play that should not pay, not just honest play. Sitting idle scores zero. Burning all three hints and solving nothing scores zero. Brute-forcing every digit until the grid fills — 161 mistakes — wins the grid and still scores zero, because mistakes cost a flat amount while every positive term is what you deduced. Hard beats medium beats easy for identical play, via a multiplier that applies to the earnings and not to the penalties.
Sudoku is free in a browser, three difficulties, no account, no download. Every grid it has ever served you had exactly one answer, and that is checked before you see it rather than hoped for afterwards.