Suguru (Tectonic) in the browser with two sound rule sets
inside. The grid is split into blocks of 1–5 cells; a block of size k
holds the digits 1..k exactly once, and no two equal digits may touch
— not even diagonally. No rows, no columns: the block and the
neighborhood are the whole rulebook. The human move is singles (last
candidate, last home); the machine move is enumerating every remaining
fill of a block — and the enumeration fires outward: a digit pinned
next to you in every witness is a digit you can never be. Measured:
singles alone finish 11% of 6×6 boards and 0% of 8×8, and they stall at
the start, not the end. The generator had its own measured trap: 97%
of naively random 8×8 partitions admit no solution at all. Puzzle #22 in
the solver-included series.
🌐 Live demo: https://sen.ltd/portfolio/suguru/
📦 GitHub: https://github.com/sen-ltd/suguru
Strip the rows and columns off Sudoku
The rules of Suguru fit in two lines. The board is partitioned into
polyomino blocks; a block of size k contains 1..k once each; equal
digits never touch, orthogonally or diagonally.
Think of it as Sudoku with the rows and columns stripped away. What is
left is a pile of tiny all-different problems (the blocks) glued
together by a single local rule (equal digits never touch). One
structural observation does most of the design work:
digits inside a block are all distinct by definition
→ the touch constraint can never fire INSIDE a block
→ the touch constraint is always a BETWEEN-blocks constraint
So each block is a small, independently enumerable bijection problem, and
blocks interact only through adjacency. That shape is exactly the two rule
sets.
The human rules: two singles
A placed digit removes itself from its block and its 8-neighborhood —
that much is bookkeeping, not deduction. What a person scans for is:
only-candidate : a cell is down to one digit → write it
only-home : a digit is down to one cell
in its block → write it
These are Sudoku's naked and hidden singles, and they run in one linear
pass.
The machine rule: witnesses, inward and then outward
The machine can do something much stronger per block: enumerate every
remaining fill — every bijection between the blank cells and the missing
digits consistent with current candidates. A block has at most 5 cells, so
there are at most 5! = 120 witnesses. Then:
- inward — a candidate that appears in no witness is dropped (generalized arc consistency for one block constraint);
- outward — for a cell o outside the block, take, per witness, the set of digits placed adjacent to o, and intersect across witnesses. Any digit in the intersection sits next to o in every possible world — so o can never hold it.
The outward shot is the fun part, and it needs no givens at all:
A 2-cell block (a domino) holds {1,2}.
An outside cell adjacent to BOTH of its cells —
whichever way the domino is filled —
sees a 1 and a 2 next to it. It can be neither.
On the 3×3 fixture in the test suite this cascades from a completely
empty board: the top domino deletes {1,2} from the cell below it, which
pins that cell to 3 in its size-3 block; the pinned 3 then constrains the
neighboring block's witnesses and forces a 4 next door. Two cells proven,
zero givens, singles silent throughout.
// outward: OR the digits each witness puts next to o, AND across witnesses
for (let o = 0; o < outside.length; o++) {
let adj = 0;
for (const b of touching[o]) adj |= assign[b];
pinned[o] &= adj; // surviving bits sit next to o in EVERY witness
}
The Hint button runs the two sets in order and names whichever rule
fired — singles before witnesses, so a human-readable move always wins
when one exists.
Measurement 1: singles die at the opening
100 generated boards per size, propagation to fixpoint, no guessing:
singles finish alone 6×6: 11% 8×8: 0%
cells placed when stuck 6×6: 3.7 of 28.8 blanks
8×8: 7.0 of 54.4 blanks
That singles lose was expected; where they lose was not. They stall
essentially at the first move. The witness rule is not a late-game
finisher — it is load-bearing from move one. It is the mirror image of a
selection effect from earlier puzzles in this series: boards that the weak
rule set could finish are mostly filtered out by the generator's
uniqueness requirement before you ever see them.
Measurement 2: the generator's trap — 97% of random partitions are dead
Generation was supposed to be three boring steps: partition, fill, carve.
The naive first step turned out to be the story.
Grow regions by picking uniformly random frontier cells and most
partitions admit no solution at all. Satisfiable partitions out of
100:
| size | loose (uniform growth) | compact (biased growth) |
|---|---|---|
| 4×4 | 21% | 76% |
| 5×5 | 16% | 52% |
| 6×6 | 9% | 46% |
| 7×7 | 3% | 27% |
| 8×8 | 3% | 14% |
The culprit is perimeter. Uniform growth produces snaky regions; snaky
regions have long borders; every extra contact between two blocks is
another chance for their digit sets to clash. Biasing growth toward
frontier cells that already touch the region on several sides (rounder
blobs), then folding 1–2 cell scraps into small neighbors, is worth a
factor of five at 8×8 — the difference between a generator that dies and
one that retries a handful of times.
The reason these percentages can be trusted at all: the fill search agrees
with an independent brute-force solver — scan-order assignment with
direct constraint checks, sharing no code with the propagators — on which
partitions are satisfiable, pinned by tests. "The search didn't find one"
and "there is none" are different claims, and only an independent counter
separates them.
Measurement 3: two carving modes
Carve givens from the solved grid, keeping only deletions that preserve
uniqueness, and you get a minimal puzzle — every remaining given
load-bearing. You also regularly get a puzzle beyond both rule sets:
minimal carving lands "hard" (propagation cannot finish)
5×5: 13% 6×6: 31% 7×7: 33% 8×8: 59%
Unique, correct, and only findable by branching (though the search stays
shallow — 5 nodes on average even at 8×8). So the default is a second
mode: a deletion is also rejected if the witness rules can no longer
finish the board. Cost: less than 0.6 extra givens on average. Benefit:
the Hint button provably never runs dry on a fresh board.
| minimal | solvable | givens | |
|---|---|---|---|
| 8×8 | easy/med/hard 1/40/59 | easy/med 1/99 | 8.8 → 9.4 |
Soundness is a cross-check, not a code review
The standard fixture of this series: sound rule sets must agree on
solution counts when combined with search. The suite counts solutions
three ways — singles+search, witness+search, brute force — on random
partitions sprinkled with random digits, satisfiable or not, and demands
identical numbers. A rule like the outward shot is exactly the kind of
plausible-looking, aggressive deduction you should not ship without this:
delete one bit too many and some board somewhere changes its solution
count, and this test rings.
The empty-fixture deductions get the same treatment in person: the fixture
deliberately has two solutions, and the test asserts both of them agree
with everything the witness rules derived. Sound propagation can only ever
state facts that hold in every solution — so check it against every
solution.
Takeaways
- Strip the constraints and Suguru is a pile of small all-different problems plus one local glue rule. Noticing that the glue never fires inside a block hands you the design: enumerate blocks, project the consequences.
- Witness enumeration can push inferences outside the constraint's own scope: what happens in every possible world of a block is a fact about cells the block doesn't even contain.
- Measure where the weak rule set fails, not just how often. Singles die at the opening — the strong rule isn't a finisher, it's the opening act.
- A generator's enemy can be satisfiability before uniqueness: 97% of naive random 8×8 partitions are dead, and region perimeter is the culprit.
- "Unique" and "solvable by logic" are different properties. Buying the second costs less than one given per board and turns a UX promise ("Hint never runs dry") into a construction-time guarantee.
- 58 tests, TypeScript, zero runtime dependencies.
All of the code is public: https://github.com/sen-ltd/suguru

Top comments (0)