
How to Play Zip¶
Figure 1:Example of a Zip game board (Source: LinkedIn Zip)
The board of Zip is made up with some numbered squares and walls (walls are optional).
- Objective
- To trace a single path that runs through all the squares on the board.
- Rule
- The path must move through numbered squares in ascending order, starting from square number 1 to the one with the highest number.
Problem Modeling¶
The best way to solve the game is to consider the squares as nodes in a directed graph (digraph), where each node has a set of pairs of edges that arrive at and depart from the nodes, as shown in the following figure.

Figure 2:Zip game modeled on a directed graph. Source: created by the author
Considering this graph model, it was enacted the Abstract LO Model by defining its following components:
Ranges
Sets
Decision variables
Parameter
Objective function
Constraints
Ranges¶
To properly formulate the LO model, we will start from the premise that the squares on the board will be the Cartesian product of the ranges of rows and of columns present on the board. In addition, it is necessary to have defined an interval for the set of numbered squares.
- The row range, where is the total number of rows (in this case, )
- The column range, where is the total number of columns (in this case, as well).
Sets¶
The definition of the sets will be more than fundamental to reducing the complexity of the model, as we will avoid having to deal with numerous exceptions. From the ranges determined previously, we can define the set of vertices () and edges () existing in the network logic. In addition to these, it is also necessary to consider the subsets of numbered vertices () and blocked edges (), since, in some instances of Zip game, there may be walls between two squares that prevent the path from being traced between them.
- Set of all squares (vertices). There must be at least 2 vertices in the grid, as it is only possible to trace a path with an initial and a final square. Therefore,
- Set of all edges present in the graph. Its definition is a bit more complex as it is necessary to clarify that, for each vertex, there will only be edges between neighbors immediately to the left, immediately to the right, immediately above, and immediately below it, if they exist.
- Set of numbered squares, which will consist of a set of all vertices indexed by a number on board game.
- Set of blocked edges (walls) that may appear on the grid.
Decision Variables¶
By using the network logic already presented, it is possible to define the edges as binary decision variables , which again makes our LOP a BLOP. In addition to , variables will also be considered, representing the absolute ordinal position of the square within the path (these variables will be better explained later).
- , if the edge that starts from square and arrives at square is part of the path
- , otherwise
- Absolute ordinal positions of the squares within the path.
Parameters¶
- For each numbered board square in set, it’ll be defined a integer that matches its number displayed on game board.
Objective Function¶
Once again, our problem does not have a function to be optimized, since we only want to find a solution that fulfills all the rules of the game. Therefore, a Zip BLOP is a feasibility problem, and the objective function of the model consists of maximizing (or minimizing) an arbitrary constant.
Constraints¶
- Decision variables domain constraints
First, let’s not forget to define our variables as binary and the variables as non-negative real numbers. To simplify, whenever possible, we can replace the notation with and with just , since the tuple always refers to some vertex belonging to set.
- Blocked edges constraints
If there is a wall between a pair of squares, then the sum of the edges connecting those ones must be equal to 0.
- Continuity constraints
Since it is only possible to enter and exit a node once (except for the initial and destination nodes), then we will have the following:
- Outgoing-Edges Constraints
- The sum of the edges emanating from a node must equal 1 (except for the destination square).
- Incoming-Edges Constraints
- The sum of the edges arriving at a node must equal 1 (except for the initial square).
- Arrival Square Constraint
- The sum of the edges emanating from the destination node must be 0.
- Starting Square Constraint
- The sum of the edges arriving at the initial node must be 0.
- Subroute Elimination Constraints
The advantage of the MTZ formulation is that the number of constraints to be included to guarantee subroute elimination is equal to , that is, the number of edges in the graph. Thus, the number of constraints grows linearly as a function of the number of edges, instead of exponentially as a function of the number of vertices. For it to work, it requires the addition of more decision variables (the previously mentioned ), which represent the absolute ordinal position of square within the path.
- Position of Starting Square Constraint
- First, the position of the starting square must be equal to 1, since it will always be the first position on the path.
- MTZ Constraints
- Then, the positions of the remaining squares on the board should be given as follows.
- Positon of Arrival Square Constraint
- Although not necessary, one can restrict the position of the arrival square to be equal to the number of squares on the board, just to make the range from 1 to 36.
- Order of Precedence Constraints
- In order for the path to traverse the numbered squares in ascending numerical order, it is necessary to impose the constraint that the position of a numbered square must be at least equal to the position subsequent to the last numbered square.
Abstract Model¶
With all the formulas elaborated, we have the complete abstract model for the Zip minigame.
Concrete Model¶
The game selected to be solved is the Zip No. 166, published on LinkedIn on August 29th, 2025

