JUL 9, 2026
Back to the solver

Under the hood

How the Solver Works

Pips is the New York Times' daily domino logic puzzle: you drop a fixed set of dominoes onto a board so that every shaded region's rule — a target sum, all-equal, all-different — comes out true. Underneath it's a finite-domain constraint-satisfaction problem, and solving it exactly is NP-hard in general. Here's how this solver cracks it in (almost always) under a millisecond.

Interactive demonstration

Watch it backtrack

Step through a miniature puzzle as the solver places a piece, hits a dead end, and recovers.

8
Available pieces

Empty board. The teal region (top-left three cells) must sum to exactly 8.

01The model

Pips as a constraint problem

A puzzle is three things: a set of cells (the board can be non-rectangular, with holes), a multiset of dominoes — each a pair of pips from 0–6 — and a set of regions, each with a rule. A solution tiles every cell with dominoes so that every region rule holds at once.

The five region rules reduce to simple aggregates over the cells they cover:

sum = N
the covered pips add to exactly N
equal
every covered pip is the same value
unequal
every covered pip is distinct
< N / > N
the covered pips sum to less than / greater than N
empty
no rule — any pips are fine

Crucially — and unlike classic dominoes — touching halves do not need to match. Only the region rules matter, which is exactly what makes the search interesting.

02The search

Backtracking, most-constrained-cell first

At heart it's depth-first backtracking: place a domino, recurse, and undo on failure. The leverage comes from which cell you fill next. Instead of scanning in reading order, the solver uses the minimum-remaining-values (MRV) heuristic — it always fills the empty cell with the fewest empty neighbours, i.e. the most boxed-in one, and stops looking as soon as it finds a cell with one option or none. Forced moves get made immediately and dead ends surface early, which collapses the branching factor.

solve():
  if every cell filled            -> SOLUTION
  cell = empty cell with the
         fewest empty neighbours          # MRV
  for each empty neighbour n of cell:
    for each remaining domino (a, b):
      for each orientation:
        place (a,b) over (cell, n)
        if regionsValid()                 # incremental check
           and noCellIsolated()           # structural prune
           and regionsStillSatisfiable(): # forward check
          if solve(): return SOLUTION
        undo placement                    # backtrack
  return FAIL
03Propagation

Incremental constraint checks

Re-scanning the whole board after every placement would be wasteful, so the solver keeps live aggregates for each region: a running sum, a filled-cell count, and a per-value tally. From those, every rule is an O(1) test:

  • sum fails fast if the partial sum already exceeds the target;
  • equalis just “at most one distinct value present”;
  • unequalis “distinct count equals filled count”.

Because a domino only ever touches the (at most two) regions containing its two cells, placing or removing one is an O(1) update — no board sweep required.

04Look-ahead

Forward checking with a pip pool

Before recursing, the solver asks a cheap but powerful question: can each unfinished region still be satisfied with the pips that are left? It keeps a pip pool— how many of each value 0–6 remain in unplaced dominoes — and, using a greedy minimum/maximum achievable sum, prunes branches where a target has already become unreachable, an “all-equal” region no longer has enough matching pips, or an “all-different” region has run out of distinct values. Every test is a necessary condition, so it never discards a real solution — it just amputates doomed branches before they grow.

05Pruning

Cheap structural cuts

  • No isolated cells. A placement that strands an empty cell with no empty neighbour can never be tiled by a 1×2 domino — reject it. Only cells adjacent to the piece you just placed can become newly isolated, so the check stays local and cheap.
  • Symmetric dominoes.A piece like 3|3 is identical when flipped, so the redundant orientation is skipped — halving that piece's branching.
06Representation

Flat, allocation-free state

Everything constant is precomputed once into typed arrays — neighbour adjacency, each cell's region membership, and the domino pip-pairs as parallel arrays. The board is an Int8Array, the pip pool an Int32Array, and the per-region tallies are typed arrays too. The hot loop has no string keys, no Map rebuilds, and no per-node allocations: the search is just integer pointer-chasing, which is what keeps it fast under heavy backtracking.

07In practice

What the numbers look like

The worst case is exponential, but real NYT puzzles are far from worst case once MRV and propagation are doing their jobs:

Difficulty
Nodes explored
Time
Easy
~10–60
< 1 ms
Medium
hundreds – low thousands
≈ 1 ms
Hard
up to ~10⁵
tens of ms

A few puzzles even admit more than one valid tiling — the solver returns the first it proves correct.