multi-puzzle-solver 0.9.13__py3-none-any.whl → 0.9.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of multi-puzzle-solver might be problematic. Click here for more details.

@@ -6,7 +6,7 @@ from typing import Optional, Union
6
6
  from ortools.sat.python import cp_model
7
7
  import numpy as np
8
8
 
9
- from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_pos, in_bounds, Direction, get_next_pos
9
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_pos, in_bounds, Direction, get_next_pos, polyominoes_with_shape_id
10
10
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
11
11
 
12
12
 
@@ -14,79 +14,6 @@ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution,
14
14
  Shape = frozenset[Pos]
15
15
 
16
16
 
17
- def polyominoes(N):
18
- """Generate all polyominoes of size N. Every rotation and reflection is considered different and included in the result.
19
- Translation is not considered different and is removed from the result (otherwise the result would be infinite).
20
-
21
- Below is the number of unique polyominoes of size N (not including rotations and reflections) and the lenth of the returned result (which includes all rotations and reflections)
22
- N name #shapes #results
23
- 1 monomino 1 1
24
- 2 domino 1 2
25
- 3 tromino 2 6
26
- 4 tetromino 5 19
27
- 5 pentomino 12 63
28
- 6 hexomino 35 216
29
- 7 heptomino 108 760
30
- 8 octomino 369 2,725
31
- 9 nonomino 1,285 9,910
32
- 10 decomino 4,655 36,446
33
- 11 undecomino 17,073 135,268
34
- 12 dodecomino 63,600 505,861
35
- Source: https://en.wikipedia.org/wiki/Polyomino
36
-
37
- Args:
38
- N (int): The size of the polyominoes to generate.
39
-
40
- Returns:
41
- set[(frozenset[Pos], int)]: A set of all polyominoes of size N (rotated and reflected up to D4 symmetry) along with a unique ID for each polyomino.
42
- """
43
- assert N >= 1, 'N cannot be less than 1'
44
- # need a frozenset because regular sets are not hashable
45
- shapes: set[Shape] = {frozenset({Pos(0, 0)})}
46
- for i in range(1, N):
47
- next_shapes: set[Shape] = set()
48
- for s in shapes:
49
- # frontier: all 4-neighbors of existing cells not already in the shape
50
- frontier = {get_next_pos(pos, direction)
51
- for pos in s
52
- for direction in Direction
53
- if get_next_pos(pos, direction) not in s}
54
- for cell in frontier:
55
- t = s | {cell}
56
- # normalize by translation only: shift so min x,y is (0,0). This removes translational symmetries.
57
- minx = min(pos.x for pos in t)
58
- miny = min(pos.y for pos in t)
59
- t0 = frozenset(Pos(x=pos.x - minx, y=pos.y - miny) for pos in t)
60
- next_shapes.add(t0)
61
- shapes = next_shapes
62
- # shapes is now complete, now classify up to D4 symmetry (rotations/reflections), translations ignored
63
- mats = (
64
- ( 1, 0, 0, 1), # regular
65
- (-1, 0, 0, 1), # reflect about x
66
- ( 1, 0, 0,-1), # reflect about y
67
- (-1, 0, 0,-1), # reflect about x and y
68
- # trnaspose then all 4 above
69
- ( 0, 1, 1, 0), ( 0, 1, -1, 0), ( 0,-1, 1, 0), ( 0,-1, -1, 0),
70
- )
71
- # compute canonical representative for each shape (lexicographically smallest normalized transform)
72
- shape_to_canon: dict[Shape, tuple[Pos, ...]] = {}
73
- for s in shapes:
74
- reps: list[tuple[Pos, ...]] = []
75
- for a, b, c, d in mats:
76
- pts = {Pos(x=a*p.x + b*p.y, y=c*p.x + d*p.y) for p in s}
77
- minx = min(p.x for p in pts)
78
- miny = min(p.y for p in pts)
79
- rep = tuple(sorted(Pos(x=p.x - minx, y=p.y - miny) for p in pts))
80
- reps.append(rep)
81
- canon = min(reps)
82
- shape_to_canon[s] = canon
83
-
84
- canon_set = set(shape_to_canon.values())
85
- canon_to_id = {canon: i for i, canon in enumerate(sorted(canon_set))}
86
- result = {(s, canon_to_id[shape_to_canon[s]]) for s in shapes}
87
- return result
88
-
89
-
90
17
  @dataclass(frozen=True)
