Books · Solving Puzzles through Mathematical Programming
Chapter 14
Rolling Cubes
A standard die is placed at the bottom-left corner of a grid with face upward, face towards the north, and face towards the east. The die rolls, one cell at a time, onto an adjacent cell; a roll cycles four of the six faces (the top, the bottom, and the two faces in the direction of travel) and leaves the two faces parallel to the roll axis fixed. A tour is a Hamiltonian path on the grid: the die visits each of the nine cells exactly once. Each visited cell records the face that happens to be on top at the moment of the visit. We ask: what is the maximum, and the minimum, total of the nine recorded top faces over all legal tours?
The puzzle is an integer rolling-cube search in the tradition of Berlekamp, Conway, and Guy’s Winning Ways (Volume , games on grids). Its combinatorial shape is a Hamiltonian path problem with a side-state (die orientation) that evolves locally. Both features are natural for CP-SAT: the tour is a permutation constraint; the die state is an table of transition tuples.
Die orientations and their transitions
A standard die has three pairs of opposite faces: , , . The cube has twenty-four rotational orientations, each specified completely by the pair with derived. We encode each orientation as a triple of face numbers and enumerate the valid triples by closure under the four rolls. A roll in direction sends to:
The programming model
Variables
Number the grid cells row-major. Let be the cell visited at time , with and all distinct. Let for be the rolling direction taken from step to step , and let be the index of the die’s orientation at step , with equal to the initial orientation .
Grid transitions
Express cell as via integer division and modulo. Adjacency in the grid translates directly into equalities on : direction requires and , and similarly for the other directions.
Die-rolling transitions
The triple must belong to the precomputed transition table of size , imposed as an constraint.
Top-face readouts
Let be the top-face value at step . Link to by a table constraint .
Objective
Maximise or minimise .
A solver in sixty lines
from ortools.sat.python import cp_model
OPP = {1: 6, 2: 5, 3: 4, 4: 3, 5: 2, 6: 1}
def roll(o, d):
t, n, e = o
if d == 'N': return (OPP[n], t, e)
if d == 'S': return (n, OPP[t], e)
if d == 'E': return (OPP[e], n, t)
return (e, n, OPP[t])
def orientations():
pool = {(1, 2, 3)}
frontier = set(pool)
while frontier:
nxt = set()
for o in frontier:
for d in "NSEW":
r = roll(o, d)
if r not in pool:
pool.add(r); nxt.add(r)
frontier = nxt
return sorted(pool)
def solve(objective="max"):
O = orientations()
idx = {o: i for i, o in enumerate(O)}
trans = [(i, di, idx[roll(O[i], d)])
for i in range(24)
for di, d in enumerate("NSEW")]
top_tbl = [[i, O[i][0]] for i in range(24)]
m = cp_model.CpModel()
c = [m.NewIntVar(0, 8, "") for _ in range(9)]
m.AddAllDifferent(c)
m.Add(c[0] == 0)
r = [m.NewIntVar(0, 2, "") for _ in range(9)]
k = [m.NewIntVar(0, 2, "") for _ in range(9)]
for t in range(9):
m.AddDivisionEquality(r[t], c[t], 3)
m.AddModuloEquality(k[t], c[t], 3)
d = [m.NewIntVar(0, 3, "") for _ in range(8)]
o = [m.NewIntVar(0, 23, "") for _ in range(9)]
m.Add(o[0] == idx[(1, 2, 3)])
f = [m.NewIntVar(1, 6, "") for _ in range(9)]
for t in range(8):
isN = m.NewBoolVar(""); isS = m.NewBoolVar("")
isE = m.NewBoolVar(""); isW = m.NewBoolVar("")
for v, b in zip(range(4), [isN, isS, isE, isW]):
m.Add(d[t] == v).OnlyEnforceIf(b)
m.Add(d[t] != v).OnlyEnforceIf(b.Not())
m.AddBoolOr([isN, isS, isE, isW])
m.Add(r[t + 1] == r[t] + 1).OnlyEnforceIf(isN)
m.Add(k[t + 1] == k[t]).OnlyEnforceIf(isN)
m.Add(r[t + 1] == r[t] - 1).OnlyEnforceIf(isS)
m.Add(k[t + 1] == k[t]).OnlyEnforceIf(isS)
m.Add(r[t + 1] == r[t]).OnlyEnforceIf(isE)
m.Add(k[t + 1] == k[t] + 1).OnlyEnforceIf(isE)
m.Add(r[t + 1] == r[t]).OnlyEnforceIf(isW)
m.Add(k[t + 1] == k[t] - 1).OnlyEnforceIf(isW)
m.AddAllowedAssignments(
[o[t], d[t], o[t + 1]], trans)
for t in range(9):
m.AddAllowedAssignments([o[t], f[t]], top_tbl)
total = m.NewIntVar(0, 54, "")
m.Add(total == sum(f))
if objective == "max": m.Maximize(total)
else: m.Minimize(total)
s = cp_model.CpSolver(); s.Solve(m)
return s.Value(total), [s.Value(f[t]) for t in range(9)]
The maximum total is , achieved by the tour shown in Figure 14.1; the minimum is , achieved by Figure 14.2. Both are found in about milliseconds.
Sources. Rolling-cube puzzles are a long-running theme in recreational mathematics. Martin Gardner’s Scientific American column (, “Dice tricks”) posed a number of rolling-cube questions on and boards; Berlekamp, Conway, and Guy’s Winning Ways for Your Mathematical Plays (Volume , second edition, ) devotes a chapter to the rolling block family, which includes the present tour puzzle as an integer variant. The specific form — Hamiltonian tour with extremal top-face sum — appears to have been popularised by I. Stewart in his Guardian column in the s. The generalisation to and larger grids remains a clean CP-SAT exercise; the search space grows roughly by a factor of per grid-edge length, and the same model solves the extremal problem in seconds.