{"slug": "solving-a-corn-puzzle-with-cp-sat", "title": "Solving a corn puzzle with CP-SAT", "summary": "A software engineer at the Recurse Center used Google's OR-Tools CP-SAT constraint solver, suggested by Anthropic's Claude, to solve a 3D-printed corn cob puzzle instead of writing a recursive backtracking algorithm. The engineer modeled the puzzle as an exact cover problem with binary variables for each piece placement and two constraint classes: each piece placed exactly once and each cob space covered exactly once. The approach worked better than the backtracking solution the engineer would have written.", "body_md": "# Solving a corn puzzle with CP-SAT\n\nOne random Friday at [RC](recurse.com), I was bored and playing around with a\npuzzle someone had 3d printed:\n\nIt’s a physical puzzle consisting of a white “cob” with grooves in it and a bunch of “corn” pieces, each made of 3-10 kernels stuck together in a particular shape, that can slide into the grooves. The goal is to slide them on so that they fit together exactly, with no overlaps and no gaps.\n\nLike all software engineers in the year of our lord 2026, I quickly got tired of solving the puzzle myself and asked Claude to do it for me:\n\nAfter a little bit of wrangling - it turns out computer vision isn’t *quite*\nthat good yet, so I had to clarify some of the piece shapes - Claude wrote a\nscript in python that gave a solution.\n\nIt was much better than I would have written myself, and I ended up learning from it.\n\n### What I expected\n\nIf I was solving this problem myself, I would naively reach for recursive backtracking. If you’re a programmer, you’ve probably seen this algorithm in an intro CS class.\n\nBasically, it’s the brute force approach - try something at random, then try something else, keep going until the puzzle is solved. If you ever get stuck and there are no legal moves left, undo your last move and try again from there. If all moves from that point fail, undo another move, and so on.\n\nThis is just an organized way of trying all the options, and it does admit some nice optimizations based on the structure of the problem. For example, you can take into account the symmetry that rotating the cob doesn’t change the solution in any way, or fail early if you ever isolate a single kernel space on its own (so that no piece can cover it). So a well thought-out approach to backtracking would likely work.\n\nHowever, when I looked at Claude’s script I realized from the first line that I was being dumb:\n\n``` python\nfrom ortools.sat.python import cp_model\n```\n\n### This wheel has already been invented\n\nInstead of backtracking, Claude just imported an industrial-strength library\nmade to solve these sorts of problems. [OR-Tools\nCP-SAT](https://developers.google.com/optimization/cp/cp_solver) is put out by\nGoogle and is made for solving constrained optimization problems, as well as\nsatisfiability problems like this one.\n\nThe idea here is to model any such problem as a set of variables - classically binary - and a set of constraints or assertions about those variables that all have to be simultaneously satisfied. (As well as an objective function, for optimization problems.) Then the library draws on many decades of research to efficiently find a setting for those variables that meets all of the constraints.\n\nThe corn problem is a variant of the [Exact\nCover](https://en.wikipedia.org/wiki/Exact_cover) problem, so those in the know\ncan probably already see how it would be represented, but I’ll go through it as\nit was new to me.\n\nThe variables in this problem are binary - true or false - and each one represents the idea “X piece is placed in a particular way”. So for each of $N_i$ ways that piece $i$ could be placed, we have one variable $V_{i,n}$, which is true ($1$) if the piece is placed there, false ($0$) if it isn’t.\n\n**Constraint class 1: All pieces are placed exactly once**\n\nThis part is easy - for each piece $i$, we have\n\n$ \\sum_{n=1}^{N_i} V_{i,n} = 1 $\n\nThis enforces that exactly one of the $V_{i, n}$ variables is $1$, and the rest are $0$.\n\n**Constraint class 2: Each spot is covered exactly once**\n\nThis is a little harder, but just tedious, not confusing. For each piece placement we have to go through all of the spaces on the cob that it covers and add it to the set of covering pieces $C_s$. Then for all spaces $s$ on the cob, we have a very similar looking constraint:\n\n$ \\sum_{V_{i,n} \\in C_s} V_{i,n} = 1 $\n\nSo every piece is used once, and every spot is covered once. If we assume that this is a well constructed puzzle - so the total number of kernels in the pieces is equal to the number of spaces on the cob - this is all the constraints we need!\n\nClaude’s python script to solve this was pretty complicated because it had to actually enumerate all the possible positions of all the pieces, but actually invoking the solver was very straightforward:\n\n```\n    for plist in by_piece:\n        m.Add(sum(v for v, _ in plist) == 1)\n    for clist in by_cell:\n        m.Add(sum(clist) == 1)\n \n    # ...\n    \n    s.solve(m)\n```\n\n### An artisanal hand-crafted CP-SAT invocation\n\nTo learn more about this and try to lock in the lesson not to reinvent the\nwheel, I got together with [Zaki](https://github.com/zmughal) and\n[Tommy](https://tommymaranges.com) at recurse to do what is probably the most classic\napplication of cp-sat to a puzzle, Sudoku.\n\nThis is a classic beginner application of the tool and easier to apply than for the corn puzzle; we simply have one variable per cell, with a value 1-9, and our constraint classes are that every row, column, and box has to have one of each. We don’t need to have utility methods for setting this up, as the corn puzzle did for rotating pieces:\n\n```\n  model_vars = [ [ None for _ in range(9) ] for _ in range(9) ]\n  for i in range(9):\n    for j in range(9):\n      board_var_name = f\"s[{i},{j}]\"\n      if board[i][j] == -1:\n        model_vars[i][j] = model.new_int_var(1, 9, board_var_name)\n      else:\n        # for given values, there's only one legal value for the variable\n        model_vars[i][j] = model.new_int_var(board[i][j], board[i][j], board_var_name)\n\n  # rows\n  for r in range(9):\n    model.add_all_different( [ model_vars[r][c] for c in range(9) ] )\n\n  # columns\n  for c in range(9):\n    model.add_all_different( [ model_vars[r][c] for r in range(9) ] )\n\n  # boxes\n  for off_r in range(3):\n    for off_c in range(3):\n      model.add_all_different( [\n          model_vars[3*off_r + numpad % 3][3*off_c + numpad // 3]\n            for numpad in range(9) ] )\n```\n\nThis indeed solved a reference sudoku we gave it, quickly.\n\nThere’s a lot more left to learn about these, but we’ve now learned the most important thing - when I get another cp-sat shaped problem in the future, I’ll know to try a solver and read the docs for whatever else I need, rather than roll my own.\n\n#### Links\n\n[The conversation with claude about solving the corn puzzle (with claude’s python script)](https://claude.ai/share/37b27e39-f11b-4a9a-98b0-04b98fa84662)", "url": "https://wpnews.pro/news/solving-a-corn-puzzle-with-cp-sat", "canonical_source": "https://thill.me/2026/07/16/corn-puzzle-sat-solver.html", "published_at": "2026-09-27 20:58:54+00:00", "updated_at": "2026-09-27 21:31:16.001077+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools"], "entities": ["Google", "OR-Tools CP-SAT", "Claude", "Anthropic", "Recurse Center"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/solving-a-corn-puzzle-with-cp-sat", "markdown": "https://wpnews.pro/news/solving-a-corn-puzzle-with-cp-sat.md", "text": "https://wpnews.pro/news/solving-a-corn-puzzle-with-cp-sat.txt", "jsonld": "https://wpnews.pro/news/solving-a-corn-puzzle-with-cp-sat.jsonld"}}