multi-puzzle-solver 0.9.22__py3-none-any.whl → 0.9.25__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.22.dist-info → multi_puzzle_solver-0.9.25.dist-info}/METADATA +323 -4
- {multi_puzzle_solver-0.9.22.dist-info → multi_puzzle_solver-0.9.25.dist-info}/RECORD +14 -10
- puzzle_solver/__init__.py +5 -1
- puzzle_solver/core/utils.py +157 -2
- puzzle_solver/core/utils_ortools.py +6 -10
- puzzle_solver/puzzles/binairo/binairo.py +98 -0
- puzzle_solver/puzzles/flip/flip.py +48 -0
- puzzle_solver/puzzles/lits/lits.py +2 -28
- puzzle_solver/puzzles/palisade/palisade.py +104 -0
- puzzle_solver/puzzles/rectangles/rectangles.py +130 -0
- puzzle_solver/puzzles/slitherlink/slitherlink.py +12 -131
- puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py +4 -1
- {multi_puzzle_solver-0.9.22.dist-info → multi_puzzle_solver-0.9.25.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.9.22.dist-info → multi_puzzle_solver-0.9.25.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,7 @@ import numpy as np
|
|
|
2
2
|
from collections import defaultdict
|
|
3
3
|
from ortools.sat.python import cp_model
|
|
4
4
|
|
|
5
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_pos, Direction, get_row_pos, get_col_pos, get_next_pos, in_bounds, get_opposite_direction
|
|
5
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_pos, Direction, get_row_pos, get_col_pos, get_next_pos, in_bounds, get_opposite_direction, render_grid
|
|
6
6
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
7
7
|
|
|
8
8
|
|
|
@@ -20,6 +20,10 @@ class Board:
|
|
|
20
20
|
self.cell_borders_to_corners: dict[CellBorder, set[Corner]] = defaultdict(set) # for every cell border, a set of all corners it is connected to
|
|
21
21
|
self.corners_to_cell_borders: dict[Corner, set[CellBorder]] = defaultdict(set) # opposite direction
|
|
22
22
|
|
|
23
|
+
# 2N^2 + 2N edges
|
|
24
|
+
# 4*edges (fully connected component)
|
|
25
|
+
# model variables = edges (on/off) + 4*edges (fully connected component)
|
|
26
|
+
# = 9N^2 + 9N
|
|
23
27
|
self.model = cp_model.CpModel()
|
|
24
28
|
self.model_vars: dict[CellBorder, cp_model.IntVar] = {} # one entry for every unique variable in the model
|
|
25
29
|
self.cell_borders: dict[CellBorder, cp_model.IntVar] = {} # for every position and direction, one entry for that edge (thus the same edge variables are used in opposite directions of neighboring cells)
|
|
@@ -87,10 +91,11 @@ class Board:
|
|
|
87
91
|
if not val.isdecimal():
|
|
88
92
|
continue
|
|
89
93
|
self.model.Add(sum(variables) == int(val))
|
|
94
|
+
|
|
95
|
+
corner_sum_domain = cp_model.Domain.FromValues([0, 2]) # sum of edges touching a corner is 0 or 2
|
|
90
96
|
for corner in self.corner_vars: # a corder always has 0 or 2 active edges
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
self.model.Add(sum(self.corner_vars[corner]) == 2).OnlyEnforceIf(g)
|
|
97
|
+
self.model.AddLinearExpressionInDomain(sum(self.corner_vars[corner]), corner_sum_domain)
|
|
98
|
+
|
|
94
99
|
# single connected component
|
|
95
100
|
def is_neighbor(cb1: CellBorder, cb2: CellBorder) -> bool:
|
|
96
101
|
cb1_corners = self.cell_borders_to_corners[cb1]
|
|
@@ -118,131 +123,7 @@ class Board:
|
|
|
118
123
|
continue
|
|
119
124
|
c = ''.join(sorted(single_res.assignment[pos]))
|
|
120
125
|
set_char(res, pos, c)
|
|
121
|
-
|
|
126
|
+
# replace " " with "·"
|
|
127
|
+
board = np.where(self.board == ' ', '·', self.board)
|
|
128
|
+
print(render_grid(cell_flags=res, center_char=board))
|
|
122
129
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=999)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def render_grid(cell_flags: np.ndarray = None,
|
|
129
|
-
H: np.ndarray = None,
|
|
130
|
-
V: np.ndarray = None,
|
|
131
|
-
mark_centers: bool = True,
|
|
132
|
-
center_char: str = '·',
|
|
133
|
-
show_axes: bool = True,
|
|
134
|
-
scale_x: int = 2) -> str:
|
|
135
|
-
"""
|
|
136
|
-
AI generated this because I don't currently care about the details of rendering to the terminal and I did it in a quick and dirty way while the AI made it in a pretty way, and this looks good during my development.
|
|
137
|
-
cell_flags: np.ndarray of shape (N, N) with characters 'U', 'D', 'L', 'R' to represent the edges of the cells.
|
|
138
|
-
OR:
|
|
139
|
-
H: (N+1, N) horizontal edges between corners
|
|
140
|
-
V: (N, N+1) vertical edges between corners
|
|
141
|
-
scale_x: horizontal stretch factor (>=1). Try 2 or 3 for squarer cells.
|
|
142
|
-
"""
|
|
143
|
-
if cell_flags is not None:
|
|
144
|
-
N = cell_flags.shape[0]
|
|
145
|
-
H = np.zeros((N+1, N), dtype=bool)
|
|
146
|
-
V = np.zeros((N, N+1), dtype=bool)
|
|
147
|
-
for r in range(N):
|
|
148
|
-
for c in range(N):
|
|
149
|
-
s = cell_flags[r, c]
|
|
150
|
-
if 'U' in s: H[r, c] = True # edge between (r,c) and (r, c+1) above the cell
|
|
151
|
-
if 'D' in s: H[r+1, c] = True # edge below the cell
|
|
152
|
-
if 'L' in s: V[r, c] = True # edge left of the cell
|
|
153
|
-
if 'R' in s: V[r, c+1] = True # edge right of the cell
|
|
154
|
-
assert H is not None and V is not None, 'H and V must be provided'
|
|
155
|
-
# Bitmask for corner connections
|
|
156
|
-
U, R, D, L = 1, 2, 4, 8
|
|
157
|
-
JUNCTION = {
|
|
158
|
-
0: ' ',
|
|
159
|
-
U: '│', D: '│', U|D: '│',
|
|
160
|
-
L: '─', R: '─', L|R: '─',
|
|
161
|
-
U|R: '└', R|D: '┌', D|L: '┐', L|U: '┘',
|
|
162
|
-
U|D|L: '┤', U|D|R: '├', L|R|U: '┴', L|R|D: '┬',
|
|
163
|
-
U|R|D|L: '┼',
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
assert scale_x >= 1
|
|
167
|
-
N = V.shape[0]
|
|
168
|
-
assert H.shape == (N+1, N) and V.shape == (N, N+1)
|
|
169
|
-
|
|
170
|
-
rows = 2*N + 1
|
|
171
|
-
cols = 2*N*scale_x + 1 # stretched width
|
|
172
|
-
canvas = [[' ']*cols for _ in range(rows)]
|
|
173
|
-
|
|
174
|
-
def x_corner(c): # x of corner column c
|
|
175
|
-
return (2*c) * scale_x
|
|
176
|
-
def x_between(c,k): # kth in-between column (1..scale_x) between c and c+1 corners
|
|
177
|
-
return (2*c) * scale_x + k
|
|
178
|
-
|
|
179
|
-
# horizontal edges: fill the stretched band between corners with '─'
|
|
180
|
-
for r in range(N+1):
|
|
181
|
-
rr = 2*r
|
|
182
|
-
for c in range(N):
|
|
183
|
-
if H[r, c]:
|
|
184
|
-
# previously: for k in range(1, scale_x*2, 2):
|
|
185
|
-
for k in range(1, scale_x*2): # 1..(2*scale_x-1), no gaps
|
|
186
|
-
canvas[rr][x_between(c, k)] = '─'
|
|
187
|
-
|
|
188
|
-
# vertical edges: draw at the corner columns (no horizontal stretching needed)
|
|
189
|
-
for r in range(N):
|
|
190
|
-
rr = 2*r + 1
|
|
191
|
-
for c in range(N+1):
|
|
192
|
-
if V[r, c]:
|
|
193
|
-
canvas[rr][x_corner(c)] = '│'
|
|
194
|
-
|
|
195
|
-
# junctions at corners
|
|
196
|
-
for r in range(N+1):
|
|
197
|
-
rr = 2*r
|
|
198
|
-
for c in range(N+1):
|
|
199
|
-
m = 0
|
|
200
|
-
if r > 0 and V[r-1, c]: m |= U
|
|
201
|
-
if c < N and H[r, c]: m |= R
|
|
202
|
-
if r < N and V[r, c]: m |= D
|
|
203
|
-
if c > 0 and H[r, c-1]: m |= L
|
|
204
|
-
canvas[rr][x_corner(c)] = JUNCTION[m]
|
|
205
|
-
|
|
206
|
-
# centers (help count exact widths/heights)
|
|
207
|
-
if mark_centers:
|
|
208
|
-
for r in range(N):
|
|
209
|
-
rr = 2*r + 1
|
|
210
|
-
for c in range(N):
|
|
211
|
-
# center lies midway across the stretched span
|
|
212
|
-
xc = x_corner(c) + scale_x # middle-ish; works for any integer scale_x
|
|
213
|
-
canvas[rr][xc] = center_char if isinstance(center_char, str) else center_char(c, r)
|
|
214
|
-
|
|
215
|
-
# turn canvas rows into strings
|
|
216
|
-
art_rows = [''.join(row) for row in canvas]
|
|
217
|
-
|
|
218
|
-
if not show_axes:
|
|
219
|
-
return '\n'.join(art_rows)
|
|
220
|
-
|
|
221
|
-
# ── Axes ────────────────────────────────────────────────────────────────
|
|
222
|
-
gut = max(2, len(str(N-1))) # left gutter width
|
|
223
|
-
gutter = ' ' * gut
|
|
224
|
-
top_tens = list(gutter + ' ' * cols)
|
|
225
|
-
top_ones = list(gutter + ' ' * cols)
|
|
226
|
-
|
|
227
|
-
for c in range(N):
|
|
228
|
-
xc_center = x_corner(c) + scale_x
|
|
229
|
-
if N >= 10:
|
|
230
|
-
top_tens[gut + xc_center] = str((c // 10) % 10)
|
|
231
|
-
top_ones[gut + xc_center] = str(c % 10)
|
|
232
|
-
|
|
233
|
-
# tiny corner labels
|
|
234
|
-
if gut >= 2:
|
|
235
|
-
top_tens[gut-2:gut] = list(' ')
|
|
236
|
-
top_ones[gut-2:gut] = list(' ')
|
|
237
|
-
|
|
238
|
-
labeled = []
|
|
239
|
-
for r, line in enumerate(art_rows):
|
|
240
|
-
if r % 2 == 1: # cell-center row
|
|
241
|
-
label = str(r//2).rjust(gut)
|
|
242
|
-
else:
|
|
243
|
-
label = ' ' * gut
|
|
244
|
-
labeled.append(label + line)
|
|
245
|
-
|
|
246
|
-
return ''.join(top_tens) + '\n' + ''.join(top_ones) + '\n' + '\n'.join(labeled)
|
|
247
|
-
|
|
248
|
-
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# THIS PARSER IS STILL VERY BUGGY
|
|
1
2
|
|
|
2
3
|
def extract_lines(bw):
|
|
3
4
|
horizontal = np.copy(bw)
|
|
@@ -157,11 +158,13 @@ def extract_yinyang_board(image_path, debug=False):
|
|
|
157
158
|
return board
|
|
158
159
|
|
|
159
160
|
if __name__ == "__main__":
|
|
161
|
+
# THIS PARSER IS STILL VERY BUGGY
|
|
160
162
|
# python .\src\puzzle_solver\puzzles\yin_yang\parse_map\parse_map.py | python .\src\puzzle_solver\utils\visualizer.py --read_stdin
|
|
161
163
|
import cv2
|
|
162
164
|
import numpy as np
|
|
163
165
|
from pathlib import Path
|
|
164
|
-
image_path = Path(__file__).parent / "input_output" / "
|
|
166
|
+
image_path = Path(__file__).parent / "input_output" / "OTozLDY2MSw3MjE=.png"
|
|
167
|
+
# image_path = Path(__file__).parent / "input_output" / "MzoyLDcwMSw2NTY=.png"
|
|
165
168
|
# image_path = Path(__file__).parent / "input_output" / "Njo5MDcsNDk4.png"
|
|
166
169
|
# image_path = Path(__file__).parent / "input_output" / "MTE6Niw0NjEsMTIx.png"
|
|
167
170
|
assert image_path.exists(), f"Image file does not exist: {image_path}"
|
|
File without changes
|
|
File without changes
|