91
18
  class SingleSolution:
92
19
  assignment: dict[Pos, Union[str, int]]
@@ -117,7 +44,7 @@ class Board:
117
44
  assert all((str(c.item()).isdecimal() for c in np.nditer(board))), 'board must contain only digits'
118
45
  self.board = board
119
46
  self.polyomino_degrees = polyomino_degrees
120
- self.polyominoes = polyominoes(self.polyomino_degrees)
47
+ self.polyominoes = polyominoes_with_shape_id(self.polyomino_degrees)
121
48
 
122
49
  self.block_numbers = set([int(c.item()) for c in np.nditer(board)])
123
50
  self.blocks = {i: set() for i in self.block_numbers}
@@ -233,23 +160,3 @@ class Board:
233
160
  print('[\n' + '\n'.join([' ' + str(res[row].tolist()) + ',' for row in range(self.V)]) + '\n]')
234
161
  pass
235
162
  return generic_solve_all(self, board_to_solution, callback=callback if verbose_callback else None, verbose=verbose, max_solutions=max_solutions)
236
-
237
- def solve_then_constrain(self, verbose: bool = True):
238
- tic = time.time()
239
- all_solutions = []
240
- while True:
241
- solutions = self.solve_and_print(verbose=False, verbose_callback=verbose, max_solutions=1)
242
- if len(solutions) == 0:
243
- break
244
- all_solutions.extend(solutions)
245
- assignment = solutions[0].assignment
246
- # constrain the board to not return the same solution again
247
- lits = [self.model_vars[p].Not() if assignment[p] == 1 else self.model_vars[p] for p in assignment.keys()]
248
- self.model.AddBoolOr(lits)
249
- self.model.ClearHints()
250
- for k, v in solutions[0].all_other_variables['fc'].items():
251
- self.model.AddHint(self.fc[k], v)
252
- print(f'Solutions found: {len(all_solutions)}')
253
- toc = time.time()
254
- print(f'Time taken: {toc - tic:.2f} seconds')
255
- return all_solutions
@@ -5,7 +5,7 @@ from ortools.sat.python import cp_model
5
5
  from ortools.sat.python.cp_model import LinearExpr as lxp
6
6
 
7
7
  from puzzle_solver.core.utils import Pos, get_all_pos, set_char, in_bounds, Direction, get_next_pos, get_char, get_opposite_direction
8
- from puzzle_solver.core.utils_ortools import and_constraint, or_constraint, generic_solve_all, SingleSolution
8
+ from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, SingleSolution, force_connected_component
9
9
 
10
10
 
11
11
  class Board:
@@ -18,7 +18,6 @@ class Board:
18
18
  self.model = cp_model.CpModel()
19
19
  self.cell_active: dict[Pos, cp_model.IntVar] = {}
20
20
  self.cell_direction: dict[tuple[Pos, Direction], cp_model.IntVar] = {}
21
- self.reach_layers: list[dict[Pos, cp_model.IntVar]] = [] # R_t[p] booleans, t = 0..T
22
21
 
23
22
  self.create_vars()
24
23
  self.add_all_constraints()
@@ -28,18 +27,11 @@ class Board:
28
27
  self.cell_active[pos] = self.model.NewBoolVar(f"a[{pos}]")
29
28
  for direction in Direction:
30
29
  self.cell_direction[(pos, direction)] = self.model.NewBoolVar(f"b[{pos}]->({direction.name})")
31
- # Percolation layers R_t (monotone flood fill)
32
- T = self.V * self.H # large enough to cover whole board
33
- for t in range(T + 1):
34
- Rt: dict[Pos, cp_model.IntVar] = {}
35
- for pos in get_all_pos(self.V, self.H):
36
- Rt[pos] = self.model.NewBoolVar(f"R[{t}][{pos}]")
37
- self.reach_layers.append(Rt)
38
30
 
39
31
  def add_all_constraints(self):
40
32
  self.force_direction_constraints()
41
33
  self.force_wb_constraints()
42
- self.connectivity_percolation()
34
+ self.force_connected_component()
43
35
 
44
36
  def force_wb_constraints(self):
45
37
  for pos in get_all_pos(self.V, self.H):
@@ -91,40 +83,16 @@ class Board:
91
83
  else:
92
84
  self.model.Add(self.cell_direction[(pos, direction)] == 0)
93
85
 
