A solver for Murdle-style logic grid puzzles. Pure Python, no dependencies.
./murdle_solver.py example.txtsuspects weapons places
Miss Saffron pool cue lobby
Sgt Gunmetal dumbbell gym
Coach Raspberry poisoned tea pool
Run the tests with python3 -m unittest.
A puzzle file declares its categories first, then one clue per line. # starts a comment.
suspects: Miss Saffron, Sgt Gunmetal, Coach Raspberry
weapons: dumbbell, pool cue, poisoned tea
places: gym, pool, lobby
Sgt Gunmetal + dumbbell # these go together
not Coach Raspberry + (gym | lobby) # so Raspberry was at the pool
pool cue + (pool | lobby) # the cue was at one of the two
Miss Saffron + pool cue or Sgt Gunmetal + pool cue
Every category must list the same number of items, and every item name must be unique across the whole puzzle. A row of the answer grid is one item from each category — the suspect, the weapon they used, where they were, and so on.
clue := option ("or" option)*
option := ["not"] term ("+" term)*
term := item | "(" item ("|" item)* ")"
| Syntax | Means |
|---|---|
A + B |
some row contains both A and B |
A + B + C |
some row contains all three |
not A + B |
no row contains both — not negates the whole line |
A + (B | C) |
some row contains A and at least one of B or C |
A + B or C + D |
at least one of the two holds |
& is a synonym for +, | and either ... or are synonyms for or, and inside parentheses a
comma works like |. So not A + (B, C) reads as "A was with neither B nor C".
Note that not distributes over the whole option: not A + B is "never A and B together", not
"A with something other than B".
Names are matched loosely, so clues can read the way the puzzle does. Case and extra whitespace are
ignored, a leading the is optional in either direction, and any unambiguous substring works:
places: the boat, the lighthouse
the lighthouse + revenge
lighthouse + revenge # same clue
An unknown or ambiguous name is an error naming the line and the candidates, rather than a wrong answer — so a typo can't quietly change the puzzle.
Depth-first search over the answer grid, one cell at a time in row-major order.
The first category is pinned to the identity assignment: rows have no inherent order, so row r is
defined as the row holding item r of the first category. That removes the row permutation
symmetry, leaving (n!)^(k-1) candidate grids for k categories of n items.
After every single cell assignment, each clue is tested for whether it can still be satisfied — a positive clue fails once no row could possibly match it, and a negative one fails once some row definitely does. That test is monotone: a clue that has become unsatisfiable can never recover as more cells are filled in, so failing it is a sound reason to backtrack immediately, and once the grid is full the test is exact. In practice this settles a full 5×5×5×5×5 puzzle instantly.
An underconstrained puzzle can have an enormous number of solutions, so the solver stops after
MAX_SOLUTIONS (1000) and says so.