A Sudoku corpus built for a research paper ended up turning into nine playable puzzle games on the side. The path between those two facts is the kind of side project that becomes something interesting when nobody says no to it.
This is a walk through what the pipeline actually does, why the constraint-modeling approach produces cleaner puzzles than the usual “shuffle a solution, remove some numbers” generators, and where the approach gets brittle. If you write puzzle generators, teach constraint programming, or just want to know why some puzzles feel fair and others feel like guessing, the rest of this is for you. The work comes from a recent post on zayenz.se that walks through the generator code and the MiniZinc models behind the published packs.
What makes generated Sudoku mediocre
The standard generator (the kind you find in any open source Sudoku app) goes like this: take a fully solved board, randomly remove N numbers, ship it. The output is technically a valid Sudoku. It is also frequently ugly.
Three things go wrong with this approach:
- Asymmetry. Published Sudoku tends to have visual symmetry. The rotated or reflected clue patterns feel intentional. Pure-random removal does not produce symmetry.
- Multiple solutions. Removing numbers without checking whether the resulting board has a unique solution is the single most common bug. A “Sudoku” with two valid completions is not a puzzle. It is a coin flip.
- Bad difficulty labeling. Difficulty labels are usually hand-coded by the puzzle author and do not survive the shuffle. Players notice. A puzzle the generator calls “easy” can take twenty minutes because the clue pattern happens to require a chain that no easy-solver would have run.
The constraint-programming approach fixes all three, at a cost.
The MiniZinc + Gecode pipeline
The whole pipeline runs on two pieces of open source software. MiniZinc is a high-level constraint-modeling language (a language designed to express what a valid solution looks like, not how to find one). Gecode is the solver (the program that takes the model and searches for solutions). You write a model in MiniZinc that describes what a valid Sudoku solution looks like, and Gecode does the work of actually filling in numbers.
Sudoku itself is compact in this project and reads exactly like the rules of the game: one number per row, one per column, one per box, the clues are fixed, find a solution. That is the entire point of constraint modeling. You describe the rules, not the search.
On top of the solver, four pipeline stages run in order:
- Generation. Start from a solved grid, drop cells until the board has a unique solution, count how many deductions a fast propagation pass can make without branching. The propagation pass is the same logic Gecode uses internally to prune the search space (eliminate possibilities that cannot lead to a valid solution). If the propagation pass solves the puzzle on its own, the puzzle is “easy.” If propagation gets stuck and Gecode has to backtrack, the puzzle is harder.
- Symmetrification. Random removal gives asymmetric clue patterns. A symmetrification pass permutes rows, columns, bands, and stacks within the constraint that the puzzle structure must remain valid, then fills missing rotational partners from the known solution. The output looks like a designed puzzle.
- Uniqueness check. Re-run Gecode with a second-solution constraint. If it finds one, discard the candidate. This is the step the simple generators skip, and it is the reason most generated Sudoku is mediocre.
- Difficulty ranking. Combine propagation counts, search nodes, failure counts, and a deterministic-deductions metric. The result is a relative ordering within the pack, not an absolute guarantee that any human player will find the puzzle easy or hard.
The published pack is 500 base puzzles from a pinned corpus, sizes 6×6 and 9×9, reproducible from the seed.
The other eight games
The pipeline generalizes. Queens is a one-per-region Star Battle variant where the generator grows colored regions around a valid no-touch queen permutation and perturbs boundary cells to remove second solutions. Zip is a path-drawing puzzle modeled as a circuit constraint (a constraint that forces every cell to be visited exactly once in a single loop), with the numbered clues as additional ordering constraints. Nonogram uses a regular-language constraint (a constraint that matches cell states against a finite-state pattern, here the row and column run-length clues). Tents, Loopy, Patches, Wend, and Swend each get their own model.
The common pattern is the same. Start with a valid solution or source image, perturb until the resulting puzzle has a unique solution, rank difficulty using the same metrics, ship the static pack. The browser never runs the solver. It receives a fixed puzzle file plus its solution and renders the board.
Where the approach gets brittle
Constraint-modeling is clean for puzzles that are easy to express as constraints and miserable for puzzles that are not. Sudoku, Queens, and Zip all have clean mathematical structure. Puzzles like Slitherlink or Heyawake can be modeled in MiniZinc, but the models get longer and the difficulty ranking becomes less meaningful because the propagation pass alone does not solve them.
Icon-based generation introduces a second failure mode. Nonogram packs start from SVGs (vector image files) from pinned releases of two open source icon sets, rasterise them, and threshold the result into filled and empty cells. If the icon set changes shape (a redesign, a release with different icons), the puzzles in the pack change meaning. Pinning the icon set is the only reason this is reproducible.
Difficulty labels are also honest about their limits. The ranking is relative within a pack, not absolute. A puzzle labeled “medium” in a 9×9 pack might be easier than a puzzle labeled “easy” in a 36×36 pack. Players who treat the label as a promise will be occasionally disappointed.
What I would tell past me
Three things, if I were going to write a puzzle generator for the first time.
- Do not skip the uniqueness check. It is the one step that separates a generator from a coin flip. Gecode will do it for free if you ask. There is no excuse for shipping a puzzle with two solutions.
- Model the rules, not the search. If your generator code is longer than your model file, you are doing it the hard way. MiniZinc models read like the puzzle rules for a reason.
- Pinned dependencies matter. A reproducible corpus means pinning everything from the icon set to the solver version. The day a new release changes shape, your “easy” pack is suddenly a “hard” pack.
Trade-offs
Constraint-modeling is not free. Both the symmetrification pass and the uniqueness check add CPU cost that the naive shuffle-and-remove approach skips. For 500 puzzles, the cost is invisible. For 500,000, you are going to want a faster solver or a smaller uniqueness check.
Portability is a separate trade-off. MiniZinc and Gecode are both open source and both work on a plain Linux box. If you have ever tried to ship a constraint solver to a web browser, you already know this is not a solved problem. The author chose to ship static puzzles with stored solutions. The browser never sees the solver. That keeps the player-facing app tiny, but it also means the puzzles are fixed. There is no “generate a new one” button. If you want infinite puzzles, you are going to need a different architecture.
For a research project or a one-off pack, the constraint-modeling approach is the cleanest option I have seen. For a commercial puzzle subscription, the static-pack limitation becomes a real product constraint. The math is the same. The audience is different.
Bottom line
The best puzzle generators I have read about all use the same shape: model the rules in a constraint language, check uniqueness with a second-solution constraint, rank difficulty with the solver’s own propagation and search counts, ship a static pack with the solutions stored. The shuffle-and-remove approach is faster to write and produces puzzles that are visibly worse. If you are going to ship a generated puzzle pack, do it the constraint-modeling way, and pin your dependencies so the puzzles you shipped last year still mean the same thing next year.