A few months ago, I created a programming challenge, an invitation to do some old-fashioned programming from before the LLM era. The goal was to write a solver for Rushhour. Give it a try! It is a lot of fun, almost as much fun as playing the game!
I implemented two classical search algorithms: BFS and .
According to the Book π, is cost-optimal if it uses an admissible heuristic, i.e., one that never overestimates the cost to reach the goal. The heuristic I used counts the number of blocking cars in front of the red car π. This heuristic is admissible because one would need at least that many moves to reach the goal. Right? Therefore, the solutions provided by should be cost-optimal.
So, whatβs the point of expressing Rushhour as an optimization problem?
A few calculations will quickly make you reconsider. With a board of and a maximum number of steps, plus variables for representing the cars and other auxiliary variables, you end up with thousands or tens of thousands of discrete variables and constraints, possibly including nonlinear or indicator constraints linking the car moves to their positions on the board.
In short, there is no practical point. Itβs just for fun.
The main reason I wanted to do this is that when my son brought home this game from the library, the first thing I wondered was if I could formulate it as an optimization problem. So here we are.
If you prefer reading the code, you can jump directly to it here.
Representing the board and the cars
First, we must decide how to represent the board and the cars. The board can be modeled as a three-dimensional binary variable , where is the maximum number of time steps and represents occupied and represents free. Each car can also be represented as a binary variable of shape , .
I spent quite some time trying to figure out how to link carβs movement to the board, which is non-trivial in a declarative formulation. One cannot simply βassignβ a variable based on the outcome of another variable.
It took me a 40-minute walk to come up with a different solution that doesnβt require complicated constraints. Rather than modeling the board and the cars separately, I model them together, adding one more dimension to the board .
This approach simplifies the logic but it adds a lot of unnecessary variables, even if only a few rows and columns have active constraints.
I ended up combining both approaches by modeling the cars and the board as separate variables but operating on them as if the cars were an additional dimension of the board.
β Parameter: , maximum number of time steps.
β Parameter: , board size.
β Parameter: , the set of cars in the current game instance, e.g, .
β Variable: , the board.
β Variable: , the cars.
β Constraint 1: The link between the board and the cars. This set of constraints is probably best understood by looking at the code.
For all if car is in row or column .
β Constraint 2: Non-overlapping cars
The following figures illustrate valid and invalid car positions according to Constraint 2.
Figure: Valid car positions - Car A board (left), Car X board (center), and superposition without overlap (right) at some time
The following arrangement is not allowed:
Figure: Unfeasible car positions - Car A board (left), Car X board (center), and superposition with overlap (right) at some time
β Constraint 3: Constant car size.
can leave the board; thatβs the objective of the game. However, I consider the position in which is about to leave to be the last one.
Even if the number of blocks that make up a car is constrained to remain constant, it doesnβt mean that the blocks are automatically contiguous.
β Variable:
This variable indicates the starting position of the car: the leftmost and uppermost positions for horizontal and vertical cars, respectively.
β Constraint 4: Cars can only start at one position.
However, I suspect that this condition is redundant and can be removed.
β Constraint 5: The blocks that make up a car must be contiguous.
Preventing βTunnelingβ
The crux of this formulation is the constraint that ensures cars of length two cannot βtunnelβ through other cars. This is not an issue for cars of length three because the non-overlapping constraint guarantees that all moves are valid.
As shown in the figure below, the two moves in the middle (, ) are not problematic because they are accounted for by the non-overlapping constraint. However, the move on the far right is problematic (pun intended π) because the red car would go through the purple car.
Figure. A car that is two tiles long starts in the third row, first column. In a single time step, it moves right by 1, 2, and 3 tiles, respectively.
Model 0
This is the base model, the first one I implemented. It restricts cars from moving more than one tile per turn. This condition prevents tunneling. The objective function is the number of steps it takes to move car π to the exit.
This model solves a normalized version of the puzzle. The optimal value is the number of one-tile moves required to solve the puzzle. This number is equal to the number of one-tile moves in the optimal solution. Unfortunately, there is no one-to-one correspondence between one-tile moves and actual moves. The solution obtained by aggregating one-tile moves generally does not correspond to the optimal solution. You can think of this solution as an upper bound though.
Model 1
This model builds on Model 0. It introduces the variable , which indirectly counts the actual number of moves. A move is defined as a car that is different from the car in the previous and that moves any number of tiles. The objective function is the sum over . The resulting sequence is equivalent to the optimal one (although it must be compressed), but the objective value does not represent the actual number of moves (at least not directly). This objective function is not well-behaved, and the problem can become intractable for larger instances.
Model 2
This model uses the same objective function as in Model 0, but it allows cars to move more than one tile per turn. To prevent tunneling, a representing the tiles between the starting and the end positions of a single move is introduced. This constraint, which is only active if , is added to the board as if it were an additional dimension. The rest of the post describes this model in detail.
Definition of Model 2
β Variable: , auxiliary variable to count the number of tiles a car moves in a single turn.
β Variable: , indicates which car can move at each time step.
β Constraint 6: Only one car can move per time step.
We will need a few tricks to represent Boolean conditions in a linear way.
| 1 | ||
| 2 | ||
| 3 |
Table: Tricks for representing AND and OR logical conditions. Source: jump.dev
β Constraint 7: Cars can only move if it is their turn.
For all , and all .
Preventing tunneling
These constraints are rather convoluted, but I havenβt found a simpler way yet. Ideas are welcome!
The basic idea is to create a mask representing the tiles between the carβs starting and ending positions in the current turn. This mask is then added to the board as an additional dimension to ensure there are no obstacles in the path.
β Variable: , is the set of variables to implement the approach described above.
is just an index to identify the different variables.
β Constraint 8: Calculate the mask representing the carβs path as it moves through the current turn.
For all . The last constraint is implemented using the auxiliary variable and big-M constraints. The Boolean operations are implemented using the aforementioned tricks. See the code for details.
Success criterium
The goal of the game is to get the red car π to the exit.
Figure: Car X at the exit position
β Variable: , indicates wheter car X is at the exit position at time step .
β Constraint 9: Car X must be at the exit at some time step .
Objective function
β Objective: Minimize the number of time steps it takes for car X to reach the exit.
Conclusions
A note on the solvers
I tried HiGHS and cuOpt which was recently open-sourced. However, I only had success with HiGHS. I didnβt investigate why cuOpt failed in these instances.
Based on this and previous experiences, my only conclusion is that HiGHS is a superb solver.
Open questions
I am convinced that this is one is not the best possible formulation. Ideas are welcome!
Final thoughts
I enjoy doing this kind of work because I always learn something new. If you compare the Branch and Bound (B&B) algorithm used by HiGHS with the algorithm, you will see that they are very similar. I learned that someone in the past tried to generalize these ideas βsearch algorithms, dynamic programming, and B&Bβ into a single concept. Check out this article.
And a final though on optimization, and why the title of this post is βDeath, taxes and optimizationβ.
Optimization is one of those beautiful, pervasive theories that has made an indelible mark on the history of mathematics, engineering, and many other disciplines since its inception. It will continue to be relevant, even if hidden behind something more showy and bombastic. Mathematical optimization is a fundamental discovery. It will continue to be relevant in the foreseeable future and will survive all the hype.
If those lines sound dramatic and opinionated, they are. Theyβre also true π.