Figure 3:Zip No. 166, August 29th, 2025 (Source: LinkedIn Zip)
Based on this specific case, we have the following concrete model:
S.t.:
- Outgoing edges constraints
- (Square (1,1))
- (Square (1,2))
- (Square (1,3))
- (Square (1,4))
- (Square (1,5))
- (Square (1,6))
- (Square (2,1))
- (Square (2,2))
- (Square (2,3))
- (Square (2,4))
- (Square (2,5))
- (Square (2,6))
- (Square (3,1))
- (Square (3,2))
- (Square (3,3))
- (Square (3,4))
- (Square (3,5))
- (Square (3,6))
- (Square (4,1))
- (Square (4,2))
- (Square (4,3))
- (Square (4,4))
- (Square (4,5))
- (Square (4,6))
- (Square (5,1))
- (Square (5,2))
- (Square (5,3))
- (Square (5,4))
- (Square (5,5))
- (Square (5,6))
- (Square (6,1))
- (Square (6,2))
- (Square (6,3))
- (Square (6,4))
- (Square (6,5))
- (Square (6,6))
- Incoming edges constraints
- (Square (1,1))
- (Square (1,2))
- (Square (1,3))
- (Square (1,4))
- (Square (1,5))
- (Square (1,6))
- (Square (2,1))
- (Square (2,2))
- (Square (2,3))
- (Square (2,4))
- (Square (2,5))
- (Square (2,6))
- (Square (3,1))
- (Square (3,2))
- (Square (3,3))
- (Square (3,4))
- (Square (3,5))
- (Square (3,6))
- (Square (4,1))
- (Square (4,2))
- (Square (4,3))
- (Square (4,4))
- (Square (4,5))
- (Square (4,6))
- (Square (5,1))
- (Square (5,2))
- (Square (5,3))
- (Square (5,4))
- (Square (5,5))
- (Square (5,6))
- (Square (6,1))
- (Square (6,2))
- (Square (6,3))
- (Square (6,4))
- (Square (6,5))
- (Square (6,6))
- Ordinal position constraints
- (Square 1)
- (Square 2)
- (Square 3)
- (Square 4)
- (Square 5)
- (Square 6)
- (Square 7)
- (Square 8)
- (Square 9)
- (Square 10)
- (Square 11)
- (Square 12)
- (Square 13)
- (Square 14)
- (Square 15)
- (Square 16)
- (Square 16)
- Subroute elimination constraints
- Binarity constraints for edge variables
- Posivite constraints for positional variables
- (Square (1,1))
- (Square (1,2))
- (Square (1,3))
- (Square (1,4))
- (Square (1,5))
- (Square (1,6))
- (Square (2,1))
- (Square (2,2))
- (Square (2,3))
- (Square (2,4))
- (Square (2,5))
- (Square (2,6))
- (Square (3,1))
- (Square (3,2))
- (Square (3,3))
- (Square (3,4))
- (Square (3,5))
- (Square (3,6))
- (Square (4,1))
- (Square (4,2))
- (Square (4,3))
- (Square (4,4))
- (Square (4,5))
- (Square (4,6))
- (Square (5,1))
- (Square (5,2))
- (Square (5,3))
- (Square (5,4))
- (Square (5,5))
- (Square (5,6))
- (Square (6,1))
- (Square (6,2))
- (Square (6,3))
- (Square (6,4))
- (Square (6,5))
- (Square (6,6))
Solving Zip¶
The linkedin-games library counts on Zip class, that implements the Zip game and its constraints, as well as methods to solve it and visualize the solution.
To instantiate the game, it is necessary to provide two sets of inputs regarding the game in question:
numbered_squares- A dictionary of
(row, column): numberitems representing the starting filled squares, wherenumberis the required relative position of the square on the path. walls- A tuple of pairs of squares (edges) representing walls on game board.
from linkedin_games import Zip
# Solving Zip No. 166
numbered_squares = [
(6,5), (6,6), (5,6), (6,4), (5,3), (4,2), (3,1), (2,1),
(1,1), (1,2), (1,3), (3,5), (2,4), (3,4), (4,3), (4,6)
]
zip_game = Zip(6, numbered_squares)The method solve() calls a modeler to build the Linear Optimization logic behind the Zip’s game before solving it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99import pyomo.environ as pyo from .._shared.optimization.modeler.opt_modeler import OptModeler from .._shared.utils.taxicab_distance import TaxicabDistance from ._game_grid import ZipGrid class ZipOptModeler(OptModeler[ZipGrid]): def _set_range_sets(self) -> None: super()._set_range_sets() self._model.K = pyo.RangeSet(len(self._game.numbered_squares)) def _set_composite_sets(self) -> None: super()._set_composite_sets() self._model.E = pyo.Set( # Edges initialize=lambda model: [((i,j), (i+1, j)) for i in model.I for j in model.J if i+1 in model.I] + [((i,j), (i-1, j)) for i in model.I for j in model.J if i-1 in model.I] + [((i,j), (i, j+1)) for i in model.I for j in model.J if j+1 in model.J] + [((i,j), (i, j-1)) for i in model.I for j in model.J if j-1 in model.J] ) self._model.W = pyo.Set( # Walls initialize=self._game.walls, domain=self._model.E ) self._model.N = pyo.Set( initialize=self._game.numbered_squares, domain=self._model.S ) def _set_parameters(self) -> None: self._model.M = self._model.m * self._model.n def _set_decision_variables(self) -> None: self._model.x = pyo.Var( # Decision to go from square (i,j) to (r,s) self._model.E, domain=pyo.Binary, initialize=0 ) self._model.u = pyo.Var( # Visitiation order of a square (i, j) self._model.S, domain=pyo.PositiveIntegers, initialize=1, bounds=(1, self._model.M) ) def _set_constraints(self) -> None: self.__set_edge_constraints() self.__set_blocked_path_contraints() self.__set_subroute_elimination_constraints() self.__set_visitation_order_constraints() def __set_edge_constraints(self) -> None: neighbors = { # This dictionary is important to access all neighbors of a square quickly. (i, j): [ (r, s) for r, s in [ (i-1, j ), (i+1, j ), (i , j-1), (i , j+1) ] if (r, s) in self._model.S ] for (i, j) in self._model.S } self._model.outgoing_edges_constraints = pyo.Constraint( self._model.S, rule=lambda model, i, j: pyo.quicksum(model.x[(i,j), w] for w in neighbors[(i,j)]) == 0 if model.N.at(len(model.K)) == (i,j) else pyo.quicksum(model.x[(i,j), w] for w in neighbors[(i,j)]) == 1 ) self._model.incoming_edges_constraints = pyo.Constraint( self._model.S, rule=lambda model, i, j: pyo.quicksum(model.x[s, (i,j)] for s in neighbors[(i,j)]) == 0 if model.N.at(1) == (i,j) else pyo.quicksum(model.x[s, (i,j)] for s in neighbors[(i,j)]) == 1 ) def __set_blocked_path_contraints(self) -> None: self._model.wall_constraints = pyo.Constraint( self._model.W, rule=lambda model, i, j, r, s: model.x[i,j,r,s] + model.x[r,s,i,j] == 0 ) def __set_subroute_elimination_constraints(self) -> None: self._model.miller_tucker_zemlin_constraints = pyo.Constraint( self._model.E, rule=lambda model, i, j, r, s: model.u[r,s] >= model.u[i,j] + 1 - model.M * (1 - model.x[i,j,r,s]) + (model.M - 2) * model.x[r,s,i,j] ) def __set_visitation_order_constraints(self) -> None: self._model.visitation_order_constraints = pyo.Constraint( self._model.K, rule= lambda model, k: model.u[model.N.at(k)] == 1 if k == 1 else model.u[model.N.at(k)] == model.M if k == len(model.N) else model.u[model.N.at(k)] >= model.u[model.N.at(k-1)] + TaxicabDistance.calculate(model.N.at(k), model.N.at(k-1)) )
Program 1:The Zip’s modeler.
The method display(), in its turn, calls a renderer to show the game’s board with the positions on the path, which can be accessed by solution attribute.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74import matplotlib.pyplot as plt import networkx as nx from .._shared.utils.color_generator import ColorGenerator from .._shared.visualization.renderer.renderer import GameRenderer from ._game_grid import ZipGrid class ZipRenderer(GameRenderer[ZipGrid]): _SCALE_FACTOR = .7 def _set_grid(self) -> None: super()._set_grid() nx.set_edge_attributes( self._grid, name="value", values={((i-1, j-1), (r-1, s-1)): k for ((i, j), (r, s)), k in self._game.edges.items()} ) def display(self) -> None: plt.figure(figsize=(self._width, self._height)) path_color = ColorGenerator.generate_hex_code() labels = { (i-1, j-1): k+1 for k, (i,j) in enumerate(self._game.numbered_squares) } pos= {(i,j): (j,-i) for i, j in self._grid.nodes()} if self._game.walls != []: walls = nx.draw_networkx_edges( self._grid, pos=pos, edgelist=[((i-1, j-1), (r-1, s-1)) for (i,j), (r,s) in self._game.walls], edge_color="#000000", hide_ticks=True, arrows=False, width=30, ) walls.set_zorder(0) squares = nx.draw_networkx_nodes( self._grid, pos= pos, node_shape="s", node_size= 1100, node_color= "#FFFFFF", linewidths= 2, ) squares.set_zorder(1) nx.draw( # Drawing the path self._grid, pos= pos, with_labels= True, labels=labels, arrows=False, node_shape="o" if self._game.is_solved else "s", node_size= 800, node_color= [ "white" if (i,j) in self._game.numbered_squares else path_color for (i,j) in self._game.squares ], edge_color= path_color, edgecolors= path_color, linewidths= 1, width= 30, edgelist= [ ((i-1, j-1), (r-1, s-1)) for ((i,j), (r,s)), value in self._game.edges.items() if value == 1 ] ) plt.show()
Program 2:The Zip’s renderer
So to solve the game and display its results, just call the methods solve() and display(), at this order.
zip_game.solve().display()Game solved successfully!

As expected, the displayed path satisfies all the rules of Zip No. 166.

Figure 4:Zip game solution. Source: LinkedIn Zip