2

I tried to make solver for flow game using google-OR tools.

enter image description here

I made a few rules for the corner to only contains corner pipes, but other than that, i can not figure out how to make the pipe connected to each other nor how to tell the model to make a pipe that is connecting to each other.

A few snippet

pipe_types = {
0: " ",
1: "-",
2: "|",
3: "┗" ,
4: "┛" ,
5: "┓",
6: "┏",
7: "●" 
}
model = cp_model.CpModel()
filled_map = [[0,0,0,0],
             [0,0,7,0],
             [0,0,0,0],
             [0,7,0,0]]

mesh_size = int(np.sqrt(len(np.array(filled_map).flatten())))

target_map = [[model.NewIntVar(1, 6, 'column: %i' % i) for i in range(mesh_size)] for j in range(mesh_size)]

flow_map = init_map(model, target_map, filled_map)

for i in range(len(flow_map)):
    for j in range(len(flow_map[0])):
        
        # check if top or bottom side
        if (i == 0) or (i == len(flow_map)-1):
            model.Add(flow_map[i][j] != 2)
        
        # check if left or right side
        if (j == 0) or (j == len(flow_map[0])-1):
            model.Add(flow_map[i][j] != 1)
        
        # left up corner
        if (i == 0) & (j == 0):
            model.Add(flow_map[i][j] != 3)
            model.Add(flow_map[i][j] != 4)
            model.Add(flow_map[i][j] != 5)
        
        
        # right up corner
        if (i == 0) & (j == len(flow_map[0])-1):
            model.Add(flow_map[i][j] != 3)
            model.Add(flow_map[i][j] != 4)
            model.Add(flow_map[i][j] != 6)
        
        
        # left bottom corner
        if (i == len(flow_map)-1) & (j == 0):
            model.Add(flow_map[i][j] != 4)
            model.Add(flow_map[i][j] != 5)
            model.Add(flow_map[i][j] != 6)
        
        
        # right bottom corner
        if (i == len(flow_map)-1) & (j == len(flow_map[0])-1):
            model.Add(flow_map[i][j] != 3)
            model.Add(flow_map[i][j] != 5)
            model.Add(flow_map[i][j] != 6)
# Solving
status = solver.Solve(model)

res = []
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
    for i in range(len(flow_map)):
        for j in range(len(flow_map[0])):
            res.append(solver.Value(flow_map[i][j]))
            print(solver.Value(flow_map[i][j]), end=" ")
        print()

This would results horizontal pipes on the center of the mesh. Later on, i would have to figure out how to add color and such on this too.

Is there any pointer on how to make this on OR tools?

Edit 1:

Based on David Eisenstat's answer, I can find solution. Visualizing this solution based on JohanC's answer, I get this result.

enter image description here

Can I get the pathing made from google-OR tools?

Edit 2:

Using hamilton path from "Hamiltonian" path using Python I could generate somewhat correct pathing.

enter image description here

But it feels so weird since OR tools already calculate the pathing, and I have to recalculate the path. The path generated from "Hamiltonian" path using Python doesn't show all possible combinations. If I can take the path from OR tools, I think that would be my best interest.

Darwin Harianto
  • 435
  • 4
  • 15
  • Note that my coloring code assumes there aren't any unnecessary folds in the paths (the solver only generates that type of paths, and allows for empty cells). If extra folds are necessary not only a color per cell, but also the connections needs to be represented by the data structure. – JohanC May 08 '21 at 13:36

2 Answers2

2

As I don't have experience with OR-tools, here is an approach with Z3.

  • The initial board is represented by numbers for the end points, one number for each color. The idea is a bit similar to how Sudoku is represented.
  • Each other cell on the board will get either a value for zero, or a number. This number should be equal to exactly two of its neighbors.
  • The initial endpoints should have exactly one neighbor with its color.
from z3 import Solver, Sum, Int, If, And, Or, sat

def plot_solution(S):
    import matplotlib.pyplot as plt

    ax = plt.gca()
    colors = plt.cm.tab10.colors
    for i in range(M):
        for j in range(N):
            if board[i][j] != 0:
                ax.scatter(j, i, s=500, color=colors[board[i][j]])
            if S[i][j] != 0:
                for k in range(M):
                    for l in range(N):
                        if abs(k - i) + abs(l - j) == 1 and S[i][j] == S[k][l]:
                            ax.plot([j, l], [i, k], color=colors[S[i][j]], lw=15)
    ax.set_ylim(M - 0.5, -0.5)
    ax.set_xlim(-0.5, N - 0.5)
    ax.set_aspect('equal')
    ax.set_facecolor('black')
    ax.set_yticks([i + 0.5 for i in range(M - 1)], minor=True)
    ax.set_xticks([j + 0.5 for j in range(N - 1)], minor=True)
    ax.grid(b=True, which='minor', color='white')
    ax.set_xticks([])
    ax.set_yticks([])
    ax.tick_params(axis='both', which='both', length=0)
    plt.show()

