<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SEN LLC</title>
    <description>The latest articles on DEV Community by SEN LLC (@sendotltd).</description>
    <link>https://dev.to/sendotltd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3871729%2F54856949-f27c-4da1-a1c3-3db40b93c8d8.png</url>
      <title>DEV Community: SEN LLC</title>
      <link>https://dev.to/sendotltd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sendotltd"/>
    <language>en</language>
    <item>
      <title>Nurimisaki: clue positions the setter never chooses — and half the information lives in the blank cells</title>
      <dc:creator>SEN LLC</dc:creator>
      <pubDate>Thu, 13 Aug 2026 23:19:34 +0000</pubDate>
      <link>https://dev.to/sendotltd/nurimisaki-clue-positions-the-setter-never-chooses-and-half-the-information-lives-in-the-blank-2191</link>
      <guid>https://dev.to/sendotltd/nurimisaki-clue-positions-the-setter-never-chooses-and-half-the-information-lives-in-the-blank-2191</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nurimisaki&lt;/strong&gt; in the browser with &lt;strong&gt;four rule sets&lt;/strong&gt; inside. Shade some&lt;br&gt;
cells of an n×n grid: the white cells form one connected region, no 2×2&lt;br&gt;
is all white or all black, and a white cell with exactly one white&lt;br&gt;
neighbor is a &lt;strong&gt;cape&lt;/strong&gt; — with the circled cells being &lt;strong&gt;exactly&lt;/strong&gt; the&lt;br&gt;
capes. A number counts the white cells of the straight run leaving its&lt;br&gt;
cape, circle included. Puzzle #38 in the solver series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Demo: &lt;a href="https://sen.ltd/portfolio/nurimisaki/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/nurimisaki/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/sen-ltd/nurimisaki" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/nurimisaki&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwfyy31xnkrmli4uq9s1b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwfyy31xnkrmli4uq9s1b.png" alt="Nurimisaki" width="799" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I picked this puzzle for two reasons, and both live inside the single word&lt;br&gt;
&lt;em&gt;exactly&lt;/em&gt; — the clue rule is an iff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blank cells are clues
&lt;/h2&gt;