94
- def connectivity_percolation(self):
95
- """
96
- Layered percolation:
97
- - root is exactly the first cell
98
- - R_t is monotone nondecreasing in t (R_t+1 >= R_t)
99
- - A cell can 'turn on' at layer t+1 iff has a neighbor on at layer t and the neighbor is pointing to it (or is root)
100
- - Final layer is all connected
101
- """
102
- # Seed: R0 = root
103
- for i, pos in enumerate(get_all_pos(self.V, self.H)):
104
- if i == 0:
105
- self.model.Add(self.reach_layers[0][pos] == 1) # first cell is root
106
- else:
107
- self.model.Add(self.reach_layers[0][pos] == 0)
108
-
109
- for t in range(1, len(self.reach_layers)):
110
- Rt_prev = self.reach_layers[t - 1]
111
- Rt = self.reach_layers[t]
112
- for p in get_all_pos(self.V, self.H):
113
- # Rt[p] = Rt_prev[p] | (white[p] & Rt_prev[neighbour #1]) | (white[p] & Rt_prev[neighbour #2]) | ...
114
- # Create helper (white[p] & Rt_prev[neighbour #X]) for each neighbor q
115
- neigh_helpers: list[cp_model.IntVar] = []
116
- for direction in Direction:
117
- q = get_next_pos(p, direction)
118
- if not in_bounds(q, self.V, self.H):
119
- continue
120
- a = self.model.NewBoolVar(f"A[{t}][{p}]<-({q})")
121
- and_constraint(self.model, target=a, cs=[Rt_prev[q], self.cell_direction[(q, get_opposite_direction(direction))]])
122
- neigh_helpers.append(a)
123
- or_constraint(self.model, target=Rt[p], cs=[Rt_prev[p]] + neigh_helpers)
124
-
125
- # every pearl must be reached by the final layer
126
- for p in get_all_pos(self.V, self.H):
127
- self.model.Add(self.reach_layers[-1][p] == 1).OnlyEnforceIf(self.cell_active[p])
86
+ def force_connected_component(self):
87
+ def is_neighbor(pd1: tuple[Pos, Direction], pd2: tuple[Pos, Direction]) -> bool:
88
+ p1, d1 = pd1
89
+ p2, d2 = pd2
90
+ if p1 == p2 and d1 != d2: # same position, different direction, is neighbor
91
+ return True
92
+ if get_next_pos(p1, d1) == p2 and d2 == get_opposite_direction(d1):
93
+ return True
94
+ return False
95
+ force_connected_component(self.model, self.cell_direction, is_neighbor=is_neighbor)
128
96
 
129
97
 
130
98
  def solve_and_print(self, verbose: bool = True):
@@ -2,7 +2,7 @@ import numpy as np
2
2
  from ortools.sat.python import cp_model
3
3
 
4
4
  from puzzle_solver.core.utils import Pos, get_all_pos, set_char, get_neighbors4, in_bounds, Direction, get_next_pos, get_char
5
- from puzzle_solver.core.utils_ortools import and_constraint, or_constraint, generic_solve_all, SingleSolution
5
+ from puzzle_solver.core.utils_ortools import and_constraint, or_constraint, generic_solve_all, SingleSolution, force_connected_component
6
6
 
7
7
 
8
8
  def get_ray(pos: Pos, V: int, H: int, direction: Direction) -> list[Pos]:
@@ -27,9 +27,6 @@ class Board:
27
27
  # Core vars
28
28
  self.b: dict[Pos, cp_model.IntVar] = {} # 1=black, 0=white
29
29
  self.w: dict[Pos, cp_model.IntVar] = {} # 1=white, 0=black
30
- # Connectivity helpers
31
- self.root: dict[Pos, cp_model.IntVar] = {} # exactly one root; root <= w
32
- self.reach_layers: list[dict[Pos, cp_model.IntVar]] = [] # R_t[p] booleans, t = 0..T
33
30
 
34
31
  self.create_vars()
35
32
  self.add_all_constraints()
@@ -41,18 +38,6 @@ class Board:
41
38
  self.w[pos] = self.model.NewBoolVar(f"w[{pos}]")
42
39
  self.model.AddExactlyOne([self.b[pos], self.w[pos]])
43
40
 
