Kakuro is a crossword made of sums. Each run of white cells carries a clue, the digits come from 1 to 9, and no digit repeats inside a run. That last rule is the whole puzzle — it turns a clue into a question about which sets of distinct digits hit a total.
👉 Live, runs in your browser: https://dev48.in/game/day82-kakuro.html
I built the page to show one thing: candidate sets beat raw arithmetic. It did show that. It also showed me three defects in my own code, and none of them was the one I first went looking for.
Defect 1: it never finished
The comparison was supposed to be "solver with candidate sets" against "solver with only a running-total check". The second one hung. Not slow — hung:
$ timeout 120 node kakuro.mjs
$ echo $?
143
Exit 143 is SIGTERM. Nine white cells is 9⁹ = 387,420,489 assignments, and a running total only prunes after it has already overshot.
The fix was not to make it faster. It was to give it a budget and report hitting it:
| board | white cells | rules only | with candidate sets |
|---|---|---|---|
| the smallest cross | 4 | 138 | 46 |
| a 16-in-two corner | 4 | 4,210 | 27 |
| three across, three down | 9 | did not finish in 2,000,000 | 750 |
| an L of runs | 5 | 53,782 | 27 |
| a 4×4 with a hole | 6 | 34,933 | 144 |
"Did not finish in two million nodes" is a stronger claim than any speedup ratio. A ratio invites you to imagine the baseline eventually getting there.
Defect 2: the baseline was solving a different puzzle
Giving it a budget made it terminate, which let me compare solution counts for the first time. They disagreed:
the smallest cross withC sols 1 | sumOnly sols 2
The real puzzle has exactly one solution. My baseline found two.
Distinctness-within-a-run had been sitting behind the useCandidates flag:
function allowed(ri, d) {
if (st.sum + d > run.clue) return false;
if (!useCandidates) return true; // <-- everything below is skipped
if (st.used & (1 << (d - 1))) return false; // distinctness lived HERE
...
}
So "no pruning" also meant "repeats allowed". That is not a slower solver, it is a different puzzle. And the page's self-check said:
the candidate-set solver finds exactly what the sum-only solver finds — the prune is sound, it discards no solution
It was comparing two different games and reporting that they agreed.
Distinctness is a rule, not an optimisation. It belongs above the flag:
if (st.sum + d > run.clue) return false; // rule: never overshoot
if (st.used & (1 << (d - 1))) return false; // rule: digits in a run are DISTINCT
if (!useCandidates) return true; // everything below is inference
Everything above the flag is a rule both solvers must obey. Everything below only changes how fast the same answer is reached.
Defect 3: a self-check that was wrong about the arithmetic
t("and 16 in two cells is NOT forced", () => combinations(2, 16).length === 2,
"7+9 and... only 7+9 at length 2");
It failed, and it was right to. Two distinct digits summing to 16: 8+8 repeats, so {7,9} is the only pair. 16-in-two is as forced as 17-in-two. The label even argued itself out of the claim mid-sentence.
That is why the board I named "a 16-in-two corner" has no solutions at all rather than several.
Counting the same thing three ways
The all-23 board has 12 solutions. I did not want to take my own solver's word for it:
- Cell-at-a-time search (the page's solver): 12
- Run-at-a-time search — place a whole permutation of a valid combination per run, sharing no code with the first: 12
-
Combinatorics. The only three distinct digits summing to 23 are
{6,8,9}, since the maximum is 9+8+7=24. So every row and every column is a permutation of one 3-set — the board is a 3×3 Latin square on three symbols, and there are exactly 3! × 2 = 12.
Three routes, one number.
What I would take from it
The budget did not just stop a hang. Making the baseline terminate is what made its answers comparable, and comparing them is what exposed that it had never been the same puzzle. A test that cannot finish is not a weak test — it is a test that has never actually run, and it will agree with you forever.
Verified by a run-at-a-time solver plus the Latin-square count: 2,062 asserts, 0 failures. 28 in-page checks.
Top comments (0)