multi-puzzle-solver 0.9.13__py3-none-any.whl → 0.9.15__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.
- {multi_puzzle_solver-0.9.13.dist-info → multi_puzzle_solver-0.9.15.dist-info}/METADATA +120 -9
- {multi_puzzle_solver-0.9.13.dist-info → multi_puzzle_solver-0.9.15.dist-info}/RECORD +18 -15
- puzzle_solver/__init__.py +3 -1
- puzzle_solver/core/utils.py +228 -127
- puzzle_solver/core/utils_ortools.py +237 -172
- puzzle_solver/puzzles/battleships/battleships.py +1 -0
- puzzle_solver/puzzles/black_box/black_box.py +313 -0
- puzzle_solver/puzzles/filling/filling.py +117 -192
- puzzle_solver/puzzles/galaxies/galaxies.py +110 -0
- puzzle_solver/puzzles/galaxies/parse_map/parse_map.py +216 -0
- puzzle_solver/puzzles/inertia/tsp.py +4 -1
- puzzle_solver/puzzles/lits/lits.py +2 -95
- puzzle_solver/puzzles/pearl/pearl.py +12 -44
- puzzle_solver/puzzles/range/range.py +2 -51
- puzzle_solver/puzzles/singles/singles.py +9 -50
- puzzle_solver/puzzles/tracks/tracks.py +12 -41
- {multi_puzzle_solver-0.9.13.dist-info → multi_puzzle_solver-0.9.15.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.9.13.dist-info → multi_puzzle_solver-0.9.15.dist-info}/top_level.txt +0 -0
|
@@ -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,
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
self.model
|
|
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
|
|
|
File without changes
|
|
File without changes
|