44
- # Root
45
- for pos in get_all_pos(self.V, self.H):
46
- self.root[pos] = self.model.NewBoolVar(f"root[{pos}]")
47
-
48
- # Percolation layers R_t (monotone flood fill)
49
- T = self.V * self.H # large enough to cover whole board
50
- for t in range(T + 1):
51
- Rt: dict[Pos, cp_model.IntVar] = {}
52
- for pos in get_all_pos(self.V, self.H):
53
- Rt[pos] = self.model.NewBoolVar(f"R[{t}][{pos}]")
54
- self.reach_layers.append(Rt)
55
-
56
41
  def add_all_constraints(self):
57
42
  self.no_adjacent_blacks()
58
43
  self.white_connectivity_percolation()
@@ -69,41 +54,7 @@ class Board:
69
54
 
70
55
 
71
56
  def white_connectivity_percolation(self):
72
- """
73
- Layered percolation:
74
- - root is exactly the first white cell
75
- - R_t is monotone nondecreasing in t (R_t+1 >= R_t)
76
- - A cell can 'turn on' at layer t+1 iff it's white and has a neighbor on at layer t (or is root)
77
- - Final layer is equal to the white mask: R_T[p] == w[p] => all whites are connected to the unique root
78
- """
79
- # to find unique solutions easily, we make only 1 possible root allowed; root is exactly the first white cell
80
- prev_cells_black: list[cp_model.IntVar] = []
81
- for pos in get_all_pos(self.V, self.H):
82
- and_constraint(self.model, target=self.root[pos], cs=[self.w[pos]] + prev_cells_black)
83
- prev_cells_black.append(self.b[pos])
84
-
85
- # Seed: R0 = root
86
- for pos in get_all_pos(self.V, self.H):
87
- self.model.Add(self.reach_layers[0][pos] == self.root[pos])
88
-
89
- T = len(self.reach_layers)
90
- for t in range(1, T):
91
- Rt_prev = self.reach_layers[t - 1]
92
- Rt = self.reach_layers[t]
93
- for p in get_all_pos(self.V, self.H):
94
- # Rt[p] = Rt_prev[p] | (white[p] & Rt_prev[neighbour #1]) | (white[p] & Rt_prev[neighbour #2]) | ...
95
- # Create helper (white[p] & Rt_prev[neighbour #X]) for each neighbor q
96
- neigh_helpers: list[cp_model.IntVar] = []
97
- for q in get_neighbors4(p, self.V, self.H):
98
- a = self.model.NewBoolVar(f"A[{t}][{p}]<-({q})")
99
- and_constraint(self.model, target=a, cs=[self.w[p], Rt_prev[q]])
100
- neigh_helpers.append(a)
101
- or_constraint(self.model, target=Rt[p], cs=[Rt_prev[p]] + neigh_helpers)
102
-
103
- # All whites must be reached by the final layer
104
- RT = self.reach_layers[T - 1]
105
- for p in get_all_pos(self.V, self.H):
106
- self.model.Add(RT[p] == self.w[p])
57
+ force_connected_component(self.model, self.w)
107
58
 
108
59
  def range_clues(self):
109
60
  # For each numbered cell c with value k:
@@ -2,7 +2,7 @@ import numpy as np
2
2
  from ortools.sat.python import cp_model
3
3
 
4
4
  from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_neighbors4, get_all_pos_to_idx_dict, get_row_pos, get_col_pos
5
- from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, and_constraint, or_constraint
5
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
6
6
 
7
7
 
8
8
  class Board:
@@ -17,34 +17,26 @@ class Board:
17
17
 
18
18
  self.model = cp_model.CpModel()
19
19
  self.B = {} # black squares
20
+ self.W = {} # white squares
20
21
  self.Num = {} # value of squares (Num = N + idx if black, else board[pos])
21
- # Connectivity helpers
22
- self.root: dict[Pos, cp_model.IntVar] = {} # exactly one root; root <= w
23
- self.reach_layers: list[dict[Pos, cp_model.IntVar]] = [] # R_t[p] booleans, t = 0..T
24
22
 
25
23
  self.create_vars()
26
24
  self.add_all_constraints()
27
25
 
28
26
  def create_vars(self):
29
27
  for pos in get_all_pos(self.V, self.H):
30
- self.B[pos] = self.model.NewBoolVar(f'{pos}')
28
+ self.B[pos] = self.model.NewBoolVar(f'B:{pos}')
29
+ self.W[pos] = self.model.NewBoolVar(f'W:{pos}')
30
+ # either black or white
31
+ self.model.AddExactlyOne([self.B[pos], self.W[pos]])
31
32
  self.Num[pos] = self.model.NewIntVar(0, 2*self.N, f'{pos}')