board = [[1, 0, 0, 2, 3],
         [0, 0, 0, 4, 0],
         [0, 0, 4, 0, 0],
         [0, 2, 3, 0, 5],
         [0, 1, 5, 0, 0]]
M = len(board)
N = len(board[0])
B = [[Int(f'B_{i}_{j}') for j in range(N)] for i in range(M)]
s = Solver()
s.add(([If(board[i][j] != 0, B[i][j] == board[i][j], And(B[i][j] >= 0, B[i][j] < 10))
        for j in range(N) for i in range(M)]))
for i in range(M):
    for j in range(N):
        same_neighs_ij = Sum([If(B[i][j] == B[k][l], 1, 0)
                              for k in range(M) for l in range(N) if abs(k - i) + abs(l - j) == 1])
        if board[i][j] != 0:
            s.add(same_neighs_ij == 1)
        else:
            s.add(Or(same_neighs_ij == 2, B[i][j] == 0))

if s.check() == sat:
    m = s.model()
    S = [[m[B[i][j]].as_long() for j in range(N)] for i in range(M)]
    print(S)
    plot_solution(S)

Solution:

[[1, 2, 2, 2, 3],
 [1, 2, 4, 4, 3],
 [1, 2, 4, 3, 3],
 [1, 2, 3, 3, 5],
 [1, 1, 5, 5, 5]]

As mentioned in the comments, a possible requirement is that all cells would need to be colored. This would need a more complicated approach. Here is an example of such a configuration for which the above code could create a solution that connects all end points without touching all cells:

board = [[0, 1, 2, 0, 0, 0, 0],
         [1, 3, 4, 0, 3, 5, 0],
         [0, 0, 0, 0, 0, 0, 0],
         [0, 2, 0, 4, 0, 0, 0],
         [0, 0, 0, 0, 0, 0, 0],
         [0, 5, 0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0, 0, 0]]

solution to flow game

