{"slug": "solving-the-nytimes-pips-puzzle-with-a-constraint-solver", "title": "Solving the NYTimes Pips puzzle with a constraint solver", "summary": "The article describes how the author used the MiniZinc constraint solver to solve the New York Times' daily Pips puzzle, where dominoes must be placed on a grid to satisfy specific conditions like sums and equality of pips. The author found that constraint solvers allow users to simply define the problem's constraints rather than coding a step-by-step solution, and the solver was able to find solutions in milliseconds after less than two hours of learning. The post includes examples of how constraints are expressed in code and demonstrates that the solver can handle both easy and more difficult puzzles.", "body_md": "The New York Times recently introduced a new daily puzzle called [Pips](https://www.nytimes.com/games/pips).\nYou place a set of dominoes on a grid, satisfying various conditions.\nFor instance, in the puzzle below,\nthe pips (dots) in the purple squares must sum to 8,\nthere must be fewer than 5 pips in the red square, and the pips in the three green squares must be equal.\n(It doesn't take much thought to solve this \"easy\" puzzle, but the \"medium\" and \"hard\" puzzles\nare more challenging.)\n\nI was wondering about how to solve these puzzles with a computer.\nRecently, I saw an article on [Hacker News](https://news.ycombinator.com/item?id=45222695)—\"[Many hard LeetCode problems are easy constraint problems](https://buttondown.com/hillelwayne/archive/many-hard-leetcode-problems-are-easy-constraint/)\"—that described the benefits and flexibility of a system called\na constraint solver.\nA constraint solver takes a set of constraints and finds solutions that satisfy the constraints: exactly\nwhat Pips requires.\n\nI figured that solving Pips with a constraint solver would be a good way to learn more about these solvers, but I had several questions. Did constraint solvers require incomprehensible mathematics? How hard was it to express a problem? Would the solver quickly solve the problem, or would it get caught in an exponential search?\n\nIt turns out that using a constraint solver was straightforward; it took me under two hours from\nknowing nothing about constraint solvers to solving the problem.\nThe solver found solutions in milliseconds (for the most part).\nHowever, there were a few bumps along the way.\nIn this blog post, I'll discuss my experience with the [MiniZinc](https://www.minizinc.org/)[1](#fn:alternatives) constraint\nmodeling system and show how it can solve Pips.\n\n## Approaching the problem\n\nWriting a program for a constraint solver is very different from writing a regular program.\nInstead of telling the computer *how* to solve the problem, you tell it *what* you want:\nthe conditions that must be satisfied.\nThe solver then \"magically\" finds solutions that satisfy the problem.\n\nTo solve the problem, I created an array called `pips`\n\nthat holds the number of domino pips at each position\nin the grid.\nThen, the three constraints for the above problem can be expressed as follows.\nYou can see how the constraints directly express the conditions in the puzzle.\n\n```\nconstraint pips[1,1] + pips[2,1] == 8;\nconstraint pips[2,3] < 5;\nconstraint all_equal([pips[3,1], pips[3,2], pips[3,3]]);\n```\n\nNext, I needed to specify where dominoes could be placed for the puzzle.\nTo do this, I defined an array called `grid`\n\nthat indicated the allowable positions: 1 indicates a valid\nposition and 0 indicates an invalid position. (If you compare with the puzzle at the top of the article,\nyou can see that the grid below matches its shape.)\n\n```\ngrid = [|\n1,1,0|\n1,1,1|\n1,1,1|];\n```\n\nI also defined the set of dominoes for the problem above, specifying the number of spots in each half:\n\n```\nspots = [|5,1| 1,4| 4,2| 1,3|];\n```\n\nSo far, the constraints directly match the problem.\nHowever, I needed to write some more code to specify how these pieces interact.\nBut\nbefore I describe that code, I'll show a solution.\nI wasn't sure what to expect: would the constraint solver give me a solution or would it spin\nforever?\nIt turned out to find the unique solution in 109 milliseconds, printing out the\nsolution arrays.\nThe `pips`\n\narray shows the number of pips in each position, while the `dominogrid`\n\narray shows which\ndomino (1 through 4) is in each position.\n\n```\npips = \n[| 4, 2, 0\n | 4, 5, 3\n | 1, 1, 1\n |];\ndominogrid = \n[| 3, 3, 0\n | 2, 1, 4\n | 2, 1, 4\n |];\n```\n\nThe text-based solution above is a bit ugly. But it is easy to create graphical output. MiniZinc provides a JavaScript API, so you can easily display solutions on a web page. I wrote a few lines of JavaScript to draw the solution, as shown below. (I just display the numbers since I was too lazy to draw the dots.) Solving this puzzle is not too impressive—it's an \"easy\" puzzle after all—but I'll show below that the solver can also handle considerably more difficult puzzles.\n\n### Details of the code\n\nWhile the above code specifies a particular puzzle, a bit more code is required to define how dominoes and the grid interact. This code may appear strange because it is implemented as constraints, rather than the procedural operations in a normal program.\n\nMy main design decision was how to specify the locations of dominoes.\nI considered assigning a grid position and orientation\nto each domino, but it seemed inconvenient to deal with multiple orientations.\nInstead, I decided to position each half of the domino independently, with an `x`\n\nand `y`\n\ncoordinate in\nthe grid.[2](#fn:xy) I added a constraint that the two halves of each domino had to be in neighboring cells,\nthat is, either the X or Y coordinates had to differ by 1.\n\n```\nconstraint forall(i in DOMINO) (abs(x[i, 1] - x[i, 2]) + abs(y[i, 1] - y[i, 2]) == 1);\n```\n\nIt took a bit of thought to fill in the `pips`\n\narray with the number of spots on each domino.\nIn a normal programming language, one would loop over the dominoes and store the values into `pips`\n\n.\nHowever, here it is done with a constraint so the solver makes sure the values are assigned.\nSpecifically, for each half-domino, the `pips`\n\narray entry at\nthe domino's x/y coordinate must equal the corresponding `spots`\n\non the domino:\n\n```\nconstraint forall(i in DOMINO, j in HALF) (pips[y[i,j], x[i, j]] == spots[i, j]);\n```\n\nI decided to add another array to keep track of which domino is in which position.\nThis array is useful to see the domino locations in the output, but it also\nkeeps dominoes from overlapping.\nI used a constraint to put each domino's number (1, 2, 3, etc.) into the occupied position of `dominogrid`\n\n:\n\n```\nconstraint forall(i in DOMINO, j in HALF) (dominogrid[y[i,j], x[i, j]] == i);\n```\n\nNext, how do we make sure that dominoes only go into positions allowed by `grid`\n\n?\nI used a constraint that a square in `dominogrid`\n\nmust be empty or the corresponding `grid`\n\nmust allow a domino.[3](#fn:iff)\nThis uses the \"or\" condition, which is expressed as `\\/`\n\n, an unusual stylistic\nchoice. (Likewise, \"and\" is expressed as `/\\`\n\n. These correspond to the logical symbols\n∨ and ∧.)\n\n```\nconstraint forall(i in 1..H, j in 1..W) (dominogrid[i, j] == 0 \\/ grid[i, j] != 0);\n```\n\nHonestly, I was worried that I had too many arrays and the solver would end up in a rathole ensuring that the arrays were consistent. But I figured I'd try this brute-force approach and see if it worked. It turns out that it worked for the most part, so I didn't need to do anything more clever.\n\nFinally, the program requires a few lines to define some constants and variables. The constants below define the number of dominoes and the size of the grid for a particular problem:\n\n```\nint: NDOMINO = 4; % Number of dominoes in the puzzle\nint: W = 3; % Width of the grid in this puzzle\nint: H = 3; % Height of the grid in this puzzle\n```\n\nNext, datatypes are defined to specify the allowable values.\nThis is very important for the solver; it is a \"finite domain\" solver, so limiting the size of\nthe domains reduces the size of the problem.\nFor this problem, the values are integers in a particular range, called a `set`\n\n:\n\n```\nset of int: DOMINO = 1..NDOMINO; % Dominoes are numbered 1 to NDOMINO\nset of int: HALF = 1..2; % The domino half is 1 or 2\nset of int: xcoord = 1..W; % Coordinate into the grid\nset of int: ycoord = 1..H;\n```\n\nAt last, I define the sizes and types of the various arrays that I use.\nOne very important syntax is `var`\n\n, which indicates variables that the solver must determine.\nNote that the first two arrays, `grid`\n\nand `spots`\n\ndo not have `var`\n\nsince they are constant,\ninitialized to specify the problem.\n\n```\narray[1..H,1..W] of 0..1: grid; % The grid defining where dominoes can go\narray[DOMINO, HALF] of int: spots; % The number of spots on each half of each domino\narray[DOMINO, HALF] of var xcoord: x; % X coordinate of each domino half\narray[DOMINO, HALF] of var ycoord: y; % Y coordinate of each domino half\narray[1..H,1..W] of var 0..6: pips; % The number of pips (0 to 6) at each location.\narray[1..H,1..W] of var 0..NDOMINO: dominogrid; % The domino sequence number at each location\n```\n\nYou can find all the code on [GitHub](https://github.com/shirriff/pips).\nOne weird thing is that because the code is not procedural, the lines can be in any order.\nYou can use arrays or constants before you use them.\nYou can even move `include`\n\nstatements to the end of the file if you want!\n\n## Complications\n\nOverall, the solver was much easier to use than I expected. However, there were a few complications.\n\nBy changing a setting, the solver can find multiple solutions instead of stopping after the first.\nHowever, when I tried this, the solver generated thousands of meaningless solutions.\nA closer look showed that the problem was that the solver was putting arbitrary numbers into the \"empty\"\ncells, creating valid but pointlessly different solutions.\nIt turns out that I didn't explicitly forbid this, so the sneaky constraint solver went ahead and\ngenerated tons of solutions that I didn't want.\nAdding another constraint fixed the problem.\nThe moral is that even if you think your constraints are clear, solvers are very good at finding unwanted\nsolutions that technically satisfy the constraints.\n[4](#fn:multiple)\n\nA second problem is that if you do something wrong, the solver simply says that the problem is unsatisfiable. Maybe there's a clever way of debugging, but I ended up removing constraints until the problem can be satisfied, and then see what I did wrong with that constraint. (For instance, I got the array indices backward at one point, making the problem insoluble.)\n\nThe most concerning issue is the unpredictability of the solver:\nmaybe it will take milliseconds or maybe it will take hours.\nFor instance, the Oct 5 hard Pips puzzle (below) caused the solver to take minutes for no apparent reason.\nHowever, the MiniZinc IDE supports different solver backends. I switched from the default [Gecode](https://www.gecode.dev/publications.html) solver to\n[Chuffed](https://github.com/chuffed/chuffed), and it immediately found numerous solutions, 384 to\nbe precise.\n(Sometimes the Pips puzzles sometimes have multiple solutions, which players find [controversial](https://www.reddit.com/r/nytpips/comments/1nyfk5u/sunday_oct_5_2025_pips_49_thread/).)\nI suspect that the multiple solutions messed up the Gecode solver somehow, perhaps because\nit couldn't narrow down a \"good\" branch in the search tree.\nFor a benchmark of the different solvers, see the footnote.[5](#fn:comparison)\n\n## How does a constraint solver work?\n\nIf you were writing a program to solve Pips from scratch, you'd probably have a loop to try assigning dominoes to positions. The problem is that the problem grows exponentially. If you have 16 dominoes, there are 16 choices for the first domino, 15 choices for the second, and so forth, so about 16! combinations in total, and that's ignoring orientations. You can think of this as a search tree: at the first step, you have 16 branches. For the next step, each branch has 15 sub-branches. Each sub-branch has 14 sub-sub-branches, and so forth.\n\nAn easy optimization is to check the constraints after each domino is added. For instance, as soon\nas the\n\"less than 5\" constraint is violated, you can [backtrack](https://en.wikipedia.org/wiki/Backtracking) and skip that entire\nsection of the tree.\nIn this way, only a subset of the tree needs to be searched; the number of branches will be large, but\nhopefully manageable.\n\nA constraint solver works similarly, but in a more abstract way. The constraint solver assigns values to the variables, backtracking when a conflict is detected. Since the underlying problem is typically NP-complete, the solver uses heuristics to attempt to improve performance. For instance, variables can be assigned in different orders. The solver attempts to generate conflicts as soon as possible so large pieces of the search tree can be pruned sooner rather than later. (In the domino case, this corresponds to placing dominoes in places with the tightest constraints, rather than scattering them around the puzzle in \"easy\" spots.)\n\nAnother technique is constraint propagation. The idea is that you can derive new constraints and catch conflicts earlier. For instance, suppose you have a problem with the constraints \"a equals c\" and \"b equals c\". If you assign \"a=1\" and \"b=2\", you won't find a conflict until later, when you try to find a value for \"c\". But with constraint propagation, you can derive a new constraint \"a equals b\", and the problem will turn up immediately. (Solvers handle more complicated constraint propagation, such as inequalities.) The tradeoff is that generating new constraints takes time and makes the problem larger, so constraint propagation can make the solver slower. Thus, heuristics are used to decide when to apply constraint propagation.\n\nResearchers are actively developing new\nalgorithms, heuristics, and optimizations[6](#fn:solvers) such as backtracking more aggressively\n(called \"backjumping\"),\nkeeping track of failing variable assignments (called \"nogoods\"), and\nleveraging Boolean SAT (satisfiability) solvers.\nSolvers compete in [annual challenges](https://www.minizinc.org/challenge/) to test\nthese techniques against each other.\nThe nice thing about a constraint solver is that you don't need to know anything about these techniques;\nthey are applied automatically.\n\n## Conclusions\n\nI hope this has convinced you that constraint solvers are interesting, not too scary, and can solve\nreal problems with little effort.\nEven as a beginner, I was able to get started with MiniZinc quickly.\n(I read half the [tutorial](https://docs.minizinc.dev/en/stable/modelling.html) and then jumped into programming.)\n\nOne reason to look at constraint solvers is that they are a completely different programming paradigm. Using a constraint solver is like programming on a higher level, not worrying about how the problem gets solved or what algorithm gets used. Moreover, analyzing a problem in terms of constraints is a different way of thinking about algorithms. Some of the time it's frustrating when you can't use familiar constructs such as loops and assignments, but it expands your horizons.\n\nFinally, writing code to solve Pips is more fun than solving the problems by hand, at least in my opinion, so give it a try!\n\nFor more, follow me on\nBluesky ([@righto.com](https://bsky.app/profile/righto.com)),\nMastodon ([@[email protected]](https://oldbytes.space/@kenshirriff)),\n[RSS](http://www.righto.com/feeds/posts/default), or subscribe [here](https://righto.kit.com/20bf534dff).\n\n`all_equal`\n\nand `alldifferent`\n\nconstraint functions.## Notes and references\n\n-\nI started by downloading the\n\n[MiniZinc IDE](https://www.minizinc.org/)and reading the[MiniZinc tutorial](https://docs.minizinc.dev/en/stable/part_2_tutorial.html). The MiniZinc IDE is straightforward, with an editor window at the top and an output window at the bottom. Clicking the \"Run\" button causes it to generate a solution.Screenshot of the MiniZinc IDE. Click for a larger view. -\nIt might be cleaner to combine the X and Y coordinates into a single\n\n`Point`\n\ntype, using a MiniZinc[record type](https://docs.minizinc.dev/en/stable/tuple_and_record_types.html).[↩](#fnref:xy) -\nI later decided that it made more sense to enforce that\n\n`dominogrid`\n\nis empty if and only if`grid`\n\nis 0 at that point, although it doesn't affect the solution. This constraint uses the \"if and only if\" operator`<->`\n\n.\n\n``` php\nconstraint forall(i in 1..H, j in 1..W) (dominogrid[i, j] == 0 <-> grid[i, j] == 0);\n```\n\n[↩](#fnref:iff) -\nTo prevent the solver from putting arbitrary numbers in the unused positions of\n\n`pips`\n\n, I added a constraint to force these values to be zero:\n\n``` php\nconstraint forall(i in 1..H, j in 1..W) (grid[i, j] == 0 -> pips[i, j] == 0);\n```\n\nGenerating multiple solutions had a second issue, which I expected: A symmetric domino can be placed in two redundant ways. For instance, a double-six domino can be flipped to produce a solution that is technically different but looks the same. I fixed this by adding constraints for each symmetric domino to allow only one of the two redundant positions. The constraint below forces a preferred orientation for symmetric dominoes.\n\n```\nconstraint forall(i in DOMINO) (spots[i,1] != spots[i,2] \\/ x[i,1] > x[i,2] \\/ (x[i,1] == x[i,2] /\\ y[i,1] > y[i,2]));\n```\n\nTo enable multiple solutions in MiniZinc, the setting is under Show Configuration Editor > User Defined Behavior > Satisfaction Problems or the\n\n`--all`\n\nflag from the command line.[↩](#fnref:multiple) -\nMiniZinc has five solvers that can solve this sort of integer problem:\n\n[Chuffed](https://github.com/chuffed/chuffed),[OR Tools CP-SAT](https://developers.google.com/optimization/cp/cp_solver),[Gecode](https://github.com/Gecode/gecode),[HiGHS](https://highs.dev/), and[Coin-OR BC](https://github.com/coin-or/Cbc). I measured the performance of the five solvers against 20 different Pips puzzles. Most of the solvers found solutions in under a second, most of the time, but there is a lot of variation.Timings for different solvers on 20 Pip puzzles.Overall, Chuffed had the best performance on the puzzles that I tested, taking well under a second. Google's OR-Tools won all the categories in the\n\n[2025 MiniZinc challenge](https://www.minizinc.org/challenge/2025/results/), but it was considerably slower than Chuffed for my Pips programs. The default Gecode solver performed very well most of the time, but it did terribly on a few problems, taking over 15 minutes. HiGHs was slower in general, taking a few minutes on the hardest problems, but it didn't fail as badly as Gecode. (Curiously, Gecode and HiGHS sometimes found different problems to be difficult.) Finally, Coin-OR BC was uniformly bad; at best it took a few seconds, but one puzzle took almost two hours and others weren't solved before I gave up after two hours. (I left Coin-OR BC off the graph because it messed up the scale.)Don't treat these results too seriously because different solvers are optimized for different purposes. (In particular, Coin-OR BC is designed for linear problems.) But the results demonstrate the unpredictability of solvers: maybe you get a solution in a second and maybe you get a solution in hours.\n\n[↩](#fnref:comparison) -\nIf you want to read more about solvers,\n\n[Constraint Satisfaction Problems](https://zoo.cs.yale.edu/classes/cs470/lectures/s2019/07-CSP.pdf)is an overview presentation. The Gecode algorithms are described in a nice technical report:[Constraint Programming Algorithms used in Gecode](https://www.researchgate.net/publication/311953428_Constraint_Programming_Algorithms_used_in_Gecode). Chuffed is more complicated: \"Chuffed is a state of the art lazy clause solver designed from the ground up with lazy clause generation in mind. Lazy clause generation is a hybrid approach to constraint solving that combines features of finite domain propagation and Boolean satisfiability.\" The Chuffed paper[Lazy clause generation reengineered](https://people.eng.unimelb.edu.au/pstuckey/papers/cp09-lc.pdf)and[slides](https://school.a4cp.org/summer2011/slides/Gent/Peter%20Stuckey%20-%20Lazy%20Clause%20Generation.pdf)are more of a challenge.[↩](#fnref:solvers)", "url": "https://wpnews.pro/news/solving-the-nytimes-pips-puzzle-with-a-constraint-solver", "canonical_source": "http://www.righto.com/2025/10/solve-nyt-pips-with-constraints.html", "published_at": "2025-10-18 15:41:00+00:00", "updated_at": "2026-05-23 17:40:02.480519+00:00", "lang": "en", "topics": ["developer-tools", "research"], "entities": ["New York Times", "MiniZinc", "Hacker News", "LeetCode"], "alternates": {"html": "https://wpnews.pro/news/solving-the-nytimes-pips-puzzle-with-a-constraint-solver", "markdown": "https://wpnews.pro/news/solving-the-nytimes-pips-puzzle-with-a-constraint-solver.md", "text": "https://wpnews.pro/news/solving-the-nytimes-pips-puzzle-with-a-constraint-solver.txt", "jsonld": "https://wpnews.pro/news/solving-the-nytimes-pips-puzzle-with-a-constraint-solver.jsonld"}}