&lt;p&gt;If every cape must be circled, then a cell with nothing printed on it is a&lt;br&gt;
promise: &lt;em&gt;no cape here&lt;/em&gt;. Should that cell end up white, at least two of its&lt;br&gt;
neighbors are white too. Part of the puzzle is written in the places where&lt;br&gt;
nothing is written, and for the first time in this series I could measure&lt;br&gt;
how much (spoiler: on 6×6 boards, dropping that promise from the semantics&lt;br&gt;
takes unique boards from 12/60 down to 2/60 and triples the typical&lt;br&gt;
solution count — roughly &lt;strong&gt;half the information is negative space&lt;/strong&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The setter never places a clue
&lt;/h2&gt;

&lt;p&gt;In every sibling puzzle of this series, the setter finishes a board and&lt;br&gt;
then chooses where the clues go. In Nurimisaki the answer forces them:&lt;br&gt;
circles sit exactly on the capes of the finished shading. Clues cannot be&lt;br&gt;
added or moved — only their numbers can be removed. &lt;strong&gt;The board deals its&lt;br&gt;
own clue budget&lt;/strong&gt;: about 15 capes per 100 cells at every size, with run&lt;br&gt;
lengths averaging 3.0 regardless of size. The demand for information grows&lt;br&gt;
with n²; the budget doesn't. That structural starvation shows up in every&lt;br&gt;
table below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The configuration space
&lt;/h2&gt;

&lt;p&gt;Strip the numbers and count what a finished board can be (white connected,&lt;br&gt;
no monochrome 2×2 of either color): 12 boards on 2×2, 103 on 3×3, 2,286 on&lt;br&gt;
4×4, by plain exhaustive loops. The white region is a &lt;em&gt;skinny&lt;/em&gt; polyomino —&lt;br&gt;
it contains no 2×2 — yet the all-black ban means it must also touch every&lt;br&gt;
2×2 window: thin, but everywhere. The cape histogram comes free: of the&lt;br&gt;
2,286 4×4 boards, 14 have no cape at all (white loops and lone dots), 48&lt;br&gt;
have exactly one (a loop with a tail), and 3 capes is the mode. The two&lt;br&gt;
capeless 3×3 boards — a single center dot, and the ring around it — are&lt;br&gt;
precisely the solutions of the &lt;em&gt;empty&lt;/em&gt; puzzle, because a puzzle with no&lt;br&gt;
circles bans capes everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four rule sets
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;th&gt;rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cape&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the iff, both halves: enumerate which neighbor of a circle is its single white one (numbered runs lay out their cells and terminator), force what every viable direction agrees on; blank cells must keep two reachable white neighbors or go black&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;window&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no monochrome 2×2, either color: three-of-a-kind turns the fourth. A printed rule here — in the siblings the 2×2 pattern was connectivity's emergent shadow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bridge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;white connectivity made local with one articulation-point DFS: pockets can't be white, bridges must be. The black cells owe nothing — this rule has no black half&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;probe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;assume a color on one cell, run the rules below to a fixpoint, drop what contradicts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The law returns — and the uniqueness cliff
&lt;/h2&gt;

&lt;p&gt;300 raw boards per size, every cape numbered (density 1.0 is the natural&lt;br&gt;
baseline: the circle set is forced, numbers are the only dial there is).&lt;br&gt;
Fraction finished by the fixpoint alone:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;cape&lt;/th&gt;
&lt;th&gt;+window&lt;/th&gt;
&lt;th&gt;+bridge&lt;/th&gt;
&lt;th&gt;+probe&lt;/th&gt;
&lt;th&gt;unique in the raw stream&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;td&gt;4.0%&lt;/td&gt;
&lt;td&gt;14.0%&lt;/td&gt;
&lt;td&gt;24.3%&lt;/td&gt;
&lt;td&gt;24.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;0.3%&lt;/td&gt;
&lt;td&gt;0.3%&lt;/td&gt;
&lt;td&gt;3.0%&lt;/td&gt;
&lt;td&gt;7.0%&lt;/td&gt;
&lt;td&gt;7.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.7%&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things stare back. &lt;strong&gt;Probe equals uniqueness at every size&lt;/strong&gt; — the law&lt;br&gt;
that Kurotto broke last time holds again. Kurotto's ambiguity lived in&lt;br&gt;
paired-cell block rearrangements invisible to single-cell probing;&lt;br&gt;
Nurimisaki's window and connectivity rules apparently drag that kind of&lt;br&gt;
superposition back into a one-cell probe's field of view.&lt;/p&gt;

&lt;p&gt;And the uniqueness column is a cliff. Print &lt;strong&gt;every number the board can&lt;br&gt;
carry&lt;/strong&gt; and still only 1.3% of 10×10 boards have one answer. The siblings&lt;br&gt;
could always turn the density dial up to 100%-unique territory. Nurimisaki&lt;br&gt;
has no dial left to turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ablation: zero redundancy, a series first
&lt;/h2&gt;

&lt;p&gt;Full ladder minus one rule; the last column keeps every printed rule and&lt;br&gt;
drops only the blank cells' no-cape promise from the propagator:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;full&lt;/th&gt;
&lt;th&gt;−cape&lt;/th&gt;
&lt;th&gt;−window&lt;/th&gt;
&lt;th&gt;−bridge&lt;/th&gt;
&lt;th&gt;−blank promise&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;24.3%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;6.0%&lt;/td&gt;
&lt;td&gt;9.3%&lt;/td&gt;
&lt;td&gt;7.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;7.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.3%&lt;/td&gt;
&lt;td&gt;0.7%&lt;/td&gt;
&lt;td&gt;1.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Removing &lt;code&gt;cape&lt;/code&gt; collapses everything — the only rule that reads a clue,&lt;br&gt;
same as Cave's &lt;code&gt;−count&lt;/code&gt;. But look at the 10×10 row: &lt;strong&gt;removing any single&lt;br&gt;
rule collapses the ladder to 0.0%&lt;/strong&gt;. Kurotto's four rungs were one theorem&lt;br&gt;
poked four ways — you could delete three of them for free. Cave's corner&lt;br&gt;
rule was bridge's perfect shadow. After five straight&lt;br&gt;
redundant-but-not-useless findings, this is the series' first puzzle with&lt;br&gt;
&lt;em&gt;no redundancy at all&lt;/em&gt;: four different theorems, all load-bearing at once,&lt;br&gt;
because the boards are too starved to afford spares.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dial that never reaches
&lt;/h2&gt;

&lt;p&gt;8×8, 150 raw boards per density point, thinning only the numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;density&lt;/th&gt;
&lt;th&gt;unique&lt;/th&gt;
&lt;th&gt;probe-solved&lt;/th&gt;
&lt;th&gt;bridge-solved&lt;/th&gt;
&lt;th&gt;mean numbers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;4.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;5.3%&lt;/td&gt;
&lt;td&gt;5.3%&lt;/td&gt;
&lt;td&gt;3.3%&lt;/td&gt;
&lt;td&gt;9.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Probe = unique holds at every point of the dial, and the top of the dial&lt;br&gt;
is 5.3%. Circles alone (density 0) never pinned an 8×8 board in 150 tries —&lt;br&gt;
but small boards manage it almost: the shipped 5×5 probe-grade boards keep&lt;br&gt;
a median of &lt;strong&gt;1 number&lt;/strong&gt; on their 4 circles. There are boards whose cape&lt;br&gt;
&lt;em&gt;geometry&lt;/em&gt; nearly names the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bank you fish for, not thin down
&lt;/h2&gt;

&lt;p&gt;The siblings generate banks by thinning a generous reveal. Nurimisaki's&lt;br&gt;
full reveal is often not even unique — my test fixtures pin two hand-drawn&lt;br&gt;
boards whose fully numbered puzzles have 4 and 6 solutions, certified by&lt;br&gt;
brute force. So the generator fishes the raw stream for the rare unique&lt;br&gt;
boards, then removes numbers adversarially. A 10×10 probe-grade board&lt;br&gt;
keeps a median of 6 numbers on 17 circles.&lt;/p&gt;

&lt;p&gt;The grade lineup is dealt by the board too: past 6×6, cape-graded and&lt;br&gt;
window-graded boards &lt;strong&gt;stop existing&lt;/strong&gt; — zero found in thousands of&lt;br&gt;
attempts. The UI greys out the difficulties your chosen size cannot&lt;br&gt;
produce. Certifying uniqueness takes a median of 6 guesses at 10×10 with&lt;br&gt;
the full ladder propagating; the connectivity-free rungs already explode&lt;br&gt;
at 6×6 (mean 139.5 guesses for &lt;code&gt;cape&lt;/code&gt; alone).&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A brute force sharing no code with the ladder — DFS over free cells,
pruned only by restatements of the printed rules, leaves scored by a
standalone validator — agrees with the propagating search at all four
levels on every board it can reach: &lt;strong&gt;560/560&lt;/strong&gt; (board, level) pairs,
plus &lt;strong&gt;120/120&lt;/strong&gt; for the variant engines with the blank-cell promise
dropped.&lt;/li&gt;
&lt;li&gt;Exhaustive anchors: 12 / 103 / 2,286 circle-free configurations at
2×2 / 3×3 / 4×4, of which 4 / 2 / 14 are capeless — and those capeless
boards are exactly what every engine returns as the solutions of the
empty puzzle.&lt;/li&gt;
&lt;li&gt;Uniqueness is never inferred from solvability: every shipped board is
re-checked by the searching engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All 40 tests: &lt;code&gt;npm test&lt;/code&gt;. TypeScript + Vite, no runtime dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An iff clue rule makes silence informative: the no-cape promise of blank
cells carries roughly half the puzzle's information.&lt;/li&gt;
&lt;li&gt;Forced clue positions cap the clue budget at ~15 capes per 100 cells —
the most information-starved puzzle in the series (1.3% unique at 10×10
with every number printed).&lt;/li&gt;
&lt;li&gt;The probe-equals-uniqueness law is back — and ablation redundancy hit
zero for the first time: at 10×10, every rule is a single point of
failure.&lt;/li&gt;
&lt;li&gt;Difficulty grades are dealt, not designed: no cape- or window-grade
boards exist past 6×6.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/sen-ltd/nurimisaki" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/nurimisaki&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>puzzle</category>
    </item>
    <item>
      <title>Kurotto: the puzzle with no global rule — and the first crack in the probe-equals-uniqueness law</title>
      <dc:creator>SEN LLC</dc:creator>
      <pubDate>Thu, 13 Aug 2026 00:14:25 +0000</pubDate>
      <link>https://dev.to/sendotltd/kurotto-the-puzzle-with-no-global-rule-and-the-first-crack-in-the-probe-equals-uniqueness-law-4aaj</link>
      <guid>https://dev.to/sendotltd/kurotto-the-puzzle-with-no-global-rule-and-the-first-crack-in-the-probe-equals-uniqueness-law-4aaj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kurotto&lt;/strong&gt; in the browser with &lt;strong&gt;four rule sets&lt;/strong&gt; inside. Some cells of&lt;br&gt;
an n×n grid carry a circled number and are never shaded. Shade any of&lt;br&gt;
the remaining cells so that each circled number equals the &lt;strong&gt;total size&lt;br&gt;
of the shaded blocks&lt;/strong&gt; — orthogonally connected groups — &lt;strong&gt;sharing an&lt;br&gt;
edge with that circle&lt;/strong&gt;. That is the whole rule book. Puzzle #37 in the&lt;br&gt;
solver series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Demo: &lt;a href="https://sen.ltd/portfolio/kurotto/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/kurotto/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/sen-ltd/kurotto" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/kurotto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqw0skeoe52blo0o87ndh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqw0skeoe52blo0o87ndh.png" alt="Kurotto" width="799" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I picked this puzzle because it has &lt;strong&gt;no global rule at all&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The legal boards are… all of them
&lt;/h2&gt;

&lt;p&gt;Strip the circles away and ask what a finished board can look like:&lt;br&gt;
anything. No connectivity rule, no 2×2 ban, no border rule. All 16&lt;br&gt;
shadings of the 2×2 board are valid, all 512 of the 3×3 — checked&lt;br&gt;
exhaustively. The configuration space is the full hypercube, 2^(n²)&lt;br&gt;
boards.&lt;/p&gt;

&lt;p&gt;This series has spent entries hunting bijections between legal boards and&lt;br&gt;
samplable objects — Yin-Yang's ridge paths, Cave's lattice cycles. Kurotto&lt;br&gt;
is the degenerate end of the hunt: the bijection is the identity, and a&lt;br&gt;
uniform sample is n² coin flips. The generator needs no theorem.&lt;/p&gt;

&lt;p&gt;In exchange, everything the earlier puzzles got for free from&lt;br&gt;
connectivity theorems, Kurotto must buy with arithmetic alone — and the&lt;br&gt;
only global force left in the puzzle is the demand that &lt;strong&gt;the answer be&lt;br&gt;
unique&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four rungs — but not four theorems
&lt;/h2&gt;

&lt;p&gt;A circle's clue is bracketed by two flood fills. The &lt;strong&gt;floor&lt;/strong&gt;: the shaded&lt;br&gt;
cells already attached to it. The &lt;strong&gt;ceiling&lt;/strong&gt;: everything still reachable&lt;br&gt;
through non-white cells (its &lt;em&gt;potential region&lt;/em&gt;). The floor only rises,&lt;br&gt;
the ceiling only falls.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;th&gt;rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;clue at the ceiling → the whole potential region shades; clue at the floor → the frontier seals white (a 0-clue is this with an empty floor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;door&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a circle below its clue must grow, and every growth shades a frontier cell; one frontier cell means a forced shade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;echo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;assume one color on one cell, re-run every circle's floor/ceiling arithmetic, drop the assumption if some clue falls out of its bracket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;probe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;assume one color on one cell, run the rules below to a fixpoint, drop the assumption on contradiction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Unlike the earlier ladders these are not four different theorems — they&lt;br&gt;
are &lt;strong&gt;one theorem probed harder&lt;/strong&gt;: the same two floods read directly, at&lt;br&gt;
the frontier, under a hypothesis, under a propagated hypothesis. The&lt;br&gt;
ablation table makes this brutally visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The law breaks
&lt;/h2&gt;

&lt;p&gt;Measured on 300 raw generator boards per size (circle density 0.8 over the&lt;br&gt;
white cells, unfiltered — fraction finished by the fixpoint alone, no&lt;br&gt;
guessing):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;sum&lt;/th&gt;
&lt;th&gt;+door&lt;/th&gt;
&lt;th&gt;+echo&lt;/th&gt;
&lt;th&gt;+probe&lt;/th&gt;
&lt;th&gt;unique answers in the raw stream&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;21.7%&lt;/td&gt;
&lt;td&gt;48.7%&lt;/td&gt;
&lt;td&gt;56.3%&lt;/td&gt;
&lt;td&gt;62.0%&lt;/td&gt;
&lt;td&gt;63.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;10.7%&lt;/td&gt;
&lt;td&gt;33.0%&lt;/td&gt;
&lt;td&gt;43.0%&lt;/td&gt;
&lt;td&gt;52.0%&lt;/td&gt;
&lt;td&gt;52.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;2.0%&lt;/td&gt;
&lt;td&gt;21.7%&lt;/td&gt;
&lt;td&gt;33.0%&lt;/td&gt;
&lt;td&gt;42.0%&lt;/td&gt;
&lt;td&gt;42.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In every earlier puzzle of this series, the probe column &lt;strong&gt;equaled&lt;/strong&gt; the&lt;br&gt;
uniqueness column at every size: a sound fixpoint can't decide a cell two&lt;br&gt;
answers disagree on, so uniqueness is a ceiling — and the ladder always&lt;br&gt;
sat exactly on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kurotto is the first puzzle where it doesn't.&lt;/strong&gt; 4 of the 190 unique 6×6&lt;br&gt;
boards, 2 of 158 at 8×8, 1 of 127 at 10×10 are unique — yet no chain of&lt;br&gt;
single-cell assumptions finishes them. Confirmed independently: a brute&lt;br&gt;
force that shares no code with the ladder (pruned only by the definitional&lt;br&gt;
floor/ceiling restatement, every leaf scored by a standalone validator)&lt;br&gt;
counts exactly &lt;strong&gt;one&lt;/strong&gt; solution on each gap board, in 906–3,078 nodes&lt;br&gt;
(&lt;code&gt;tools/gapcheck.mts&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Why here? Because block arithmetic can hold two reshuffling ambiguities&lt;br&gt;
&lt;strong&gt;in superposition&lt;/strong&gt;: every single cell survives both hypotheses alone,&lt;br&gt;
and only a &lt;em&gt;pair&lt;/em&gt; of assumptions dies. The uniqueness proof lives outside&lt;br&gt;
the world a one-cell probe can see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ablation: remove anything, nothing happens
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;full&lt;/th&gt;
&lt;th&gt;−sum&lt;/th&gt;
&lt;th&gt;−door&lt;/th&gt;
&lt;th&gt;−echo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;62.0%&lt;/td&gt;
&lt;td&gt;61.7%&lt;/td&gt;
&lt;td&gt;62.0%&lt;/td&gt;
&lt;td&gt;61.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;52.0%&lt;/td&gt;
&lt;td&gt;52.0%&lt;/td&gt;
&lt;td&gt;52.0%&lt;/td&gt;
&lt;td&gt;50.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;42.0%&lt;/td&gt;
&lt;td&gt;42.0%&lt;/td&gt;
&lt;td&gt;42.0%&lt;/td&gt;
&lt;td&gt;39.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In Cave, removing the one clue-reading rule collapsed everything to 0.0%.&lt;br&gt;
Kurotto has no "only rule that reads the clues" — &lt;strong&gt;every rung reads the&lt;br&gt;
same arithmetic&lt;/strong&gt;, so every rung below probe is nearly redundant against&lt;br&gt;
the rest. Fifth entry in a row with redundant-but-not-useless rules, but&lt;br&gt;
this time it's not one rule — it's the ladder's whole lower half shadowing&lt;br&gt;
itself. The rungs still earn their keep: they define genuine difficulty&lt;br&gt;
grades in the shipped bank, and they are the deductions a human actually&lt;br&gt;
makes, in that order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the ambiguity lives — the oracle needed three certificates
&lt;/h2&gt;

&lt;p&gt;A circle can only speak about its potential region. A free cell outside&lt;br&gt;
every region is a free second solution before a single deduction is made —&lt;br&gt;
so uniqueness first demands &lt;strong&gt;full coverage&lt;/strong&gt;. Then two assassins remain:&lt;br&gt;
&lt;strong&gt;silent flips&lt;/strong&gt; (one cell whose lone flip changes no sum) and — new in&lt;br&gt;
this puzzle — &lt;strong&gt;silent pair flips&lt;/strong&gt;: two cells whose joint flip reshuffles&lt;br&gt;
blocks without any circle noticing.&lt;/p&gt;

&lt;p&gt;The uniqueness oracle behind every table reads all three certificates&lt;br&gt;
straight off the drawn solution before it ever searches; only the&lt;br&gt;
nearly-unique survivors go to the counting search. Before the pair-flip&lt;br&gt;
check existed, naive counting stalled for minutes per board — &lt;strong&gt;the&lt;br&gt;
measuring instrument itself had to learn the puzzle's ambiguity&lt;br&gt;
structure&lt;/strong&gt;. Guess-cap overflows across all sections: zero.&lt;/p&gt;

&lt;p&gt;Circle density sweep at 10×10, 150 raw boards per point:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;density&lt;/th&gt;
&lt;th&gt;unique&lt;/th&gt;
&lt;th&gt;no-silent-flip ceiling&lt;/th&gt;
&lt;th&gt;echo-solved&lt;/th&gt;
&lt;th&gt;sum-solved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;4.7%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;18.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;2.0%&lt;/td&gt;
&lt;td&gt;49.3%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;td&gt;11.3%&lt;/td&gt;
&lt;td&gt;79.3%&lt;/td&gt;
&lt;td&gt;5.3%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.78&lt;/td&gt;
&lt;td&gt;36.0%&lt;/td&gt;
&lt;td&gt;90.0%&lt;/td&gt;
&lt;td&gt;28.0%&lt;/td&gt;
&lt;td&gt;0.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;67.3%&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;60.7%&lt;/td&gt;
&lt;td&gt;20.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two honest surprises. The coverage ceiling &lt;strong&gt;never binds&lt;/strong&gt;: even at&lt;br&gt;
density 0.30, all 150 boards were fully covered — empty-board floods&lt;br&gt;
reach nearly everything, so the "cell no circle can see" assassin only&lt;br&gt;
appears in far cornerier regimes than a random stream produces. And where&lt;br&gt;
Cave's uniqueness hugged its flip ceiling all the way up, &lt;strong&gt;Kurotto's&lt;br&gt;
crawls far below it&lt;/strong&gt;: at density 0.54 the single-flip ceiling allows&lt;br&gt;
49.3% and reality delivers 2.0%. Kurotto's ambiguity is dominated by&lt;br&gt;
multi-cell block reshuffles — the same structure that broke the&lt;br&gt;
probe-equals-uniqueness law. In the series' coordinate system, Kurotto is&lt;br&gt;
Yin-Yang's kind of hunger, taken to the extreme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search effort to certify uniqueness
&lt;/h2&gt;

&lt;p&gt;On boards that pass all three certificates (100 per size):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;rule set&lt;/th&gt;
&lt;th&gt;median guesses&lt;/th&gt;
&lt;th&gt;mean&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;sum&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;29.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;door&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;8.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;echo&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;probe&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;echo&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;probe&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once echo propagates, proving uniqueness is barely a search at all — a&lt;br&gt;
median of 0 guesses at 10×10. The weak levels' searches explode with the&lt;br&gt;
free-cell count past 6×6, so they never see the larger stream. The&lt;br&gt;
shipped bank pins a 10×10 board with a median of 27–35 circles, where a&lt;br&gt;
random reveal needs ~48 (density 0.84 over the white cells) for a&lt;br&gt;
coin-flip's chance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solution counts.&lt;/strong&gt; Brute force vs propagating search at each of the
four rule levels, on every board where the brute force is feasible:
&lt;strong&gt;520/520&lt;/strong&gt; (board, level) pairs agree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exhaustive anchors.&lt;/strong&gt; 16/16 shadings of the circle-free 2×2 validate,
512/512 of the 3×3 — there really is no hidden rule. A hand-enumerated
corner circle splits its 8 completions 2/2/3/1 by sum; the brute force
reproduces it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The gap.&lt;/strong&gt; Every probe-stuck unique board behind the headline claim is
re-counted by the brute force.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All 26 tests: &lt;code&gt;npm test&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play it
&lt;/h2&gt;

&lt;p&gt;The Hint button fills in the first cell the selected level can prove, one&lt;br&gt;
deduction at a time. Every shaded block of two or more wears its current&lt;br&gt;
size in the corner — the number a Kurotto player keeps recounting.&lt;br&gt;
Circles turn green when exactly satisfied and sealed, red when they can no&lt;br&gt;
longer be met.&lt;/p&gt;

&lt;p&gt;Next up: another puzzle with a solver inside.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;SEN LLC&lt;/strong&gt; — software development experiments, shipped in public.&lt;br&gt;
More: &lt;a href="https://sen.ltd/portfolio/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>puzzle</category>
    </item>
    <item>
      <title>Cave: every answer is a hole-free polyomino — counted as lattice cycles, generated with a 3 3 stamp</title>
      <dc:creator>SEN LLC</dc:creator>
      <pubDate>Wed, 12 Aug 2026 14:38:29 +0000</pubDate>
      <link>https://dev.to/sendotltd/cave-every-answer-is-a-hole-free-polyomino-counted-as-lattice-cycles-generated-with-a-3x3-stamp-1dh6</link>
      <guid>https://dev.to/sendotltd/cave-every-answer-is-a-hole-free-polyomino-counted-as-lattice-cycles-generated-with-a-3x3-stamp-1dh6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cave&lt;/strong&gt; (also sold as Corral or Bag) in the browser with &lt;strong&gt;four rule&lt;br&gt;
sets&lt;/strong&gt; inside. Shade some cells of an n×n grid. The unshaded cells — the&lt;br&gt;
&lt;strong&gt;cave&lt;/strong&gt; — form one orthogonally connected group, every shaded &lt;strong&gt;wall&lt;/strong&gt;&lt;br&gt;
connects through walls to the border, and each number sits in the cave&lt;br&gt;
and counts the cave cells it sees in the &lt;strong&gt;four directions&lt;/strong&gt;, itself&lt;br&gt;
included, before a wall or the edge stops the view. Puzzle #36 in the&lt;br&gt;
solver series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Demo: &lt;a href="https://sen.ltd/portfolio/cave/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/cave/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/sen-ltd/cave" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/cave&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffjr5t4vrnjt57cyjh06u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffjr5t4vrnjt57cyjh06u.png" alt="Cave" width="799" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I picked this one because the answer space has a name of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The answers are the hole-free polyominoes
&lt;/h2&gt;

&lt;p&gt;Strip the numbers away and ask what a finished board can look like. The&lt;br&gt;
cave is connected; every wall reaches the border — and the second rule is&lt;br&gt;
just "no pocket of walls sealed inside the cave", i.e. &lt;strong&gt;no holes&lt;/strong&gt;. So a&lt;br&gt;
valid configuration is exactly a &lt;strong&gt;hole-free polyomino&lt;/strong&gt;, and its boundary&lt;br&gt;
— the grid edges between a cave cell and a wall or the outside — is a&lt;br&gt;
&lt;strong&gt;single closed non-crossing loop&lt;/strong&gt; on the lattice points.&lt;/p&gt;

&lt;p&gt;The correspondence is a bijection: one valid board per simple cycle of the&lt;br&gt;
(n+1)×(n+1) grid graph. That is why the same puzzle ships under two names —&lt;br&gt;
shade the cells and it's Cave, draw the fence and it's Corral. Same object.&lt;/p&gt;

&lt;p&gt;Counted, both ways. Exhaustive enumeration of clue-free boards: &lt;strong&gt;13&lt;/strong&gt; on&lt;br&gt;
2×2, &lt;strong&gt;213&lt;/strong&gt; on 3×3, &lt;strong&gt;9,349&lt;/strong&gt; on 4×4. An independently written&lt;br&gt;
simple-cycle counter on the lattice — it knows nothing about polyominoes —&lt;br&gt;
returns &lt;strong&gt;13 / 213 / 9,349&lt;/strong&gt;. All three match OEIS A140517, the cycle&lt;br&gt;
counts of the grid graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern no rule mentions
&lt;/h2&gt;

&lt;p&gt;A checkered 2×2 — cave on one diagonal, wall on the other — never survives,&lt;br&gt;
though no stated rule bans it. The cave path joining the two cave corners&lt;br&gt;
closes into a curve that &lt;strong&gt;fences one of the wall cells away from the&lt;br&gt;
border&lt;/strong&gt;. In boundary-loop language, a checkered window is a &lt;strong&gt;pinch&lt;br&gt;
point&lt;/strong&gt;, where the loop touches itself. It is connectivity's local shadow,&lt;br&gt;
and it does a full rule set's worth of work in the solver.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generator: a 3×3 stamp from digital image topology
&lt;/h2&gt;

&lt;p&gt;How do you sample hole-free polyominoes? Backtracking over shadings fights&lt;br&gt;
the structure instead of using it. Digital image processing already has the&lt;br&gt;
right tool: a cell whose flip preserves the topology of the picture is&lt;br&gt;
called a &lt;strong&gt;simple point&lt;/strong&gt;, and it is recognized &lt;strong&gt;locally&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Read the 8 cells around a candidate in a circle (off-grid counts as wall).&lt;br&gt;
The flip is safe exactly when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the circle changes color &lt;strong&gt;exactly twice&lt;/strong&gt; — one cave arc, one wall arc;&lt;/li&gt;
&lt;li&gt;an orthogonal neighbor already has the flip's new color.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consecutive ring cells are orthogonal neighbors of each other, so an arc is&lt;br&gt;
automatically connected; two changes mean both sides of the flip stay in&lt;br&gt;
one piece, and the arc condition keeps the pinch — the checkered 2×2 — out.&lt;/p&gt;

&lt;p&gt;Seed one random cell, grow, then mix with thousands of random safe flips.&lt;br&gt;
&lt;strong&gt;Every intermediate picture is a valid configuration&lt;/strong&gt;, no global&lt;br&gt;
connectivity check ever runs, and no backtracking exists to be needed.&lt;br&gt;
Verified on 1,500 generated configurations: one boundary component, zero&lt;br&gt;
pinch points, V = E (closed), 1,500/1,500.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four rule sets
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;th&gt;rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the clue arithmetic, made exact: a clue v splits as 1 + four ray extents; an extent claims its cells cave and its stopper wall; convolve the four feasible-extent sets as sum bitmasks and force what every completion agrees on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;corner&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a 2×2 admits 14 of its 16 shadings — only the two checkered ones die, and only connectivity says so&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bridge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;both connectivity rules made local, one articulation-point DFS each: cave cells stay mutually reachable through (cave ∪ free); walls stay reachable from a virtual outside node through (wall ∪ free); sealed-off cells take the other color, cut vertices take the color they carry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;probe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;assume one color on one cell, run the rules below to a fixpoint, drop the assumption if that alone is a contradiction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Incremental vs ablation — the cleanest split yet
&lt;/h2&gt;

&lt;p&gt;Measured on 300 raw generator boards per size (85% of the cave cells&lt;br&gt;
clued; unfiltered by uniqueness or difficulty, so the numbers aren't shaped&lt;br&gt;
by the property they measure — fraction finished by the fixpoint alone, no&lt;br&gt;
guessing):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;count&lt;/th&gt;
&lt;th&gt;+corner&lt;/th&gt;
&lt;th&gt;+bridge&lt;/th&gt;
&lt;th&gt;+probe&lt;/th&gt;
&lt;th&gt;unique answers in the raw stream&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;16.0%&lt;/td&gt;
&lt;td&gt;44.0%&lt;/td&gt;
&lt;td&gt;72.7%&lt;/td&gt;
&lt;td&gt;73.3%&lt;/td&gt;
&lt;td&gt;73.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;6.3%&lt;/td&gt;
&lt;td&gt;29.0%&lt;/td&gt;
&lt;td&gt;55.3%&lt;/td&gt;
&lt;td&gt;55.7%&lt;/td&gt;
&lt;td&gt;55.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;2.3%&lt;/td&gt;
&lt;td&gt;23.0%&lt;/td&gt;
&lt;td&gt;52.0%&lt;/td&gt;
&lt;td&gt;53.0%&lt;/td&gt;
&lt;td&gt;53.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The incremental table credits &lt;code&gt;corner&lt;/code&gt; with 21–28 points — a solid&lt;br&gt;
supporting role. Then run the ablation (full ladder minus one rule):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;full&lt;/th&gt;
&lt;th&gt;−count&lt;/th&gt;
&lt;th&gt;−corner&lt;/th&gt;
&lt;th&gt;−bridge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;73.3%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;73.3%&lt;/td&gt;
&lt;td&gt;44.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;55.7%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;55.7%&lt;/td&gt;
&lt;td&gt;29.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;53.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;53.0%&lt;/td&gt;
&lt;td&gt;23.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Removing &lt;code&gt;corner&lt;/code&gt; costs &lt;strong&gt;0.0 points at every size&lt;/strong&gt; — not one board.&lt;br&gt;
Corner is the shadow of connectivity, and once bridge — connectivity&lt;br&gt;
itself — enters the room, the shadow vanishes. Removing &lt;code&gt;count&lt;/code&gt; drops&lt;br&gt;
&lt;strong&gt;everything to 0.0%&lt;/strong&gt;: it is the only rule that reads the clues at all.&lt;br&gt;
That's the fourth puzzle in a row where a rule is redundant but not&lt;br&gt;
useless: corner still defines a genuine difficulty grade in the shipped&lt;br&gt;
bank, and it is the pattern a human actually spots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probe's solve rate = the uniqueness rate, again
&lt;/h2&gt;

&lt;p&gt;At every size the probe column equals the uniqueness column exactly. A&lt;br&gt;
sound fixpoint cannot decide a cell that two answers disagree on, so the&lt;br&gt;
uniqueness rate is a ceiling — and the ladder sits on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cave's ambiguity lives in single-cell flips
&lt;/h2&gt;

&lt;p&gt;Cave is starving for clues under random reveals — clues can only sit on&lt;br&gt;
cave cells, so &lt;strong&gt;every wall is a free cell forever&lt;/strong&gt;. But it starves in the&lt;br&gt;
opposite way from Yin-Yang.&lt;/p&gt;

&lt;p&gt;Call a flip &lt;em&gt;silent&lt;/em&gt; when it keeps the board valid &lt;strong&gt;and leaves every clue&lt;br&gt;
count unchanged&lt;/strong&gt;. Each silent flip is a free second solution, so&lt;br&gt;
P(unique) ≤ P(no silent flip). 10×10, 150 raw boards per point:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;density&lt;/th&gt;
&lt;th&gt;unique&lt;/th&gt;
&lt;th&gt;no-silent-flip ceiling&lt;/th&gt;
&lt;th&gt;bridge-solved&lt;/th&gt;
&lt;th&gt;count-solved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;td&gt;3.3%&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;6.0%&lt;/td&gt;
&lt;td&gt;12.0%&lt;/td&gt;
&lt;td&gt;6.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;20.7%&lt;/td&gt;
&lt;td&gt;26.0%&lt;/td&gt;
&lt;td&gt;20.7%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;38.0%&lt;/td&gt;
&lt;td&gt;47.3%&lt;/td&gt;
&lt;td&gt;37.3%&lt;/td&gt;
&lt;td&gt;2.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;65.3%&lt;/td&gt;
&lt;td&gt;72.7%&lt;/td&gt;
&lt;td&gt;64.7%&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;2.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Yin-Yang's measured uniqueness crawled far &lt;em&gt;below&lt;/em&gt; its flip ceiling — its&lt;br&gt;
ambiguity lived in multi-cell reroutes of the curve. Cave's uniqueness&lt;br&gt;
&lt;strong&gt;hugs the ceiling all the way up&lt;/strong&gt;. As bare configurations the space is&lt;br&gt;
loose (a random 10×10 board has ~50 flippable cells), but the sight-line&lt;br&gt;
arithmetic silences almost all of them, and what ambiguity survives is&lt;br&gt;
almost entirely single silent flips.&lt;/p&gt;

&lt;p&gt;Two columns deserve a stare. Clue every cave cell (density 1.00) and the&lt;br&gt;
board is &lt;strong&gt;always unique&lt;/strong&gt; — yet &lt;code&gt;count&lt;/code&gt; alone still finishes only 2% of&lt;br&gt;
them. The numbers pin the answer without being able to &lt;em&gt;reach&lt;/em&gt; it: only&lt;br&gt;
connectivity knows the cave is one room and the walls have doors. And once&lt;br&gt;
connectivity propagates, proving uniqueness stops being a search: a median&lt;br&gt;
of &lt;strong&gt;0 guesses&lt;/strong&gt; with bridge at 10×10 (mean 1.0) — while the&lt;br&gt;
connectivity-free levels already explode at 6×6, riding the never-clued&lt;br&gt;
free walls.&lt;/p&gt;

&lt;p&gt;The shipped bank's boards, thinned adversarially instead of randomly, pin a&lt;br&gt;
10×10 board with a median of 23–27 clues — half the budget a random reveal&lt;br&gt;
needs for a coin-flip's chance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;Every claim is counted two independent ways. One engine searches with the&lt;br&gt;
rule ladder propagating at each of the four levels. The other is a brute&lt;br&gt;
force sharing no code with any of them: row-major DFS over free cells,&lt;br&gt;
pruned only by the &lt;strong&gt;three definitional rules&lt;/strong&gt; (clue min/max brackets,&lt;br&gt;
cave reachability, wall-to-border reachability), every leaf scored by a&lt;br&gt;
standalone validator. On every board the brute force can reach, all levels&lt;br&gt;
must agree on the solution count: &lt;strong&gt;400/400&lt;/strong&gt; (board, level) pairs.&lt;/p&gt;

&lt;p&gt;Exhaustive anchors: clue-free 2×2 / 3×3 / 4×4 boards number &lt;strong&gt;13 / 213 /&lt;br&gt;
9,349&lt;/strong&gt;, matching the independent lattice-cycle counter on all three&lt;br&gt;
(A140517). The boundary-loop theorem is verified not just on generator&lt;br&gt;
output (that would be circular) but on &lt;strong&gt;every valid board up to 4×4&lt;/strong&gt;. And&lt;br&gt;
every flip the 3×3 stamp accepts is re-checked by the standalone validator&lt;br&gt;
— never a broken board. All 25 tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play it
&lt;/h2&gt;

&lt;p&gt;The Hint button proves one cell at a time at the selected level; the rule&lt;br&gt;
selector doubles as the difficulty dial, and the bank only serves boards&lt;br&gt;
that first become guess-free at that level. The cave's boundary is drawn&lt;br&gt;
live in purple — you can watch it close into a single fence as the board&lt;br&gt;
finishes. The moment it closes, you have also solved the Corral.&lt;/p&gt;

&lt;p&gt;Next up: probably another puzzle with a solver inside.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;SEN LLC&lt;/strong&gt; — software development experiments, shipped in public.&lt;br&gt;
More portfolio: &lt;a href="https://sen.ltd/portfolio/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>puzzle</category>
    </item>
    <item>
      <title>Yin-Yang: two global rules force one unbroken curve — every solved board is literally a yin-yang symbol</title>
      <dc:creator>SEN LLC</dc:creator>
      <pubDate>Tue, 11 Aug 2026 00:03:18 +0000</pubDate>
      <link>https://dev.to/sendotltd/yin-yang-two-global-rules-force-one-unbroken-curve-every-solved-board-is-literally-a-yin-yang-16gn</link>
      <guid>https://dev.to/sendotltd/yin-yang-two-global-rules-force-one-unbroken-curve-every-solved-board-is-literally-a-yin-yang-16gn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Yin-Yang&lt;/strong&gt; (Shiromaru-Kuromaru) in the browser with &lt;strong&gt;four rule sets&lt;/strong&gt;&lt;br&gt;
inside. Color every cell of an n×n grid black or white. Two rules only:&lt;br&gt;
&lt;strong&gt;each color forms one orthogonally connected group&lt;/strong&gt;, and &lt;strong&gt;no 2×2&lt;br&gt;
square is a single color&lt;/strong&gt;. Puzzle #35 in the solver series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Demo: &lt;a href="https://sen.ltd/portfolio/yin-yang/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/yin-yang/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/sen-ltd/yin-yang" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/yin-yang&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb20nmzuto7quapvjgqfx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb20nmzuto7quapvjgqfx.png" alt="Yin-Yang" width="799" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I picked this one because the name is a theorem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The name is a theorem
&lt;/h2&gt;

&lt;p&gt;Read the frontier — the set of grid edges separating a black cell from a&lt;br&gt;
white one — as a curve on the lattice points. The two rules translate&lt;br&gt;
directly into properties of that curve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;monochrome 2×2&lt;/strong&gt; is an interior lattice point the curve &lt;strong&gt;misses&lt;/strong&gt;:
the four cells around a point agree exactly when no frontier edge touches
it;&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;checkered 2×2&lt;/strong&gt; is a point where the curve &lt;strong&gt;crosses itself&lt;/strong&gt; — and
it's forbidden even though no rule says so: the diagonal black pair and
the diagonal white pair would each need a connecting path, and two
disjoint paths between opposite corners of a square cannot avoid crossing
in a planar grid. Connectivity's local shadow;&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;second frontier component&lt;/strong&gt; would cut the board into three or more
monochrome regions, two of which share a color they cannot reach.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So in every legal board the frontier is &lt;strong&gt;a single non-crossing curve&lt;br&gt;
through every interior lattice point&lt;/strong&gt; — a Hamiltonian path on the&lt;br&gt;
(n−1)×(n−1) interior grid, extended one edge to the border at each end.&lt;br&gt;
Rarely it closes into a Hamiltonian cycle instead, and one color becomes&lt;br&gt;
landlocked.&lt;/p&gt;

&lt;p&gt;Sanity check at 2×2: one interior point, the curve just passes through it,&lt;br&gt;
C(4,2) = 6 ways to pick its two edges, times two colorings — &lt;strong&gt;12 boards&lt;/strong&gt;.&lt;br&gt;
Exhaustive enumeration also says 12. The 3×3 count is 34 (2 of them&lt;br&gt;
landlocked: a lone stone in the center — the purest yin-yang symbols), the&lt;br&gt;
4×4 count is 96. Six counting engines agree on all of these.&lt;/p&gt;

&lt;p&gt;Across 1,500 generated boards: frontier components always 1, crossings&lt;br&gt;
always 0, border color changes always 0 or 2. Zero exceptions — which is&lt;br&gt;
why the demo's win message says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generator runs the theorem backwards
&lt;/h2&gt;

&lt;p&gt;The first generator was naive: place random colors on random cells,&lt;br&gt;
propagate, backtrack on contradiction. 6×6 took 3ms per board. &lt;strong&gt;10×10 took&lt;br&gt;
5.6 seconds&lt;/strong&gt; — scattered placements plant connectivity failures that only&lt;br&gt;
surface deep in the search.&lt;/p&gt;

&lt;p&gt;So use the theorem instead. Valid board ⟺ Hamiltonian path, so draw the&lt;br&gt;
curve, not the coloring:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;start from a serpentine Hamiltonian path over the interior lattice;&lt;/li&gt;
&lt;li&gt;mix it with &lt;strong&gt;backbite moves&lt;/strong&gt; (reattach an endpoint to a random
neighbor and reverse the tail — the standard Markov chain for random
Hamiltonian paths on grids);&lt;/li&gt;
&lt;li&gt;keep mixing until both endpoints sit on the interior's edge, extend each
to the border;&lt;/li&gt;
&lt;li&gt;flood the two sides of the curve with the two colors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Zero backtracking, &lt;strong&gt;0.6ms per 10×10 board&lt;/strong&gt; — about 9,000× faster. The&lt;br&gt;
generator benefits from the theorem before the solver ever does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four rule sets
&lt;/h2&gt;

&lt;p&gt;The solver whittles per-cell candidates &lt;code&gt;cand[k] ∈ {black, white, both}&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;th&gt;rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;window&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;enumerate the &lt;strong&gt;12 legal colorings&lt;/strong&gt; of a 2×2 (16 minus 2 monochrome minus 2 checkered) consistent with the current cells; fix what every survivor agrees on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;border&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the border cells form a cycle and each color may occupy at most one arc of it — decided border cells may never show more than 2 color changes around the ring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bridge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;connectivity made local: placed cells of a color must stay mutually reachable through (that color ∪ free); a free cell sealed off from a color can't take it, a free cut vertex between two placed cells must. One Tarjan articulation-point pass per color, O(V+E)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;probe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;assume one color on one cell, run the rules below to a fixpoint, drop the assumption if that alone is a contradiction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only &lt;strong&gt;half of the window rule is stated in the puzzle&lt;/strong&gt;. The checkered ban&lt;br&gt;
and the border-arc rule are both cheap local shadows of the one expensive&lt;br&gt;
global rule. The independent validator doesn't know about either — it&lt;br&gt;
checks the two definitional rules only, and the shadows have to emerge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incremental vs ablation — they disagree again, harder than ever
&lt;/h2&gt;

&lt;p&gt;Three sizes × 300 boards from the raw generator stream, &lt;strong&gt;unfiltered&lt;/strong&gt; by&lt;br&gt;
uniqueness or difficulty (so the numbers aren't shaped by the property they&lt;br&gt;
measure). Clue density 0.8, fraction finished by the fixpoint alone:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;window&lt;/th&gt;
&lt;th&gt;+border&lt;/th&gt;
&lt;th&gt;+bridge&lt;/th&gt;
&lt;th&gt;+probe&lt;/th&gt;
&lt;th&gt;unique in the raw stream&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;2.3%&lt;/td&gt;
&lt;td&gt;16.7%&lt;/td&gt;
&lt;td&gt;57.3%&lt;/td&gt;
&lt;td&gt;57.3%&lt;/td&gt;
&lt;td&gt;57.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;2.7%&lt;/td&gt;
&lt;td&gt;39.3%&lt;/td&gt;
&lt;td&gt;39.3%&lt;/td&gt;
&lt;td&gt;39.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;16.7%&lt;/td&gt;
&lt;td&gt;17.0%&lt;/td&gt;
&lt;td&gt;17.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ablation — full ladder minus one rule:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;full&lt;/th&gt;
&lt;th&gt;−window&lt;/th&gt;
&lt;th&gt;−border&lt;/th&gt;
&lt;th&gt;−bridge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;57.3%&lt;/td&gt;
&lt;td&gt;15.3%&lt;/td&gt;
&lt;td&gt;57.3%&lt;/td&gt;
&lt;td&gt;16.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;39.3%&lt;/td&gt;
&lt;td&gt;2.3%&lt;/td&gt;
&lt;td&gt;39.3%&lt;/td&gt;
&lt;td&gt;2.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;17.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;17.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Incrementally, &lt;code&gt;border&lt;/code&gt; looks like a supporting actor: +14.4pt at 6×6.&lt;br&gt;
Ablate it and &lt;strong&gt;nothing drops, at any size&lt;/strong&gt; — window+bridge+probe catch&lt;br&gt;
every board it would have caught. Remove &lt;code&gt;window&lt;/code&gt; or &lt;code&gt;bridge&lt;/code&gt; instead and&lt;br&gt;
10×10 collapses to 0.0%. This series keeps finding incremental/ablation&lt;br&gt;
disagreements; this is the most extreme one yet.&lt;/p&gt;

&lt;p&gt;Border still earns its keep, though: it defines a real difficulty grade —&lt;br&gt;
its bank boards (median 11 clues at 6×6) sit exactly between window's (20)&lt;br&gt;
and bridge's (8) clue budgets — and it's the rule human solvers actually&lt;br&gt;
play with. Redundant is not the same as useless, third time running.&lt;/p&gt;

&lt;p&gt;Certifying uniqueness (solution count = 1) on 100 raw 10×10 boards needs a&lt;br&gt;
median of &lt;strong&gt;527 search guesses with window alone, 21 with border, 2 with&lt;br&gt;
bridge&lt;/strong&gt;. The moment connectivity enters the propagation, uniqueness&lt;br&gt;
checking almost stops being a search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probe's solve rate = the uniqueness rate, again
&lt;/h2&gt;

&lt;p&gt;The probe column equals the uniqueness column at every size. A sound&lt;br&gt;
fixpoint cannot decide a cell that two answers disagree on, so the&lt;br&gt;
uniqueness rate is a ceiling — and the ladder sits on it, at every size and&lt;br&gt;
every density point below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yin-Yang is starving for clues
&lt;/h2&gt;

&lt;p&gt;The density sweep, 10×10, 150 boards per point:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;density&lt;/th&gt;
&lt;th&gt;unique&lt;/th&gt;
&lt;th&gt;flip ceiling E[d^flips]&lt;/th&gt;
&lt;th&gt;bridge-solved&lt;/th&gt;
&lt;th&gt;window-solved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;80.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;2.7%&lt;/td&gt;
&lt;td&gt;88.5%&lt;/td&gt;
&lt;td&gt;2.7%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;16.7%&lt;/td&gt;
&lt;td&gt;91.1%&lt;/td&gt;
&lt;td&gt;16.7%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;60.7%&lt;/td&gt;
&lt;td&gt;95.1%&lt;/td&gt;
&lt;td&gt;60.7%&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;88.7%&lt;/td&gt;
&lt;td&gt;97.5%&lt;/td&gt;
&lt;td&gt;88.7%&lt;/td&gt;
&lt;td&gt;8.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Reveal 90% of the board at random and 4 boards in 10 still have more than&lt;br&gt;
one answer.&lt;/strong&gt; Far worse than Slant's threshold (50% unique at density 0.75).&lt;/p&gt;

&lt;p&gt;To find out why, I counted &lt;strong&gt;flippable cells&lt;/strong&gt;: cells whose lone color flip&lt;br&gt;
keeps the whole board valid. Any flippable cell left unclued is a free&lt;br&gt;
second solution, so P(unique) ≤ E[density^flips]. But random boards average&lt;br&gt;
only 0.4 flippable cells at 10×10, so that ceiling is high — and the&lt;br&gt;
measured curve sits far &lt;em&gt;below&lt;/em&gt; it everywhere short of 1.0. The gap is the&lt;br&gt;
finding: Yin-Yang's ambiguity lives in &lt;strong&gt;multi-cell reroutes of the curve&lt;/strong&gt;,&lt;br&gt;
not single-cell flips. Where the frontier crosses a clue-free region, many&lt;br&gt;
paths fit, and one clue kills only one of them.&lt;/p&gt;

&lt;p&gt;The shipped bank is thinned adversarially instead (start from the full&lt;br&gt;
reveal, delete clues while the target level still finishes without&lt;br&gt;
guessing): a probe-grade 10×10 board pins the answer with a &lt;strong&gt;median of 24&lt;br&gt;
clues&lt;/strong&gt; — certainty that a random reveal of 90 cells still fails to buy 40%&lt;br&gt;
of the time. A clue is only worth something where it pins the curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Landlocked boards have a parity
&lt;/h2&gt;

&lt;p&gt;Boards whose frontier closes into a loop — one color never touching the&lt;br&gt;
border — exist, but they're rare and they carry a parity. Exhaustively:&lt;br&gt;
exactly 2 at 3×3, &lt;strong&gt;none at 4×4 or 6×6&lt;/strong&gt; (full enumeration of all interior&lt;br&gt;
subsets), and explicit constructions at 5×5 and 7×7 (they're in the tests).&lt;br&gt;
On even boards the corner 2×2 windows pin interior stones into a monochrome&lt;br&gt;
square. In 1,500 random configurations: zero closed curves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;Every claim is counted two independent ways: a propagating search at each&lt;br&gt;
of the four rule levels, and a brute force that shares no code with any of&lt;br&gt;
them — row-major DFS over free cells, pruned only by the &lt;strong&gt;two definitional&lt;br&gt;
rules&lt;/strong&gt; (a completed monochrome 2×2 is dead; placed cells of a color must&lt;br&gt;
stay mutually reachable), every leaf scored by a standalone validator.&lt;br&gt;
Wherever the brute force is feasible, all levels must agree on the solution&lt;br&gt;
count: &lt;strong&gt;360/360 (board, level) pairs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Exact anchors: the clue-free 2×2 / 3×3 / 4×4 boards count &lt;strong&gt;12 / 34 / 96&lt;/strong&gt;,&lt;br&gt;
and the frontier theorem is verified over &lt;em&gt;all&lt;/em&gt; valid boards up to 4×4 —&lt;br&gt;
not just generated ones, which would be circular, since the generator draws&lt;br&gt;
boards from curves. All 33 tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play it
&lt;/h2&gt;

&lt;p&gt;The Hint button proves one cell at a time at the selected level; the rule&lt;br&gt;
selector doubles as the difficulty dial, and the bank only serves boards&lt;br&gt;
that first become guess-free at that level. The black–white frontier is&lt;br&gt;
drawn live in purple — you can watch it join up into a single curve as the&lt;br&gt;
board closes.&lt;/p&gt;

&lt;p&gt;Next up: probably another puzzle with a solver inside.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;SEN LLC&lt;/strong&gt; — software development experiments, shipped in public.&lt;br&gt;
More portfolio: &lt;a href="https://sen.ltd/portfolio/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>puzzle</category>
    </item>
    <item>
      <title>Gokigen Naname (Slant): the no-loop rule is a counting theorem — every board is a forest with exactly 2n+1 trees</title>
      <dc:creator>SEN LLC</dc:creator>
      <pubDate>Sun, 09 Aug 2026 23:22:51 +0000</pubDate>
      <link>https://dev.to/sendotltd/gokigen-naname-slant-the-no-loop-rule-is-a-counting-theorem-every-board-is-a-forest-with-5ddo</link>
      <guid>https://dev.to/sendotltd/gokigen-naname-slant-the-no-loop-rule-is-a-counting-theorem-every-board-is-a-forest-with-5ddo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gokigen Naname&lt;/strong&gt; (Slant) in the browser with &lt;strong&gt;four rule sets&lt;/strong&gt; inside.&lt;br&gt;
Fill every cell of an n×n grid with one diagonal — &lt;strong&gt;/ or \**. A circled&lt;br&gt;
number on a lattice point counts the diagonals whose tip touches it, and&lt;br&gt;
the diagonals must **never close a loop&lt;/strong&gt;. Puzzle #34 in the solver series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Demo: &lt;a href="https://sen.ltd/portfolio/gokigen-naname/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/gokigen-naname/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/sen-ltd/gokigen-naname" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/gokigen-naname&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F92e6exf1fg95468bos60.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F92e6exf1fg95468bos60.png" alt="Gokigen Naname" width="799" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I picked this one because the rule that looks topological — no loops — turns&lt;br&gt;
out to be arithmetic.&lt;/p&gt;
&lt;h2&gt;
  
  
  The no-loop rule is V − E
&lt;/h2&gt;

&lt;p&gt;Every cell adds exactly &lt;strong&gt;one edge&lt;/strong&gt; to a graph on the (n+1)×(n+1) lattice&lt;br&gt;
points: \ joins the cell's top-left and bottom-right corners, / joins the&lt;br&gt;
other two. Either way, one edge. A finished board is therefore n² edges on&lt;br&gt;
(n+1)² vertices.&lt;/p&gt;

&lt;p&gt;A graph with no cycle has exactly V − E connected components, and&lt;br&gt;
(n+1)² − n² = 2n+1. &lt;strong&gt;Every legal Slant board is a spanning forest with&lt;br&gt;
exactly 2n+1 trees, whatever the clues say.&lt;/strong&gt; The component count is not a&lt;br&gt;
degree of freedom. Measured over 1,500 generated boards: 2n+1 trees, zero&lt;br&gt;
exceptions — which is why the demo's win message says so.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;edgeOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;br&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cornersOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;orient&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;BACK&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;br&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bl&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole puzzle is a paraphrase of that function, and every rule in the&lt;br&gt;
solver is a sentence about it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The four rule sets
&lt;/h2&gt;

&lt;p&gt;The solver prunes a candidate array &lt;code&gt;cand[k] ∈ {/, \, both}&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;th&gt;rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one clue at a time: placed diagonals reach the number → the rest turn away; the undecided cells are exactly the missing ones → they all turn in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cycle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a union-find over the lattice points; an orientation that would join two already-connected points is dead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;duet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;two orthogonally adjacent clues read as one constraint&lt;/strong&gt;: enumerate the ≤64 joint assignments of their ≤6 cells, keep only what every survivor agrees on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;probe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;assume one diagonal, run the rules below to a fixpoint, drop the assumption if that alone is a contradiction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;duet&lt;/code&gt; swallows the textbook tricks whole. "Adjacent 3s force the four outer&lt;br&gt;
diagonals in", "adjacent 1s make their flanking cells equal" — special cases&lt;br&gt;
of the enumeration.&lt;/p&gt;

&lt;p&gt;The second of those hides a &lt;strong&gt;trap&lt;/strong&gt;. What the textbook rule actually&lt;br&gt;
concludes about the two cells flanking adjacent 1s is "A and B point the same&lt;br&gt;
way" — the output is not a cell value but an &lt;strong&gt;equality between two cells&lt;/strong&gt;.&lt;br&gt;
A unary propagator cannot write that down, and the enumeration confirms it:&lt;br&gt;
both A=B=/ and A=B=\ survive, so no cell is forced. Try to translate classic&lt;br&gt;
techniques into propagators one-for-one and you discover some of them output&lt;br&gt;
relations, not values. &lt;code&gt;duet&lt;/code&gt; extracts the unary part; the leftover equality&lt;br&gt;
fires through &lt;code&gt;count&lt;/code&gt; the moment one side decides; whatever still escapes is&lt;br&gt;
&lt;code&gt;probe&lt;/code&gt;'s job. The test for adjacent 3s shows exactly this shape — four outer&lt;br&gt;
cells forced, the two middle cells still open as an XOR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;cellId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)]).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BACK&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// outer: forced by duet&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;cellId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)]).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BOTH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// middle: only a relation survives&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Incremental and ablation tell different stories
&lt;/h2&gt;

&lt;p&gt;300 raw generator boards per size, clue density 0.75, &lt;strong&gt;unfiltered&lt;/strong&gt; by&lt;br&gt;
uniqueness or difficulty — so the numbers are not shaped by the property they&lt;br&gt;
measure. Fraction finished by the fixpoint alone:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;count&lt;/th&gt;
&lt;th&gt;+cycle&lt;/th&gt;
&lt;th&gt;+duet&lt;/th&gt;
&lt;th&gt;+probe&lt;/th&gt;
&lt;th&gt;unique answers in the raw stream&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5×5&lt;/td&gt;
&lt;td&gt;78.7%&lt;/td&gt;
&lt;td&gt;82.0%&lt;/td&gt;
&lt;td&gt;82.7%&lt;/td&gt;
&lt;td&gt;83.3%&lt;/td&gt;
&lt;td&gt;83.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7×7&lt;/td&gt;
&lt;td&gt;56.3%&lt;/td&gt;
&lt;td&gt;62.7%&lt;/td&gt;
&lt;td&gt;66.7%&lt;/td&gt;
&lt;td&gt;68.3%&lt;/td&gt;
&lt;td&gt;68.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;31.7%&lt;/td&gt;
&lt;td&gt;42.7%&lt;/td&gt;
&lt;td&gt;52.7%&lt;/td&gt;
&lt;td&gt;55.0%&lt;/td&gt;
&lt;td&gt;55.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now the same rules ranked by &lt;strong&gt;ablation&lt;/strong&gt; — full ladder minus one rule, same&lt;br&gt;
boards:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;full&lt;/th&gt;
&lt;th&gt;−count&lt;/th&gt;
&lt;th&gt;−cycle&lt;/th&gt;
&lt;th&gt;−duet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5×5&lt;/td&gt;
&lt;td&gt;83.3%&lt;/td&gt;
&lt;td&gt;71.7%&lt;/td&gt;
&lt;td&gt;79.3%&lt;/td&gt;
&lt;td&gt;83.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7×7&lt;/td&gt;
&lt;td&gt;68.3%&lt;/td&gt;
&lt;td&gt;55.7%&lt;/td&gt;
&lt;td&gt;60.3%&lt;/td&gt;
&lt;td&gt;68.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;55.0%&lt;/td&gt;
&lt;td&gt;39.0%&lt;/td&gt;
&lt;td&gt;40.7%&lt;/td&gt;
&lt;td&gt;55.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Incrementally, &lt;code&gt;duet&lt;/code&gt; looks like the hero of 10×10: +10.0 points. Ablate it&lt;br&gt;
and &lt;strong&gt;not a single board is lost&lt;/strong&gt; — the probe behind it catches every board&lt;br&gt;
it would have caught. Remove &lt;code&gt;cycle&lt;/code&gt; instead and the full ladder drops 14.3&lt;br&gt;
points: probe cannot substitute for it. Same "measured against the bench"&lt;br&gt;
problem as the last two puzzles in the series. But redundant is not useless:&lt;br&gt;
what &lt;code&gt;duet&lt;/code&gt; buys is finishing boards &lt;em&gt;without guessing&lt;/em&gt; that &lt;code&gt;cycle&lt;/code&gt; alone&lt;br&gt;
cannot — precisely what a difficulty grade measures, so the shipped bank's&lt;br&gt;
four grades include a duet tier, and every (size, grade) bucket is full.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;−count&lt;/code&gt; column is my favourite: delete the fundamental rule and the&lt;br&gt;
ladder still solves 39% of 10×10 boards, because &lt;code&gt;duet&lt;/code&gt; quietly re-derives&lt;br&gt;
counting for every clue that has an orthogonal neighbour. At density 0.75&lt;br&gt;
most clues do; only the isolated ones go completely invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probe = uniqueness, again
&lt;/h2&gt;

&lt;p&gt;In the first table the &lt;code&gt;probe&lt;/code&gt; column equals the unique-answer column &lt;strong&gt;at&lt;br&gt;
every size&lt;/strong&gt; — and across all eleven points of the density sweep below, too.&lt;br&gt;
A sound fixpoint can never finish a board with two answers, so "solved by&lt;br&gt;
rules" is capped by the uniqueness rate. The ladder sits exactly on that&lt;br&gt;
ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slant is clue-hungry
&lt;/h2&gt;

&lt;p&gt;Sweeping clue density on 10×10 boards, 150 per point:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;density&lt;/th&gt;
&lt;th&gt;unique&lt;/th&gt;
&lt;th&gt;probe&lt;/th&gt;
&lt;th&gt;count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.55&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;11.3%&lt;/td&gt;
&lt;td&gt;11.3%&lt;/td&gt;
&lt;td&gt;2.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;50.7%&lt;/td&gt;
&lt;td&gt;50.7%&lt;/td&gt;
&lt;td&gt;28.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;90.7%&lt;/td&gt;
&lt;td&gt;90.7%&lt;/td&gt;
&lt;td&gt;85.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The threshold is sharp: nearly nothing decides at 0.55, everything at 0.95.&lt;br&gt;
One clue is a weak constraint — a number 0–4 over at most four cells —&lt;br&gt;
against n² bits of board freedom. Published Slant puzzles put numbers on more&lt;br&gt;
than half the lattice points, and that is not a stylistic choice; it is&lt;br&gt;
information-theoretic necessity. (Degree distribution of the raw stream at&lt;br&gt;
10×10: 0/1/2/3/4 = 8.6 / 38.5 / 34.8 / 15.1 / 2.9% — clue 4s are rare by&lt;br&gt;
statistics, not by design.)&lt;/p&gt;

&lt;p&gt;Certifying uniqueness barely needs search at the top of the ladder: over 100&lt;br&gt;
raw 10×10 boards, the median number of branchings to confirm the solution&lt;br&gt;
count was 1 for &lt;code&gt;count&lt;/code&gt;, 1 for &lt;code&gt;cycle&lt;/code&gt;, and &lt;strong&gt;0 for both &lt;code&gt;duet&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;probe&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking it
&lt;/h2&gt;

&lt;p&gt;Two independent counters score every board: a propagating search run at each&lt;br&gt;
of the four rule levels, and a brute force that shares none of their code —&lt;br&gt;
row-major cell order, nothing but degree arithmetic and a rollback&lt;br&gt;
union-find, every leaf judged by a standalone validator. All levels must&lt;br&gt;
agree with the brute force on the number of solutions of every board — a&lt;br&gt;
sound rule set cannot change how many solutions a board has.&lt;/p&gt;

&lt;p&gt;One exact anchor: &lt;strong&gt;a clueless 2×2 board has 15 fillings&lt;/strong&gt; — 2⁴ = 16&lt;br&gt;
assignments minus the single diamond loop around the centre point. All five&lt;br&gt;
counters agree on 15. 27 tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play it
&lt;/h2&gt;

&lt;p&gt;The Hint button fills one provable cell at the selected level; the rules&lt;br&gt;
selector doubles as difficulty, and the bank only serves boards that first&lt;br&gt;
become guess-free at that level. A probe-grade 10×10 needs an assumption even&lt;br&gt;
after count, cycle and duet have all had their say.&lt;/p&gt;

&lt;p&gt;Next up: another puzzle with the solver built in.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;SEN LLC&lt;/strong&gt; — public experiments in software.&lt;br&gt;
More of the portfolio: &lt;a href="https://sen.ltd/portfolio/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>puzzle</category>
    </item>
    <item>
      <title>Tentai Show: one mirror map is the whole puzzle, and every deduction happens twice or not at all</title>
      <dc:creator>SEN LLC</dc:creator>
      <pubDate>Sat, 08 Aug 2026 23:30:04 +0000</pubDate>
      <link>https://dev.to/sendotltd/tentai-show-one-mirror-map-is-the-whole-puzzle-and-every-deduction-happens-twice-or-not-at-all-29ml</link>
      <guid>https://dev.to/sendotltd/tentai-show-one-mirror-map-is-the-whole-puzzle-and-every-deduction-happens-twice-or-not-at-all-29ml</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tentai Show&lt;/strong&gt; (Spiral Galaxies) in the browser with &lt;strong&gt;four rule sets&lt;/strong&gt;&lt;br&gt;
inside. Divide the grid into regions, one per dot, so that every region&lt;br&gt;
&lt;strong&gt;contains its dot, is connected, and maps onto itself under a 180° turn&lt;br&gt;
about that dot&lt;/strong&gt;. Dots sit on cell centres, edge midpoints, or lattice&lt;br&gt;
points. Puzzle #33 in the solver series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Demo: &lt;a href="https://sen.ltd/portfolio/tentai-show/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/tentai-show/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/sen-ltd/tentai-show" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/tentai-show&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2qnvs23j8zbg5bxcdjh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2qnvs23j8zbg5bxcdjh.png" alt="Tentai Show" width="799" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I picked this one because the whole puzzle folds into a single map. The rules&lt;br&gt;
say three things — contains its dot, connected, 180°-symmetric — and once you&lt;br&gt;
start implementing, two and a half of those turn out to be sentences about one&lt;br&gt;
one-line function.&lt;/p&gt;
&lt;h2&gt;
  
  
  Doubled coordinates and the three kinds of dot
&lt;/h2&gt;

&lt;p&gt;A dot can sit on a cell centre, an edge midpoint, or a lattice point. That&lt;br&gt;
looks like a case analysis until you double the coordinates. Put the centre of&lt;br&gt;
cell (r, c) at (2r+1, 2c+1); a dot position (cy, cx) with integers in&lt;br&gt;
[1, 2n−1] is a cell centre when both are odd, an edge when exactly one is&lt;br&gt;
even, a lattice point when both are even. And the mirror of cell k through dot&lt;br&gt;
g is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mirrorOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Galaxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;rowOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;colOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2·cy − (2r+1) is odd whatever the parity of cy, so &lt;strong&gt;the mirror of a cell is&lt;br&gt;
always exactly one cell&lt;/strong&gt;, for every kind of dot. The case analysis only ever&lt;br&gt;
resurfaces when enumerating the 1, 2 or 4 cells a dot physically touches.&lt;/p&gt;
&lt;h2&gt;
  
  
  The four-rule ladder
&lt;/h2&gt;

&lt;p&gt;The solver prunes a candidate array &lt;code&gt;cand[g][k]&lt;/code&gt; — "galaxy g can still own&lt;br&gt;
cell k".&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;th&gt;rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mirror&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a cell and its twin stand or fall together — in both directions: a dead twin kills the cell, and a pinned cell pins its twin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reach&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a region is connected and contains its dot, so g dies wherever a cell cannot walk home to the dot through surviving candidates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bridge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a cell pinned to g must stay connected to its dot; a cell on the only remaining corridor is taken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;probe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;assume one pairing, run the rules below to a fixpoint, drop the pairing if that alone is a contradiction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  The pair theorem — every deduction happens twice
&lt;/h2&gt;

&lt;p&gt;One invariant falls out of the construction: &lt;strong&gt;each galaxy's candidate set&lt;br&gt;
stays 180°-symmetric about its dot through the entire ladder.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each rule commutes with the rotation. The &lt;code&gt;mirror&lt;/code&gt; closure is symmetric by&lt;br&gt;
definition. The &lt;code&gt;reach&lt;/code&gt; BFS walks a symmetric set from symmetric sources (the&lt;br&gt;
cells the dot touches), so the reached set is symmetric. If a corridor cell&lt;br&gt;
forced by &lt;code&gt;bridge&lt;/code&gt; is an articulation point, so is its twin. A &lt;code&gt;probe&lt;/code&gt;&lt;br&gt;
contradiction replays verbatim on the mirrored side. So &lt;strong&gt;prune a candidate on&lt;br&gt;
one side of a dot and the twin falls in the same round&lt;/strong&gt;. Deductions happen&lt;br&gt;
twice or not at all.&lt;/p&gt;

&lt;p&gt;This is measurable: 3 sizes × 300 boards × 4 levels = &lt;strong&gt;3,600 fixpoints,&lt;br&gt;
zero asymmetric candidate sets&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A corollary comes free. A galaxy whose dot sits on an edge or corner covers an&lt;br&gt;
even number of cells; a centre dot covers an odd number. That parity rule&lt;br&gt;
looks worth implementing — and &lt;strong&gt;it can never fire&lt;/strong&gt;. Under the symmetric&lt;br&gt;
closure, undecided cells arrive in twins, and a twin contributes zero or two&lt;br&gt;
cells: every parity the rule could check is already correct. A rule that&lt;br&gt;
cannot fire is a theorem of the rules below it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The trap I stepped in — writing half of the mirror rule
&lt;/h2&gt;

&lt;p&gt;My first &lt;code&gt;mirror&lt;/code&gt; said: "if g is dead at the twin, kill it here". That&lt;br&gt;
transfers candidate &lt;strong&gt;death&lt;/strong&gt;. But the definition of symmetry is stronger — if&lt;br&gt;
a cell is &lt;em&gt;in&lt;/em&gt; g's region, so is its twin. The information "this cell's&lt;br&gt;
candidates collapsed to g" never reaches the twin through death-transfer&lt;br&gt;
alone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Ownership transfers too: a cell pinned to g pins its twin through g.&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;nc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;assignedAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mirror&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;nc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;galaxies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;cand&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;nc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="nx"&gt;cand&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;nc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that half missing, grading boards by the weakest level that finishes them&lt;br&gt;
&lt;strong&gt;collapsed to {mirror, probe}&lt;/strong&gt;: all sixteen 7×7 bank boards graded probe.&lt;br&gt;
Pinned cells never propagated, so every deduction the middle rules should have&lt;br&gt;
made was swallowed by probe. With both directions in, the same generator and&lt;br&gt;
seeds spread the 7×7 bank to mirror 9 / reach 6 / bridge 1. Before measuring&lt;br&gt;
how strong your rule sets are, check that each rule uses &lt;strong&gt;all&lt;/strong&gt; of its own&lt;br&gt;
definition.&lt;/p&gt;

&lt;h2&gt;
  
  
  The connectivity cliff
&lt;/h2&gt;

&lt;p&gt;300 raw boards per size, &lt;strong&gt;unfiltered&lt;/strong&gt; — filtering the population by the&lt;br&gt;
property you measure makes the column meaningless. Fraction finished by the&lt;br&gt;
fixpoint alone:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;mirror&lt;/th&gt;
&lt;th&gt;+reach&lt;/th&gt;
&lt;th&gt;+bridge&lt;/th&gt;
&lt;th&gt;+probe&lt;/th&gt;
&lt;th&gt;unique answers in the raw stream&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5×5&lt;/td&gt;
&lt;td&gt;94.3%&lt;/td&gt;
&lt;td&gt;97.7%&lt;/td&gt;
&lt;td&gt;98.0%&lt;/td&gt;
&lt;td&gt;98.0%&lt;/td&gt;
&lt;td&gt;98.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7×7&lt;/td&gt;
&lt;td&gt;63.3%&lt;/td&gt;
&lt;td&gt;93.3%&lt;/td&gt;
&lt;td&gt;95.3%&lt;/td&gt;
&lt;td&gt;95.3%&lt;/td&gt;
&lt;td&gt;95.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;2.7%&lt;/td&gt;
&lt;td&gt;87.3%&lt;/td&gt;
&lt;td&gt;92.3%&lt;/td&gt;
&lt;td&gt;93.0%&lt;/td&gt;
&lt;td&gt;93.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;mirror&lt;/code&gt; falls off a cliff, from 94.3% on 5×5 to &lt;strong&gt;2.7%&lt;/strong&gt; on 10×10. Mirrors&lt;br&gt;
preserve distance, so "the twin is off the board" prunes less and less as&lt;br&gt;
boards grow — and &lt;code&gt;mirror&lt;/code&gt; never once asks whether a cell can still &lt;em&gt;walk&lt;br&gt;
home&lt;/em&gt;. &lt;code&gt;reach&lt;/code&gt; asks exactly that and lifts 10×10 back to 87.3%. Connectivity&lt;br&gt;
is the rule that pays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ablation measured "value relative to the bench", again
&lt;/h2&gt;

&lt;p&gt;Same boards, all four rules minus one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;full&lt;/th&gt;
&lt;th&gt;−mirror&lt;/th&gt;
&lt;th&gt;−reach&lt;/th&gt;
&lt;th&gt;−bridge&lt;/th&gt;
&lt;th&gt;−probe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5×5&lt;/td&gt;
&lt;td&gt;98.0%&lt;/td&gt;
&lt;td&gt;5.3%&lt;/td&gt;
&lt;td&gt;95.0%&lt;/td&gt;
&lt;td&gt;98.0%&lt;/td&gt;
&lt;td&gt;98.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7×7&lt;/td&gt;
&lt;td&gt;95.3%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;74.0%&lt;/td&gt;
&lt;td&gt;95.3%&lt;/td&gt;
&lt;td&gt;95.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;93.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;31.0%&lt;/td&gt;
&lt;td&gt;92.7%&lt;/td&gt;
&lt;td&gt;92.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Incrementally (the +bridge column of the ladder), &lt;code&gt;bridge&lt;/code&gt; looks like&lt;br&gt;
&lt;strong&gt;+5.0 points&lt;/strong&gt; on 10×10. In the ablation it is worth &lt;strong&gt;−0.3 points&lt;/strong&gt; — take&lt;br&gt;
it out and probe picks up nearly every ball it drops. This is the same lesson&lt;br&gt;
the previous puzzle in this series taught: a propagator has no intrinsic&lt;br&gt;
contribution, only a contribution relative to a particular bench. Publish the&lt;br&gt;
incremental ladder &lt;em&gt;and&lt;/em&gt; the ablation, because either one alone will lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solve rate is capped by uniqueness — and the ladder hits the cap
&lt;/h2&gt;

&lt;p&gt;Look at the table again: the solve rate at &lt;code&gt;probe&lt;/code&gt; equals the unique-answer&lt;br&gt;
rate of the raw stream, to the decimal, at all three sizes (98.0 / 95.3 /&lt;br&gt;
93.0%). Not a coincidence. Sound propagation can never kill a true solution,&lt;br&gt;
so &lt;strong&gt;a fixpoint can never finish a board that has two answers&lt;/strong&gt;. "Solved by&lt;br&gt;
rules" is bounded above by "unique", and this ladder reaches the bound&lt;br&gt;
exactly. Every unsolved board in those columns is a multi-solution board,&lt;br&gt;
where no sound rule could ever help.&lt;/p&gt;

&lt;p&gt;That 93–98% of the raw stream is unique at all is this puzzle's own&lt;br&gt;
personality: the mirror constraint is brutal on its own. Even a brute force&lt;br&gt;
with no candidate arrays — walk cells row-major, assign twin pairs in one&lt;br&gt;
stroke, never look at connectivity until a leaf hands the finished division to&lt;br&gt;
an independent flood-fill validator — enumerates &lt;em&gt;all&lt;/em&gt; solutions of a 5×5 in a&lt;br&gt;
&lt;strong&gt;median of 5 nodes&lt;/strong&gt;. The uniqueness check on a 10×10 needs a median of 142.5&lt;br&gt;
guesses at &lt;code&gt;mirror&lt;/code&gt;, and &lt;strong&gt;0&lt;/strong&gt; once &lt;code&gt;reach&lt;/code&gt; is in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Count agreement&lt;/strong&gt;: the propagating search at all four levels and the
brute force above count the same number of solutions on random small boards.
A sound rule set cannot change the count, so one level disagreeing would
convict that rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent validator&lt;/strong&gt;: shares no code with the model or the rules;
flood-fills every region at every leaf and every solver exit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soundness&lt;/strong&gt;: the generator's own division survives the probe-level
fixpoint with zero candidates lost, on random boards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pair theorem&lt;/strong&gt;: symmetry of every candidate set audited after every
fixpoint (the 3,600-run version of the same check).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;27 tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Doubled coordinates collapse three kinds of dot into one one-line mirror map&lt;/li&gt;
&lt;li&gt;Candidate sets stay 180°-symmetric through the whole ladder (3,600
fixpoints, zero violations): deductions happen twice or not at all&lt;/li&gt;
&lt;li&gt;The parity rule cannot fire — it is a theorem of the mirror closure. Knowing
which rules you don't have to write is also a payoff&lt;/li&gt;
&lt;li&gt;The mirror rule has two directions, death and ownership; write half of it
and your difficulty grades quietly collapse&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mirror&lt;/code&gt; drops to 2.7% on 10×10; connectivity brings back 87.3%&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bridge&lt;/code&gt;: +5.0pt incremental, −0.3pt in ablation — publish both numbers&lt;/li&gt;
&lt;li&gt;A sound fixpoint's solve rate is capped by the uniqueness rate, and this
ladder hits the cap at every size&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;SEN LLC builds small, well-measured software and writes up what it learns.&lt;br&gt;
More at &lt;a href="https://sen.ltd/portfolio/" rel="noopener noreferrer"&gt;sen.ltd/portfolio&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>puzzle</category>
    </item>
    <item>
      <title>Shakashaka: the rule is about whole regions, and it is exactly a rule about one lattice point</title>
      <dc:creator>SEN LLC</dc:creator>
      <pubDate>Fri, 07 Aug 2026 23:37:55 +0000</pubDate>
      <link>https://dev.to/sendotltd/shakashaka-the-rule-is-about-whole-regions-and-it-is-exactly-a-rule-about-one-lattice-point-20ec</link>
      <guid>https://dev.to/sendotltd/shakashaka-the-rule-is-about-whole-regions-and-it-is-exactly-a-rule-about-one-lattice-point-20ec</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Shakashaka&lt;/strong&gt; in the browser with &lt;strong&gt;five rule sets&lt;/strong&gt; inside. Drop black&lt;br&gt;
triangles into the white cells — each fills half a cell, right angle in one&lt;br&gt;
of the four corners — so that &lt;strong&gt;every piece of white left over is a&lt;br&gt;
rectangle&lt;/strong&gt;, upright or turned 45°. A number on a black cell says how many of&lt;br&gt;
the four cells around it hold a triangle. Puzzle #32 in the solver series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Demo: &lt;a href="https://sen.ltd/portfolio/shakashaka/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/shakashaka/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/sen-ltd/shakashaka" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/shakashaka&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff15ikr4efqdavf91wdir.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff15ikr4efqdavf91wdir.png" alt="Shakashaka" width="799" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I picked this one because its main rule is stated in the worst possible shape&lt;br&gt;
for a solver. "Every white region is a rectangle" is a sentence about connected&lt;br&gt;
components: you cannot ask it anything until the last cell is filled, and once&lt;br&gt;
you finally can, answering it costs a flood fill. Every other puzzle in this&lt;br&gt;
series hands you something local to propagate. This one hands you a region.&lt;/p&gt;

&lt;p&gt;Except it does not, quite. The rule turns out to be &lt;strong&gt;exactly&lt;/strong&gt; equivalent to a&lt;br&gt;
condition on the white angle at a single lattice point — eight bits and a table&lt;br&gt;
lookup, no regions anywhere — and that changes what a solver can be.&lt;/p&gt;

&lt;p&gt;Three findings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The global rule is local, and provably so.&lt;/strong&gt; Every maximal run of white 45°
sectors around a lattice point must be 90°, 180° or 360°. I checked that
against a flood-fill oracle on 12.5 million configurations, including &lt;em&gt;every&lt;/em&gt;
picture on &lt;em&gt;every&lt;/em&gt; 3×3 board. Zero disagreements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The obvious local rule is wrong in both directions at once.&lt;/strong&gt; "The white
angle at a point is a multiple of 90°" accepts 13 958 pictures on an empty
4×4 board where only 23 are legal — while rejecting 21 of those 23.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adding a redundant propagator moved another propagator's ablation score by
27 points.&lt;/strong&gt; Same rule, same boards, same metric. A propagator does not have
a contribution; it has a contribution &lt;em&gt;relative to a set&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The rule that will not propagate
&lt;/h2&gt;

&lt;p&gt;Here is the whole puzzle. Cells are black (given, sometimes numbered) or white.&lt;br&gt;
In a white cell you may place one black right triangle filling half the cell,&lt;br&gt;
right angle in one of the four corners, or leave the cell alone. So each white&lt;br&gt;
cell has five states.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;E&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// no triangle: white to the edges&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TNW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// right angle at the NW corner&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TNE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TSE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TSW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then: the white area left over must be a disjoint set of rectangles,&lt;br&gt;
upright or turned 45°.&lt;/p&gt;

&lt;p&gt;A search wants to hear &lt;em&gt;no&lt;/em&gt; as early as possible. This rule says nothing at all&lt;br&gt;
until every cell is decided, at which point a flood fill tells you whether the&lt;br&gt;
last five hours were wasted. That is a leaf test, and a leaf-only search over&lt;br&gt;
84 white cells has 5^84 ≈ 10^59 states to walk.&lt;/p&gt;
&lt;h2&gt;
  
  
  Eight bits around a point
&lt;/h2&gt;

&lt;p&gt;Stand on a lattice point — a corner of the grid, not a cell. Four cells meet&lt;br&gt;
there. Each one contributes a white angle of 0°, 45° or 90°, and nothing else&lt;br&gt;
about the board is visible from where you are standing.&lt;/p&gt;

&lt;p&gt;Chop the full turn into eight 45° sectors, numbered clockwise from north:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sectors 0,1 → the cell NE of the point      2,3 → SE
        4,5 → SW                            6,7 → NW
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each cell state fills two of those bits. A black or off-board cell fills none.&lt;br&gt;
An empty white cell fills both of its quadrant's bits. A triangle fills both if&lt;br&gt;
its right angle is in the far corner, none if it is in the near corner, and one&lt;br&gt;
of the two otherwise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PAT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[][]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="c1"&gt;// q0: the cell NE of the point; the point is that cell's SW corner.&lt;/span&gt;
  &lt;span class="c1"&gt;//     E     TNW   TNE   TSE   TSW&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mb"&gt;0b11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b00&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mb"&gt;0b11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b01&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// q1: SE&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mb"&gt;0b11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b11&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// q2: SW&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mb"&gt;0b11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mb"&gt;0b10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// q3: NW&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the claim. Take the maximal unbroken cyclic runs of white sectors around&lt;br&gt;
the point. Then:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The white area is a disjoint set of rectangles ⟺ every run is 2, 4 or 8&lt;br&gt;
sectors long.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;90°, 180° or 360°. A corner, a straight edge, or the inside of something.&lt;/p&gt;

&lt;p&gt;Why that is enough: every vertex of the white boundary sits on a lattice point&lt;br&gt;
(triangle hypotenuses go corner to corner), so the boundary of a white region&lt;br&gt;
is a polygon with lattice-point corners. A run of 4 is a straight point, a run&lt;br&gt;
of 8 is an interior point, and a run of 2 is a corner turning 90°. Rule out&lt;br&gt;
everything else and a region's boundary is a simple polygon whose every corner&lt;br&gt;
turns 90° in the same direction — which has exactly four corners, because the&lt;br&gt;
turns must sum to 360°. Four corners, all right angles: a rectangle. And the&lt;br&gt;
edges are cell sides or cell diagonals, so consecutive perpendicular edges are&lt;br&gt;
either both axis-aligned or both diagonal. Upright or 45°, nothing in between.&lt;/p&gt;

&lt;p&gt;Runs of 6 are the reflex corners that make an L an L. Odd runs are 45° wedges —&lt;br&gt;
the tip of a lone triangle. Both are exactly what the rule forbids, and there&lt;br&gt;
is no third failure to rule out.&lt;/p&gt;

&lt;p&gt;So the entire rule is a 256-entry table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VALID_FULL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;arcs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;arcs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;L&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;L&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;L&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Runs, not the total — and this is where I would have shipped a bug
&lt;/h2&gt;

&lt;p&gt;The version you write first is &lt;em&gt;"the white angle at each point adds up to 0°,&lt;br&gt;
90°, 180° or 360°"&lt;/em&gt;. One popcount, no arc decomposition, obviously the same&lt;br&gt;
thing.&lt;/p&gt;

&lt;p&gt;It is not the same thing, and it is not even a safe approximation. It is wrong&lt;br&gt;
in both directions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;legal&lt;/th&gt;
&lt;th&gt;naive says legal&lt;/th&gt;
&lt;th&gt;legal ones it rejects&lt;/th&gt;
&lt;th&gt;illegal ones it accepts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;empty 2×2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;0 (0%)&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty 3×3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2 (67%)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty 4×4&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;13958&lt;/td&gt;
&lt;td&gt;21 (91%)&lt;/td&gt;
&lt;td&gt;13956&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On an empty 4×4 there are 23 legal pictures. The naive rule accepts 13 958 of&lt;br&gt;
them and agrees with reality on two.&lt;/p&gt;

&lt;p&gt;Both failures come from one fact: &lt;strong&gt;two rectangles may touch at a single&lt;br&gt;
point.&lt;/strong&gt; Two white 1×1 squares on a diagonal, with black cells on the other&lt;br&gt;
diagonal, is the most ordinary picture in the puzzle. At the shared corner that&lt;br&gt;
is 180° of white — but split into two separate right angles. Sum the angle and&lt;br&gt;
you cannot tell it from a straight edge, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;45° + 45° in opposite quadrants sums to 90° and gets accepted, though it is
two illegal wedges;&lt;/li&gt;
&lt;li&gt;180° + 90° as two separate runs sums to 270° and gets rejected, though it is
a perfectly legal edge next to a perfectly legal corner.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The temptation is to file this under "edge case". It is not. Counting the&lt;br&gt;
lattice points of the answers shipped with the demo:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;lattice points&lt;/th&gt;
&lt;th&gt;empty&lt;/th&gt;
&lt;th&gt;one corner&lt;/th&gt;
&lt;th&gt;one edge&lt;/th&gt;
&lt;th&gt;interior&lt;/th&gt;
&lt;th&gt;two arcs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;784&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;td&gt;25% (on 16/16 boards)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;1296&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;28% (on 16/16 boards)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;1936&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;35% (on 16/16 boards)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A third of the lattice points on a 10×10 answer are places where two separate&lt;br&gt;
pieces of white touch at a point. The naive rule is wrong on every board there&lt;br&gt;
is.&lt;/p&gt;
&lt;h2&gt;
  
  
  How do you test a claim like that?
&lt;/h2&gt;

&lt;p&gt;An equivalence between a global property and a local one is not something a&lt;br&gt;
unit test can assert directly. What you can do is build the other reading —&lt;br&gt;
properly, independently — and run both on everything.&lt;/p&gt;

&lt;p&gt;The literal reading needed to avoid every concept the local rule uses: no&lt;br&gt;
angles, no corners, no sectors. Here is what I landed on.&lt;/p&gt;

&lt;p&gt;Cut every cell into four quarter triangles with both diagonals. Every legal&lt;br&gt;
half-cell triangle is a union of two of them, so the quarters are the atoms of&lt;br&gt;
the picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;black corner NW → black quarters N, W      NE → N, E
             SE → S, E                     SW → S, W
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flood fill the white quarters — edge adjacency only, so two regions touching at&lt;br&gt;
a point stay two regions. Then, for each region, the rectangle test is &lt;em&gt;areas&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A quarter triangle has area 1 in doubled coordinates.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upright&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Xmax&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Xmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Ymax&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Ymin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tilted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Umax&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Umin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Vmax&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Vmin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// = 2 × the tilted box area&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;upright&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;tilted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* not a rectangle */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;U = X + Y&lt;/code&gt; and &lt;code&gt;V = X − Y&lt;/code&gt;. A region always fits inside its own bounding&lt;br&gt;
box, so if the areas are equal the region &lt;em&gt;is&lt;/em&gt; the box — no shape comparison&lt;br&gt;
needed, and it is exact integer arithmetic because doubling the coordinates&lt;br&gt;
puts cell corners on even numbers and cell centres on odd ones. Upright&lt;br&gt;
rectangles are boxes in &lt;code&gt;(x, y)&lt;/code&gt;; 45° ones are boxes in &lt;code&gt;(x+y, x−y)&lt;/code&gt;. There is&lt;br&gt;
no third orientation to worry about.&lt;/p&gt;

&lt;p&gt;Then run both readings on everything I could afford:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;configurations&lt;/th&gt;
&lt;th&gt;source&lt;/th&gt;
&lt;th&gt;legal ones among them&lt;/th&gt;
&lt;th&gt;disagreements&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;625&lt;/td&gt;
&lt;td&gt;every picture on an empty 2×2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,953,125&lt;/td&gt;
&lt;td&gt;every picture on an empty 3×3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;46,656&lt;/td&gt;
&lt;td&gt;every picture on every 2×3 board, black cells and all&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,077,696&lt;/td&gt;
&lt;td&gt;every picture on every 3×3 board, black cells and all&lt;/td&gt;
&lt;td&gt;328&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400,000&lt;/td&gt;
&lt;td&gt;random pictures on crowded random 5×5 boards&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;75,264&lt;/td&gt;
&lt;td&gt;every one- and two-cell edit of every answer in the bank&lt;/td&gt;
&lt;td&gt;232&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That fourth row is 6^9: nine cells, each of them black or one of five states.&lt;br&gt;
Every Shakashaka board of that size, and every picture on it.&lt;/p&gt;

&lt;p&gt;The last row is the one that matters, though. Look at the "legal" column on the&lt;br&gt;
random 5×5 sweep: &lt;strong&gt;one&lt;/strong&gt; legal picture in 400 000. Random boards only exercise&lt;br&gt;
the easy side of the claim — both readings say no, loudly, for unrelated&lt;br&gt;
reasons. Boards that are one or two cells away from a real answer are the ones&lt;br&gt;
sitting on the boundary, and 232 of those 75 264 are legal. That is the sweep I&lt;br&gt;
would trust if I had to pick one.&lt;/p&gt;

&lt;p&gt;A separate test pins the table itself to the geometry: for all 1296 ways four&lt;br&gt;
cells can meet at a point, step a hair away from the point in the middle of&lt;br&gt;
each of the eight sectors and ask the actual picture — &lt;code&gt;x + y &amp;gt; 1&lt;/code&gt;, &lt;code&gt;y &amp;gt; x&lt;/code&gt; —&lt;br&gt;
whether that spot is white. If &lt;code&gt;PAT&lt;/code&gt; had one bit wrong, everything above would&lt;br&gt;
be a self-consistent verification of the wrong thing.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it buys
&lt;/h2&gt;

&lt;p&gt;The local rule is a constraint over four cells with five values each, so it is&lt;br&gt;
an arc-consistency propagator. Each cell's domain collapses to at most four&lt;br&gt;
distinct 2-bit patterns, which makes the whole constraint 16 bits in, 16 bits&lt;br&gt;
out, memoised:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;packed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;STATESET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;][(&lt;/span&gt;&lt;span class="nx"&gt;packed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same search, same boards, same answers — the only difference is whether the&lt;br&gt;
rules may look before the leaves:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;white cells&lt;/th&gt;
&lt;th&gt;answers&lt;/th&gt;
&lt;th&gt;nodes, propagating&lt;/th&gt;
&lt;th&gt;nodes, leaf checks only&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3×3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;19,531&lt;/td&gt;
&lt;td&gt;19,531×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3×3&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;488,281&lt;/td&gt;
&lt;td&gt;162,760×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4×4&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;61,035,156&lt;/td&gt;
&lt;td&gt;61,035,156×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And on the boards the demo ships, the right-hand column stops being a number&lt;br&gt;
you can print:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;white cells&lt;/th&gt;
&lt;th&gt;nodes to settle it&lt;/th&gt;
&lt;th&gt;states without pruning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;1 (median)&lt;/td&gt;
&lt;td&gt;5^30 ≈ 10^21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;td&gt;1.5 (median)&lt;/td&gt;
&lt;td&gt;5^54 ≈ 10^38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;84&lt;/td&gt;
&lt;td&gt;1 (median)&lt;/td&gt;
&lt;td&gt;5^84 ≈ 10^59&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A median of one node. On the shipped boards, arc consistency over the lattice&lt;br&gt;
points settles the entire answer without a single guess.&lt;/p&gt;

&lt;p&gt;The per-test speed is the boring half of the win and it is there too — 10.5 µs&lt;br&gt;
for the flood fill against 3.2 µs for the table lookups on the same finished&lt;br&gt;
boards — but three times faster is not the point. Being &lt;em&gt;askable at all&lt;/em&gt;, on a&lt;br&gt;
board that is one tenth filled, is the point.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two ways to score a propagator, and they disagree
&lt;/h2&gt;

&lt;p&gt;The full rule splits into two independent local rules: &lt;strong&gt;tips&lt;/strong&gt; (no odd runs —&lt;br&gt;
no 45° wedges) and &lt;strong&gt;reflex&lt;/strong&gt; (no runs of 6 — no corner bending the wrong way).&lt;br&gt;
Their intersection is the whole thing, which is a nice test to have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;intersect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VALID_TIPS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;VALID_REFLEX&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VALID_FULL&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added a fourth, deliberately: &lt;strong&gt;parity&lt;/strong&gt; — the white angle at a point must be&lt;br&gt;
a whole number of right angles, never mind where the sectors sit. That is one&lt;br&gt;
popcount instead of an arc decomposition, it is implied by &lt;code&gt;tips&lt;/code&gt;, and it is&lt;br&gt;
exactly the kind of cheap approximation someone reaches for.&lt;/p&gt;

&lt;p&gt;Climbing the ladder — &lt;em&gt;what can this rule do that the weaker ones cannot&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;rule set&lt;/th&gt;
&lt;th&gt;reduction&lt;/th&gt;
&lt;th&gt;finished&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;(nothing)&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;clues&lt;/td&gt;
&lt;td&gt;5.3%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;clues + parity&lt;/td&gt;
&lt;td&gt;24.0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;clues + parity + tips&lt;/td&gt;
&lt;td&gt;74.7%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;clues + parity + tips + reflex&lt;/td&gt;
&lt;td&gt;93.8%&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Leave one out — &lt;em&gt;what can this rule do that the stronger set cannot&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;dropped&lt;/th&gt;
&lt;th&gt;reduction&lt;/th&gt;
&lt;th&gt;finished&lt;/th&gt;
&lt;th&gt;vs. full set&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;(nothing)&lt;/td&gt;
&lt;td&gt;93.8%&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;clues&lt;/td&gt;
&lt;td&gt;68.8%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;−25.0% reduction, −27 boards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;parity&lt;/td&gt;
&lt;td&gt;93.8%&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;td&gt;−0.0% reduction, −0 boards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;tips&lt;/td&gt;
&lt;td&gt;32.9%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;−60.9% reduction, −27 boards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;reflex&lt;/td&gt;
&lt;td&gt;74.7%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;−19.1% reduction, −27 boards&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Incrementally &lt;code&gt;parity&lt;/code&gt; is the second-biggest jump on the ladder — it more than&lt;br&gt;
four-times the reduction over clues alone. By ablation it is worth &lt;strong&gt;zero&lt;/strong&gt;, to&lt;br&gt;
the last decimal, at every board size. That much I expected; it is redundant by&lt;br&gt;
construction, and it is the same shape of result I got out of the previous&lt;br&gt;
puzzle in this series.&lt;/p&gt;

&lt;p&gt;What I did not expect was the other direction. Score &lt;code&gt;tips&lt;/code&gt; by ablation against&lt;br&gt;
two different sets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;set&lt;/th&gt;
&lt;th&gt;without &lt;code&gt;tips&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;drop in reduction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;clues + tips + reflex&lt;/td&gt;
&lt;td&gt;6.1%&lt;/td&gt;
&lt;td&gt;−87.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;clues + parity + tips + reflex&lt;/td&gt;
&lt;td&gt;32.9%&lt;/td&gt;
&lt;td&gt;−60.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same rule. Same boards. Same metric. The number moved by 27 points because a&lt;br&gt;
&lt;em&gt;different&lt;/em&gt; rule — one that is worth nothing on its own showing — was standing&lt;br&gt;
behind it to catch what it dropped. Ablation does not measure a propagator. It&lt;br&gt;
measures a propagator against a particular bench of substitutes, and adding a&lt;br&gt;
redundant rule to the set quietly deflates the ablation score of the rule it&lt;br&gt;
duplicates.&lt;/p&gt;

&lt;p&gt;Which is the honest version of a thing I already believed: a propagator has no&lt;br&gt;
contribution of its own. Report both directions or you are stating a&lt;br&gt;
preference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who actually makes the answer unique
&lt;/h2&gt;

&lt;p&gt;Take boards with exactly one answer, switch off one rule, count again:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;boards&lt;/th&gt;
&lt;th&gt;clues dropped: still unique&lt;/th&gt;
&lt;th&gt;shape rule down to &lt;code&gt;tips&lt;/code&gt;: still unique&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;5 (8%), median 6.5 answers&lt;/td&gt;
&lt;td&gt;20 (33%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;0 (0%), median 52.5 answers&lt;/td&gt;
&lt;td&gt;4 (7%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;0 (0%), median 335 answers&lt;/td&gt;
&lt;td&gt;0 (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both halves load-bearing, and the shape rule is the bigger one — which is why&lt;br&gt;
these boards carry so few numbers. The generator numbers every black cell from&lt;br&gt;
the answer and then takes numbers away for as long as the answer survives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;black cells&lt;/th&gt;
&lt;th&gt;numbers kept&lt;/th&gt;
&lt;th&gt;share&lt;/th&gt;
&lt;th&gt;which digits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;1.7&lt;/td&gt;
&lt;td&gt;28%&lt;/td&gt;
&lt;td&gt;0:6% 1:37% 2:40% 3:16% 4:0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;3.3&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;td&gt;0:9% 1:25% 2:45% 3:16% 4:5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;16.0&lt;/td&gt;
&lt;td&gt;5.5&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;0:6% 1:29% 2:41% 3:21% 4:2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two thirds of the numbers are things the geometry already knew. A 10×10 board&lt;br&gt;
here ships with about five numbers on it and one answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-checking
&lt;/h2&gt;

&lt;p&gt;Every count is produced three ways, and the tests fail if they disagree:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Propagating search&lt;/strong&gt; — smallest-domain-first backtracking driven by arc
consistency over the lattice points and the clues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same search with the propagators unplugged&lt;/strong&gt; — assign cells in
row-major order, test the rules only at the leaves. Same answers, up to 61
million times the nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brute force through &lt;code&gt;validate&lt;/code&gt;&lt;/strong&gt; — enumerate every assignment and re-read
the rule book from the top: clue arithmetic, then flood fill and bounding
boxes. The only pruning it is allowed is counting to four, so the thing
under test stays untouched.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rung labels are checked too, not just believed: for a generated board the&lt;br&gt;
test takes the difficulty label, runs that rung, and asserts the domains it&lt;br&gt;
reaches are the intended answer cell by cell. And a soundness test asserts that&lt;br&gt;
every rung, on every board, keeps every real solution inside its domains —&lt;br&gt;
which is the property that makes a propagator a propagator rather than a guess.&lt;/p&gt;

&lt;p&gt;26 tests. TypeScript, no runtime dependencies.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/sen-ltd/shakashaka" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/shakashaka&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://sen.ltd/portfolio/shakashaka/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/shakashaka/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>puzzle</category>
    </item>
    <item>
      <title>Tatamibari: three of the four rules are an exact-cover matrix, and the fourth is the one that matters</title>
      <dc:creator>SEN LLC</dc:creator>
      <pubDate>Thu, 06 Aug 2026 23:25:04 +0000</pubDate>
      <link>https://dev.to/sendotltd/tatamibari-three-of-the-four-rules-are-an-exact-cover-matrix-and-the-fourth-is-the-one-that-2h2d</link>
      <guid>https://dev.to/sendotltd/tatamibari-three-of-the-four-rules-are-an-exact-cover-matrix-and-the-fourth-is-the-one-that-2h2d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tatamibari&lt;/strong&gt; in the browser with &lt;strong&gt;five rule sets&lt;/strong&gt; inside. Cut the grid&lt;br&gt;
into rectangles so that every rectangle holds exactly one clue, and the clue&lt;br&gt;
says what shape it is: &lt;code&gt;+&lt;/code&gt; a square, &lt;code&gt;-&lt;/code&gt; wider than tall, &lt;code&gt;|&lt;/code&gt; taller than&lt;br&gt;
wide. And one more rule, which belongs to no rectangle in particular:&lt;br&gt;
&lt;strong&gt;no four rectangles may meet at a point.&lt;/strong&gt; Puzzle #31 in the solver series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Demo: &lt;a href="https://sen.ltd/portfolio/tatamibari/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/tatamibari/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/sen-ltd/tatamibari" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/tatamibari&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4rsbdf9p003dqx97ht9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4rsbdf9p003dqx97ht9.png" alt="Tatamibari" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I picked this puzzle because the first three rules are &lt;em&gt;exact cover written&lt;br&gt;
out verbatim&lt;/em&gt; — a rare thing, and a good excuse to write dancing links. What I&lt;br&gt;
did not expect was how much of the puzzle turned out to live in the one rule&lt;br&gt;
that would not go in the matrix.&lt;/p&gt;

&lt;p&gt;Three findings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The fourth rule cannot be a column, and it is the rule that makes the
answer unique.&lt;/strong&gt; Switch it off on 10×10 boards that have exactly one
solution, and only 17% of them still do. One went to 1953.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental reach and ablation gave me opposite verdicts on the same
rule.&lt;/strong&gt; One propagator looks like the second-biggest contributor climbing the
ladder and is worth &lt;em&gt;literally zero&lt;/em&gt; removed from the top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rule that does not fit does not help the search either.&lt;/strong&gt; It costs
5–20% more nodes, because it has no column, so the column-choice heuristic
cannot see it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Three rules are a matrix
&lt;/h2&gt;

&lt;p&gt;"Every cell belongs to exactly one rectangle" plus "every clue gets exactly one&lt;br&gt;
rectangle, of the shape it asks for" is &lt;a href="https://en.wikipedia.org/wiki/Exact_cover" rel="noopener noreferrer"&gt;exact&lt;br&gt;
cover&lt;/a&gt; with nothing left over. One&lt;br&gt;
column per cell, one row per candidate rectangle, hand it to Algorithm X.&lt;/p&gt;

&lt;p&gt;The pleasant surprise is what you &lt;em&gt;don't&lt;/em&gt; need. My first sketch had &lt;code&gt;n²&lt;/code&gt;&lt;br&gt;
columns for the cells plus one column per clue, to force one rectangle per&lt;br&gt;
clue. That second block is redundant. A candidate rectangle only exists if it&lt;br&gt;
contains &lt;strong&gt;exactly one&lt;/strong&gt; clue — a rectangle swallowing two clues is not a legal&lt;br&gt;
piece — so the column for that clue's own cell already forces exactly one&lt;br&gt;
rectangle per clue. The matrix is &lt;code&gt;n²&lt;/code&gt; columns wide and that is all of it.&lt;/p&gt;

&lt;p&gt;Generating the candidates is the whole model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;r0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r0&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;r1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;r0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;c0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c0&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;c1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c1&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;c0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;|&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;clueCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// ← the load-bearing filter&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;clueCount&lt;/code&gt; is a 2-D prefix sum, so "how many clues does this rectangle&lt;br&gt;
contain" is four array reads. It matters: a centre cell on a 10×10 board sits&lt;br&gt;
inside 900 rectangles, and almost every large one swallows a second clue.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fourth rule is not a column
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;No four rectangles may meet at a point.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not a statement about a cell being covered once. It is a statement&lt;br&gt;
about a &lt;strong&gt;lattice point&lt;/strong&gt; being cornered at most three times. Exact cover&lt;br&gt;
speaks "exactly one" (primary columns) and, with Knuth's secondary columns,&lt;br&gt;
"at most one". There is no way to write "at most three".&lt;/p&gt;

&lt;p&gt;So it rides along the search as an incremental counter. Every rectangle knows&lt;br&gt;
which interior lattice points it corners at, and when dancing links offers a&lt;br&gt;
row, those counters get bumped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;D&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;D&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="c1"&gt;// The one rule that is not a column.&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;useCorners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cands&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;corners&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;cornerCount&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;cornerCount&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ...cover, recurse, uncover... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else in the function is textbook Algorithm X. The one rule I could&lt;br&gt;
not express is fifteen lines bolted to the side of it.&lt;/p&gt;

&lt;p&gt;Getting the corner predicate right is fiddly enough that I gave it its own&lt;br&gt;
tests. A lattice point &lt;code&gt;(lr, lc)&lt;/code&gt; has four cells around it, and a rectangle&lt;br&gt;
owning the north-west one corners there only if it stops exactly at &lt;code&gt;lr-1&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;lc-1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cornersAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;r1&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;c1&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lc&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// NW cell&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;r1&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;c0&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// NE cell&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;r0&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt;     &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;c1&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lc&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// SW cell&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;r0&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lr&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;c0&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// SE cell&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a small lemma hiding in there that the propagator leans on: &lt;strong&gt;no&lt;br&gt;
single rectangle can corner at two quadrants of the same point.&lt;/strong&gt; NW wants&lt;br&gt;
&lt;code&gt;c1 = lc-1&lt;/code&gt;, NE wants &lt;code&gt;c0 = lc&lt;/code&gt;, and &lt;code&gt;c0 &amp;gt; c1&lt;/code&gt; is not a rectangle. So "all four&lt;br&gt;
cells corner here" really does imply "four distinct rectangles", which is what&lt;br&gt;
lets the propagator work in terms of cornering rather than in terms of&lt;br&gt;
identity. There is a randomised test asserting exactly this over 500 rectangles&lt;br&gt;
× 25 points, because I did not want to find out later that I had been assuming&lt;br&gt;
it.&lt;/p&gt;

&lt;h2&gt;
  
  
  And it is the rule that makes the answer unique
&lt;/h2&gt;

&lt;p&gt;Here is the measurement I did not see coming. Take boards with &lt;strong&gt;exactly one&lt;/strong&gt;&lt;br&gt;
solution under all four rules. Switch off the one rule that would not fit in&lt;br&gt;
the matrix. Count again. 60 freshly generated boards per size:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;boards&lt;/th&gt;
&lt;th&gt;still unique&lt;/th&gt;
&lt;th&gt;solutions without it: median&lt;/th&gt;
&lt;th&gt;mean&lt;/th&gt;
&lt;th&gt;max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;31 (52%)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2.2&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;24 (40%)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3.2&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;10 (17%)&lt;/td&gt;
&lt;td&gt;5.5&lt;/td&gt;
&lt;td&gt;66.4&lt;/td&gt;
&lt;td&gt;1953&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 10×10, five out of six well-posed puzzles stop being puzzles. The matrix on&lt;br&gt;
its own hands you a median of five and a half answers, and one board in the&lt;br&gt;
sample had &lt;strong&gt;1953&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The trend is the point. On 6×6 the corner rule is close to decorative — you&lt;br&gt;
could nearly drop it and still ship. By 10×10 it is carrying the puzzle. The&lt;br&gt;
part of the problem that fits the framework beautifully is not the part that&lt;br&gt;
makes the problem well-posed, and the gap widens with size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most tilings are dead before a clue is written
&lt;/h2&gt;

&lt;p&gt;The same rule does the same work one level up, on the generator. Draw random&lt;br&gt;
rectangle tilings — no clues, nothing solved — and just ask how many are legal.&lt;br&gt;
4000 draws per row, &lt;code&gt;maxSide&lt;/code&gt; capping how long a piece may get:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;maxSide&lt;/th&gt;
&lt;th&gt;rectangles (mean)&lt;/th&gt;
&lt;th&gt;four rectangles meet&lt;/th&gt;
&lt;th&gt;survivors&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;18.8&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;10.7&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;7.6&lt;/td&gt;
&lt;td&gt;29%&lt;/td&gt;
&lt;td&gt;71%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;32.7&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;17.9&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;13.4&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;50.5&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;34.8&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;27.3&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;19.8&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cap the pieces at 2×2 and &lt;strong&gt;not one tiling in four thousand survives&lt;/strong&gt;, at any&lt;br&gt;
board size. That is not a near miss, it is structural: a fine tiling by small&lt;br&gt;
rectangles is &lt;em&gt;made of&lt;/em&gt; points where four pieces meet. You need long pieces to&lt;br&gt;
get away with it, which is exactly why real Tatamibari boards look the way they&lt;br&gt;
do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful going up, worthless coming down
&lt;/h2&gt;

&lt;p&gt;The solver has five rule sets, each adding one propagator: &lt;code&gt;shape&lt;/code&gt; (a clue with&lt;br&gt;
one rectangle left is placed), &lt;code&gt;cover&lt;/code&gt; (a cell only one clue can reach must be&lt;br&gt;
inside that clue's rectangle), &lt;code&gt;disjoint&lt;/code&gt; (a rectangle overlapping &lt;em&gt;every&lt;/em&gt;&lt;br&gt;
remaining rectangle of some other clue cannot be chosen), &lt;code&gt;corners&lt;/code&gt;, &lt;code&gt;probe&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Climbing the ladder, on 60 unique boards per size — &lt;em&gt;share of surplus&lt;br&gt;
candidates eliminated / share of boards finished outright&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;shape&lt;/th&gt;
&lt;th&gt;cover&lt;/th&gt;
&lt;th&gt;disjoint&lt;/th&gt;
&lt;th&gt;corners&lt;/th&gt;
&lt;th&gt;probe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;5% / 0%&lt;/td&gt;
&lt;td&gt;76% / 28%&lt;/td&gt;
&lt;td&gt;88% / 48%&lt;/td&gt;
&lt;td&gt;99% / 97%&lt;/td&gt;
&lt;td&gt;100% / 100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;3% / 0%&lt;/td&gt;
&lt;td&gt;67% / 12%&lt;/td&gt;
&lt;td&gt;87% / 32%&lt;/td&gt;
&lt;td&gt;98% / 87%&lt;/td&gt;
&lt;td&gt;100% / 100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;4% / 0%&lt;/td&gt;
&lt;td&gt;54% / 2%&lt;/td&gt;
&lt;td&gt;81% / 12%&lt;/td&gt;
&lt;td&gt;96% / 82%&lt;/td&gt;
&lt;td&gt;100% / 100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;disjoint&lt;/code&gt; looks indispensable. It takes 10×10 from 54% to 81% of candidates&lt;br&gt;
cut and from 2% to 12% of boards finished — the biggest jump on the board&lt;br&gt;
except for &lt;code&gt;corners&lt;/code&gt;. I was pleased with it. It is the one rule in there I had&lt;br&gt;
to think about.&lt;/p&gt;

&lt;p&gt;Now drop one propagator from the &lt;strong&gt;full&lt;/strong&gt; set instead. Same 60 boards, same&lt;br&gt;
code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;dropped&lt;/th&gt;
&lt;th&gt;6×6 cut / done&lt;/th&gt;
&lt;th&gt;8×8 cut / done&lt;/th&gt;
&lt;th&gt;10×10 cut / done&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;100% / 60&lt;/td&gt;
&lt;td&gt;100% / 60&lt;/td&gt;
&lt;td&gt;100% / 60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shape&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100% / 60&lt;/td&gt;
&lt;td&gt;100% / 60&lt;/td&gt;
&lt;td&gt;100% / 60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cover&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;72% / 1&lt;/td&gt;
&lt;td&gt;70% / 0&lt;/td&gt;
&lt;td&gt;69% / 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disjoint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100% / 60&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100% / 60&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100% / 60&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;corners&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;93% / 31&lt;/td&gt;
&lt;td&gt;96% / 24&lt;/td&gt;
&lt;td&gt;93% / 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;probe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;99% / 58&lt;/td&gt;
&lt;td&gt;98% / 52&lt;/td&gt;
&lt;td&gt;96% / 49&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;disjoint&lt;/code&gt; is worth &lt;strong&gt;exactly nothing&lt;/strong&gt;. Not "a percentage point" — zero, at&lt;br&gt;
every size, on both measures. So is &lt;code&gt;shape&lt;/code&gt;. Anything they can prove, &lt;code&gt;cover&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;corners&lt;/code&gt; prove anyway.&lt;/p&gt;

&lt;p&gt;Two tables, same boards, same code, opposite verdicts. They are answering&lt;br&gt;
different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incremental reach&lt;/strong&gt; asks &lt;em&gt;"what can this rule do that the weaker ones
cannot?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ablation&lt;/strong&gt; asks &lt;em&gt;"what can this rule do that the stronger ones cannot?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rule sitting in the middle of a ladder answers the first question loudly and&lt;br&gt;
the second one not at all. And the natural way to build a hint ladder — switch&lt;br&gt;
propagators on one at a time and watch the numbers climb — measures only the&lt;br&gt;
first. I would have shipped &lt;code&gt;disjoint&lt;/code&gt; as a headline result on the strength of&lt;br&gt;
that first table alone, and it would have been a headline about a rule that does&lt;br&gt;
nothing.&lt;/p&gt;

&lt;p&gt;This is the second time in this series I have gone looking for what a rule is&lt;br&gt;
worth and got a different answer depending on where I stood. Last time&lt;br&gt;
(&lt;a href="https://dev.to/sendotltd/ripple-effect-the-rule-that-matters-most-never-shows-up-in-the-difficulty-rating-4n4g"&gt;Ripple Effect&lt;/a&gt;)&lt;br&gt;
it was the solver-vs-generator split: a rule worth almost nothing to the solver&lt;br&gt;
was deciding which boards existed at all. This time it is inside the solver:&lt;br&gt;
the same rule, on the same boards, measured two defensible ways, comes out&lt;br&gt;
essential and worthless. A propagator does not have "a" contribution. It has a&lt;br&gt;
contribution &lt;em&gt;relative to a set&lt;/em&gt;, and if you only ever report one set you are&lt;br&gt;
reporting a preference, not a measurement.&lt;/p&gt;

&lt;p&gt;They stay in the codebase, by the way — a hint ladder needs rungs a player can&lt;br&gt;
reason with, and "that rectangle blocks that clue entirely" is an explanation,&lt;br&gt;
not just an elimination. But they are not there to make the solver stronger,&lt;br&gt;
because they don't.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;corners&lt;/code&gt; is the only rule here load-bearing in both directions. Drop it and&lt;br&gt;
10×10 goes from 60 boards finished to 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that does not fit does not help the search either
&lt;/h2&gt;

&lt;p&gt;The corner check cannot inform Knuth's column-choice heuristic. It has no&lt;br&gt;
column, so it has no size, so &lt;code&gt;S&lt;/code&gt; cannot see it. All it can do is reject a row&lt;br&gt;
after the matrix has already offered one. DLX nodes to the &lt;strong&gt;first&lt;/strong&gt; solution,&lt;br&gt;
summed over the shipped bank:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;boards&lt;/th&gt;
&lt;th&gt;nodes with the check&lt;/th&gt;
&lt;th&gt;nodes without&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;291&lt;/td&gt;
&lt;td&gt;238&lt;/td&gt;
&lt;td&gt;1.22×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;291&lt;/td&gt;
&lt;td&gt;277&lt;/td&gt;
&lt;td&gt;1.05×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;746&lt;/td&gt;
&lt;td&gt;626&lt;/td&gt;
&lt;td&gt;1.19×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;5–20% more nodes. Which is the honest shape of the whole thing: the rule that&lt;br&gt;
makes these puzzles well-posed is the rule the machinery cannot help with, and&lt;br&gt;
it pays for its own enforcement.&lt;/p&gt;

&lt;p&gt;I am not sure there is a fix, only a trade. You could encode "at most three of&lt;br&gt;
these four" with auxiliary rows and secondary columns, at which point the&lt;br&gt;
heuristic &lt;em&gt;can&lt;/em&gt; see it — and you have added &lt;code&gt;(n-1)²&lt;/code&gt; structures to a matrix&lt;br&gt;
that was &lt;code&gt;n²&lt;/code&gt; columns of pure signal. I did not try it. The counter is fifteen&lt;br&gt;
lines and it is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the clue sits inside its rectangle
&lt;/h2&gt;

&lt;p&gt;There is no "prune the redundant givens" step in this generator, which is&lt;br&gt;
unusual for the series. The clue count is fixed the moment the tiling is drawn&lt;br&gt;
— one per rectangle. The only free variable left is &lt;em&gt;where inside its rectangle&lt;/em&gt;&lt;br&gt;
each clue sits.&lt;/p&gt;

&lt;p&gt;That turns out to matter enormously, and for a reason specific to this puzzle:&lt;br&gt;
a rectangle that would swallow a second clue is not a candidate at all. Move&lt;br&gt;
one dot and the candidate sets of every clue near it change. Fix a tiling,&lt;br&gt;
redraw only the clue positions, 16 layouts each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;tilings&lt;/th&gt;
&lt;th&gt;unique layouts&lt;/th&gt;
&lt;th&gt;tilings with ≥1 unique layout&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;td&gt;64%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 10×10 not one of 960 randomly placed layouts was unique. The generator still&lt;br&gt;
finds them — 5247 layouts over 10127 tilings for sixteen boards — but roughly&lt;br&gt;
one draw in three hundred lands.&lt;/p&gt;

&lt;p&gt;One more small thing, from the same run. Mean candidate rectangles kept per&lt;br&gt;
clue:&lt;/p&gt;

&lt;p&gt;| board | &lt;code&gt;+&lt;/code&gt; (square) | &lt;code&gt;-&lt;/code&gt; (wide) | &lt;code&gt;\|&lt;/code&gt; (tall) |&lt;br&gt;
|---|---|---|---|&lt;br&gt;
| 6×6 | 2.3 | 5.6 | 5.4 |&lt;br&gt;
| 8×8 | 3.7 | 12.8 | 11.2 |&lt;br&gt;
| 10×10 | 3.7 | 15.3 | 12.3 |&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;+&lt;/code&gt; pins both dimensions at once; &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;|&lt;/code&gt; only put them in order. Same&lt;br&gt;
symbol size, four times the information — and &lt;code&gt;+&lt;/code&gt; barely grows with the board&lt;br&gt;
(2.3 → 3.7) while &lt;code&gt;-&lt;/code&gt; triples, because a square anchored at a cell has O(n)&lt;br&gt;
shapes and an oblong has O(n²).&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-checking
&lt;/h2&gt;

&lt;p&gt;Every shipped board is counted by two solvers that enumerate the problem in&lt;br&gt;
opposite directions, and they have to agree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;dancing links&lt;/strong&gt; over the cell columns, choosing the column with the fewest
live rows;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;an anchored brute force&lt;/strong&gt;, keyed on the observation that the first
uncovered cell in row-major order is necessarily the top-left corner of
whichever rectangle owns it. Branching factor is "candidates anchored exactly
here", and the enumeration order shares nothing with the matrix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A third counter runs the rule sets to a fixpoint and branches on the clue with&lt;br&gt;
the fewest survivors. The tests check all three agree, with the corner rule on&lt;br&gt;
&lt;em&gt;and&lt;/em&gt; off — the "off" case matters, because a corner-rule bug that only fires&lt;br&gt;
when the rule is on would otherwise hide behind agreement.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;validate&lt;/code&gt; re-reads all four rules from scratch against a proposed tiling,&lt;br&gt;
sharing no code with the model or any solver: it builds its own cell→rectangle&lt;br&gt;
owner map and reads the corner rule straight off it. That is the one that would&lt;br&gt;
catch a subtly wrong &lt;code&gt;cornersAt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One test failure was worth the trip. I had asserted that on an unsolvable board&lt;br&gt;
the propagators either report a contradiction or leave the board unfinished.&lt;br&gt;
They report the contradiction — and &lt;code&gt;isSolved&lt;/code&gt; &lt;em&gt;still&lt;/em&gt; returned true, because a&lt;br&gt;
fixpoint that aborts half-way leaves an array where every clue happens to have&lt;br&gt;
one candidate left. The contract was fine (every caller already guards on &lt;code&gt;ok&lt;/code&gt;);&lt;br&gt;
my assertion was not. It is now a documented precondition rather than a thing&lt;br&gt;
you find out about.&lt;/p&gt;

&lt;p&gt;36 tests. TypeScript, no runtime dependencies.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/sen-ltd/tatamibari" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/tatamibari&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://sen.ltd/portfolio/tatamibari/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/tatamibari/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>puzzle</category>
    </item>
    <item>
      <title>Ripple Effect: the rule that matters most never shows up in the difficulty rating</title>
      <dc:creator>SEN LLC</dc:creator>
      <pubDate>Thu, 06 Aug 2026 15:11:38 +0000</pubDate>
      <link>https://dev.to/sendotltd/ripple-effect-the-rule-that-matters-most-never-shows-up-in-the-difficulty-rating-4n4g</link>
      <guid>https://dev.to/sendotltd/ripple-effect-the-rule-that-matters-most-never-shows-up-in-the-difficulty-rating-4n4g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ripple Effect&lt;/strong&gt; (also sold as Hakyuu and 波及効果) in the browser with&lt;br&gt;
&lt;strong&gt;five rule sets&lt;/strong&gt; inside. Every outlined region of &lt;em&gt;s&lt;/em&gt; cells holds &lt;strong&gt;1 to s&lt;/strong&gt;,&lt;br&gt;
each exactly once. And the rule it is named for: if the same value &lt;em&gt;k&lt;/em&gt; shows up&lt;br&gt;
twice in one row or one column, the two cells must be &lt;strong&gt;more than k apart&lt;/strong&gt; — a&lt;br&gt;
1 needs one clear cell between them, a 5 needs five. Puzzle #30 in the solver&lt;br&gt;
series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Demo: &lt;a href="https://sen.ltd/portfolio/ripple-effect/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/ripple-effect/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/sen-ltd/ripple-effect" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/ripple-effect&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmtij2pfdoin1ar609c2g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmtij2pfdoin1ar609c2g.png" alt="Ripple Effect" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things came out of this one, and only the first was on the plan.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A row is not one constraint graph.&lt;/strong&gt; It is a different graph for every value
you might place in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most boards do not exist.&lt;/strong&gt; 87% of randomly drawn region partitions have no
solution at all, before a single number is written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rule I was proudest of never appears in a difficulty rating.&lt;/strong&gt; It is
worth one percentage point to the solver and everything to the generator.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  A row is not one graph, it is one graph per value
&lt;/h2&gt;

&lt;p&gt;In Sudoku a row is an all-different clique. Nine cells, all mutually exclusive,&lt;br&gt;
and that stays true whichever digit you happen to be holding. Every propagator&lt;br&gt;
you write can take "the row" as a fixed object.&lt;/p&gt;

&lt;p&gt;Here the row is a different object for every value. Two equal values &lt;em&gt;k&lt;/em&gt; must sit&lt;br&gt;
more than &lt;em&gt;k&lt;/em&gt; apart, so the row splits into overlapping windows of width &lt;em&gt;k&lt;/em&gt;+1 —&lt;br&gt;
and the window depends on &lt;em&gt;k&lt;/em&gt;. The conflict graph for 1s is just the grid's own&lt;br&gt;
adjacency graph. The graph for 5s is eleven cells wide. They share no edges&lt;br&gt;
beyond the trivial ones. One board carries as many line-conflict graphs as it has&lt;br&gt;
distinct values.&lt;/p&gt;

&lt;p&gt;So the reach function takes the value as an argument, and this is the entire&lt;br&gt;
puzzle in six lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rippleReach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rowOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;colOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no such thing as "the cells &lt;em&gt;k&lt;/em&gt; conflicts with". Only "the cells &lt;em&gt;k&lt;/em&gt;&lt;br&gt;
conflicts with &lt;strong&gt;when it holds a 4&lt;/strong&gt;".&lt;/p&gt;

&lt;p&gt;Measured on an empty board — the mean number of cells a single placed &lt;em&gt;v&lt;/em&gt;&lt;br&gt;
forbids, against how many &lt;em&gt;v&lt;/em&gt;'s the whole board can hold at once:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;v=1&lt;/th&gt;
&lt;th&gt;v=2&lt;/th&gt;
&lt;th&gt;v=3&lt;/th&gt;
&lt;th&gt;v=4&lt;/th&gt;
&lt;th&gt;v=5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;deg 3.3, room 18&lt;/td&gt;
&lt;td&gt;deg 6.0, room 12&lt;/td&gt;
&lt;td&gt;deg 8.0, room 12&lt;/td&gt;
&lt;td&gt;deg 9.3, room 12&lt;/td&gt;
&lt;td&gt;deg 10.0, room 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;deg 3.5, room 32&lt;/td&gt;
&lt;td&gt;deg 6.5, room 24&lt;/td&gt;
&lt;td&gt;deg 9.0, room 16&lt;/td&gt;
&lt;td&gt;deg 11.0, room 16&lt;/td&gt;
&lt;td&gt;deg 12.5, room 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;deg 3.6, room 50&lt;/td&gt;
&lt;td&gt;deg 6.8, room 40&lt;/td&gt;
&lt;td&gt;deg 9.6, room 30&lt;/td&gt;
&lt;td&gt;deg 12.0, room 20&lt;/td&gt;
&lt;td&gt;deg 14.0, room 20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two halves pull opposite ways, and the tension between them is where the&lt;br&gt;
puzzle lives. A big value reaches far but turns up rarely — only a region of size&lt;br&gt;
&lt;em&gt;v&lt;/em&gt; or more ever holds a &lt;em&gt;v&lt;/em&gt;. A 1 barely reaches at all, but &lt;strong&gt;every&lt;/strong&gt; region&lt;br&gt;
owns one, so 1s are the most crowded thing on the board.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the counting arguments can and cannot live
&lt;/h2&gt;

&lt;p&gt;I wrote this in the header of the solver on day one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every counting argument here is region-scoped, because a region &lt;em&gt;must&lt;/em&gt; hold&lt;br&gt;
each of 1..s while a row promises nothing at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first half is right. A region of size &lt;em&gt;s&lt;/em&gt; has to contain a 3 if &lt;em&gt;s&lt;/em&gt; ≥ 3, so&lt;br&gt;
"only one cell in this region can still take a 3" pins that cell — the ordinary&lt;br&gt;
hidden single. A row has no such duty. A row of a Ripple Effect board need not&lt;br&gt;
contain a 3 anywhere, so there is no line-scoped hidden single, and I concluded&lt;br&gt;
there was nothing to count along a line.&lt;/p&gt;

&lt;p&gt;That is wrong, and the region partition is what makes it wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A region that lies entirely inside one row promises that row a 1, and a 2, and&lt;br&gt;
so on up to its size.&lt;/strong&gt; Trapped regions are disjoint, so their demands add. On&lt;br&gt;
the supply side, the ripple rule caps how many &lt;em&gt;v&lt;/em&gt;'s fit in &lt;em&gt;n&lt;/em&gt; cells at&lt;br&gt;
⌈n/(v+1)⌉. Demand against capacity, per line, per value — a line-scoped counting&lt;br&gt;
argument after all, and the only place where the region-scoped "must contain" and&lt;br&gt;
the line-scoped "must be far apart" ever meet.&lt;/p&gt;

&lt;p&gt;It prunes, too, not just refutes. If a line owes &lt;em&gt;need&lt;/em&gt; copies of &lt;em&gt;v&lt;/em&gt;, then a&lt;br&gt;
cell that cannot be part of any packing of at least &lt;em&gt;need&lt;/em&gt; copies cannot hold &lt;em&gt;v&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;spots&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;spots&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;maxPacked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;maxPacked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;need&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;strike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;maxPacked&lt;/code&gt; is greedy left-to-right, which is optimal here for the usual&lt;br&gt;
interval-scheduling reason. A worked case, with no givens on the board at all:&lt;br&gt;
row 0 holds two 1×3 regions, so it owes two 3s, and a row of 6 has room for&lt;br&gt;
exactly two — they must sit four apart. Columns 2 and 3 are in no such pair, so&lt;br&gt;
neither can hold a 3, and the rule says so from an empty grid.&lt;/p&gt;
&lt;h2&gt;
  
  
  Most boards do not exist
&lt;/h2&gt;

&lt;p&gt;Take the same argument global. Every region of size ≥ &lt;em&gt;v&lt;/em&gt; spends exactly one &lt;em&gt;v&lt;/em&gt;,&lt;br&gt;
those regions are disjoint, and each of the &lt;em&gt;n&lt;/em&gt; rows holds at most ⌈n/(v+1)⌉ of&lt;br&gt;
them. Cut a 6×6 board into eighteen dominoes and it is over: &lt;strong&gt;eighteen regions&lt;br&gt;
want a 2 and the board has room for twelve&lt;/strong&gt;. No numbers, no givens, no solution.&lt;/p&gt;

&lt;p&gt;The smallest case in the whole family is prettier still — two size-1 regions&lt;br&gt;
sitting orthogonally adjacent. A size-1 region is a forced 1, so that is two 1s&lt;br&gt;
one step apart, and 1s need a gap.&lt;/p&gt;

&lt;p&gt;Which made me wonder how often a random partition survives any of this. The&lt;br&gt;
answer is: rarely. The solver is the ground truth here, and I brute-force&lt;br&gt;
verified a sample of the verdicts with a counter that enumerates region&lt;br&gt;
permutations, because "most of your boards are impossible" is exactly the kind of&lt;br&gt;
claim that is usually a bug.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;size-1 share&lt;/th&gt;
&lt;th&gt;dead&lt;/th&gt;
&lt;th&gt;adjacent singletons&lt;/th&gt;
&lt;th&gt;board capacity&lt;/th&gt;
&lt;th&gt;line rule&lt;/th&gt;
&lt;th&gt;any of them&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;31%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;62%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;48%&lt;/td&gt;
&lt;td&gt;93%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;td&gt;32%&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;66%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;66%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;93%&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the middle 6×6 row: 87% of partitions are dead; the adjacent-singleton test&lt;br&gt;
explains 56% of the corpses, the line rule another handful, and &lt;strong&gt;38% of them are&lt;br&gt;
dead for reasons no static test I have catches&lt;/strong&gt;. Across every row, the cheap&lt;br&gt;
tests never once killed a partition that had a solution.&lt;/p&gt;

&lt;p&gt;At 10×10 it is worse: 93% dead, and the counting tests stop explaining any of it.&lt;/p&gt;

&lt;p&gt;This is a genuinely different shape from the other puzzles in this series. In&lt;br&gt;
Kurodoko or Yajilin you paint a legal board first and the skeleton comes along&lt;br&gt;
for free. Here the skeleton is chosen blind and is usually a corpse, and the&lt;br&gt;
generator's real job is drawing skeletons until one is alive.&lt;/p&gt;
&lt;h2&gt;
  
  
  The prefilter that made things slower
&lt;/h2&gt;

&lt;p&gt;Obvious next move: the solver is expensive, the counting tests are arithmetic, so&lt;br&gt;
skip the solver on partitions that are obviously hopeless. Free speed.&lt;/p&gt;

&lt;p&gt;It is not free. Median of 5 trials, order alternated to cancel warm-up:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;live / drawn&lt;/th&gt;
&lt;th&gt;solver only&lt;/th&gt;
&lt;th&gt;with prefilter&lt;/th&gt;
&lt;th&gt;speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;40 / 396&lt;/td&gt;
&lt;td&gt;35 ms&lt;/td&gt;
&lt;td&gt;44 ms&lt;/td&gt;
&lt;td&gt;0.80×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;20 / 312&lt;/td&gt;
&lt;td&gt;753 ms&lt;/td&gt;
&lt;td&gt;760 ms&lt;/td&gt;
&lt;td&gt;0.99×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Slower at 6×6, indistinguishable at 8×8. The premise was wrong. A dead partition is dead for reasons the ordinary&lt;br&gt;
propagators hit within a round or two, so the solver refutes it almost&lt;br&gt;
immediately — there is no expensive search to skip. Meanwhile the "cheap" test&lt;br&gt;
rebuilds the line structures for every candidate, which is the most expensive&lt;br&gt;
thing in the loop.&lt;/p&gt;

&lt;p&gt;There is a second lesson buried in that table, and it cost me an hour. The first&lt;br&gt;
version of this measurement took &lt;strong&gt;one&lt;/strong&gt; sample per configuration, and on two&lt;br&gt;
consecutive runs it read 0.4× and 2.4× — the second one while a test suite was&lt;br&gt;
running on the same machine. I had already written the "it is negative" paragraph&lt;br&gt;
around the first sample. A timing claim from a single shot is not a measurement;&lt;br&gt;
it now takes five, alternates the order, and reports a median, and the honest&lt;br&gt;
answer turns out to be "no difference worth having" rather than the more dramatic&lt;br&gt;
thing I nearly published.&lt;/p&gt;

&lt;p&gt;So the prefilter came out of the generator. &lt;code&gt;looksDead&lt;/code&gt; stays in the codebase&lt;br&gt;
because it answers a different question — it says &lt;em&gt;why&lt;/em&gt; a partition is dead, one&lt;br&gt;
named reason at a time, which is what the mortality table above is made of. It is&lt;br&gt;
not there to save time, because it doesn't.&lt;/p&gt;
&lt;h2&gt;
  
  
  The strongest rule never shows up in a difficulty rating
&lt;/h2&gt;

&lt;p&gt;Five rule sets, each strictly containing the one below:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;what it adds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;region&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a region of size &lt;em&gt;s&lt;/em&gt; holds 1..&lt;em&gt;s&lt;/em&gt;, once each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ripple&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a placed &lt;em&gt;v&lt;/em&gt; erases &lt;em&gt;v&lt;/em&gt; within &lt;em&gt;v&lt;/em&gt; steps along the row and column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hidden&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a value with only one seat left in its region takes it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lines&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;demand from trapped regions against the line's capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;probe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;singleton consistency: assume a value, propagate, watch it die&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reach, measured on boards carrying the same number of givens as the shipped banks&lt;br&gt;
but &lt;strong&gt;never filtered for solvability&lt;/strong&gt;, so the last column is not circular. Each&lt;br&gt;
cell reads &lt;em&gt;surplus candidates eliminated&lt;/em&gt; / &lt;em&gt;boards finished outright&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;region&lt;/th&gt;
&lt;th&gt;ripple&lt;/th&gt;
&lt;th&gt;hidden&lt;/th&gt;
&lt;th&gt;lines&lt;/th&gt;
&lt;th&gt;probe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6 (N=40)&lt;/td&gt;
&lt;td&gt;15% / 0%&lt;/td&gt;
&lt;td&gt;52% / 0%&lt;/td&gt;
&lt;td&gt;63% / 5%&lt;/td&gt;
&lt;td&gt;63% / 5%&lt;/td&gt;
&lt;td&gt;82% / 13%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8 (N=25)&lt;/td&gt;
&lt;td&gt;18% / 0%&lt;/td&gt;
&lt;td&gt;56% / 0%&lt;/td&gt;
&lt;td&gt;64% / 0%&lt;/td&gt;
&lt;td&gt;64% / 0%&lt;/td&gt;
&lt;td&gt;81% / 0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;lines&lt;/code&gt; adds nothing. Not "a little" — the columns are identical. The ablation&lt;br&gt;
says the same thing from the other direction, dropping one propagator at a time&lt;br&gt;
from the strongest fixpoint over 30 solvable 6×6 boards:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;dropped&lt;/th&gt;
&lt;th&gt;candidates eliminated&lt;/th&gt;
&lt;th&gt;boards finished&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;30/30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;region&lt;/td&gt;
&lt;td&gt;97%&lt;/td&gt;
&lt;td&gt;27/30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ripple&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;td&gt;0/30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hidden&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;10/30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lines&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;td&gt;29/30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One percentage point and one board. And not a single board in the shipped bank of&lt;br&gt;
48 is labelled &lt;code&gt;lines&lt;/code&gt; — they come out as &lt;code&gt;ripple&lt;/code&gt; or &lt;code&gt;hidden&lt;/code&gt;, every time.&lt;/p&gt;

&lt;p&gt;The rule with the sharpest argument behind it, the one that took the most care to&lt;br&gt;
get right, is dead weight to the solver. Its entire contribution is upstream, on&lt;br&gt;
the skeleton, deciding which boards get to exist at all. By the time a board has&lt;br&gt;
givens on it, the cheaper rules have already been everywhere it would go.&lt;/p&gt;

&lt;p&gt;The lesson I am taking from it: &lt;strong&gt;a difficulty rating measures the solver, and a&lt;br&gt;
solver is only one of the two consumers of a rule.&lt;/strong&gt; A rule can be load-bearing&lt;br&gt;
for the generator and worthless for the player, and nothing in the difficulty&lt;br&gt;
column will ever tell you which. If I had only ever measured rule sets the usual&lt;br&gt;
way — reach on finished boards — I would have deleted &lt;code&gt;lines&lt;/code&gt; as dead code and&lt;br&gt;
never found out that it is the thing that explains why most boards do not exist.&lt;/p&gt;

&lt;p&gt;Meanwhile the eponymous rule is exactly as important as the name suggests:&lt;br&gt;
without &lt;code&gt;ripple&lt;/code&gt; the solver eliminates 16% of the candidates and finishes nothing&lt;br&gt;
at all.&lt;/p&gt;
&lt;h2&gt;
  
  
  Generation: one monotone knob and one that isn't
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;givens&lt;/strong&gt; are the pleasant half. A given is written on a cell whose value the&lt;br&gt;
intended solution already fixes, so &lt;strong&gt;adding one cannot change the solution&lt;/strong&gt; — it&lt;br&gt;
can only remove rivals. Termination is free: in the worst case every cell becomes&lt;br&gt;
a given and the board is pinned by construction. Size-1 regions are kept out of&lt;br&gt;
the pool, since a given there would only repeat what the partition already says.&lt;/p&gt;

&lt;p&gt;Then the walk runs backwards and drops every given that is not pulling its weight.&lt;br&gt;
The test is not "is it still unique" but "can the rule set still finish it", which&lt;br&gt;
is strictly stronger — every propagator here is sound, so a rule set that decides&lt;br&gt;
every cell has also &lt;em&gt;proved&lt;/em&gt; the board unique. The sweep removes 43–48% of them.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;partition&lt;/strong&gt; has no such courtesy. It is monotone in nothing, it is usually&lt;br&gt;
dead, and the only thing to do with a dead one is throw it away whole.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cross-checking
&lt;/h2&gt;

&lt;p&gt;Three counters, and any unsound propagator shows up as a disagreement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;by cells&lt;/strong&gt; — walk the grid in row-major order, trying every value a cell
could hold&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;by regions&lt;/strong&gt; — hand each region a permutation of 1..&lt;em&gt;s&lt;/em&gt; at a time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;with propagation&lt;/strong&gt; — the rule sets plus search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first two enumerate opposite halves of the problem and share nothing but the&lt;br&gt;
geometry helpers. &lt;code&gt;validate&lt;/code&gt; re-reads the distance rule from scratch by brute&lt;br&gt;
force over every ordered pair in every line, and shares no code with the&lt;br&gt;
propagators at all. The soundness tests take boards with known solutions, run each&lt;br&gt;
propagator, and insist the solution's value survived in every cell — which is the&lt;br&gt;
property that makes "finished without search" a uniqueness certificate rather than&lt;br&gt;
a guess that happened to work.&lt;/p&gt;

&lt;p&gt;34 tests. Puzzle #30 in the solver series.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev        &lt;span class="c"&gt;# demo at http://localhost:5173&lt;/span&gt;
npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run generate   &lt;span class="c"&gt;# rebuild the bank&lt;/span&gt;
npm run stats      &lt;span class="c"&gt;# every number in this post&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MIT. No runtime dependencies.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>puzzle</category>
    </item>
    <item>
      <title>Solving Kurodoko: whether a rule looks useless depends on where you measure it</title>
      <dc:creator>SEN LLC</dc:creator>
      <pubDate>Thu, 06 Aug 2026 00:41:58 +0000</pubDate>
      <link>https://dev.to/sendotltd/solving-kurodoko-whether-a-rule-looks-useless-depends-on-where-you-measure-it-3ob8</link>
      <guid>https://dev.to/sendotltd/solving-kurodoko-whether-a-rule-looks-useless-depends-on-where-you-measure-it-3ob8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kurodoko&lt;/strong&gt; (also sold as Kuromasu and 黒マスはどこだ) in the browser with&lt;br&gt;
&lt;strong&gt;four rule sets&lt;/strong&gt; inside. Blacken some cells. A numbered cell is always&lt;br&gt;
white, and its number is how many white cells it can &lt;strong&gt;see&lt;/strong&gt; — itself, plus&lt;br&gt;
the unbroken runs of white in all four directions, each stopping at the first&lt;br&gt;
black cell or the edge. &lt;strong&gt;No two black cells touch&lt;/strong&gt;, and &lt;strong&gt;all the white&lt;br&gt;
cells form one connected group&lt;/strong&gt;. Puzzle #29 in the solver series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Demo: &lt;a href="https://sen.ltd/portfolio/kurodoko/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/kurodoko/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/sen-ltd/kurodoko" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/kurodoko&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsen.ltd%2Fportfolio%2Fkurodoko%2Fassets%2Fscreenshot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsen.ltd%2Fportfolio%2Fkurodoko%2Fassets%2Fscreenshot.png" alt="Kurodoko" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two measurement stories this time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The cross-check caught an unsound propagator&lt;/strong&gt; — one that looked obviously
correct while I was writing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same rule looked worthless and then decisive&lt;/strong&gt;, with nothing changing
but where I measured it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The variable is not the cell
&lt;/h2&gt;

&lt;p&gt;A clue with number &lt;em&gt;k&lt;/em&gt; says&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 + up + right + down + left == k
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where each term is the length of an unbroken white run with a hard geometric&lt;br&gt;
cap. &lt;strong&gt;Four bounded integers summing to a constant&lt;/strong&gt; is a shape a solver&lt;br&gt;
already knows what to do with: a direction's run is at least the total minus&lt;br&gt;
what the other three could contribute at their most generous, and at most the&lt;br&gt;
total minus what they must contribute at their meanest. Plain interval&lt;br&gt;
propagation.&lt;/p&gt;

&lt;p&gt;How much smaller does that make the problem? Measuring the size of one clue's&lt;br&gt;
domain — the compositions of &lt;em&gt;k&lt;/em&gt;−1 into four capped parts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;mean compositions per clue&lt;/th&gt;
&lt;th&gt;largest&lt;/th&gt;
&lt;th&gt;raw cell space&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;9.9&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;2³⁶ ≈ 6.9e10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;19.0&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;2⁶⁴ ≈ 1.8e19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;35.2&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;2¹⁰⁰ ≈ 1.3e30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Thirty-five options at 10×10, against a raw cell space of 2¹⁰⁰.&lt;/p&gt;

&lt;p&gt;And the reach of each rule set, measured on boards carrying the same number of&lt;br&gt;
clues as the shipped banks but &lt;strong&gt;never filtered for solvability&lt;/strong&gt;, so the last&lt;br&gt;
column is not circular:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;cells&lt;/th&gt;
&lt;th&gt;rays&lt;/th&gt;
&lt;th&gt;connect&lt;/th&gt;
&lt;th&gt;probe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6 cells decided&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8 cells decided&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;34%&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;86%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reading each clue on its own gets 8%. Lifting it to four integers gets 25–34%.&lt;/p&gt;
&lt;h2&gt;
  
  
  The rule that looked obviously right and wasn't
&lt;/h2&gt;

&lt;p&gt;Once a direction's run is pinned to &lt;code&gt;[lo, hi]&lt;/code&gt;, two conclusions seem to follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the run is at least &lt;code&gt;lo&lt;/code&gt;, so the first &lt;code&gt;lo&lt;/code&gt; cells of that ray are white&lt;/li&gt;
&lt;li&gt;the run is at most &lt;code&gt;hi&lt;/code&gt;, so the cell at &lt;code&gt;hi&lt;/code&gt; is black&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The first is sound. The second is not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"At most &lt;code&gt;hi&lt;/code&gt;" does not put a black cell at index &lt;code&gt;hi&lt;/code&gt;. If the run stops short,&lt;br&gt;
the cell at &lt;code&gt;hi&lt;/code&gt; sits &lt;strong&gt;beyond the stopping cell&lt;/strong&gt; — outside this clue's view&lt;br&gt;
entirely — and is completely free. The step is only justified once the whole&lt;br&gt;
prefix is already known white, which pins the run at exactly &lt;code&gt;hi&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That rule shipped into the first working version, and the boards it generated&lt;br&gt;
looked perfectly fine. What caught it was the counters disagreeing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6x6 #0: byCells=1 byRays=1 byRules=0   &amp;lt;- only the propagation-backed counter
                                           says there are no solutions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Propagation was killing the real solution, which is exactly what an unsound&lt;br&gt;
propagator does. Fixing it re-labelled the difficulty of the whole bank — the&lt;br&gt;
weaker rule sets had been "solving" boards by smuggling in the answer.&lt;/p&gt;

&lt;p&gt;The two brute-force counters are deliberately written to enumerate &lt;strong&gt;opposite&lt;br&gt;
halves&lt;/strong&gt; of the problem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;by cells&lt;/strong&gt; — walk the grid row-major, pruning on clue bounds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;by rays&lt;/strong&gt; — walk the clues, choosing each one's four ray lengths from the
compositions of &lt;em&gt;k&lt;/em&gt;−1, and paint the consequences&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They share no logic with the propagators and none with each other, so one&lt;br&gt;
mistake cannot show up identically in both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whether a rule looks useless depends on where you measure it
&lt;/h2&gt;

&lt;p&gt;This was the interesting part.&lt;/p&gt;

&lt;p&gt;Drop one propagator from the strongest &lt;strong&gt;fixpoint&lt;/strong&gt; and count again (6×6, 30&lt;br&gt;
boards):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;dropped&lt;/th&gt;
&lt;th&gt;free cells still decided&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shaded-adjacency&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;plain per-clue counting&lt;/td&gt;
&lt;td&gt;35% (no change — the lifting subsumes it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;the ray lifting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;connectivity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The lifting is load-bearing: 35% collapses to 7% without it. Expected.&lt;/p&gt;

&lt;p&gt;And connectivity moves the number by &lt;strong&gt;one point&lt;/strong&gt;. On that table alone the&lt;br&gt;
conclusion writes itself: dead weight. It looks exactly like the checkerboard&lt;br&gt;
colouring argument from &lt;a href="https://dev.to/sendotltd/solving-yajilin-clues-can-pin-the-shading-but-they-can-never-steer-the-loop-4f00"&gt;the previous entry&lt;/a&gt;,&lt;br&gt;
which stayed at 78% whether you kept it or not.&lt;/p&gt;

&lt;p&gt;It isn't. Measure the same rule inside &lt;strong&gt;singleton consistency&lt;/strong&gt; — assume a&lt;br&gt;
cell, propagate, see whether the board dies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;free cells decided&lt;/th&gt;
&lt;th&gt;boards finished&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;probe &lt;strong&gt;with&lt;/strong&gt; connectivity&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25/25&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;probe &lt;strong&gt;without&lt;/strong&gt; connectivity&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8/25&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same propagator. Same boards. Opposite verdict.&lt;/p&gt;

&lt;p&gt;The two measurements ask the rule to do different jobs. At fixpoint, the job is&lt;br&gt;
&lt;strong&gt;decide a cell on your own&lt;/strong&gt; — and connectivity almost never can, because&lt;br&gt;
early on there are too few known white cells for an articulation point to&lt;br&gt;
separate any of them.&lt;/p&gt;

&lt;p&gt;Inside singleton consistency the job is &lt;strong&gt;refute an assumption&lt;/strong&gt;. The moment&lt;br&gt;
you assume a cell is black, the board becomes far more constrained: more whites&lt;br&gt;
are forced, more blacks are forced, and severing the white region becomes a&lt;br&gt;
real possibility. As a decider it is powerless; as a refuter it is the&lt;br&gt;
difference between finishing 8 boards and finishing 25.&lt;/p&gt;

&lt;p&gt;I called Yajilin's colouring argument "true and worthless" last time, and that&lt;br&gt;
verdict holds — it was worthless at fixpoint &lt;em&gt;and&lt;/em&gt; inside probing, refuting 1&lt;br&gt;
wrong guess in 1090. This one is not the same situation, and the lesson is:&lt;br&gt;
&lt;strong&gt;before concluding a rule does nothing, measure it in both of the jobs a&lt;br&gt;
propagator can hold.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a clue never changes the answer
&lt;/h2&gt;

&lt;p&gt;The generator has exactly the opposite character to the last one.&lt;/p&gt;

&lt;p&gt;In Yajilin a clue cell is &lt;em&gt;off&lt;/em&gt; the loop, so adding a clue pulls a cell out of&lt;br&gt;
the solution and &lt;strong&gt;changes it&lt;/strong&gt; — and routing ambiguity could not be fixed by&lt;br&gt;
clues at any price.&lt;/p&gt;

&lt;p&gt;In Kurodoko a clue cell is just &lt;strong&gt;a white cell carrying a number&lt;/strong&gt;, and it was&lt;br&gt;
white already. So:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Adding a clue cannot change the solution. It can only remove rivals.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That makes the whole pipeline monotone and termination free. Paint a legal&lt;br&gt;
board, then keep adding clues until nothing else solves it — in the worst case&lt;br&gt;
every white cell becomes a clue and the board is pinned by construction. Then&lt;br&gt;
run it backwards and drop every clue that turns out not to be pulling its&lt;br&gt;
weight:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;clues when the walk stopped&lt;/th&gt;
&lt;th&gt;after the sweep&lt;/th&gt;
&lt;th&gt;removed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;td&gt;6.2&lt;/td&gt;
&lt;td&gt;21%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;12.5&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;18.1&lt;/td&gt;
&lt;td&gt;15.1&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The test for keeping a clue is not "is the board still unique" but "&lt;strong&gt;can the&lt;br&gt;
rule set still finish it&lt;/strong&gt;" — which is strictly stronger, because a rule set&lt;br&gt;
that decides every cell has &lt;em&gt;proved&lt;/em&gt; uniqueness (every propagator is sound). A&lt;br&gt;
board that is unique but unreachable by propagation leaves the demo's hint&lt;br&gt;
button with nothing to say.&lt;/p&gt;

&lt;p&gt;I got that distinction wrong first, pruned for uniqueness only, and then&lt;br&gt;
rejected every board because its post-sweep difficulty came back &lt;code&gt;null&lt;/code&gt;. Two&lt;br&gt;
hundred attempts, zero output.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Tarjan pass instead of a flood fill per candidate
&lt;/h2&gt;

&lt;p&gt;The connectivity propagator asks which still-open cells would strand a white&lt;br&gt;
cell if blackened. The obvious implementation blanks each candidate and redoes&lt;br&gt;
the flood fill — O(cells²) per pass. And it sits &lt;em&gt;inside&lt;/em&gt; singleton&lt;br&gt;
consistency, so that factor gets cubed and a 10×10 board simply stops&lt;br&gt;
finishing.&lt;/p&gt;

&lt;p&gt;One Tarjan pass answers it for every cell at once: an articulation point of the&lt;br&gt;
not-black graph separates whites exactly when some child subtree holds a white&lt;br&gt;
cell and at least one white cell lies outside it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;blunt&lt;/th&gt;
&lt;th&gt;Tarjan&lt;/th&gt;
&lt;th&gt;speedup&lt;/th&gt;
&lt;th&gt;disagreements&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6, 200 partial boards&lt;/td&gt;
&lt;td&gt;11ms&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;td&gt;5.5×&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10, 200 partial boards&lt;/td&gt;
&lt;td&gt;52ms&lt;/td&gt;
&lt;td&gt;5ms&lt;/td&gt;
&lt;td&gt;10.4×&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The blunt version stays in the test suite as the reference the fast one is&lt;br&gt;
checked against — on 60 random partial boards, with an assertion that at least&lt;br&gt;
one of them actually forced a cell, so the comparison cannot pass by testing&lt;br&gt;
nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Lift a clue to &lt;strong&gt;four bounded integers summing to k−1&lt;/strong&gt; and a whole-board
counting rule becomes ordinary interval propagation: 8% of cells decided
becomes 25–34%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The run is at most &lt;code&gt;hi&lt;/code&gt;, so cells[hi] is black" is unsound&lt;/strong&gt; — the run can
stop short and leave that cell outside the clue's view. Found by two
independent counters disagreeing with a third.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether a rule looks useless depends on where you measure it.&lt;/strong&gt;
Connectivity is worth one point at fixpoint and takes probing from 8/25 to
25/25 boards finished. Measure a propagator as a &lt;em&gt;refuter&lt;/em&gt;, not only as a
&lt;em&gt;decider&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;A clue cell here is already white, so &lt;strong&gt;adding clues never changes the
answer&lt;/strong&gt; — generation is monotone and terminates for free. The redundancy
sweep then removes 17–21% of them.&lt;/li&gt;
&lt;li&gt;Prune against "&lt;strong&gt;can the rule set still finish it&lt;/strong&gt;", not "is it still
unique". The former implies the latter and keeps the boards playable.&lt;/li&gt;
&lt;li&gt;Articulation points: one &lt;strong&gt;Tarjan pass&lt;/strong&gt;, not a flood fill per candidate
(5.5–10.4×, zero disagreements).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;27 tests. Puzzle #29 in the solver series.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>puzzle</category>
    </item>
    <item>
      <title>Solving Yajilin: clues can pin the shading, but they can never steer the loop</title>
      <dc:creator>SEN LLC</dc:creator>
      <pubDate>Wed, 05 Aug 2026 23:54:38 +0000</pubDate>
      <link>https://dev.to/sendotltd/solving-yajilin-clues-can-pin-the-shading-but-they-can-never-steer-the-loop-4f00</link>
      <guid>https://dev.to/sendotltd/solving-yajilin-clues-can-pin-the-shading-but-they-can-never-steer-the-loop-4f00</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Yajilin&lt;/strong&gt; in the browser with &lt;strong&gt;four rule sets&lt;/strong&gt; inside. Shade some cells,&lt;br&gt;
then run &lt;strong&gt;one closed loop&lt;/strong&gt; through every cell you did not shade. &lt;strong&gt;No two&lt;br&gt;
shaded cells touch.&lt;/strong&gt; Each black clue sits outside the loop, and its arrow&lt;br&gt;
counts the &lt;strong&gt;shaded cells in that direction&lt;/strong&gt;, all the way to the edge of the&lt;br&gt;
board. Puzzle #28 in the solver series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Demo: &lt;a href="https://sen.ltd/portfolio/yajilin/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/yajilin/&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/sen-ltd/yajilin" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/yajilin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsen.ltd%2Fportfolio%2Fyajilin%2Fassets%2Fscreenshot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsen.ltd%2Fportfolio%2Fyajilin%2Fassets%2Fscreenshot.png" alt="Yajilin" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is also the entry where &lt;strong&gt;the idea I built the whole thing around turned&lt;br&gt;
out to be worthless&lt;/strong&gt;, and I have the measurements to prove it. Both results up&lt;br&gt;
front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What worked was noticing an &lt;strong&gt;asymmetry&lt;/strong&gt;. Clues can pin the shading down;
they can never steer the loop's routing. So routing uniqueness cannot be
repaired after the fact — it decides the entire design of the generator.&lt;/li&gt;
&lt;li&gt;What did not work was the &lt;strong&gt;checkerboard colouring argument&lt;/strong&gt;. It is true,
it is free, it is pretty. Measured, it decides zero extra cells on &lt;strong&gt;0 of 40
boards&lt;/strong&gt; and refutes &lt;strong&gt;1 wrong guess in 1090&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Two puzzles tangled together — but not symmetrically
&lt;/h2&gt;

&lt;p&gt;In Yajilin a cell is shaded exactly when the loop misses it, so every deduction&lt;br&gt;
about one is a deduction about the other. That much is familiar from&lt;br&gt;
&lt;a href="https://github.com/sen-ltd/norinori" rel="noopener noreferrer"&gt;Norinori&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://github.com/sen-ltd/lits" rel="noopener noreferrer"&gt;LITS&lt;/a&gt; earlier in this series.&lt;/p&gt;

&lt;p&gt;What is different here is that the tangle is &lt;strong&gt;not symmetric&lt;/strong&gt;, and it took a&lt;br&gt;
badly behaved generator to make me see it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A clue cell is off the loop by definition.&lt;/strong&gt; So adding a clue can take a&lt;br&gt;
cell &lt;em&gt;off&lt;/em&gt; the loop. That is the only thing it can do to the loop. It can&lt;br&gt;
never steer the routing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That has teeth. If the set of cells the loop covers admits &lt;strong&gt;two different&lt;br&gt;
Hamiltonian circuits&lt;/strong&gt;, the board has two solutions — and &lt;strong&gt;no amount of extra&lt;br&gt;
clues will ever fix it&lt;/strong&gt;. Shading ambiguity is fixable by clues. Routing&lt;br&gt;
ambiguity is not. A new clue changes which cells are available, but you cannot&lt;br&gt;
aim it at one circuit and kill it.&lt;/p&gt;

&lt;p&gt;Which means &lt;strong&gt;routing uniqueness has to be designed into the loop's shape&lt;br&gt;
before a single clue exists&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I did not start there. The first generator did the obvious thing — grow a loop,&lt;br&gt;
split the leftover cells into shaded cells and clues, then add clues until the&lt;br&gt;
board is unique — and it produced &lt;strong&gt;8×8 boards with 24 clues and zero shaded&lt;br&gt;
cells&lt;/strong&gt;. It was converting shaded cells into clues, over and over, trying to&lt;br&gt;
kill an ambiguity that clues cannot touch, until there was nothing left to&lt;br&gt;
shade.&lt;/p&gt;
&lt;h2&gt;
  
  
  Growing a loop by inflation
&lt;/h2&gt;

&lt;p&gt;The loop needs to be a random closed circuit. I grow it by &lt;strong&gt;inflation&lt;/strong&gt;: start&lt;br&gt;
from a 2×2 square, the smallest legal loop, then repeatedly take one edge and&lt;br&gt;
push it out into the neighbouring 2×2, replacing that edge with a three-edge&lt;br&gt;
detour through two fresh cells.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; x───y         x       y
               └───┐
 p   q         p───q      (x-y becomes x-p-q-y; p and q join the loop)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This move &lt;strong&gt;always&lt;/strong&gt; leaves one closed circuit. No connectivity check, no&lt;br&gt;
rejection, and the loop grows by exactly two cells each time. Whatever the loop&lt;br&gt;
misses becomes either a shaded cell (a greedy independent subset, since shaded&lt;br&gt;
cells may not touch) or a clue, and the arrow numbers are read straight off the&lt;br&gt;
finished shading.&lt;/p&gt;
&lt;h2&gt;
  
  
  A freely grown loop essentially never has a unique routing
&lt;/h2&gt;

&lt;p&gt;Grown to 62% coverage, then counting the Hamiltonian circuits the cell set&lt;br&gt;
admits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;loops with a unique circuit&lt;/th&gt;
&lt;th&gt;median rival circuits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;7/40 (18%)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;60+ (counting cap)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero at 8×8 and above, with a median of 52 rival circuits. No wonder the first&lt;br&gt;
generator ate all the shading: it was fighting an ambiguity it had no weapon&lt;br&gt;
against.&lt;/p&gt;
&lt;h3&gt;
  
  
  Two attempts to ban it locally, both failed
&lt;/h3&gt;

&lt;p&gt;If the ambiguity has a shape, ban the shape. The obvious culprit is a &lt;strong&gt;2×2&lt;br&gt;
square whose four cells are all on the loop and which the loop enters by two&lt;br&gt;
parallel edges&lt;/strong&gt; — swap that pair for the other pair and every degree stays at&lt;br&gt;
two, so you may well have a second circuit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1: ban the shape outright during growth.&lt;/strong&gt; Uniqueness went to 100% —&lt;br&gt;
40/40 at every size. But &lt;strong&gt;growth stalls&lt;/strong&gt;: against a 62% target it reached&lt;br&gt;
21–43%. That leaves most of the board off-loop, and most of &lt;em&gt;that&lt;/em&gt; becomes&lt;br&gt;
clues. A 10×10 with 38 clues is not a puzzle. Useless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2: ban only the swaps that really produce a rival.&lt;/strong&gt; Do the swap, and&lt;br&gt;
if the result is still a single circuit it is a genuine rival; if it splits the&lt;br&gt;
circuit in two it is harmless. Sharper condition, checked at every move. Result&lt;br&gt;
at 8×8: &lt;strong&gt;0/40&lt;/strong&gt;. It did nothing at all.&lt;/p&gt;

&lt;p&gt;The reason is that the property is &lt;strong&gt;not monotone&lt;/strong&gt;. A square whose swap splits&lt;br&gt;
the circuit today becomes a genuine rival after two more inflations &lt;em&gt;somewhere&lt;br&gt;
else&lt;/em&gt;. A test applied at move time is worthless by the time the loop is&lt;br&gt;
finished. The shape ban is monotone but kills growth; the effect ban preserves&lt;br&gt;
growth but does not hold. Local conditions do not reach.&lt;/p&gt;
&lt;h2&gt;
  
  
  Descending the shape itself
&lt;/h2&gt;

&lt;p&gt;That leaves the only honest objective: &lt;strong&gt;how many Hamiltonian circuits the cell&lt;br&gt;
set admits&lt;/strong&gt;, counted for real.&lt;/p&gt;

&lt;p&gt;To change the shape without changing the size, pair a &lt;strong&gt;deflation&lt;/strong&gt; with an&lt;br&gt;
&lt;strong&gt;inflation&lt;/strong&gt;. Deflation is exactly the inverse of the growth step: where the&lt;br&gt;
loop runs &lt;code&gt;x-p-q-y&lt;/code&gt; and &lt;code&gt;x&lt;/code&gt; is adjacent to &lt;code&gt;y&lt;/code&gt;, drop &lt;code&gt;p&lt;/code&gt; and &lt;code&gt;q&lt;/code&gt; and join &lt;code&gt;x&lt;/code&gt;&lt;br&gt;
to &lt;code&gt;y&lt;/code&gt;. It also preserves the single circuit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deflate:  x-p-q-y  -&amp;gt;  x-y      (two cells smaller)
inflate:  x-y      -&amp;gt;  x-p-q-y  (two cells larger)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take a random deflation and a random inflation, keep the pair if the circuit&lt;br&gt;
count did not go up, roll back otherwise. That is the whole thing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;reached target size with exactly one circuit&lt;/th&gt;
&lt;th&gt;seeds abandoned&lt;/th&gt;
&lt;th&gt;time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40/40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;54ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40/40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;952ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10×10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40/40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;17.9s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;0/40 to 40/40, at the target size. Norinori's generator taught me that&lt;br&gt;
hill-climbing on &lt;em&gt;solution count&lt;/em&gt; oscillates and never converges; this&lt;br&gt;
objective behaves because it depends only on the loop's shape, with no shading&lt;br&gt;
or clues entangled in it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding clues by looking at the counterexample
&lt;/h2&gt;

&lt;p&gt;With the routing pinned, the only ambiguity left is in the &lt;strong&gt;shading&lt;/strong&gt; — and&lt;br&gt;
clues can fix that.&lt;/p&gt;

&lt;p&gt;The first version picked at random: if the board has more than one solution,&lt;br&gt;
turn some random shaded cell into a clue. It works, but it is wasteful, and it&lt;br&gt;
strips the board of nearly all its shading before it happens to hit the cells&lt;br&gt;
that were actually ambiguous.&lt;/p&gt;

&lt;p&gt;The fix is to ask the solver for &lt;strong&gt;one rival solution&lt;/strong&gt; and look at the&lt;br&gt;
disagreement. Pick a cell that our solution shades and the rival does not, and&lt;br&gt;
convert &lt;em&gt;that&lt;/em&gt; one. The move is guaranteed on both sides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it &lt;strong&gt;kills this rival&lt;/strong&gt; — nobody can shade that cell any more&lt;/li&gt;
&lt;li&gt;it &lt;strong&gt;leaves our solution standing&lt;/strong&gt; — the cell was off-loop already, so the
loop is untouched&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At 8×8 the random version converged on 24 clues and 0 shaded cells. The&lt;br&gt;
counterexample version gives &lt;strong&gt;12–18 clues and 6–12 shaded cells&lt;/strong&gt;; at 10×10,&lt;br&gt;
21–28 clues and 10–17 shaded. Those are Yajilin-shaped boards.&lt;/p&gt;
&lt;h2&gt;
  
  
  And now the pretty argument that does nothing
&lt;/h2&gt;

&lt;p&gt;This is the part I most wanted to write.&lt;/p&gt;

&lt;p&gt;A grid graph is &lt;strong&gt;bipartite&lt;/strong&gt;. Colour it like a checkerboard and every pair of&lt;br&gt;
adjacent cells differs in colour, so a &lt;strong&gt;closed loop alternates colours&lt;/strong&gt; and&lt;br&gt;
must cover &lt;strong&gt;equally many of each&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;W&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt; the non-clue cells of each colour and &lt;code&gt;sw&lt;/code&gt;, &lt;code&gt;sb&lt;/code&gt; the shaded counts,&lt;br&gt;
the loop covers &lt;code&gt;W - sw&lt;/code&gt; white cells and &lt;code&gt;B - sb&lt;/code&gt; black ones, so&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;W - sw == B - sb    &amp;lt;=&amp;gt;    sw - sb == W - B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the right-hand side is a &lt;strong&gt;constant fixed by the clue positions alone&lt;/strong&gt; —&lt;br&gt;
known before you shade a cell or draw an edge. A whole-board constraint, for one&lt;br&gt;
O(cells) pass, that no amount of local arrow-counting can see.&lt;/p&gt;

&lt;p&gt;I was sure this would carry the entry. Measured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;boards where colouring decided more than the arrows:   0 / 40
mean gap:                                             +0.0 cells
wrong guesses the arrows accept and colouring refutes:  1 / 1090  (0%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Nothing.&lt;/strong&gt; Drop it from the strongest rule set and the fixpoint stays at 78%,&lt;br&gt;
unchanged to the point. Look at rule-set reach and &lt;code&gt;arrows&lt;/code&gt; and &lt;code&gt;parity&lt;/code&gt; are&lt;br&gt;
&lt;em&gt;identical&lt;/em&gt;: 30%/30% at 6×6, 32%/32% at 8×8.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why it does nothing
&lt;/h3&gt;

&lt;p&gt;The algebra shows it. Write the unknown cells that will end up shaded as &lt;code&gt;xw&lt;/code&gt;&lt;br&gt;
(0 to &lt;code&gt;uw&lt;/code&gt;) on white and &lt;code&gt;xb&lt;/code&gt; (0 to &lt;code&gt;ub&lt;/code&gt;) on black, and the identity becomes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xw - xb == T        (T a constant fixed by the board)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;whose left side ranges over &lt;code&gt;[-ub, uw]&lt;/code&gt;. &lt;strong&gt;This constraint forces something&lt;br&gt;
only when &lt;code&gt;T&lt;/code&gt; sits at one end of that interval&lt;/strong&gt; — &lt;code&gt;T == uw&lt;/code&gt; means every unknown&lt;br&gt;
white cell is shaded and every unknown black one is on the loop, and &lt;code&gt;T == -ub&lt;/code&gt;&lt;br&gt;
is the mirror image.&lt;/p&gt;

&lt;p&gt;And the interval stays wide until the board is nearly solved. Twenty unknown&lt;br&gt;
cells means an interval about twenty wide, and &lt;code&gt;T&lt;/code&gt; landing exactly on an&lt;br&gt;
endpoint is rare. As a contradiction detector it is the same story: one wrong&lt;br&gt;
cell moves &lt;code&gt;T&lt;/code&gt; by one, which almost never pushes it outside a wide interval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;True, free, and never tight.&lt;/strong&gt; That is what 0/40 and 1/1090 are saying.&lt;/p&gt;

&lt;p&gt;I locked the result into a test — &lt;code&gt;parity&lt;/code&gt; must decide exactly as many cells as&lt;br&gt;
&lt;code&gt;arrows&lt;/code&gt; — so that a future change to the propagator in either direction breaks&lt;br&gt;
the build instead of quietly invalidating this article. And it ships in the demo&lt;br&gt;
as the rule set that loses; switch the hint level between &lt;code&gt;arrows&lt;/code&gt; and &lt;code&gt;parity&lt;/code&gt;&lt;br&gt;
and watch nothing change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually carries the load
&lt;/h2&gt;

&lt;p&gt;Drop one propagator at a time from the strongest fixpoint (6×6, 30 boards):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;dropped&lt;/th&gt;
&lt;th&gt;free cells still decided&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the arrow counting&lt;/td&gt;
&lt;td&gt;47%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shaded-adjacency&lt;/td&gt;
&lt;td&gt;62%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;the colouring argument&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;78% (no change)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the loop's structure&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;39%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It is the &lt;strong&gt;loop structure&lt;/strong&gt; — degrees, dead ends, circuits that would close too&lt;br&gt;
early, cells stranded in an unreachable component. Drop it and 78% falls to 39%.&lt;br&gt;
The puzzle is named after its arrows, but the loop is what breaks it open.&lt;/p&gt;

&lt;p&gt;Rule-set reach, measured on boards generated with the uniqueness filter&lt;br&gt;
&lt;strong&gt;switched off&lt;/strong&gt; (filtering by solvability and then reporting solvability is&lt;br&gt;
circular — a trap this series has fallen into before):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;board&lt;/th&gt;
&lt;th&gt;arrows&lt;/th&gt;
&lt;th&gt;parity&lt;/th&gt;
&lt;th&gt;loop&lt;/th&gt;
&lt;th&gt;probe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6×6 cells decided&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8×8 cells decided&lt;/td&gt;
&lt;td&gt;32%&lt;/td&gt;
&lt;td&gt;32%&lt;/td&gt;
&lt;td&gt;66%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Soundness: three counters, and one external sequence
&lt;/h2&gt;

&lt;p&gt;Two brute-force counters share no logic with the propagators:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;shading first&lt;/strong&gt; — enumerate the shadings, then count Hamiltonian circuits
on whatever cells are left&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;loop first&lt;/strong&gt; — enumerate the circuit edge by edge, read the shading off as
the complement&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Plus a propagation-backed counter and an independent validator that re-derives&lt;br&gt;
every rule from scratch. All of them must agree on solution counts.&lt;/p&gt;

&lt;p&gt;But agreement between counters I wrote myself does not rule out being wrong in&lt;br&gt;
the same way twice. So the circuit counter is pinned to an &lt;strong&gt;external&lt;/strong&gt; source:&lt;br&gt;
the number of Hamiltonian cycles in an &lt;em&gt;n&lt;/em&gt;×&lt;em&gt;n&lt;/em&gt; grid graph is published as&lt;br&gt;
&lt;a href="https://oeis.org/A003763" rel="noopener noreferrer"&gt;OEIS A003763&lt;/a&gt; — 1, 6, 1072 — and the counter&lt;br&gt;
reproduces it exactly. That single test did more for my confidence than the&lt;br&gt;
other twenty-nine combined.&lt;/p&gt;

&lt;p&gt;30 tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Clues pin the shading; they can never steer the routing&lt;/strong&gt;, because a clue
cell is off the loop by definition. Routing ambiguity has no post-hoc repair,
so it must be built into the loop's shape.&lt;/li&gt;
&lt;li&gt;Freely grown loops are unique &lt;strong&gt;0% of the time&lt;/strong&gt; at 8×8 and above (median 52
rivals). Two attempts to ban the ambiguity locally both failed — the blunt ban
stalls growth at 21–43% coverage, and the sharp one is &lt;strong&gt;not monotone&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deflate + inflate&lt;/strong&gt; descent on the real circuit count: 0/40 to &lt;strong&gt;40/40&lt;/strong&gt;, at
target size.&lt;/li&gt;
&lt;li&gt;Add clues by &lt;strong&gt;walking counterexamples&lt;/strong&gt;, not at random: degenerate 0-shaded
boards became boards with 6–12 shaded cells.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;checkerboard colouring argument is correct and worthless&lt;/strong&gt;: 0/40 boards,
1/1090 refutations, 0% change when removed. It only bites at the ends of an
interval that stays wide. Pinned by a test; shipped as the losing rival.&lt;/li&gt;
&lt;li&gt;Pin at least one counter to a &lt;strong&gt;published sequence&lt;/strong&gt;, not just to another
counter you wrote.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Puzzle #28 in the solver series.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>puzzle</category>
    </item>
    <item>
      <title>Solving Fillomino: the third rule isn't a rule — it's why the puzzle can be printed with no borders</title>
      <dc:creator>SEN LLC</dc:creator>
      <pubDate>Mon, 03 Aug 2026 23:50:23 +0000</pubDate>
      <link>https://dev.to/sendotltd/solving-fillomino-the-third-rule-isnt-a-rule-its-why-the-puzzle-can-be-printed-with-no-borders-5c6g</link>
      <guid>https://dev.to/sendotltd/solving-fillomino-the-third-rule-isnt-a-rule-its-why-the-puzzle-can-be-printed-with-no-borders-5c6g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Fillomino&lt;/strong&gt; in the browser with &lt;strong&gt;three rule sets&lt;/strong&gt; inside. It is normally&lt;br&gt;
stated in three clauses: cut the grid into polyomino &lt;strong&gt;regions&lt;/strong&gt;; a region of&lt;br&gt;
N cells is written N everywhere; and &lt;strong&gt;two regions of the same size may not&lt;br&gt;
share an edge&lt;/strong&gt;. That third clause is where this build starts. Read a filled&lt;br&gt;
grid the only way a grid of numbers can be read — a region &lt;em&gt;is&lt;/em&gt; a maximal&lt;br&gt;
connected run of equal numbers — and the third clause becomes impossible to&lt;br&gt;
break: two touching regions of size two are, on the page, one run of four 2s,&lt;br&gt;
and 4 ≠ 2, which clause 2 already rejects. Fillomino has &lt;strong&gt;one rule&lt;/strong&gt;.&lt;br&gt;
Puzzle #28 in the solver-included series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🌐 &lt;strong&gt;Live demo&lt;/strong&gt;: &lt;a href="https://sen.ltd/portfolio/fillomino/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/fillomino/&lt;/a&gt;&lt;br&gt;
📦 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/sen-ltd/fillomino" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/fillomino&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8w94h03hqyoru6b8w2v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8w94h03hqyoru6b8w2v.png" alt="Screenshot" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The rules, in one sentence
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every connected run of equal numbers is exactly that many cells.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A run of 3s is three cells. A run of 5s is five. That is all. The engine checks&lt;br&gt;
that one sentence and nothing else — it never asks whether same-size regions&lt;br&gt;
touch.&lt;/p&gt;
&lt;h2&gt;
  
  
  So what is the third rule doing?
&lt;/h2&gt;

&lt;p&gt;Saying "rule 3 is redundant" is only half right. Precisely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Formulate the puzzle over &lt;strong&gt;pictures&lt;/strong&gt; (grids of numbers) and clause 3 is not
expressible as a separate constraint. It is already implied.&lt;/li&gt;
&lt;li&gt;Formulate it over &lt;strong&gt;partitions&lt;/strong&gt; (a cut into regions, plus labels) and
clause 3 is a real constraint. Drop it and the solutions multiply.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So clause 3 does not narrow the answer. It guarantees the answer can be&lt;br&gt;
&lt;strong&gt;written down&lt;/strong&gt;. That is what makes Fillomino visually unique among the region&lt;br&gt;
puzzles in this series — Heyawake, Norinori, LITS, Shikaku all have to print&lt;br&gt;
their region walls, because numbers alone would not locate them. A Fillomino&lt;br&gt;
prints nothing but numbers, and rule 3 is exactly the condition under which the&lt;br&gt;
picture gives the partition back.&lt;/p&gt;

&lt;p&gt;The demo makes the point literally: &lt;strong&gt;the borders are not in the data&lt;/strong&gt;. As you&lt;br&gt;
write numbers, a wall is drawn wherever two adjacent cells disagree, and the&lt;br&gt;
region map appears out of nowhere as the grid fills.&lt;/p&gt;
&lt;h2&gt;
  
  
  Counting the theorem
&lt;/h2&gt;

&lt;p&gt;If the claim is true, two independent enumerators must agree on every board.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;countByRegions&lt;/code&gt; walks &lt;strong&gt;regions&lt;/strong&gt;: take the first blank cell, try every
polyomino that could be its region, and check clause 3 &lt;strong&gt;out loud&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;countByCells&lt;/code&gt; walks &lt;strong&gt;cells&lt;/strong&gt; in reading order: write a number, then check
only that the run through that cell has not outgrown its number and can still
reach it. It has never heard of clause 3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They share no code. On empty grids (&lt;code&gt;npm run stats&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;grid&lt;/th&gt;
&lt;th&gt;pictures (region walk)&lt;/th&gt;
&lt;th&gt;pictures (cell walk)&lt;/th&gt;
&lt;th&gt;partitions, rule 3 dropped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1×1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2×2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3×3&lt;/td&gt;
&lt;td&gt;445&lt;/td&gt;
&lt;td&gt;445&lt;/td&gt;
&lt;td&gt;1,434&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4×4&lt;/td&gt;
&lt;td&gt;259,728&lt;/td&gt;
&lt;td&gt;259,728&lt;/td&gt;
&lt;td&gt;1,691,690&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two middle columns agree exactly, on every board, which is the theorem.&lt;br&gt;
The third column is the same region walk with clause 3 switched off: it counts&lt;br&gt;
&lt;strong&gt;cuts&lt;/strong&gt; rather than &lt;strong&gt;pictures&lt;/strong&gt;, and by 4×4 it is counting each answer&lt;br&gt;
&lt;strong&gt;6.51 times over&lt;/strong&gt;. That factor is the ambiguity clause 3 removes.&lt;/p&gt;

&lt;p&gt;On real boards the ablation is brutal. Of the sixteen 6×6 boards this repo&lt;br&gt;
ships, &lt;strong&gt;16/16 are unique&lt;/strong&gt; as pictures — and only &lt;strong&gt;2/16&lt;/strong&gt; are unique as&lt;br&gt;
partitions once clause 3 is dropped. The median board admits &lt;strong&gt;20&lt;/strong&gt; different&lt;br&gt;
ways to cut it up; the worst admits &lt;strong&gt;12,244&lt;/strong&gt;. Every one of them writes the&lt;br&gt;
same numbers.&lt;/p&gt;

&lt;p&gt;The 2×2 case is small enough to check by hand: all-4s (one way), or a lone&lt;br&gt;
corner &lt;strong&gt;1&lt;/strong&gt; with an L-tromino of &lt;strong&gt;3&lt;/strong&gt;s (four ways). Five. Cutting it into two&lt;br&gt;
dominoes of 2 dies as a picture — four 2s in a row, and 4 ≠ 2.&lt;/p&gt;
&lt;h2&gt;
  
  
  The variable is the cell; the domain is a set of sizes
&lt;/h2&gt;

&lt;p&gt;Every other region puzzle hands you the regions and asks what goes inside.&lt;br&gt;
Fillomino hands you nothing: &lt;strong&gt;the regions are the answer&lt;/strong&gt;. So a cell's domain&lt;br&gt;
is not "which colour" but "&lt;strong&gt;how big is the region I end up in&lt;/strong&gt;", and every&lt;br&gt;
propagator becomes a statement about how a run can still grow.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;local&lt;/code&gt; — never past the frontier
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// a run that has reached its number is finished: nothing beside it may join&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;UNKNOWN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;dropValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// a run still short of its number must grow — one way out means that cell is in&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;UNKNOWN&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;hasValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;exits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[[...&lt;/span&gt;&lt;span class="nx"&gt;exits&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Look at the first half. "A finished run blocks its own number next door" &lt;strong&gt;is&lt;/strong&gt;&lt;br&gt;
clause 3 — derived from the one-sentence formulation without ever naming it.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;region&lt;/code&gt; — the two rules that need the whole board
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;reach.&lt;/strong&gt; A region of size &lt;em&gt;v&lt;/em&gt; is &lt;em&gt;v&lt;/em&gt; connected cells that can all hold &lt;em&gt;v&lt;/em&gt;.&lt;br&gt;
So a cell keeps &lt;em&gt;v&lt;/em&gt; only while it sits inside a &lt;em&gt;v&lt;/em&gt;-capable component of at&lt;br&gt;
least &lt;em&gt;v&lt;/em&gt; cells — and a component of &lt;strong&gt;exactly&lt;/strong&gt; &lt;em&gt;v&lt;/em&gt; cells that already&lt;br&gt;
contains a &lt;em&gt;v&lt;/em&gt; &lt;strong&gt;is&lt;/strong&gt; the region, in full.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;dropValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;UNKNOWN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// component = region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;cut.&lt;/strong&gt; For a run still short of its number, delete one cell and re-flood. If&lt;br&gt;
what remains can no longer reach the number, no legal region avoids that cell,&lt;br&gt;
so the cell is in it. Articulation reasoning — and the rule that feels closest&lt;br&gt;
to how people actually play.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reachable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;reachable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;probe&lt;/code&gt; — singleton consistency
&lt;/h3&gt;

&lt;p&gt;Assume a number in a cell, run the region rules, and if the board dies the&lt;br&gt;
number is gone. Restricted to the frontier (cells that already touch something&lt;br&gt;
known) as a budget — and a budget that &lt;strong&gt;prunes less&lt;/strong&gt; can never cost you&lt;br&gt;
soundness.&lt;/p&gt;

&lt;h2&gt;
  
  
  How far does each rule set reach? (measured without circularity)
&lt;/h2&gt;

&lt;p&gt;Generate only boards a rule set can solve, then measure how often that rule set&lt;br&gt;
solves them, and you will discover it solves 100%. So these boards are&lt;br&gt;
generated with &lt;strong&gt;no solvability filter at all&lt;/strong&gt; — clues are peeled while&lt;br&gt;
brute force still says "unique", and only then is the rule set asked:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;size&lt;/th&gt;
&lt;th&gt;local&lt;/th&gt;
&lt;th&gt;region&lt;/th&gt;
&lt;th&gt;probe&lt;/th&gt;
&lt;th&gt;clues&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5×5&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;9.6 / 25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6×6&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;td&gt;13.5 / 36&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The local rules finish &lt;strong&gt;nothing&lt;/strong&gt;. The region rules finish about one board in&lt;br&gt;
ten. Probing — still no search — gets to 95%, and the last few percent need&lt;br&gt;
real backtracking whatever you do. From 900 random positions, the region rules&lt;br&gt;
decided more cells than the local rules in &lt;strong&gt;818&lt;/strong&gt; of them, &lt;strong&gt;+6.4 cells&lt;/strong&gt; on&lt;br&gt;
average, and the local rules &lt;strong&gt;never once won&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Since every propagator is sound, a rule set that finishes a board with no&lt;br&gt;
search is also a &lt;strong&gt;uniqueness certificate&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generation: most regions are never mentioned
&lt;/h2&gt;

&lt;p&gt;Generation is a randomised region packing — grow a region, refuse to touch a&lt;br&gt;
same-size neighbour, backtrack when stuck — followed by peeling clues off the&lt;br&gt;
solved grid one at a time for as long as a rule set still finishes the board.&lt;/p&gt;

&lt;p&gt;What is left over is the surprising part: &lt;strong&gt;9–14% of the regions in a shipped&lt;br&gt;
board carry no clue at all.&lt;/strong&gt; Nothing printed says they exist. Their size,&lt;br&gt;
shape and position are forced entirely by their neighbours.&lt;/p&gt;

&lt;p&gt;And they are the &lt;strong&gt;small&lt;/strong&gt; ones: a silent region averages 1.3–1.8 cells against&lt;br&gt;
2.7–2.8 for regions in general. Roughly &lt;strong&gt;half of all lone 1s are never&lt;br&gt;
printed&lt;/strong&gt; — fenced in by bigger runs on all four sides, a 1 often has nowhere&lt;br&gt;
else to be.&lt;/p&gt;

&lt;p&gt;Per cell, that is the entire economics of a Fillomino clue. The share of cells&lt;br&gt;
that end up clued falls steadily with region size — on the shipped 6×6 bank,&lt;br&gt;
&lt;code&gt;1:0.51 2:0.50 3:0.35 4:0.36 5:0.28&lt;/code&gt;, and on 8×8, &lt;code&gt;1:0.80 2:0.46 3:0.40&lt;br&gt;
4:0.29 5:0.31&lt;/code&gt;. &lt;strong&gt;One clue buys more grid the bigger the region it lands in&lt;/strong&gt;,&lt;br&gt;
which is why a board covered in 1s and 2s is a board covered in clues.&lt;/p&gt;

&lt;p&gt;One invariant falls straight out of the one rule and is checked on every board:&lt;br&gt;
the grid holds a whole number of size-&lt;em&gt;v&lt;/em&gt; regions, so &lt;strong&gt;the count of cells&lt;br&gt;
labelled &lt;em&gt;v&lt;/em&gt; is always a multiple of *v&lt;/strong&gt;*.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cap is a promise, not a crutch
&lt;/h2&gt;

&lt;p&gt;These boards declare "no region is bigger than five cells", which keeps a&lt;br&gt;
cell's domain at five values instead of a hundred and is what makes probing&lt;br&gt;
affordable in a tab. It buys speed, not solutions: re-count the shipped 6×6&lt;br&gt;
boards with the cap &lt;strong&gt;lifted to the whole grid&lt;/strong&gt; and all 16 are still unique.&lt;br&gt;
At 8×8 the same re-count re-proves 12 of 16, and the remaining 4 exhaust the&lt;br&gt;
node budget — reported as budget exhaustion rather than quietly counted as&lt;br&gt;
passes.&lt;/p&gt;

&lt;p&gt;And how much is probing actually worth, in clues? Peel the same twenty solved&lt;br&gt;
6×6 grids twice: stop while the region rules still finish and you leave &lt;strong&gt;15.1&lt;/strong&gt;&lt;br&gt;
clues on average; let &lt;code&gt;probe&lt;/code&gt; carry the board and you get to &lt;strong&gt;13.8&lt;/strong&gt; (min 12,&lt;br&gt;
max 17). The whole difficulty ladder is about 1.3 clues wide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Soundness
&lt;/h2&gt;

&lt;p&gt;Two enumerators that share no code with the propagators — one walking regions,&lt;br&gt;
one walking cells — plus a validator that re-reads every region from scratch.&lt;br&gt;
All of them must agree on solution counts across randomly generated boards; a&lt;br&gt;
disagreement is how an unsound propagator gets caught. 72 tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fillomino's third clause is &lt;strong&gt;not a constraint on the answer&lt;/strong&gt; — it is the
guarantee that the answer can be printed. Over pictures, the puzzle is one
sentence.&lt;/li&gt;
&lt;li&gt;Verified by &lt;strong&gt;two independent exhaustive enumerators&lt;/strong&gt; (259,728 legal 4×4
boards, counted both ways). Drop the clause and the same answer is counted
6.51 times over; on real boards uniqueness collapses from 16/16 to 2/16.&lt;/li&gt;
&lt;li&gt;The variable is the cell and the domain is a set of &lt;strong&gt;region sizes&lt;/strong&gt;, so
every propagator is about growth: finished runs block, unreachable numbers
die, articulation cells are forced.&lt;/li&gt;
&lt;li&gt;Rule-set reach measured on boards &lt;strong&gt;nobody filtered for solvability&lt;/strong&gt;:
local 0%, region 4–10%, probe 95–96%.&lt;/li&gt;
&lt;li&gt;After peeling, &lt;strong&gt;9–14% of regions carry no clue at all&lt;/strong&gt; — their existence is
forced by their neighbours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🌐 &lt;strong&gt;Live demo&lt;/strong&gt;: &lt;a href="https://sen.ltd/portfolio/fillomino/" rel="noopener noreferrer"&gt;https://sen.ltd/portfolio/fillomino/&lt;/a&gt;&lt;br&gt;
📦 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/sen-ltd/fillomino" rel="noopener noreferrer"&gt;https://github.com/sen-ltd/fillomino&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>puzzle</category>
    </item>
  </channel>
</rss>
