multi-puzzle-solver 1.0.4__py3-none-any.whl → 1.0.7__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-1.0.4.dist-info → multi_puzzle_solver-1.0.7.dist-info}/METADATA +1075 -556
- multi_puzzle_solver-1.0.7.dist-info/RECORD +74 -0
- puzzle_solver/__init__.py +5 -1
- puzzle_solver/core/utils.py +17 -1
- puzzle_solver/core/utils_visualizer.py +257 -201
- puzzle_solver/puzzles/abc_view/abc_view.py +75 -0
- puzzle_solver/puzzles/aquarium/aquarium.py +8 -23
- puzzle_solver/puzzles/battleships/battleships.py +39 -53
- puzzle_solver/puzzles/binairo/binairo.py +2 -2
- puzzle_solver/puzzles/black_box/black_box.py +6 -70
- puzzle_solver/puzzles/connect_the_dots/connect_the_dots.py +4 -2
- puzzle_solver/puzzles/filling/filling.py +11 -34
- puzzle_solver/puzzles/galaxies/galaxies.py +4 -2
- puzzle_solver/puzzles/heyawake/heyawake.py +72 -14
- puzzle_solver/puzzles/kakurasu/kakurasu.py +5 -13
- puzzle_solver/puzzles/kakuro/kakuro.py +6 -2
- puzzle_solver/puzzles/lits/lits.py +4 -2
- puzzle_solver/puzzles/mosaic/mosaic.py +8 -18
- puzzle_solver/puzzles/nonograms/nonograms.py +80 -85
- puzzle_solver/puzzles/nonograms/nonograms_colored.py +221 -0
- puzzle_solver/puzzles/norinori/norinori.py +5 -12
- puzzle_solver/puzzles/nurikabe/nurikabe.py +6 -2
- puzzle_solver/puzzles/palisade/palisade.py +8 -22
- puzzle_solver/puzzles/pearl/pearl.py +15 -27
- puzzle_solver/puzzles/pipes/pipes.py +2 -1
- puzzle_solver/puzzles/range/range.py +19 -55
- puzzle_solver/puzzles/rectangles/rectangles.py +4 -2
- puzzle_solver/puzzles/shingoki/shingoki.py +62 -105
- puzzle_solver/puzzles/singles/singles.py +6 -2
- puzzle_solver/puzzles/slant/slant.py +13 -19
- puzzle_solver/puzzles/slitherlink/slitherlink.py +2 -2
- puzzle_solver/puzzles/star_battle/star_battle.py +5 -2
- puzzle_solver/puzzles/stitches/stitches.py +8 -21
- puzzle_solver/puzzles/sudoku/sudoku.py +5 -11
- puzzle_solver/puzzles/tapa/tapa.py +6 -2
- puzzle_solver/puzzles/tents/tents.py +50 -80
- puzzle_solver/puzzles/tracks/tracks.py +19 -66
- puzzle_solver/puzzles/unruly/unruly.py +17 -49
- puzzle_solver/puzzles/yin_yang/yin_yang.py +3 -10
- multi_puzzle_solver-1.0.4.dist-info/RECORD +0 -72
- {multi_puzzle_solver-1.0.4.dist-info → multi_puzzle_solver-1.0.7.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-1.0.4.dist-info → multi_puzzle_solver-1.0.7.dist-info}/top_level.txt +0 -0
|
@@ -1,184 +1,54 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Callable, Optional, List, Sequence, Literal
|
|
3
3
|
from puzzle_solver.core.utils import Pos, get_all_pos, get_next_pos, in_bounds, set_char, get_char, Direction
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
def combined_function(V: int,
|
|
7
|
+
H: int,
|
|
8
|
+
cell_flags: Optional[Callable[[int, int], str]] = None,
|
|
9
|
+
is_shaded: Optional[Callable[[int, int], bool]] = None,
|
|
10
|
+
center_char: Optional[Callable[[int, int], str]] = None,
|
|
11
|
+
special_content: Optional[Callable[[int, int], str]] = None,
|
|
12
|
+
text_on_shaded_cells: bool = True,
|
|
13
|
+
scale_x: int = 2,
|
|
14
|
+
scale_y: int = 1,
|
|
15
|
+
show_axes: bool = True,
|
|
16
|
+
show_grid: bool = True,
|
|
17
|
+
) -> str:
|
|
10
18
|
"""
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
Render a V x H grid that can:
|
|
20
|
+
• draw selective edges per cell via cell_flags(r, c) containing any of 'U','D','L','R'
|
|
21
|
+
• shade cells via is_shaded(r, c)
|
|
22
|
+
• place centered text per cell via center_char(r, c)
|
|
23
|
+
• draw interior arms via special_content(r, c) returning any combo of 'U','D','L','R'
|
|
24
|
+
(e.g., 'UR', 'DL', 'ULRD', or '' to leave the interior unchanged).
|
|
25
|
+
Arms extend from the cell’s interior center toward the indicated sides.
|
|
26
|
+
• horizontal stretch (>=1). Interior width per cell = 2*scale_x - 1 (default 2)
|
|
27
|
+
• vertical stretch (>=1). Interior height per cell = scale_y (default 1)
|
|
28
|
+
• show_axes: bool = True, show the axes (columns on top, rows on the left).
|
|
29
|
+
• show_grid: bool = True, show the grid lines.
|
|
30
|
+
|
|
31
|
+
Behavior:
|
|
32
|
+
- If cell_flags is None, draws a full grid (all interior and outer borders present).
|
|
33
|
+
- Shading is applied first, borders are drawn on top, and center text is drawn last unless text_on_shaded_cells is False in which case the text is not drawn on shaded cells.
|
|
34
|
+
- Axes are shown (columns on top, rows on the left).
|
|
35
|
+
|
|
36
|
+
Draw order:
|
|
37
|
+
1) shading
|
|
38
|
+
2) borders
|
|
39
|
+
3) special_content (interior line arms)
|
|
40
|
+
4) center_char (unless text_on_shaded_cells=False and cell is shaded)
|
|
15
41
|
"""
|
|
16
|
-
assert
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
V = np.zeros((R, C+1), dtype=bool) # vertical edges between cols
|
|
22
|
-
for r in range(R):
|
|
23
|
-
for c in range(C):
|
|
24
|
-
s = cell_flags[r, c]
|
|
25
|
-
if 'U' in s: H[r, c] = True
|
|
26
|
-
if 'D' in s: H[r+1, c] = True
|
|
27
|
-
if 'L' in s: V[r, c] = True
|
|
28
|
-
if 'R' in s: V[r, c+1] = True
|
|
29
|
-
|
|
30
|
-
# Bitmask for corner connections
|
|
31
|
-
U, Rb, D, Lb = 1, 2, 4, 8
|
|
32
|
-
JUNCTION = {
|
|
33
|
-
0: ' ',
|
|
34
|
-
U: '│', D: '│', U|D: '│',
|
|
35
|
-
Lb: '─', Rb: '─', Lb|Rb: '─',
|
|
36
|
-
U|Rb: '└', Rb|D: '┌', D|Lb: '┐', Lb|U: '┘',
|
|
37
|
-
U|D|Lb: '┤', U|D|Rb: '├', Lb|Rb|U: '┴', Lb|Rb|D: '┬',
|
|
38
|
-
U|Rb|D|Lb: '┼',
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
assert scale_x >= 1
|
|
42
|
-
assert H.shape == (R+1, C) and V.shape == (R, C+1)
|
|
43
|
-
|
|
44
|
-
rows = 2*R + 1
|
|
45
|
-
cols = 2*C*scale_x + 1
|
|
46
|
-
canvas = [[' ']*cols for _ in range(rows)]
|
|
47
|
-
|
|
48
|
-
def x_corner(c): # x of corner column c (0..C)
|
|
49
|
-
return (2*c) * scale_x
|
|
50
|
-
def x_between(c,k): # kth in-between col (1..2*scale_x-1) between corners c and c+1
|
|
51
|
-
return (2*c) * scale_x + k
|
|
52
|
-
|
|
53
|
-
# horizontal edges: fill the stretched band between corners with '─'
|
|
54
|
-
for r in range(R+1):
|
|
55
|
-
rr = 2*r
|
|
56
|
-
for c in range(C):
|
|
57
|
-
if H[r, c]:
|
|
58
|
-
for k in range(1, scale_x*2): # 1..(2*scale_x-1)
|
|
59
|
-
canvas[rr][x_between(c, k)] = '─'
|
|
60
|
-
|
|
61
|
-
# vertical edges: at the corner columns
|
|
62
|
-
for r in range(R):
|
|
63
|
-
rr = 2*r + 1
|
|
64
|
-
for c in range(C+1):
|
|
65
|
-
if V[r, c]:
|
|
66
|
-
canvas[rr][x_corner(c)] = '│'
|
|
67
|
-
|
|
68
|
-
# junctions at every corner grid point
|
|
69
|
-
for r in range(R+1):
|
|
70
|
-
rr = 2*r
|
|
71
|
-
for c in range(C+1):
|
|
72
|
-
m = 0
|
|
73
|
-
if r > 0 and V[r-1, c]: m |= U
|
|
74
|
-
if c < C and H[r, c]: m |= Rb
|
|
75
|
-
if r < R and V[r, c]: m |= D
|
|
76
|
-
if c > 0 and H[r, c-1]: m |= Lb
|
|
77
|
-
canvas[rr][x_corner(c)] = JUNCTION[m]
|
|
78
|
-
|
|
79
|
-
# centers (safe for multi-character strings)
|
|
80
|
-
def put_center_text(rr: int, c: int, text: str):
|
|
81
|
-
left = x_corner(c) + 1
|
|
82
|
-
right = x_corner(c+1) - 1
|
|
83
|
-
if right < left:
|
|
84
|
-
return
|
|
85
|
-
span_width = right - left + 1
|
|
86
|
-
s = str(text)
|
|
87
|
-
if len(s) > span_width:
|
|
88
|
-
s = s[:span_width] # truncate to protect borders
|
|
89
|
-
start = left + (span_width - len(s)) // 2
|
|
90
|
-
for i, ch in enumerate(s):
|
|
91
|
-
canvas[rr][start + i] = ch
|
|
92
|
-
|
|
93
|
-
if center_char is not None:
|
|
94
|
-
for r in range(R):
|
|
95
|
-
rr = 2*r + 1
|
|
96
|
-
for c in range(C):
|
|
97
|
-
val = center_char if isinstance(center_char, str) else (center_char(r, c) if callable(center_char) else center_char[r, c])
|
|
98
|
-
put_center_text(rr, c, '' if val is None else str(val))
|
|
99
|
-
|
|
100
|
-
# rows -> strings
|
|
101
|
-
art_rows = [''.join(row) for row in canvas]
|
|
102
|
-
if not show_axes:
|
|
103
|
-
return '\n'.join(art_rows)
|
|
104
|
-
|
|
105
|
-
# Axes labels: row indices on the left, column indices on top (handle C, not R)
|
|
106
|
-
gut = max(2, len(str(R-1))) # gutter width based on row index width
|
|
107
|
-
gutter = ' ' * gut
|
|
108
|
-
top_tens = list(gutter + ' ' * cols)
|
|
109
|
-
top_ones = list(gutter + ' ' * cols)
|
|
110
|
-
|
|
111
|
-
for c in range(C):
|
|
112
|
-
xc_center = x_corner(c) + scale_x
|
|
113
|
-
if C >= 10:
|
|
114
|
-
top_tens[gut + xc_center] = str((c // 10) % 10)
|
|
115
|
-
top_ones[gut + xc_center] = str(c % 10)
|
|
42
|
+
assert V >= 1 and H >= 1, f'V and H must be >= 1, got {V} and {H}'
|
|
43
|
+
assert cell_flags is None or callable(cell_flags), f'cell_flags must be None or callable, got {cell_flags}'
|
|
44
|
+
assert is_shaded is None or callable(is_shaded), f'is_shaded must be None or callable, got {is_shaded}'
|
|
45
|
+
assert center_char is None or callable(center_char), f'center_char must be None or callable, got {center_char}'
|
|
46
|
+
assert special_content is None or callable(special_content), f'special_content must be None or callable, got {special_content}'
|
|
116
47
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
48
|
+
# Rendering constants
|
|
49
|
+
fill_char: str = '▒' # single char for shaded interiors
|
|
50
|
+
empty_char: str = ' ' # single char for unshaded interiors
|
|
120
51
|
|
|
121
|
-
labeled = []
|
|
122
|
-
for r, line in enumerate(art_rows):
|
|
123
|
-
if r % 2 == 1: # cell-center row
|
|
124
|
-
label = str(r//2).rjust(gut)
|
|
125
|
-
else:
|
|
126
|
-
label = ' ' * gut
|
|
127
|
-
labeled.append(label + line)
|
|
128
|
-
|
|
129
|
-
return ''.join(top_tens) + '\n' + ''.join(top_ones) + '\n' + '\n'.join(labeled)
|
|
130
|
-
|
|
131
|
-
def id_board_to_wall_board(id_board: np.array, border_is_wall = True) -> np.array:
|
|
132
|
-
"""In many instances, we have a 2d array where cell values are arbitrary ids
|
|
133
|
-
and we want to convert it to a 2d array where cell values are walls "U", "D", "L", "R" to represent the edges that separate me from my neighbors that have different ids.
|
|
134
|
-
Args:
|
|
135
|
-
id_board: np.array of shape (N, N) with arbitrary ids.
|
|
136
|
-
border_is_wall: if True, the edges of the board are considered to be walls.
|
|
137
|
-
Returns:
|
|
138
|
-
np.array of shape (N, N) with walls "U", "D", "L", "R".
|
|
139
|
-
"""
|
|
140
|
-
res = np.full((id_board.shape[0], id_board.shape[1]), '', dtype=object)
|
|
141
|
-
V, H = id_board.shape
|
|
142
|
-
def append_char(pos: Pos, s: str):
|
|
143
|
-
set_char(res, pos, get_char(res, pos) + s)
|
|
144
|
-
def handle_pos_direction(pos: Pos, direction: Direction, s: str):
|
|
145
|
-
pos2 = get_next_pos(pos, direction)
|
|
146
|
-
if in_bounds(pos2, V, H):
|
|
147
|
-
if get_char(id_board, pos2) != get_char(id_board, pos):
|
|
148
|
-
append_char(pos, s)
|
|
149
|
-
else:
|
|
150
|
-
if border_is_wall:
|
|
151
|
-
append_char(pos, s)
|
|
152
|
-
for pos in get_all_pos(V, H):
|
|
153
|
-
handle_pos_direction(pos, Direction.LEFT, 'L')
|
|
154
|
-
handle_pos_direction(pos, Direction.RIGHT, 'R')
|
|
155
|
-
handle_pos_direction(pos, Direction.UP, 'U')
|
|
156
|
-
handle_pos_direction(pos, Direction.DOWN, 'D')
|
|
157
|
-
return res
|
|
158
|
-
|
|
159
|
-
def render_shaded_grid(V: int,
|
|
160
|
-
H: int,
|
|
161
|
-
is_shaded: Callable[[int, int], bool],
|
|
162
|
-
empty_text: Optional[Union[str, Callable[[int, int], Optional[str]]]] = None,) -> str:
|
|
163
|
-
"""
|
|
164
|
-
Most of this function was AI generated then modified by me, I don't currently care about the details of rendering to the terminal this looked good enough during my testing.
|
|
165
|
-
Visualize a V x H grid where each cell is shaded if is_shaded(r, c) is True.
|
|
166
|
-
The grid lines are always present.
|
|
167
|
-
|
|
168
|
-
scale_x: horizontal stretch (>=1). Interior width per cell = 2*scale_x - 1.
|
|
169
|
-
scale_y: vertical stretch (>=1). Interior height per cell = scale_y.
|
|
170
|
-
fill_char: character to fill shaded cell interiors (single char).
|
|
171
|
-
empty_char: background character for unshaded interiors (single char).
|
|
172
|
-
empty_text: Optional text for unshaded cells. If a string, used for all unshaded
|
|
173
|
-
cells. If a callable (r, c) -> str|None, used per cell. Text is
|
|
174
|
-
centered within the interior row and truncated to fit.
|
|
175
|
-
"""
|
|
176
|
-
scale_x: int = 2
|
|
177
|
-
scale_y: int = 1
|
|
178
|
-
fill_char: str = '▒'
|
|
179
|
-
empty_char: str = ' '
|
|
180
|
-
show_axes: bool = True
|
|
181
|
-
assert V >= 1 and H >= 1
|
|
182
52
|
assert scale_x >= 1 and scale_y >= 1
|
|
183
53
|
assert len(fill_char) == 1 and len(empty_char) == 1
|
|
184
54
|
|
|
@@ -192,7 +62,29 @@ def render_shaded_grid(V: int,
|
|
|
192
62
|
cols = x_corner(H) + 1
|
|
193
63
|
canvas = [[empty_char] * cols for _ in range(rows)]
|
|
194
64
|
|
|
195
|
-
# ──
|
|
65
|
+
# ── Edge presence arrays derived from cell_flags ──
|
|
66
|
+
# H_edges[r, c] is the horizontal edge between rows r and r+1 above column segment c (shape: (V+1, H))
|
|
67
|
+
# V_edges[r, c] is the vertical edge between cols c and c+1 left of row segment r (shape: (V, H+1))
|
|
68
|
+
if cell_flags is None:
|
|
69
|
+
# Full grid: all horizontal and vertical segments are present
|
|
70
|
+
H_edges = [[show_grid for _ in range(H)] for _ in range(V + 1)]
|
|
71
|
+
V_edges = [[show_grid for _ in range(H + 1)] for _ in range(V)]
|
|
72
|
+
else:
|
|
73
|
+
H_edges = [[False for _ in range(H)] for _ in range(V + 1)]
|
|
74
|
+
V_edges = [[False for _ in range(H + 1)] for _ in range(V)]
|
|
75
|
+
for r in range(V):
|
|
76
|
+
for c in range(H):
|
|
77
|
+
s = cell_flags(r, c) or ''
|
|
78
|
+
if 'U' in s:
|
|
79
|
+
H_edges[r ][c] = True
|
|
80
|
+
if 'D' in s:
|
|
81
|
+
H_edges[r + 1][c] = True
|
|
82
|
+
if 'L' in s:
|
|
83
|
+
V_edges[r][c ] = True
|
|
84
|
+
if 'R' in s:
|
|
85
|
+
V_edges[r][c + 1] = True
|
|
86
|
+
|
|
87
|
+
# ── Shading first (borders will overwrite) ─────────────────────────────
|
|
196
88
|
shaded_map = [[False]*H for _ in range(V)]
|
|
197
89
|
for r in range(V):
|
|
198
90
|
top = y_border(r) + 1
|
|
@@ -204,14 +96,14 @@ def render_shaded_grid(V: int,
|
|
|
204
96
|
right = x_corner(c + 1) - 1 # inclusive
|
|
205
97
|
if left > right:
|
|
206
98
|
continue
|
|
207
|
-
shaded = bool(is_shaded(r, c))
|
|
99
|
+
shaded = bool(is_shaded(r, c)) if callable(is_shaded) else False
|
|
208
100
|
shaded_map[r][c] = shaded
|
|
209
101
|
ch = fill_char if shaded else empty_char
|
|
210
102
|
for yy in range(top, bottom + 1):
|
|
211
103
|
for xx in range(left, right + 1):
|
|
212
104
|
canvas[yy][xx] = ch
|
|
213
105
|
|
|
214
|
-
# ── Grid lines
|
|
106
|
+
# ── Grid lines (respect edge presence) ─────────────────────────────────
|
|
215
107
|
U, Rb, D, Lb = 1, 2, 4, 8
|
|
216
108
|
JUNCTION = {
|
|
217
109
|
0: ' ',
|
|
@@ -222,20 +114,22 @@ def render_shaded_grid(V: int,
|
|
|
222
114
|
U | Rb | D | Lb: '┼',
|
|
223
115
|
}
|
|
224
116
|
|
|
225
|
-
# Horizontal
|
|
117
|
+
# Horizontal segments
|
|
226
118
|
for r in range(V + 1):
|
|
227
119
|
yy = y_border(r)
|
|
228
120
|
for c in range(H):
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
121
|
+
if H_edges[r][c]:
|
|
122
|
+
base = x_corner(c)
|
|
123
|
+
for k in range(1, 2 * scale_x): # 1..(2*scale_x-1)
|
|
124
|
+
canvas[yy][base + k] = '─'
|
|
232
125
|
|
|
233
|
-
# Vertical
|
|
234
|
-
for
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
126
|
+
# Vertical segments
|
|
127
|
+
for r in range(V):
|
|
128
|
+
for c in range(H + 1):
|
|
129
|
+
if V_edges[r][c]:
|
|
130
|
+
xx = x_corner(c)
|
|
131
|
+
for ky in range(1, scale_y + 1):
|
|
132
|
+
canvas[y_border(r) + ky][xx] = '│'
|
|
239
133
|
|
|
240
134
|
# Junctions at intersections
|
|
241
135
|
for r in range(V + 1):
|
|
@@ -243,14 +137,79 @@ def render_shaded_grid(V: int,
|
|
|
243
137
|
for c in range(H + 1):
|
|
244
138
|
xx = x_corner(c)
|
|
245
139
|
m = 0
|
|
246
|
-
if r > 0
|
|
247
|
-
|
|
248
|
-
if
|
|
249
|
-
|
|
140
|
+
if r > 0 and V_edges[r - 1][c]:
|
|
141
|
+
m |= U
|
|
142
|
+
if r < V and V_edges[r][c]:
|
|
143
|
+
m |= D
|
|
144
|
+
if c > 0 and H_edges[r][c - 1]:
|
|
145
|
+
m |= Lb
|
|
146
|
+
if c < H and H_edges[r][c]:
|
|
147
|
+
m |= Rb
|
|
250
148
|
canvas[yy][xx] = JUNCTION[m]
|
|
251
149
|
|
|
252
|
-
# ──
|
|
253
|
-
def
|
|
150
|
+
# ── Special interior content (arms) + cross-cell bridges ──────────────
|
|
151
|
+
def draw_special_arms(r_cell: int, c_cell: int, code: Optional[str]):
|
|
152
|
+
if not code:
|
|
153
|
+
return
|
|
154
|
+
s = set(code)
|
|
155
|
+
# interior box
|
|
156
|
+
left = x_corner(c_cell) + 1
|
|
157
|
+
right = x_corner(c_cell + 1) - 1
|
|
158
|
+
top = y_border(r_cell) + 1
|
|
159
|
+
bottom= y_border(r_cell + 1) - 1
|
|
160
|
+
if left > right or top > bottom:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# center of interior
|
|
164
|
+
cx = left + (right - left) // 2
|
|
165
|
+
cy = top + (bottom - top) // 2
|
|
166
|
+
|
|
167
|
+
# draw arms out from center (keep inside interior; don't touch borders)
|
|
168
|
+
if 'U' in s and cy - 1 >= top:
|
|
169
|
+
for yy in range(cy - 1, top - 1, -1):
|
|
170
|
+
canvas[yy][cx] = '│'
|
|
171
|
+
if 'D' in s and cy + 1 <= bottom:
|
|
172
|
+
for yy in range(cy + 1, bottom + 1):
|
|
173
|
+
canvas[yy][cx] = '│'
|
|
174
|
+
if 'L' in s and cx - 1 >= left:
|
|
175
|
+
for xx in range(cx - 1, left - 1, -1):
|
|
176
|
+
canvas[cy][xx] = '─'
|
|
177
|
+
if 'R' in s and cx + 1 <= right:
|
|
178
|
+
for xx in range(cx + 1, right + 1):
|
|
179
|
+
canvas[cy][xx] = '─'
|
|
180
|
+
if '/' in s:
|
|
181
|
+
for xx in range(right - left + 1):
|
|
182
|
+
for yy in range(top - bottom + 1):
|
|
183
|
+
canvas[top + yy][left + xx] = '/'
|
|
184
|
+
if '\\' in s:
|
|
185
|
+
for xx in range(right - left + 1):
|
|
186
|
+
for yy in range(top - bottom + 1):
|
|
187
|
+
canvas[top + yy][left + xx] = '\\'
|
|
188
|
+
|
|
189
|
+
# center junction
|
|
190
|
+
U_b, R_b, D_b, L_b = 1, 2, 4, 8
|
|
191
|
+
m = 0
|
|
192
|
+
if 'U' in s: m |= U_b
|
|
193
|
+
if 'D' in s: m |= D_b
|
|
194
|
+
if 'L' in s: m |= L_b
|
|
195
|
+
if 'R' in s: m |= R_b
|
|
196
|
+
canvas[cy][cx] = JUNCTION.get(m, ' ')
|
|
197
|
+
|
|
198
|
+
# pass 1: draw interior arms per cell
|
|
199
|
+
special_map = [[set() for _ in range(H)] for _ in range(V)]
|
|
200
|
+
if callable(special_content):
|
|
201
|
+
for r in range(V):
|
|
202
|
+
for c in range(H):
|
|
203
|
+
flags = set(ch for ch in str(special_content(r, c) or ''))
|
|
204
|
+
special_map[r][c] = flags
|
|
205
|
+
if flags:
|
|
206
|
+
draw_special_arms(r, c, ''.join(flags))
|
|
207
|
+
|
|
208
|
+
# ── Center text (drawn last so it sits atop shading/arms) ─────────────
|
|
209
|
+
def put_center_text(r_cell: int, c_cell: int, s: Optional[str]):
|
|
210
|
+
if s is None:
|
|
211
|
+
return
|
|
212
|
+
s = str(s)
|
|
254
213
|
# interior box
|
|
255
214
|
left = x_corner(c_cell) + 1
|
|
256
215
|
right = x_corner(c_cell + 1) - 1
|
|
@@ -259,24 +218,93 @@ def render_shaded_grid(V: int,
|
|
|
259
218
|
if left > right or top > bottom:
|
|
260
219
|
return
|
|
261
220
|
span_w = right - left + 1
|
|
262
|
-
# choose middle interior row for text
|
|
263
221
|
yy = top + (bottom - top) // 2
|
|
264
|
-
s = '' if s is None else str(s)
|
|
265
222
|
if len(s) > span_w:
|
|
266
|
-
s = s[:span_w]
|
|
223
|
+
s = s[:span_w] # truncate to protect borders
|
|
267
224
|
start = left + (span_w - len(s)) // 2
|
|
268
225
|
for i, ch in enumerate(s):
|
|
269
226
|
canvas[yy][start + i] = ch
|
|
270
227
|
|
|
271
|
-
|
|
228
|
+
|
|
229
|
+
# helper to get interior-center coordinates
|
|
230
|
+
def _cell_center_rc(r_cell: int, c_cell: int):
|
|
231
|
+
left = x_corner(c_cell) + 1
|
|
232
|
+
right = x_corner(c_cell + 1) - 1
|
|
233
|
+
top = y_border(r_cell) + 1
|
|
234
|
+
bottom= y_border(r_cell + 1) - 1
|
|
235
|
+
if left > right or top > bottom:
|
|
236
|
+
return None
|
|
237
|
+
cx = left + (right - left) // 2
|
|
238
|
+
cy = top + (bottom - top) // 2
|
|
239
|
+
return cy, cx
|
|
240
|
+
|
|
241
|
+
# ── REPLACE your place_connector() and "pass 2" with the following ────
|
|
242
|
+
|
|
243
|
+
# PASS 2: merge/bridge on every border using bitmasks (works with or without borders)
|
|
244
|
+
if callable(special_content):
|
|
245
|
+
# vertical borders: c in [0..H], between (r,c-1) and (r,c)
|
|
246
|
+
for r in range(V):
|
|
247
|
+
# y (row) where we draw the junction on this border: the interior center row
|
|
248
|
+
cc = _cell_center_rc(r, 0)
|
|
249
|
+
if cc is None:
|
|
250
|
+
continue
|
|
251
|
+
cy = cc[0]
|
|
252
|
+
for c in range(H + 1):
|
|
253
|
+
x = x_corner(c)
|
|
254
|
+
mask = 0
|
|
255
|
+
# base: if the vertical grid line exists here, add U and D
|
|
256
|
+
if V_edges[r][c]:
|
|
257
|
+
mask |= U | D
|
|
258
|
+
|
|
259
|
+
# neighbors pointing toward this vertical border
|
|
260
|
+
left_flags = special_map[r][c - 1] if c - 1 >= 0 else set()
|
|
261
|
+
right_flags = special_map[r][c] if c < H else set()
|
|
262
|
+
if 'R' in left_flags:
|
|
263
|
+
mask |= Lb
|
|
264
|
+
if 'L' in right_flags:
|
|
265
|
+
mask |= Rb
|
|
266
|
+
|
|
267
|
+
# nothing to draw? leave whatever is already there
|
|
268
|
+
if mask == 0:
|
|
269
|
+
continue
|
|
270
|
+
canvas[cy][x] = JUNCTION[mask]
|
|
271
|
+
|
|
272
|
+
# horizontal borders: r in [0..V], between (r-1,c) and (r,c)
|
|
273
|
+
for c in range(H):
|
|
274
|
+
# x (col) where we draw the junction on this border: the interior center col
|
|
275
|
+
cc = _cell_center_rc(0, c)
|
|
276
|
+
if cc is None:
|
|
277
|
+
continue
|
|
278
|
+
cx = cc[1]
|
|
279
|
+
for r in range(V + 1):
|
|
280
|
+
y = y_border(r)
|
|
281
|
+
mask = 0
|
|
282
|
+
# base: if the horizontal grid line exists here, add L and R
|
|
283
|
+
if r <= V - 1 and H_edges[r][c]: # H_edges indexed [0..V] x [0..H-1]
|
|
284
|
+
mask |= Lb | Rb
|
|
285
|
+
|
|
286
|
+
# neighbors pointing toward this horizontal border
|
|
287
|
+
up_flags = special_map[r - 1][c] if r - 1 >= 0 else set()
|
|
288
|
+
down_flags = special_map[r][c] if r < V else set()
|
|
289
|
+
if 'D' in up_flags:
|
|
290
|
+
mask |= U
|
|
291
|
+
if 'U' in down_flags:
|
|
292
|
+
mask |= D
|
|
293
|
+
|
|
294
|
+
if mask == 0:
|
|
295
|
+
continue
|
|
296
|
+
canvas[y][cx] = JUNCTION[mask]
|
|
297
|
+
|
|
298
|
+
if callable(center_char):
|
|
272
299
|
for r in range(V):
|
|
273
300
|
for c in range(H):
|
|
274
|
-
if not shaded_map[r][c]:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
301
|
+
if not text_on_shaded_cells and shaded_map[r][c]:
|
|
302
|
+
continue
|
|
303
|
+
if not text_on_shaded_cells and special_map[r][c]:
|
|
304
|
+
continue
|
|
305
|
+
put_center_text(r, c, center_char(r, c))
|
|
278
306
|
|
|
279
|
-
# ── Stringify
|
|
307
|
+
# ── Stringify with axes ────────────────────────────────────────────────
|
|
280
308
|
art_rows = [''.join(row) for row in canvas]
|
|
281
309
|
if not show_axes:
|
|
282
310
|
return '\n'.join(art_rows)
|
|
@@ -308,6 +336,34 @@ def render_shaded_grid(V: int,
|
|
|
308
336
|
|
|
309
337
|
return ''.join(top_tens) + '\n' + ''.join(top_ones) + '\n' + '\n'.join(labeled)
|
|
310
338
|
|
|
339
|
+
def id_board_to_wall_fn(id_board: np.array, border_is_wall = True) -> Callable[[int, int], str]:
|
|
340
|
+
"""In many instances, we have a 2d array where cell values are arbitrary ids
|
|
341
|
+
and we want to convert it to a 2d array where cell values are walls "U", "D", "L", "R" to represent the edges that separate me from my neighbors that have different ids.
|
|
342
|
+
Args:
|
|
343
|
+
id_board: np.array of shape (N, N) with arbitrary ids.
|
|
344
|
+
border_is_wall: if True, the edges of the board are considered to be walls.
|
|
345
|
+
Returns:
|
|
346
|
+
Callable[[int, int], str] that returns the walls "U", "D", "L", "R" for the cell at (r, c).
|
|
347
|
+
"""
|
|
348
|
+
res = np.full((id_board.shape[0], id_board.shape[1]), '', dtype=object)
|
|
349
|
+
V, H = id_board.shape
|
|
350
|
+
def append_char(pos: Pos, s: str):
|
|
351
|
+
set_char(res, pos, get_char(res, pos) + s)
|
|
352
|
+
def handle_pos_direction(pos: Pos, direction: Direction, s: str):
|
|
353
|
+
pos2 = get_next_pos(pos, direction)
|
|
354
|
+
if in_bounds(pos2, V, H):
|
|
355
|
+
if get_char(id_board, pos2) != get_char(id_board, pos):
|
|
356
|
+
append_char(pos, s)
|
|
357
|
+
else:
|
|
358
|
+
if border_is_wall:
|
|
359
|
+
append_char(pos, s)
|
|
360
|
+
for pos in get_all_pos(V, H):
|
|
361
|
+
handle_pos_direction(pos, Direction.LEFT, 'L')
|
|
362
|
+
handle_pos_direction(pos, Direction.RIGHT, 'R')
|
|
363
|
+
handle_pos_direction(pos, Direction.UP, 'U')
|
|
364
|
+
handle_pos_direction(pos, Direction.DOWN, 'D')
|
|
365
|
+
return lambda r, c: res[r][c]
|
|
366
|
+
|
|
311
367
|
CellVal = Literal["B", "W", "TL", "TR", "BL", "BR"]
|
|
312
368
|
GridLike = Sequence[Sequence[CellVal]]
|
|
313
369
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ortools.sat.python import cp_model
|
|
3
|
+
|
|
4
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_pos, get_row_pos, get_col_pos
|
|
5
|
+
from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, SingleSolution
|
|
6
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Board:
|
|
10
|
+
def __init__(self, board: np.array, top: np.array, left: np.array, bottom: np.array, right: np.array, characters: list[str]):
|
|
11
|
+
self.BLANK = 'BLANK'
|
|
12
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
13
|
+
self.characters_no_blank = characters
|
|
14
|
+
self.characters = characters + [self.BLANK]
|
|
15
|
+
assert all(c.strip() in self.characters or c.strip() == '' for c in board.flatten()), f'board must contain characters in {self.characters}'
|
|
16
|
+
assert all(c.strip() in self.characters or c.strip() == '' for c in np.concatenate([top, left, bottom, right])), f'top, bottom, left, and right must contain only characters in {self.characters}'
|
|
17
|
+
self.board = board
|
|
18
|
+
self.V, self.H = board.shape
|
|
19
|
+
assert top.shape == (self.H,) and bottom.shape == (self.H,) and left.shape == (self.V,) and right.shape == (self.V,), 'top, bottom, left, and right must be 1d arrays of length board width and height'
|
|
20
|
+
self.top = top
|
|
21
|
+
self.left = left
|
|
22
|
+
self.bottom = bottom
|
|
23
|
+
self.right = right
|
|
24
|
+
|
|
25
|
+
self.model = cp_model.CpModel()
|
|
26
|
+
self.model_vars: dict[tuple[Pos, str], cp_model.IntVar] = {}
|
|
27
|
+
self.create_vars()
|
|
28
|
+
self.add_all_constraints()
|
|
29
|
+
|
|
30
|
+
def create_vars(self):
|
|
31
|
+
for pos in get_all_pos(self.V, self.H):
|
|
32
|
+
for character in self.characters:
|
|
33
|
+
self.model_vars[pos, character] = self.model.NewBoolVar(f'{pos}:{character}')
|
|
34
|
+
|
|
35
|
+
def add_all_constraints(self):
|
|
36
|
+
for pos in get_all_pos(self.V, self.H):
|
|
37
|
+
self.model.AddExactlyOne([self.model_vars[pos, character] for character in self.characters])
|
|
38
|
+
c = get_char(self.board, pos).strip() # force the clue if on the board
|
|
39
|
+
if not c:
|
|
40
|
+
continue
|
|
41
|
+
self.model.Add(self.model_vars[pos, c] == 1)
|
|
42
|
+
|
|
43
|
+
# each row and column must have exactly one of each character, except for BLANK
|
|
44
|
+
for row in range(self.V):
|
|
45
|
+
for character in self.characters_no_blank:
|
|
46
|
+
self.model.AddExactlyOne([self.model_vars[pos, character] for pos in get_row_pos(row, self.H)])
|
|
47
|
+
for col in range(self.H):
|
|
48
|
+
for character in self.characters_no_blank:
|
|
49
|
+
self.model.AddExactlyOne([self.model_vars[pos, character] for pos in get_col_pos(col, self.V)])
|
|
50
|
+
|
|
51
|
+
# a character clue on that side means the first character that appears on the side is the clue
|
|
52
|
+
for i, top_char in enumerate(self.top):
|
|
53
|
+
self.force_first_character(list(get_col_pos(i, self.V)), top_char)
|
|
54
|
+
for i, bottom_char in enumerate(self.bottom):
|
|
55
|
+
self.force_first_character(list(get_col_pos(i, self.V))[::-1], bottom_char)
|
|
56
|
+
for i, left_char in enumerate(self.left):
|
|
57
|
+
self.force_first_character(list(get_row_pos(i, self.H)), left_char)
|
|
58
|
+
for i, right_char in enumerate(self.right):
|
|
59
|
+
self.force_first_character(list(get_row_pos(i, self.H))[::-1], right_char)
|
|
60
|
+
|
|
61
|
+
def force_first_character(self, pos_list: list[Pos], target_character: str):
|
|
62
|
+
if not target_character:
|
|
63
|
+
return
|
|
64
|
+
for i, pos in enumerate(pos_list):
|
|
65
|
+
is_first_char = self.model.NewBoolVar(f'{i}:{target_character}:is_first_char')
|
|
66
|
+
and_constraint(self.model, is_first_char, [self.model_vars[pos, self.BLANK] for pos in pos_list[:i]] + [self.model_vars[pos_list[i], self.BLANK].Not()])
|
|
67
|
+
self.model.Add(self.model_vars[pos, target_character] == 1).OnlyEnforceIf(is_first_char)
|
|
68
|
+
|
|
69
|
+
def solve_and_print(self, verbose: bool = True):
|
|
70
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
71
|
+
return SingleSolution(assignment={pos: char for (pos, char), var in board.model_vars.items() if solver.Value(var) == 1 and char != board.BLANK})
|
|
72
|
+
def callback(single_res: SingleSolution):
|
|
73
|
+
print("Solution found")
|
|
74
|
+
print(combined_function(self.V, self.H, center_char=lambda r, c: single_res.assignment.get(get_pos(x=c, y=r), ''), text_on_shaded_cells=False))
|
|
75
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|