JohanC
  • 71,591
  • 8
  • 33
  • 66
  • If the lower endpoints of the orange and green were one square higher, then as I understand the problem, there would still be a unique solution, but this formulation would be infeasible. – David Eisenstat May 06 '21 at 19:15
  • In that case, this code would create a solution with the two lower left cells empty. If I understand correctly, you'd like the orange curve to make a U-turn to fill the complete board? Do you have a link to a more thorough description of the rules? Is it known when a configuration has a unique solution? I was also pondering about a variant that would allow e.g. 4 endpoints of the same color, letting the solver decide which pairs should be connected. – JohanC May 06 '21 at 19:41
  • Here's the game: https://www.bigduckgames.com/flowfree – David Eisenstat May 06 '21 at 19:49
  • OK. It seems these and other versions have the requirement that all cells get filled. There is also a post in [cs.stackexchange.com](https://cs.stackexchange.com/questions/86674/algorithms-for-solving-flow-game) with a general description of a more complicated approach. – JohanC May 06 '21 at 22:04
1

The best way is probably with AddCircuit. This constraint takes a directed graph where each arc is labeled with a literal and requires that the arcs labeled true form a subgraph where each node has in- and out-degree 1, and further that there is at most one cycle that is not a self-loop. By forcing an arc from the end to the beginning, we can use this constraint type to require that there is a single path from the beginning to the end.

The documentation is somewhat poor, so here's a working code sample. I'll leave the drawing part to you.

import collections
from ortools.sat.python import cp_model


def validate_board_and_count_colors(board):
    assert isinstance(board, list)
    assert all(isinstance(row, list) for row in board)
    assert len(set(map(len, board))) == 1
    colors = collections.Counter(square for row in board for square in row)
    del colors[0]
    assert all(count == 2 for count in colors.values())
    num_colors = len(colors)
    assert set(colors.keys()) == set(range(1, num_colors + 1))
    return num_colors


def main(board):
    num_colors = validate_board_and_count_colors(board)
    model = cp_model.CpModel()
    solution = [
        [square or model.NewIntVar(1, num_colors, "") for (j, square) in enumerate(row)]
        for (i, row) in enumerate(board)
    ]
    true = model.NewBoolVar("")
    model.AddBoolOr([true])
    for color in range(1, num_colors + 1):
        endpoints = []
        arcs = []
        for i, row in enumerate(board):
            for j, square in enumerate(row):
                if square == color:
                    endpoints.append((i, j))
                else:
                    arcs.append(((i, j), (i, j)))
                if i < len(board) - 1:
                    arcs.append(((i, j), (i + 1, j)))
                if j < len(row) - 1:
                    arcs.append(((i, j), (i, j + 1)))
        (i1, j1), (i2, j2) = endpoints
        k1 = i1 * len(row) + j1
        k2 = i2 * len(row) + j2
        arc_variables = [(k2, k1, true)]
        for (i1, j1), (i2, j2) in arcs:
            k1 = i1 * len(row) + j1
            k2 = i2 * len(row) + j2
            edge = model.NewBoolVar("")
            if k1 == k2:
                model.Add(solution[i1][j1] != color).OnlyEnforceIf(edge)
                arc_variables.append((k1, k1, edge))
            else:
                model.Add(solution[i1][j1] == color).OnlyEnforceIf(edge)
                model.Add(solution[i2][j2] == color).OnlyEnforceIf(edge)
                forward = model.NewBoolVar("")
                backward = model.NewBoolVar("")
                model.AddBoolOr([edge, forward.Not()])
                model.AddBoolOr([edge, backward.Not()])
                model.AddBoolOr([edge.Not(), forward, backward])
                model.AddBoolOr([forward.Not(), backward.Not()])
                arc_variables.append((k1, k2, forward))
                arc_variables.append((k2, k1, backward))
        model.AddCircuit(arc_variables)
    solver = cp_model.CpSolver()
    status = solver.Solve(model)
    if status == cp_model.OPTIMAL:
        for row in solution:
            print("".join(str(solver.Value(x)) for x in row))


if __name__ == "__main__":
    main(
        [
            [1, 0, 0, 2, 3],
            [0, 0, 0, 4, 0],
            [0, 0, 4, 0, 0],
            [0, 2, 3, 0, 5],
            [0, 1, 5, 0, 0],
        ]
    )
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • Thank you, i thought I have to make multiple multidimensional array to represent colors and direction. So i can represent the problem as boolean over multidimensional array. Can you give me a sample on how to print for every possible solution? – Darwin Harianto May 07 '21 at 00:52
  • @DarwinHarianto `SearchForAllSolutions` with a callback. See https://developers.google.com/optimization/cp/queens – David Eisenstat May 07 '21 at 00:56
  • Thanks. sorry, 1 more question. Is there any way to put a "non passable" mesh on this configuration? – Darwin Harianto May 07 '21 at 00:58
  • 1
    @DarwinHarianto certainly, the formulation doesn't depend on the adjacency structure. The easiest way is probably to set the appropriate entries of `solution` to `-1` (or some other unused integer). – David Eisenstat May 07 '21 at 01:20
  • For the bridges on flow game, how could i model it on addcircuit? And can I extract the forward backward information from solver value? – Darwin Harianto May 07 '21 at 03:32
  • @DarwinHarianto 1. I don't remember how bridges work, but I think you can make an "over" vertex and an "under" vertex with separate links and colors to their four neighbors. 2. Yes, save the `edge` variables. – David Eisenstat May 07 '21 at 12:12
  • I tried to save the edge variables. Printing those result gives 1 and 0 on meshes link. Can you tell me what edge means? I dont understand what edge means on points like (4, 4), (4, 4) 1 ([i1, j1], [i2, j2], bool_value). Other than that, it seems like it indicates the flow – Darwin Harianto May 09 '21 at 11:15
  • @DarwinHarianto just ignore those variables. They're there to make `AddCircuit` work properly. – David Eisenstat May 09 '21 at 13:24
  • Is there any link for the explanation? I thought I have to read that to know which I mesh is connected. I would like to know how to read and make those variables. Maybe that would help on making the bridges part. – Darwin Harianto May 09 '21 at 14:21
  • @DarwinHarianto I linked the documentation in the answer. `AddCircuit` enforces that the set of arcs chosen gives each vertex one incoming arc and one outgoing arc, so the chosen arcs form a disjoint union of cycles, and there must be at most one cycle of length greater than one. To constrain a path `a` to `b`, we need a dummy "back arc" from `b` to `a` to make the path into a cycle, and we need the option of self-loops on all other vertices so that they can be omitted from the path. – David Eisenstat May 09 '21 at 14:47
  • It's kinda hard for me to follow the logic. I think there is something I haven't understand already. I tried to comment out model.AddBoolOr([true]) since it doesn't seem to do anything, but when I tried to print out multiple solutions, it helped on the constraints. I could not understand why. Can you explain a bit about forward backward and edge? I could not understand what are those variables doing – Darwin Harianto May 10 '21 at 07:56
  • 1
    @DarwinHarianto `AddBoolOr([true])` forces `true` (note the lower case) to be a true literal. We use it to force the circuit to include the arc from the end to the beginning of the path, which makes the path a cycle. I explained `edge` for `k1 == k2`. When `k1 != k2`, the variable `edge` is equal to `forward or backward`. The variable `forward` is true if we use the edge in one direction; `backward` is true if we use the edge in the other direction. The boolean ORs force the truth table I described for these three variables. – David Eisenstat May 10 '21 at 13:10
  • For the boolean ORs truth table, I understand that the first 3 clauses forces edge to be the same as forward and backward. What about the last one? With and without it, I could still get the same result – Darwin Harianto May 11 '21 at 05:18
  • How can I make a minimal length constraint on path? – Darwin Harianto May 11 '21 at 07:06
  • I found the way to constraint minimal length. I just need to add the forward and backward, save it to a variable. Then add `model.add(variable>minimalLength)`. – Darwin Harianto May 11 '21 at 07:56