32
33
  self.model.Add(self.Num[pos] == self.N + self.idx_of[pos]).OnlyEnforceIf(self.B[pos])
33
34
  self.model.Add(self.Num[pos] == int(get_char(self.board, pos))).OnlyEnforceIf(self.B[pos].Not())
34
- # Root
35
- for pos in get_all_pos(self.V, self.H):
36
- self.root[pos] = self.model.NewBoolVar(f"root[{pos}]")
37
- # Percolation layers R_t (monotone flood fill)
38
- for t in range(self.N + 1):
39
- Rt: dict[Pos, cp_model.IntVar] = {}
40
- for pos in get_all_pos(self.V, self.H):
41
- Rt[pos] = self.model.NewBoolVar(f"R[{t}][{pos}]")
42
- self.reach_layers.append(Rt)
43
35
 
44
36
  def add_all_constraints(self):
45
37
  self.no_adjacent_blacks()
46
38
  self.no_number_appears_twice()
47
- self.white_connectivity_percolation()
39
+ self.force_connected_component()
48
40
 
49
41
  def no_adjacent_blacks(self):
50
42
  # no two black squares are adjacent
@@ -61,42 +53,9 @@ class Board:
61
53
  var_list = [self.Num[pos] for pos in get_col_pos(col, self.V)]
62
54
  self.model.AddAllDifferent(var_list)
63
55
 
64
- def white_connectivity_percolation(self):
65
- """
66
- Layered percolation:
67
- - root is exactly the first white cell
68
- - R_t is monotone nondecreasing in t (R_t+1 >= R_t)
69
- - A cell can 'turn on' at layer t+1 iff it's white and has a neighbor on at layer t (or is root)
70
- - Final layer is equal to the white mask: R_T[p] == w[p] => all whites are connected to the unique root
71
- """
72
- # to find unique solutions easily, we make only 1 possible root allowed; root is exactly the first white cell
73
- prev_cells_black: list[cp_model.IntVar] = []
74
- for pos in get_all_pos(self.V, self.H):
75
- and_constraint(self.model, target=self.root[pos], cs=[self.B[pos].Not()] + prev_cells_black)
76
- prev_cells_black.append(self.B[pos])
77
-
78
- # Seed: R0 = root
79
- for pos in get_all_pos(self.V, self.H):
80
- self.model.Add(self.reach_layers[0][pos] == self.root[pos])
81
-
82
- T = len(self.reach_layers)
83
- for t in range(1, T):
84
- Rt_prev = self.reach_layers[t - 1]
85
- Rt = self.reach_layers[t]
86
- for p in get_all_pos(self.V, self.H):
87
- # Rt[p] = Rt_prev[p] | (white[p] & Rt_prev[neighbour #1]) | (white[p] & Rt_prev[neighbour #2]) | ...
88
- # Create helper (white[p] & Rt_prev[neighbour #X]) for each neighbor q
89
- neigh_helpers: list[cp_model.IntVar] = []
90
- for q in get_neighbors4(p, self.V, self.H):
91
- a = self.model.NewBoolVar(f"A[{t}][{p}]<-({q})")
92
- and_constraint(self.model, target=a, cs=[self.B[p].Not(), Rt_prev[q]])
93
- neigh_helpers.append(a)
94
- or_constraint(self.model, target=Rt[p], cs=[Rt_prev[p]] + neigh_helpers)
56
+ def force_connected_component(self):
57
+ force_connected_component(self.model, self.W)
95
58
 
96
- # All whites must be reached by the final layer
97
- RT = self.reach_layers[T - 1]
98
- for p in get_all_pos(self.V, self.H):
99
- self.model.Add(RT[p] == self.B[p].Not())
100
59
 
101
60
 
102
61
  def solve_and_print(self, verbose: bool = True):
@@ -3,7 +3,7 @@ import numpy as np
3
3
  from ortools.sat.python import cp_model
4
4
 
5
5
  from puzzle_solver.core.utils import Pos, get_all_pos, set_char, get_char, Direction, in_bounds, get_next_pos, get_row_pos, get_col_pos, get_opposite_direction
6
- from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, and_constraint, or_constraint
6
+ from puzzle_solver.core.utils_ortools import force_connected_component, generic_solve_all, SingleSolution
7
7
 
8
8
 
9
9
  class Board:
