At some point I am hopefully formerly working on it because it’s done.
That day is not today. Here’s a minor-ish extension to the AC-3 class that’s specifically for solving zebra puzzles. I split it into its own repo: zebraPuzzle github repo.
Zebra Puzzles
The original zebra puzzle (or at least the one that gives the genre that name) is:
- There are five houses.
- The Englishman lives in the red house.
- The Spaniard owns the dog.
- Coffee is drunk in the green house.
- The Ukrainian drinks tea.
- The green house is immediately to the right of the ivory house.
- The Old Gold smoker owns snails.
- Kools are smoked in the yellow house.
- Milk is drunk in the middle house.
- The Norwegian lives in the first house.
- The man who smokes Chesterfields lives in the house next to the man with the fox.
- Kools are smoked in the house next to the house where the horse is kept.
- The Lucky Strike smoker drinks orange juice.
- The Japanese smokes Parliaments.
- The Norwegian lives next to the blue house.
Now, who drinks water? Who owns the zebra?
The ZebraPuzzle Class
Most of the logic used by the ZebraPuzzle class is the same as the AC3 class. The main additions/modifications are:
-
The assumption that each variable must be assigned to exactly one item. That is, there are five houses and five colors, with each house being a different color. No colors re-used, no colors un-used.
This means that in addition to the explicit constraints that are added based on the lines in the problem statement, there are a bunch of implicit constraints.
-
There is exactly one solution. This means that instead of the BT backtracker we can use a much simpler tail recursion method for backtracking.
-
The constraints are all of a couple of basic types. This is almost all semantic sugar…you can still declare arbitrary constraints using your own callback functions, but we’ll also have a simpler syntax to handle the most common constraints (which are almost all of the form “the [something] is the [something else]”. That is, saying that red house is also the house with the guy from England, and so on.
Modelling the problem
Under the hood the class is creating a vertex for each variable and an edge for each constraint.
Each vertex contains the domain of the variable it corresponds to. All of these start out as a list of all the possible item assignments. In our example there are five houses, so each variable starts out with a domain of [ 1, 2, 3, 4, 5 ]. That is, each variable could be assigned to any item: ‘red’ could be assigned to house #1, house #2, house #3, house #4, or house #5.
As constraints are applied, the domain of each variable is reduced. The ninth statement (“Milk is drunk in the middle house.”) reduces the domain of the “milk” variable to be [ 3 ]. Because no other drink can also be house #3, that means, for example, the domain for “coffee” can immediately be reduced to [ 1, 2, 4, 5 ].
Since each house must have at least one variable from each vertex group (it must have a color, for example), any time a house number appears in only one domain in a vertex group that solves that variable and any other elements in its domain can be removed (possibly causing some other variable to become a singleton as a result).
I’m not going to go through all the logic (unless someone wants a more complete discription). Just explaining the basics of how the problem is represented under the hood.
Declaring The Problem
To solve the problem, convert the problem statement into a ZebraPuzzleConfig instance:
zebraConfig: ZebraPuzzleConfig
variables = [
'color' ->
[ 'red', 'green', 'ivory', 'yellow', 'blue' ],
'country' ->
[ 'England', 'Spain', 'Ukraine', 'Norway', 'Japan' ],
'drink' ->
[ 'coffee', 'tea', 'milk', 'orange juice', 'water' ],
'brand' ->
[ 'Old Gold', 'Kool', 'Chesterfield', 'Lucky Strike',
'Parliament' ],
'pet' ->
[ 'dog', 'snails', 'fox', 'horse', 'zebra' ]
]
constraints = [
[ 'England', 'red' ],
[ 'Spain', 'dog' ],
[ 'coffee', 'green' ],
[ 'Ukraine', 'tea' ],
[ 'green', 'ivory', { a, b: a == b + 1 } ],
[ 'Old Gold', 'snails' ],
[ 'Kool', 'yellow' ],
[ 'milk', 3 ],
[ 'Norway', 1 ],
[ 'Chesterfield', 'fox', { a, b: abs(a - b) == 1 } ],
[ 'Kool', 'horse', { a, b: abs(a - b) == 1 } ],
[ 'Lucky Strike', 'orange juice' ],
[ 'Japan', 'Parliament' ],
[ 'Norway', 'blue', { a, b: abs(a - b) == 1 } ]
]
;
The variables declaration is an inline LookupTable whose keys are each an ID for a vertex group (color, pet, and so on) and whose values are lists of the properties in that group (“color” is “red”, “green”, “ivory”, and so on). The vertex group ID is arbitrary but needs to be consistent.
The constraints declaration is a list of constraints in one of two forms. The constraint:
[ `England`, 'red' ]
represents the second statement. It means “the house the person from England lives in is the same number as the house which is red”.
The constraint:
[ 'green', 'ivory', { a, b: a == b + 1 } ],
represents the sixth statement. It means “the number of the green house is the number of the ivory house plus one”. The function can be anything but must take two arguments, the numbers of the two houses.
Having created the config, it can be used to create a ZebraPuzzle instance:
local g = new ZebraPuzzle(zebraPuzzleConfig);
The solve() method can be called to attempt to solve the puzzle. It will return a ZebraPuzzleSolution instance on success, or nil on failure.
// Attempt to solve the puzzle.
local r = g.solve();
// Check the return value.
if(r == nil)
return(nil);
// Output the solution.
r.log();
This will output something like
Item 1:
color: yellow
country: Norway
drink: water
brand: Kool
pet: fox
Item 2:
color: blue
country: Ukraine
drink: tea
brand: Chesterfield
pet: horse
Item 3:
color: red
country: England
drink: milk
brand: Old Gold
pet: snails
Item 4:
color: ivory
country: Spain
drink: orange juice
brand: Lucky Strike
pet: dog
Item 5:
color: green
country: Japan
drink: coffee
brand: Parliament
pet: zebra
The Format of ZebraPuzzleSolution
The ZebraPuzzleSolution instance has a .data property that you can use directly as well. It will consist of an array of LookupTable instances with each LookupTable corresponding to a single house. Something like:
[
[ 'color' -> 'yellow, 'country' -> 'Norway', 'drink' -> 'water',
'brand' -> 'Kool', 'pet' -> 'fox ],
[ 'color' -> 'blue', 'country' -> 'Ukraine', 'drink' -> 'tea',
'brand' -> 'Chesterfield', 'pet' -> 'horse', ],
[ 'color' -> 'red', 'country' -> 'England', 'drink' -> 'milk',
'brand' -> 'Old Gold', 'pet' -> 'snails', ],
[ 'color' -> 'ivory', 'country' -> 'Spain', 'drink' -> 'orange juice',
'brand' -> 'Lucky Strike', 'pet' -> 'dog', ],
[ 'color' -> 'green', 'country' -> 'Japan', 'drink' -> 'coffee',
'brand' -> 'Parliament', 'pet' -> 'zebra', ]
]
Alternate Declaration Syntax
In addition to declaring the config object all at once, it can be created programmatically. For example:
local cfg = new ZebraPuzzleConfig();
cfg.addVariable('color', 'red');
cfg.addVariable('color', 'green');
cfg.addVariable('color', 'ivory');
cfg.addVariable('color', 'yellow');
cfg.addVariable('color', 'blue');
…or alternately…
cfg.addVariable('pet',
[ 'dog', 'snails', 'fox', 'horse', 'zebra' ]);
And constraints can be added via a similar syntax:
cfg.addConstraint('England', 'red');
cfg.addConstraint('green', 'ivory', { a, b: a == b + 1 });
Discussion
I shoved this into its own little module because it’s not really a datatype anymore. I kinda think maybe the BT and AC3 logic and this should be moved into their own constraint solver module.
In a different development environment I’d be more inclined to do what I started out doing…making lots of small, single-purpose modules instead of a smaller number of larger ones. But TADS3 doesn’t really make dependency management for that kind of thing easy.
Anyway, I figured things like the classic zebra problem were probably the kind of constraint satisfaction problem that most IF authors would probably be most concerned about, so I figured I’d knock this together as an example/illustration of how to implement a more specific solver using AC-3.