LITS with three rule sets inside — the puzzle whose name is a theorem.
The grid is cut into regions of four or more cells. Shade exactly four cells in every region, forming a tetromino. All shaded cells form one connected mass. No 2×2 square is ever fully shaded. And when two tetrominoes from different regions touch, they must not be congruent — rotations and reflections count as the same shape.
Live demo: https://sen.ltd/portfolio/lits/
LITS is named after the four tetrominoes you are allowed to shade: L, I, T, S. The fifth, O, is forbidden. But look at the rules again: O is exactly the tetromino that is a 2×2 square, and a fully shaded 2×2 square is already illegal. Enumerate every connected 4-subset of a 5×5 board — 228 of them, 16 containing a 2×2 square — and the square-containing ones are precisely the ones that classify as O (one of the tests does exactly this). Drop the "L, I, T, S only" clause and keep the square rule, and nothing changes. The letters in the name are a consequence, not a constraint.
Like Norinori before it, a LITS board has no numbers on it — the partition is the entire puzzle, and the natural variable is not the cell but the region. A region's one decision is which tetromino it holds, so its domain is its list of tetromino placements: 4-cell connected non-O subsets, a handful to a few dozen per region.
Under that lifting, the puzzle's signature rule — congruent tetrominoes must not touch across a border, a condition about shapes staring at each other — collapses into an ordinary binary constraint between adjacent regions: two placements are incompatible when they touch and share a shape, or when they close a 2×2 square together. Enforcing it is textbook AC-3.
What does not decompose is connectivity. One shaded mass is a property of the whole board, so it gets its own global propagator: a cell is possibly shaded when some surviving placement covers it; every placement, being connected, lives inside exactly one component of the possibly-shaded graph; a solution's shaded mass must sit in a component that still offers every region a placement. Placements in any other component are stranded, and get pruned.
Three rule sets ship:
local— count each region to four, complete no 2×2 square, repeat. The puzzle as a solver that never leaves the cell sees it. Kept as the falsified rival: it can barely finish anything.tetro— the local rules plus AC-3 over the placement domains plus the connectivity propagator, to a joint fixpoint.probe— singleton consistency on top: assume a cell, propagate, watch the board die.
A rule set that finishes a board with no search is also a uniqueness certificate: every propagator is sound, so a board it completes has exactly one solution.
The only thing a LITS generator can vary is the partition — and a random partition is essentially never a puzzle. For Norinori random partitions mostly had no solution; LITS fails in the opposite direction: with region sizes 6–10, roughly nine in ten random partitions admit many solutions, and across hundreds of trials none admitted exactly one.
Uniqueness lives at high density. Every region holds exactly four shaded
cells, so mean region size is pinned at 4 / shaded fraction — and big
regions mean big placement domains and hopelessly loose boards. Greedy
tetromino accretion jams around 55% shading; a randomised backtracking
packing reaches ~65%, which is regions of six and a half cells instead of
nine. Every unshaded cell then floods into whichever neighbouring region it
enlarges the least, measured in placements — on identical packings that one
greedy choice cuts the raw partition's median solution count from 151 to 60,
and the lower the density, the bigger the win.
What remains is closed by slack descent. The local slack of a region is the number of other tetrominoes it could hold with every other region's shading left untouched — each one a rival solution differing inside a single region, a tetromino sliding or pivoting in place. Autopsying stuck boards shows this is what ambiguity looks like at the end: a rival set of size 8 is exactly a lattice of 2³ independent one-region slides. So the generator walks white cells between regions (which can never hurt the intended solution — no shaded cell ever changes region) descending total slack to zero, peeling cells away from slacky regions when no single move improves: a smaller region has fewer placements and nowhere left to slide. A final full search rejects the rare multi-region rival that slack cannot see.
Two independent brute-force counters share no code with the propagators — one walks the cells in row-major order, one walks the regions choosing placements — plus a third propagation-backed counter per rule set, plus an independent validator that re-derives every shape from scratch. All of them must agree on solution counts across randomly generated boards; a disagreement is how an unsound propagator gets caught. 65 tests.
npm install
npm run dev # play at http://localhost:5173
npm test # vitest, 65 tests
npm run generate # regenerate the puzzle bank (src/puzzles.json)
npm run stats # remeasure every number quoted aboveNo runtime dependencies. TypeScript everywhere. MIT.