@@ -25,7 +25,6 @@ class Board:
25
25
  self.model = cp_model.CpModel()
26
26
  self.cell_active: dict[Pos, cp_model.IntVar] = {}
27
27
  self.cell_direction: dict[tuple[Pos, Direction], cp_model.IntVar] = {}
28
- self.reach_layers: list[dict[Pos, cp_model.IntVar]] = [] # R_t[p] booleans, t = 0..T
29
28
 
30
29
  self.create_vars()
31
30
  self.add_all_constraints()
@@ -35,19 +34,13 @@ class Board:
35
34
  self.cell_active[pos] = self.model.NewBoolVar(f'{pos}')
36
35
  for direction in Direction:
37
36
  self.cell_direction[(pos, direction)] = self.model.NewBoolVar(f'{pos}:{direction}')
38
- # Percolation layers R_t (monotone flood fill)
39
- for t in range(self.V * self.H + 1):
40
- Rt: dict[Pos, cp_model.IntVar] = {}
41
- for pos in get_all_pos(self.V, self.H):
42
- Rt[pos] = self.model.NewBoolVar(f"R[{t}][{pos}]")
43
- self.reach_layers.append(Rt)
44
37
 
45
38
  def add_all_constraints(self):
46
39
  self.force_hints()
47
40
  self.force_sides()
48
41
  self.force_0_or_2_active()
49
42
  self.force_direction_constraints()
50
- self.force_percolation()
43
+ self.force_connected_component()
51
44
 
52
45
 
53
46
  def force_hints(self):
@@ -108,38 +101,16 @@ class Board:
108
101
  for pos in get_row_pos(0, self.H):
109
102
  self.model.Add(self.cell_direction[(pos, Direction.UP)] == 0)
110
103
 
111
- def force_percolation(self):
112
- """
113
- Layered percolation:
114
- - root is exactly the first cell in the first column
115
- - R_t is monotone nondecreasing in t (R_t+1 >= R_t)
116
- - A cell can 'turn on' at layer t+1 iff it's active and has a neighbor on AND pointing to it at layer t
117
- - Final layer is equal to the active mask: R_T[p] == active[p] => all active cells are connected to the unique root
118
- """
119
- # only the start position is a root
120
- self.model.Add(self.reach_layers[0][self.first_col_start_pos] == 1)
121
- for pos in get_all_pos(self.V, self.H):
122
- if pos != self.first_col_start_pos:
123
- self.model.Add(self.reach_layers[0][pos] == 0)
124
-
125
- for t in range(1, len(self.reach_layers)):
126
- Rt_prev = self.reach_layers[t - 1]
127
- Rt = self.reach_layers[t]
128
- for p in get_all_pos(self.V, self.H):
129
- # Rt[p] = Rt_prev[p] | (active[p] & Rt_prev[neighbour #1]) | (active[p] & Rt_prev[neighbour #2]) | ...
130
- # Create helper (active[p] & Rt_prev[neighbour #X]) for each neighbor q
131
- neigh_helpers: list[cp_model.IntVar] = []
132
- for direction in Direction:
133
- q = get_next_pos(p, direction)
134
- if not in_bounds(q, self.V, self.H):
135
- continue
136
- a = self.model.NewBoolVar(f"A[{t}][{p}]<-({q})")
137
- and_constraint(self.model, target=a, cs=[self.cell_active[p], Rt_prev[q], self.cell_direction[(q, get_opposite_direction(direction))]])
138
- neigh_helpers.append(a)
139
- or_constraint(self.model, target=Rt[p], cs=[Rt_prev[p]] + neigh_helpers)
140
- # every avtive track must be reachible -> single connected component
141
- for pos in get_all_pos(self.V, self.H):
142
- self.model.Add(self.reach_layers[-1][pos] == 1).OnlyEnforceIf(self.cell_active[pos])
104
+ def force_connected_component(self):
105
+ def is_neighbor(pd1: tuple[Pos, Direction], pd2: tuple[Pos, Direction]) -> bool:
106
+ p1, d1 = pd1
107
+ p2, d2 = pd2
108
+ if p1 == p2: # same position, different direction, is neighbor
109
+ return True
110
+ if get_next_pos(p1, d1) == p2 and d2 == get_opposite_direction(d1):
111
+ return True
112
+ return False
113
+ force_connected_component(self.model, self.cell_direction, is_neighbor=is_neighbor)
143
114
 
144
115
 
145
116