Doppelblock in the browser with five rule sets inside. Every row
and every column of the n×n grid holds the numbers 1 … n−2 exactly once
each plus exactly two blocked cells, and the number printed beside a
line is the sum of the numbers lying strictly between that line's two
blocks — nothing else. Puzzle #44 in the solver series.
Demo: https://sen.ltd/portfolio/doppelblock/
Repo: https://github.com/sen-ltd/doppelblock
The clue doesn't say which cells it's about
In Kakuro, the scope of a sum is drawn on the paper: you can see which cells
the 17 is talking about before you write anything. Skyscrapers, Kakurasu,
Futoshiki — same thing. The clue's variable list is fixed by the grid.
Doppelblock is not like that. The clue counts the cells between the two
blocks, and where those two cells sit is exactly the thing you are trying
to work out. The constraint's variable list is itself a variable.
That sounds like a nuisance and is actually the whole design. It means the
first useful thing to do with a printed sum is not arithmetic at all — it is
geometry. A window of length L holds L distinct values drawn from 1 … n−2,
so its sum has to lie in
[ L(L+1)/2 , L + (L−1) + … + (n−2) ]
and every value in that interval is reachable (the repo checks this against
an exhaustive subset scan, for every L and every n). Read the implication
backwards and the clue is a statement about distance:
| clue on an 8×8 | window lengths it allows |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 6 | 1, 2, 3 |
| 15 | 3, 4, 5 |
| 20 | 5 |
| 21 | 6 |
A clue of 0 pins the two blocks adjacent. A clue of 21 pins them to the two
ends of the line. Everything in between leaves two or three gaps open. So
the solver's second level, scope, reads every clue purely as a constraint
on placement, without ever inspecting a digit — and it pins 37.5% of a 4×4
and 20.3% of a 6×6 doing exactly that.
The grid is a Latin square with two symbols blurred together
Each value 1 … n−2 sits once per row and once per column, so it occupies a
permutation matrix. What is left over — the blocks — is a bipartite graph
with degree exactly 2 on both sides, and by König's edge-colouring theorem
that always splits into two more permutation matrices.
So a Doppelblock answer is exactly a Latin square of order n with two of
its n symbols painted the same colour. Two consequences, both of which
earn their keep.
The generator never searches. Peel one random perfect matching off the
still-unassigned graph per symbol — König guarantees one exists at every
step, because the remaining graph stays regular — then paint two symbols.
Every draw is a legal answer. No rejection sampling, no restarts, no capped
search with a heavy tail.
export function randomLatinSquare(n: number, rng: () => number): Uint8Array {
const square = new Uint8Array(n * n).fill(255);
const free = Array.from({ length: n }, () => new Uint8Array(n).fill(1));
for (let sym = 0; sym < n; sym++) {
// Kuhn's algorithm with randomised column order. Never fails: the
// remaining graph is regular, so Hall's condition holds.
...
}
}
And it is a machine check. The map from Latin squares to grids is onto,
and the fibre over a grid has size 2^k where k is the number of cycles in
the block pattern — each cycle can be split between the two painted symbols
two ways. So the independent engine, run on a board with no clues at all,
has to satisfy Σ 2^cycles = L(n) exactly:
| n | grids enumerated | Σ 2^cycles | L(n) |
|---|---|---|---|
| 4 | 216 | 576 | 576 |
| 5 | 66,240 | 161,280 | 161,280 |
That is a much better test than "the two engines agree", because it ties the
engine to a number nobody in this repo computed.
Five rules, and four of them never change the answer
Here is where this entry parts company with the other 43 in the series.
Usually a rule ladder is five different deductions and the interesting
question is which of them is carrying the board. Here it is one constraint
at five consistency strengths:
| level | rule |
|---|---|
line |
the permutation part alone: two blocks per line, every value once per line. The clue is never read |
scope |
+ the clue as geometry only — enumerate placements of the two blocks, keep the ones whose window length can reach the printed sum. No digit inspected |
bounds |
+ bounds consistency on the sum: a placement survives only if the inner cells' candidate minima and maxima bracket the clue |
gac |
+ domain consistency: hunt one complete (block pair, digit permutation) witness per candidate, delete the values with none |
probe |
+ singleton consistency: assume one candidate, run gac, drop it when it explodes |
Adding them one at a time, on unfiltered boards with all 2n sums printed and
nothing printed inside the grid — share of cells pinned:
| board | line | scope | bounds | gac | probe |
|---|---|---|---|---|---|
| 4×4 | 0.0% | 37.5% | 65.5% | 65.5% | 65.5% |
| 5×5 | 0.0% | 32.8% | 67.6% | 73.6% | 75.9% |
| 6×6 | 0.0% | 20.3% | 38.4% | 52.8% | 73.8% |
| 7×7 | 0.0% | 11.7% | 16.3% | 26.6% | 63.0% |
| 8×8 | 0.0% | 4.7% | 5.4% | 8.5% | 41.5% |
The first column is the first result. line pins 0.0% at every size.
With nothing printed inside the grid, "two blocks per line and every value
once" has no purchase whatsoever — it is a constraint about a board you
haven't started. 100% of this puzzle is in the sums.
Now take each rule away from the full probe ladder instead. In every other
entry in this series, this table is where the surprises are. Here it is
almost empty, and that is the finding:
| board | variant | finished | cells pinned | fixpoints that moved | line assignments built | probes |
|---|---|---|---|---|---|---|
| 6×6 | full | 28.3% | 73.9% | — | 755 | 74 |
| 6×6 | −line | 28.3% | 73.9% | 0 of 60 | 907 | 74 |
| 6×6 | −scope | 28.3% | 73.9% | 0 of 60 | 755 | 74 |
| 6×6 | −bounds | 28.3% | 73.9% | 0 of 60 | 777 | 74 |
| 6×6 | −gac | 28.3% | 73.9% | 0 of 60 | 0 | 117 |
| 6×6 | −probe | 18.3% | 54.1% | 28 of 60 | 167 | 0 |
| 8×8 | full | 0.0% | 39.7% | — | 59,534 | 591 |
| 8×8 | −line | 0.0% | 39.7% | 0 of 25 | 65,043 | 591 |
| 8×8 | −scope | 0.0% | 39.7% | 0 of 25 | 59,534 | 591 |
| 8×8 | −bounds | 0.0% | 39.7% | 0 of 25 | 60,458 | 591 |
| 8×8 | −gac | 0.0% | 29.5% | 21 of 25 | 0 | 938 |
| 8×8 | −probe | 0.0% | 13.3% | 22 of 25 | 795 | 0 |
"Fixpoints that moved" is the column that matters: it counts boards where
the ablated run ended in a bit-for-bit different candidate state, not just
a different completion verdict. Three separate results fall out.
scope is dead weight. Identical fixpoint and identical cost — 59,534
line assignments either way at 8×8, to the digit. That is not a coincidence:
bounds walks the same placements as scope and applies a strictly tighter
test, so scope cannot ever be the rule that kills a placement. I had
written the two levels as separate rules because they read differently —
one is geometry, one is arithmetic — and the ablation table is what caught
that they are the same propagator with one of them handicapped. If you ship
a rule ladder without running the ablation, this is the class of mistake you
ship with it.
line and bounds are pure accelerators. Same fixpoint on every board;
removing line makes the expensive rule build 20% more line assignments at
6×6 and 9% more at 8×8. They buy time, not answers.
gac is free until it isn't. At 6×6 the probe ladder reaches the same
place without it. At 8×8 dropping it moves 21 of 25 fixpoints and costs 10
points of pinning — while still not changing the "finished" column,
because at 8×8 that number is already zero.
Only probe moves the completion column, and it moves it a lot: 28.3% →
18.3% at 6×6, 5.0% → 0.0% at 7×7.
There is a cheerful version of this — "singleton consistency is the whole
solver, everything else is a preconditioner" — and an honest one, which is
that a nested ladder cannot produce an interesting ablation table by
construction, and the only reason to run it anyway is to find the scope
bug. I ran it and it found the scope bug.
Cheap consistency as a preconditioner, measured
If four rules exist only to make the fifth cheaper, the obvious question is
whether they do. The gac implementation uses the standard support schema —
for each still-unsupported (cell, value), hunt for one complete line
assignment that uses it, rather than enumerating the line — so it already
short-circuits hard on an easy line. Against that baseline, the cheap levels
buy less than you would hope:
| board | line assignments, gac alone |
with line+scope+bounds first |
saved |
|---|---|---|---|
| 6×6 | 177 | 155 | 12.5% |
| 7×7 | 400 | 365 | 8.7% |
| 8×8 | 606 | 563 | 7.1% |
7–12%. Where the cheap rules actually pay is one level up: without gac,
probe has to make 58% more assumptions at 6×6 (117 vs 74) and 59% more at
8×8 (938 vs 591) to get to the same place. The preconditioning that matters
is not "make the strong rule faster", it is "give the strong rule fewer
questions".
The sums that pin the geometry hardest make the worst puzzles
Since scope reads clues as distance statements, every board has a natural
sharpness: the mean number of window lengths its 2n sums leave open. Sort
boards by it, and the geometry and the puzzle quality point in opposite
directions:
| board | mean gaps left open per clue | pinned by scope
|
pinned by full probe | exactly one answer |
|---|---|---|---|---|
| 6×6, sharpest third | 1.18 | 26.6% | 58.4% | 10.6% |
| 6×6, middle third | 1.33 | 23.8% | 72.6% | 27.3% |
| 6×6, vaguest third | 1.49 | 17.2% | 84.7% | 41.2% |
| 7×7, sharpest third | 1.38 | 16.9% | 51.2% | 5.0% |
| 7×7, vaguest third | 1.71 | 12.4% | 81.9% | 37.5% |
A board full of 0s and maxima is a board where scope has the most to say
and the puzzle has the least. The reason is right there in the rule: a clue
of 0 tells you exactly where both blocks are and nothing whatsoever about
any digit. Geometric sharpness and arithmetic content are in tension, and it
is the second one the puzzle needs. If you are setting Doppelblock by hand,
this is the actionable version: an answer whose sums are all extreme is a bad
answer, however tidy it looks.
What it costs to print nothing in the grid
A published Doppelblock prints its 2n sums and leaves the grid empty. Draw a
random answer and that is usually not enough:
| board | answers |
bounds finishes |
gac |
probe |
exactly one answer |
|---|---|---|---|---|---|
| 4×4 | 300 | 56.0% | 56.0% | 56.0% | 56.0% |
| 5×5 | 300 | 45.3% | 50.7% | 54.7% | 54.7% |
| 6×6 | 200 | 10.5% | 20.0% | 29.5% | 29.5% |
| 7×7 | 120 | 0.8% | 5.0% | 11.7% | 11.7% |
| 8×8 | 80 | 0.0% | 1.3% | 2.5% | 2.5% |
At 8×8, 1 answer in 40 is pinned by its own sums. A setter who wants a clean
8×8 either searches a long way or prints a few grid cells — the median is 5
of 64. And note the last two columns: probing and uniqueness agreed on all
1,000 boards, in both directions, with no exceptions.
A null result, reported
The blocks form a 2-regular bipartite graph, so they decompose into disjoint
even cycles — one long 12-cycle, or two 6-cycles, or a 4-cycle and an
8-cycle. It is a tempting difficulty knob. It isn't one: at 6×6 the probe
ladder pins 73.4% / 71.9% / 69.4% for k = 1 / 2 / 3, and uniqueness runs
24.1% / 27.9% / 22.2%. Noise.
Soundness
Two engines that share no machinery. The propagating search branches on cell
candidates; the raw engine enumerates the grid one whole row at a time —
pick the row's two blocked columns, deal out 1 … n−2, check that row's sum,
recurse — and scores every leaf with the rule text. They agreed on 518/518
board–engine pairs, plus the census identity above, plus a test that no rule
at any level ever deletes the true value from any cell in any clue regime.
41 tests.
Solver-embedded puzzle #44. The whole thing is TypeScript with no runtime
dependencies; npm run stats regenerates every table in this post.
Demo: https://sen.ltd/portfolio/doppelblock/
Repo: https://github.com/sen-ltd/doppelblock

Top comments (0)