multi-puzzle-solver 1.0.6__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.6.dist-info → multi_puzzle_solver-1.0.7.dist-info}/METADATA +340 -265
- {multi_puzzle_solver-1.0.6.dist-info → multi_puzzle_solver-1.0.7.dist-info}/RECORD +9 -8
- puzzle_solver/__init__.py +3 -1
- puzzle_solver/puzzles/abc_view/abc_view.py +75 -0
- puzzle_solver/puzzles/heyawake/heyawake.py +67 -13
- puzzle_solver/puzzles/palisade/palisade.py +5 -20
- puzzle_solver/puzzles/shingoki/shingoki.py +61 -104
- {multi_puzzle_solver-1.0.6.dist-info → multi_puzzle_solver-1.0.7.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-1.0.6.dist-info → multi_puzzle_solver-1.0.7.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
puzzle_solver/__init__.py,sha256=
|
|
1
|
+
puzzle_solver/__init__.py,sha256=CiplXqjf9w0kuDmtvive0gM8hkZEIRQNgaK1A_X_ZwE,5390
|
|
2
2
|
puzzle_solver/core/utils.py,sha256=FE0106dfQRsgCn2FRBvRq5zILLK7-Z3cPHkAlBWUX0w,8785
|
|
3
3
|
puzzle_solver/core/utils_ortools.py,sha256=ACV3HgKWpEUTt1lpqsPryK1DeZpu7kdWQKEWTLJ2tfs,10384
|
|
4
4
|
puzzle_solver/core/utils_visualizer.py,sha256=AmVeBuEMaJzVY2zBbRoONlC9x3AGLKcdaNn8mD-nrLs,23347
|
|
5
|
+
puzzle_solver/puzzles/abc_view/abc_view.py,sha256=Qr0rZKmKQ2teStHjQ5VPQ4k-XptsjJAlZ1WXWk5Aax4,4570
|
|
5
6
|
puzzle_solver/puzzles/aquarium/aquarium.py,sha256=dGqYEWMoh4di5DN4sd-GtYb6QeTpVYFQJHBkrrmrudQ,5649
|
|
6
7
|
puzzle_solver/puzzles/battleships/battleships.py,sha256=U4xJ_NJC2baHvfaAfJ01YEBjixq9gD0h8GP9L1V-_oM,7223
|
|
7
8
|
puzzle_solver/puzzles/binairo/binairo.py,sha256=qKvpixLIBUcugAyJgpBGHV-9q_4nzA1ZOxeDFnltsXA,6843
|
|
@@ -21,7 +22,7 @@ puzzle_solver/puzzles/flood_it/parse_map/parse_map.py,sha256=m7gcpvN3THZdYLowdR_
|
|
|
21
22
|
puzzle_solver/puzzles/galaxies/galaxies.py,sha256=IiKPU3fz5Aokhj5OjeT5jd_vdNuWnLzjZylGOTepsNU,5600
|
|
22
23
|
puzzle_solver/puzzles/galaxies/parse_map/parse_map.py,sha256=XmFqVN_oRfq9AZFWy5ViUJ2Szjgx-srrRkFPJXEEyFo,9358
|
|
23
24
|
puzzle_solver/puzzles/guess/guess.py,sha256=MpyrF6YVu0S1fzX-BllwxGKRGacWJpeLbNn5GetuEyo,10792
|
|
24
|
-
puzzle_solver/puzzles/heyawake/heyawake.py,sha256=
|
|
25
|
+
puzzle_solver/puzzles/heyawake/heyawake.py,sha256=cBD0xHYvVOBAOiwAB_K9sjf1Hv3zL4GBiq1Dml27F6g,9085
|
|
25
26
|
puzzle_solver/puzzles/inertia/inertia.py,sha256=-Y5fr7aK20zwmGHsZql7pYCq1kyMZglvkVZ6uIDf1HA,5658
|
|
26
27
|
puzzle_solver/puzzles/inertia/tsp.py,sha256=mAhlSjCWespASeN8uLZ0JkYDw-ZqFEpal6NM-ubpCXw,15313
|
|
27
28
|
puzzle_solver/puzzles/inertia/parse_map/parse_map.py,sha256=x0d64gTBd0HC2lO5uOpX2VKWfwj8rRiz0mQM_lqNmWs,8457
|
|
@@ -38,13 +39,13 @@ puzzle_solver/puzzles/nonograms/nonograms.py,sha256=Q-VHI0IPR2igccnE617HPThj5tnB
|
|
|
38
39
|
puzzle_solver/puzzles/nonograms/nonograms_colored.py,sha256=Qy3CGNNs0MoZQv-qHyMMP8-fK7s7f_utK-Rc1hQaX_A,10750
|
|
39
40
|
puzzle_solver/puzzles/norinori/norinori.py,sha256=ZEDWrD7zvEuqXOdXGOrELh1n_mWzhzZa3chs6Zqd3Pc,4570
|
|
40
41
|
puzzle_solver/puzzles/nurikabe/nurikabe.py,sha256=hX0VcjPwO8PfY2kiIpQV45FWIvKRosFebk588tp5wzk,6603
|
|
41
|
-
puzzle_solver/puzzles/palisade/palisade.py,sha256=
|
|
42
|
+
puzzle_solver/puzzles/palisade/palisade.py,sha256=Stvgw0k_sml9Dj5RMRVf0EmU5a_eGf1fn65IXelR6Wk,4802
|
|
42
43
|
puzzle_solver/puzzles/pearl/pearl.py,sha256=slPVCzPObQLNk4EYqe55YR4JeRCUs07Mjdks1fWKZSY,6696
|
|
43
44
|
puzzle_solver/puzzles/pipes/pipes.py,sha256=2HDHCWhD-fLYTRoJsx15gOrsgt_SbjlGF2QDS5UX6m8,4680
|
|
44
45
|
puzzle_solver/puzzles/range/range.py,sha256=sJVMKzoT5unihMKurriAUTGLY0f7OQXSZfSHWezPPkw,3387
|
|
45
46
|
puzzle_solver/puzzles/rectangles/rectangles.py,sha256=6MuJHyw7woIljlqxt76zfhw8F2_2biKMFM7oiXdXZsg,7010
|
|
46
47
|
puzzle_solver/puzzles/shakashaka/shakashaka.py,sha256=PRpg_qI7XA3ysAo_g1TRJsT3VwB5Vial2UcFyBOMwKQ,9571
|
|
47
|
-
puzzle_solver/puzzles/shingoki/shingoki.py,sha256=
|
|
48
|
+
puzzle_solver/puzzles/shingoki/shingoki.py,sha256=Sra6i4kpVzIK6CabhK9yDVDLBuZiNS4daPimtnTgQu0,6777
|
|
48
49
|
puzzle_solver/puzzles/signpost/signpost.py,sha256=38LlMvP5Fx4qrTXmw4aNCt3yUbG3fhdSk6-YXmhAHFg,3861
|
|
49
50
|
puzzle_solver/puzzles/singles/singles.py,sha256=AugO2Gnd_OEyrxXUnqg3oPypdmeiFais_fu19-gm4Eg,2945
|
|
50
51
|
puzzle_solver/puzzles/slant/slant.py,sha256=l4q9g0BfqQsA6zsySiemJC5iFsqsO6LoqTUqcTEqYEk,5897
|
|
@@ -67,7 +68,7 @@ puzzle_solver/puzzles/unruly/unruly.py,sha256=_C6FhYm9rqwhlQa6TMTxYr3rWcP_QS-E93
|
|
|
67
68
|
puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=D0JacUdK5yPrfScmGqX-p8144VbwxfDgIaqF8hwLXlM,5039
|
|
68
69
|
puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
|
|
69
70
|
puzzle_solver/utils/visualizer.py,sha256=T2g5We9J3tkhyXWoN2OrIDIJDjt6w5sDd2ksOub0ZI8,6819
|
|
70
|
-
multi_puzzle_solver-1.0.
|
|
71
|
-
multi_puzzle_solver-1.0.
|
|
72
|
-
multi_puzzle_solver-1.0.
|
|
73
|
-
multi_puzzle_solver-1.0.
|
|
71
|
+
multi_puzzle_solver-1.0.7.dist-info/METADATA,sha256=WfWXNWvZZPMkq8SmkWShIMpG6S4dDZcvayWDFIyM9-A,452620
|
|
72
|
+
multi_puzzle_solver-1.0.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
73
|
+
multi_puzzle_solver-1.0.7.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
|
|
74
|
+
multi_puzzle_solver-1.0.7.dist-info/RECORD,,
|
puzzle_solver/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from puzzle_solver.puzzles.abc_view import abc_view as abc_view_solver
|
|
1
2
|
from puzzle_solver.puzzles.aquarium import aquarium as aquarium_solver
|
|
2
3
|
from puzzle_solver.puzzles.battleships import battleships as battleships_solver
|
|
3
4
|
from puzzle_solver.puzzles.binairo import binairo as binairo_solver
|
|
@@ -58,6 +59,7 @@ from puzzle_solver.puzzles.yin_yang import yin_yang as yin_yang_solver
|
|
|
58
59
|
from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
|
|
59
60
|
|
|
60
61
|
__all__ = [
|
|
62
|
+
abc_view_solver,
|
|
61
63
|
aquarium_solver,
|
|
62
64
|
battleships_solver,
|
|
63
65
|
binairo_solver,
|
|
@@ -117,4 +119,4 @@ __all__ = [
|
|
|
117
119
|
inertia_image_parser,
|
|
118
120
|
]
|
|
119
121
|
|
|
120
|
-
__version__ = '1.0.
|
|
122
|
+
__version__ = '1.0.7'
|
|
@@ -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)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
from ortools.sat.python import cp_model
|
|
3
3
|
|
|
4
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, get_neighbors4, get_pos, get_char
|
|
4
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_col_pos, get_neighbors4, get_pos, get_char, get_row_pos
|
|
5
5
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
6
|
-
from puzzle_solver.core.utils_visualizer import combined_function
|
|
6
|
+
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def return_3_consecutives(int_list: list[int]) -> list[tuple[int, int]]:
|
|
@@ -20,6 +20,17 @@ def return_3_consecutives(int_list: list[int]) -> list[tuple[int, int]]:
|
|
|
20
20
|
out.append((begin_idx, end_idx))
|
|
21
21
|
return out
|
|
22
22
|
|
|
23
|
+
|
|
24
|
+
def get_diagonal(pos1: Pos, pos2: Pos) -> list[Pos]:
|
|
25
|
+
assert pos1 != pos2, 'positions must be different'
|
|
26
|
+
dx = pos1.x - pos2.x
|
|
27
|
+
dy = pos1.y - pos2.y
|
|
28
|
+
assert abs(dx) == abs(dy), 'positions must be on a diagonal'
|
|
29
|
+
sdx = 1 if dx > 0 else -1
|
|
30
|
+
sdy = 1 if dy > 0 else -1
|
|
31
|
+
return [get_pos(x=pos2.x + i*sdx, y=pos2.y + i*sdy) for i in range(abs(dx) + 1)]
|
|
32
|
+
|
|
33
|
+
|
|
23
34
|
class Board:
|
|
24
35
|
def __init__(self, board: np.array, region_to_clue: dict[str, int]):
|
|
25
36
|
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
@@ -59,6 +70,10 @@ class Board:
|
|
|
59
70
|
force_connected_component(self.model, self.W)
|
|
60
71
|
# A straight (orthogonal) line of connected white cells cannot span across more than 2 regions.
|
|
61
72
|
self.disallow_white_lines_spanning_3_regions()
|
|
73
|
+
# straight diagonal black lines from side wall to horizontal wall are not allowed; because they would disconnect the white cells
|
|
74
|
+
self.disallow_full_black_diagonal()
|
|
75
|
+
# disallow a diagonal black line coming out of a wall of length N then coming back in on the same wall; because it would disconnect the white cells
|
|
76
|
+
self.disallow_zigzag_on_wall()
|
|
62
77
|
|
|
63
78
|
def disallow_white_lines_spanning_3_regions(self):
|
|
64
79
|
# A straight (orthogonal) line of connected white cells cannot span across more than 2 regions.
|
|
@@ -77,22 +92,61 @@ class Board:
|
|
|
77
92
|
pos_list = [get_pos(x=col_num, y=y) for y in range(begin_idx, end_idx+1)]
|
|
78
93
|
self.model.AddBoolOr([self.B[p] for p in pos_list])
|
|
79
94
|
|
|
80
|
-
def
|
|
95
|
+
def disallow_full_black_diagonal(self):
|
|
96
|
+
corners_dx_dy = [
|
|
97
|
+
((0, 0), 1, 1),
|
|
98
|
+
((self.H-1, 0), -1, 1),
|
|
99
|
+
((0, self.V-1), 1, -1),
|
|
100
|
+
((self.H-1, self.V-1), -1, -1),
|
|
101
|
+
]
|
|
102
|
+
for (corner_x, corner_y), dx, dy in corners_dx_dy:
|
|
103
|
+
for delta in range(1, min(self.V, self.H)):
|
|
104
|
+
pos1 = get_pos(x=corner_x, y=corner_y + delta*dy)
|
|
105
|
+
pos2 = get_pos(x=corner_x + delta*dx, y=corner_y)
|
|
106
|
+
diagonal_line = get_diagonal(pos1, pos2)
|
|
107
|
+
self.model.AddBoolOr([self.W[p] for p in diagonal_line])
|
|
108
|
+
|
|
109
|
+
def disallow_zigzag_on_wall(self):
|
|
110
|
+
for pos in get_row_pos(0, self.H): # top line
|
|
111
|
+
for end_x in range(pos.x + 2, self.H, 2): # end pos is even distance away from start pos
|
|
112
|
+
end_pos = get_pos(x=end_x, y=pos.y)
|
|
113
|
+
dx = end_x - pos.x
|
|
114
|
+
mid_pos = get_pos(x=pos.x + dx//2, y=pos.y + dx//2)
|
|
115
|
+
diag_1 = get_diagonal(pos, mid_pos) # from top wall to bottom triangle tip "\"
|
|
116
|
+
diag_2 = get_diagonal(end_pos, mid_pos) # from bottom triangle tip to top wall "/"
|
|
117
|
+
self.model.AddBoolOr([self.W[p] for p in diag_1 + diag_2])
|
|
118
|
+
for pos in get_row_pos(self.V-1, self.H): # bottom line
|
|
119
|
+
for end_x in range(pos.x + 2, self.H, 2): # end pos is even distance away from start pos
|
|
120
|
+
end_pos = get_pos(x=end_x, y=pos.y)
|
|
121
|
+
dx = end_x - pos.x
|
|
122
|
+
mid_pos = get_pos(x=pos.x + dx//2, y=pos.y - dx//2)
|
|
123
|
+
diag_1 = get_diagonal(pos, mid_pos) # from bottom wall to top triangle tip "/"
|
|
124
|
+
diag_2 = get_diagonal(end_pos, mid_pos) # from top triangle tip to bottom wall "\"
|
|
125
|
+
self.model.AddBoolOr([self.W[p] for p in diag_1 + diag_2])
|
|
126
|
+
for pos in get_col_pos(0, self.V): # left line
|
|
127
|
+
for end_y in range(pos.y + 2, self.V, 2): # end pos is even distance away from start pos
|
|
128
|
+
end_pos = get_pos(x=pos.x, y=end_y)
|
|
129
|
+
dy = end_y - pos.y
|
|
130
|
+
mid_pos = get_pos(x=pos.x + dy//2, y=pos.y + dy//2)
|
|
131
|
+
diag_1 = get_diagonal(pos, mid_pos) # from left wall to right triangle tip "\"
|
|
132
|
+
diag_2 = get_diagonal(end_pos, mid_pos) # from right triangle tip to left wall "/"
|
|
133
|
+
self.model.AddBoolOr([self.W[p] for p in diag_1 + diag_2])
|
|
134
|
+
for pos in get_col_pos(self.H-1, self.V): # right line
|
|
135
|
+
for end_y in range(pos.y + 2, self.V, 2): # end pos is even distance away from start pos
|
|
136
|
+
end_pos = get_pos(x=pos.x, y=end_y)
|
|
137
|
+
dy = end_y - pos.y
|
|
138
|
+
mid_pos = get_pos(x=pos.x - dy//2, y=pos.y + dy//2)
|
|
139
|
+
diag_1 = get_diagonal(pos, mid_pos) # from right wall to left triangle tip "/"
|
|
140
|
+
|
|
141
|
+
def solve_and_print(self, verbose: bool = True, max_solutions: int = 20):
|
|
81
142
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
82
|
-
assignment:
|
|
83
|
-
for pos, var in board.B.items():
|
|
84
|
-
assignment[pos] = 1 if solver.Value(var) == 1 else 0
|
|
85
|
-
return SingleSolution(assignment=assignment)
|
|
143
|
+
return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.B.items()})
|
|
86
144
|
def callback(single_res: SingleSolution):
|
|
87
145
|
print("Solution found")
|
|
88
|
-
# res = np.full((self.V, self.H), ' ', dtype=object)
|
|
89
|
-
# for pos in get_all_pos(self.V, self.H):
|
|
90
|
-
# c = 'B' if single_res.assignment[pos] == 1 else ' '
|
|
91
|
-
# set_char(res, pos, c)
|
|
92
|
-
# print(res)
|
|
93
146
|
print(combined_function(self.V, self.H,
|
|
147
|
+
cell_flags=id_board_to_wall_fn(self.board),
|
|
94
148
|
is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1,
|
|
95
149
|
center_char=lambda r, c: self.region_to_clue.get(int(self.board[r, c]), ''),
|
|
96
150
|
text_on_shaded_cells=False
|
|
97
151
|
))
|
|
98
|
-
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=
|
|
152
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=max_solutions)
|
|
@@ -9,9 +9,7 @@ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
|
9
9
|
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
# a shape on the 2d board is just a set of positions
|
|
14
|
-
Shape = frozenset[Pos]
|
|
12
|
+
Shape = frozenset[Pos] # a shape on the 2d board is just a set of positions
|
|
15
13
|
|
|
16
14
|
@dataclass(frozen=True)
|
|
17
15
|
class ShapeOnBoard:
|
|
@@ -36,7 +34,6 @@ def get_valid_translations(shape: Shape, board: np.array) -> set[Pos]:
|
|
|
36
34
|
# min x/y is always 0
|
|
37
35
|
max_x = max(p[0] for p in shape_list)
|
|
38
36
|
max_y = max(p[1] for p in shape_list)
|
|
39
|
-
|
|
40
37
|
for dy in range(0, board.shape[0] - max_y):
|
|
41
38
|
for dx in range(0, board.shape[1] - max_x):
|
|
42
39
|
body = tuple((p[0] + dx, p[1] + dy) for p in shape_list)
|
|
@@ -48,7 +45,6 @@ def get_valid_translations(shape: Shape, board: np.array) -> set[Pos]:
|
|
|
48
45
|
yield frozenset(get_pos(x=p[0], y=p[1]) for p in body)
|
|
49
46
|
|
|
50
47
|
|
|
51
|
-
|
|
52
48
|
class Board:
|
|
53
49
|
def __init__(self, board: np.array, region_size: int):
|
|
54
50
|
assert region_size >= 1 and isinstance(region_size, int), 'region_size must be an integer greater than or equal to 1'
|
|
@@ -59,7 +55,6 @@ class Board:
|
|
|
59
55
|
self.region_size = region_size
|
|
60
56
|
self.region_count = (self.V * self.H) // self.region_size
|
|
61
57
|
assert self.region_count * self.region_size == self.V * self.H, f'region_size must be a factor of the board size, got {self.region_size} and {self.region_count}'
|
|
62
|
-
|
|
63
58
|
self.polyominoes = polyominoes(self.region_size)
|
|
64
59
|
|
|
65
60
|
self.model = cp_model.CpModel()
|
|
@@ -74,9 +69,7 @@ class Board:
|
|
|
74
69
|
uid = len(self.shapes_on_board)
|
|
75
70
|
shape_on_board = ShapeOnBoard(
|
|
76
71
|
is_active=self.model.NewBoolVar(f'{uid}:is_active'),
|
|
77
|
-
shape=shape,
|
|
78
|
-
shape_id=uid,
|
|
79
|
-
body=body,
|
|
72
|
+
shape=shape, shape_id=uid, body=body
|
|
80
73
|
)
|
|
81
74
|
self.shapes_on_board.append(shape_on_board)
|
|
82
75
|
for pos in body:
|
|
@@ -88,19 +81,11 @@ class Board:
|
|
|
88
81
|
|
|
89
82
|
def solve_and_print(self, verbose: bool = True):
|
|
90
83
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
91
|
-
|
|
92
|
-
for shape in
|
|
93
|
-
if solver.Value(shape.is_active) == 1:
|
|
94
|
-
for pos in shape.body:
|
|
95
|
-
assignment[pos] = shape.shape_id
|
|
96
|
-
return SingleSolution(assignment=assignment)
|
|
84
|
+
active_shapes = [shape for shape in board.shapes_on_board if solver.Value(shape.is_active) == 1]
|
|
85
|
+
return SingleSolution(assignment={pos: shape.shape_id for shape in active_shapes for pos in shape.body})
|
|
97
86
|
def callback(single_res: SingleSolution):
|
|
98
87
|
print("Solution found")
|
|
99
|
-
id_board = np.full((self.V, self.H), ' ', dtype=object)
|
|
100
|
-
for pos in get_all_pos(self.V, self.H):
|
|
101
|
-
region_idx = single_res.assignment[pos]
|
|
102
|
-
set_char(id_board, pos, region_idx)
|
|
103
88
|
print(combined_function(self.V, self.H,
|
|
104
|
-
cell_flags=id_board_to_wall_fn(
|
|
89
|
+
cell_flags=id_board_to_wall_fn(np.array([[single_res.assignment[get_pos(x=c, y=r)] for c in range(self.H)] for r in range(self.V)])),
|
|
105
90
|
center_char=lambda r, c: self.board[r, c] if self.board[r, c] != ' ' else '·'))
|
|
106
91
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -1,35 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
from dataclasses import dataclass
|
|
3
|
-
import time
|
|
1
|
+
from collections import defaultdict
|
|
4
2
|
|
|
5
3
|
import numpy as np
|
|
6
4
|
from ortools.sat.python import cp_model
|
|
7
5
|
|
|
8
|
-
from puzzle_solver.core.utils import Direction, Pos, get_all_pos,
|
|
9
|
-
from puzzle_solver.core.utils_ortools import generic_solve_all, force_connected_component, and_constraint
|
|
6
|
+
from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_next_pos, get_char, in_bounds, set_char, get_pos, get_opposite_direction
|
|
7
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, force_connected_component, and_constraint, SingleSolution
|
|
10
8
|
from puzzle_solver.core.utils_visualizer import combined_function
|
|
11
9
|
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
class SingleSolution:
|
|
15
|
-
assignment: dict[tuple[Pos, Pos], int]
|
|
16
|
-
|
|
17
|
-
def get_hashable_solution(self) -> str:
|
|
18
|
-
result = []
|
|
19
|
-
for (pos, neighbor), v in self.assignment.items():
|
|
20
|
-
result.append((pos.x, pos.y, neighbor.x, neighbor.y, v))
|
|
21
|
-
return json.dumps(result, sort_keys=True)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def get_ray(pos: Pos, V: int, H: int, direction: Direction) -> list[tuple[Pos, Pos]]:
|
|
11
|
+
def get_ray(pos: Pos, V: int, H: int, direction: Direction) -> list[Pos]:
|
|
25
12
|
out = []
|
|
26
|
-
prev_pos = pos
|
|
27
13
|
while True:
|
|
14
|
+
out.append(pos)
|
|
28
15
|
pos = get_next_pos(pos, direction)
|
|
29
16
|
if not in_bounds(pos, V, H):
|
|
30
17
|
break
|
|
31
|
-
out.append((prev_pos, pos))
|
|
32
|
-
prev_pos = pos
|
|
33
18
|
return out
|
|
34
19
|
|
|
35
20
|
|
|
@@ -37,8 +22,8 @@ class Board:
|
|
|
37
22
|
def __init__(self, board: np.array):
|
|
38
23
|
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
39
24
|
assert all((c.item().strip() == '') or (str(c.item())[:-1].isdecimal() and c.item()[-1].upper() in ['B', 'W']) for c in np.nditer(board)), 'board must contain only space or digits and B/W'
|
|
40
|
-
|
|
41
25
|
self.V, self.H = board.shape
|
|
26
|
+
self.board = board
|
|
42
27
|
self.board_numbers: dict[Pos, int] = {}
|
|
43
28
|
self.board_colors: dict[Pos, str] = {}
|
|
44
29
|
for pos in get_all_pos(self.V, self.H):
|
|
@@ -47,112 +32,84 @@ class Board:
|
|
|
47
32
|
continue
|
|
48
33
|
self.board_numbers[pos] = int(c[:-1])
|
|
49
34
|
self.board_colors[pos] = c[-1].upper()
|
|
35
|
+
|
|
50
36
|
self.model = cp_model.CpModel()
|
|
51
|
-
self.
|
|
37
|
+
self.cell_active: dict[Pos, cp_model.IntVar] = {}
|
|
38
|
+
self.cell_direction: dict[tuple[Pos, Direction], cp_model.IntVar] = {}
|
|
52
39
|
|
|
53
40
|
self.create_vars()
|
|
54
41
|
self.add_all_constraints()
|
|
55
42
|
|
|
56
43
|
def create_vars(self):
|
|
57
44
|
for pos in get_all_pos(self.V, self.H):
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
self.cell_active[pos] = self.model.NewBoolVar(f'{pos}')
|
|
46
|
+
for direction in Direction:
|
|
47
|
+
neighbor = get_next_pos(pos, direction)
|
|
48
|
+
opposite_direction = get_opposite_direction(direction)
|
|
49
|
+
if not in_bounds(neighbor, self.V, self.H):
|
|
50
|
+
self.cell_direction[(pos, direction)] = self.model.NewConstant(0)
|
|
51
|
+
continue
|
|
52
|
+
if (neighbor, opposite_direction) in self.cell_direction:
|
|
53
|
+
self.cell_direction[(pos, direction)] = self.cell_direction[(neighbor, opposite_direction)]
|
|
54
|
+
else:
|
|
55
|
+
self.cell_direction[(pos, direction)] = self.model.NewBoolVar(f'{pos}-{neighbor}')
|
|
63
56
|
|
|
64
57
|
def add_all_constraints(self):
|
|
65
|
-
# each corners must have either 0 or 2 neighbors
|
|
66
|
-
for pos in get_all_pos(self.V, self.H):
|
|
67
|
-
corner_connections = [self.edge_vars[(pos, n)] for n in get_neighbors4(pos, self.V, self.H)]
|
|
68
|
-
if pos not in self.board_numbers: # no color, either 0 or 2 edges
|
|
69
|
-
self.model.AddLinearExpressionInDomain(sum(corner_connections), cp_model.Domain.FromValues([0, 2]))
|
|
70
|
-
else: # color, must have exactly 2 edges
|
|
71
|
-
self.model.Add(sum(corner_connections) == 2)
|
|
72
|
-
|
|
73
|
-
# enforce colors
|
|
74
58
|
for pos in get_all_pos(self.V, self.H):
|
|
59
|
+
s = sum([self.cell_direction[(pos, direction)] for direction in Direction])
|
|
60
|
+
self.model.Add(s == 2).OnlyEnforceIf(self.cell_active[pos])
|
|
61
|
+
self.model.Add(s == 0).OnlyEnforceIf(self.cell_active[pos].Not())
|
|
75
62
|
if pos not in self.board_numbers:
|
|
76
63
|
continue
|
|
77
|
-
self.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# enforce single connected component
|
|
81
|
-
def is_neighbor(edge1: tuple[Pos, Pos], edge2: tuple[Pos, Pos]) -> bool:
|
|
82
|
-
return any(c1 == c2 for c1 in edge1 for c2 in edge2)
|
|
83
|
-
force_connected_component(self.model, self.edge_vars, is_neighbor=is_neighbor)
|
|
64
|
+
self.enforce_corner_color_and_number(pos, self.board_colors[pos], self.board_numbers[pos]) # enforce colors and number
|
|
65
|
+
self.force_connected_component() # enforce single connected component
|
|
84
66
|
|
|
85
|
-
def
|
|
86
|
-
assert pos_color in ['W', 'B'], f'Invalid color: {pos_color}'
|
|
87
|
-
|
|
88
|
-
var_r = self.edge_vars[(pos, pos_r)] if (pos, pos_r) in self.edge_vars else False
|
|
89
|
-
pos_d = get_next_pos(pos, Direction.DOWN)
|
|
90
|
-
var_d = self.edge_vars[(pos, pos_d)] if (pos, pos_d) in self.edge_vars else False
|
|
91
|
-
pos_l = get_next_pos(pos, Direction.LEFT)
|
|
92
|
-
var_l = self.edge_vars[(pos, pos_l)] if (pos, pos_l) in self.edge_vars else False
|
|
93
|
-
pos_u = get_next_pos(pos, Direction.UP)
|
|
94
|
-
var_u = self.edge_vars[(pos, pos_u)] if (pos, pos_u) in self.edge_vars else False
|
|
67
|
+
def enforce_corner_color_and_number(self, pos: Pos, pos_color: str, pos_number: int):
|
|
68
|
+
assert pos_color in ['W', 'B'] and pos_number > 0, f'Invalid color or number: {pos_color}, {pos_number}'
|
|
69
|
+
self.model.Add(self.cell_active[pos] == 1)
|
|
95
70
|
if pos_color == 'W': # White circles must be passed through in a straight line
|
|
96
|
-
self.model.Add(
|
|
97
|
-
self.model.Add(
|
|
71
|
+
self.model.Add(self.cell_direction[(pos, Direction.RIGHT)] == self.cell_direction[(pos, Direction.LEFT)])
|
|
72
|
+
self.model.Add(self.cell_direction[(pos, Direction.DOWN)] == self.cell_direction[(pos, Direction.UP)])
|
|
98
73
|
elif pos_color == 'B': # Black circles must be turned upon
|
|
99
|
-
self.model.Add(
|
|
100
|
-
self.model.Add(
|
|
101
|
-
self.model.Add(
|
|
102
|
-
self.model.Add(
|
|
74
|
+
self.model.Add(self.cell_direction[(pos, Direction.RIGHT)] == 0).OnlyEnforceIf([self.cell_direction[(pos, Direction.LEFT)]])
|
|
75
|
+
self.model.Add(self.cell_direction[(pos, Direction.LEFT)] == 0).OnlyEnforceIf([self.cell_direction[(pos, Direction.RIGHT)]])
|
|
76
|
+
self.model.Add(self.cell_direction[(pos, Direction.DOWN)] == 0).OnlyEnforceIf([self.cell_direction[(pos, Direction.UP)]])
|
|
77
|
+
self.model.Add(self.cell_direction[(pos, Direction.UP)] == 0).OnlyEnforceIf([self.cell_direction[(pos, Direction.DOWN)]])
|
|
103
78
|
else:
|
|
104
79
|
raise ValueError(f'Invalid color: {pos_color}')
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
rays = get_ray(pos, self.V, self.H, direction) # cells outward
|
|
112
|
-
if not rays:
|
|
113
|
-
continue
|
|
114
|
-
# Chain: v0 = w[ray[0]]; vt = w[ray[t]] & vt-1
|
|
115
|
-
prev = None
|
|
116
|
-
for idx, (pos1, pos2) in enumerate(rays):
|
|
117
|
-
v = self.model.NewBoolVar(f"vis[{pos1}-{pos2}]->({direction.name})[{idx}]")
|
|
80
|
+
vis_vars: list[cp_model.IntVar] = [] # The numbers in the circles show the sum of the lengths of the 2 straight lines going out of that circle.
|
|
81
|
+
for direction in Direction: # Build visibility chains in four direction
|
|
82
|
+
ray = get_ray(pos, self.V, self.H, direction) # cells outward
|
|
83
|
+
for idx in range(len(ray)):
|
|
84
|
+
v = self.model.NewBoolVar(f"vis[{pos}]->({direction.name})[{idx}]")
|
|
85
|
+
and_constraint(self.model, target=v, cs=[self.cell_direction[(p, direction)] for p in ray[:idx+1]])
|
|
118
86
|
vis_vars.append(v)
|
|
119
|
-
if idx == 0:
|
|
120
|
-
# v0 == w[cell]
|
|
121
|
-
self.model.Add(v == self.edge_vars[(pos1, pos2)])
|
|
122
|
-
else:
|
|
123
|
-
and_constraint(self.model, target=v, cs=[self.edge_vars[(pos1, pos2)], prev])
|
|
124
|
-
prev = v
|
|
125
87
|
self.model.Add(sum(vis_vars) == pos_number)
|
|
126
88
|
|
|
89
|
+
def force_connected_component(self):
|
|
90
|
+
def is_neighbor(pd1: tuple[Pos, Direction], pd2: tuple[Pos, Direction]) -> bool:
|
|
91
|
+
p1, d1 = pd1
|
|
92
|
+
p2, d2 = pd2
|
|
93
|
+
if p1 == p2 and d1 != d2: # same position, different direction, is neighbor
|
|
94
|
+
return True
|
|
95
|
+
if get_next_pos(p1, d1) == p2 and d2 == get_opposite_direction(d1):
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
force_connected_component(self.model, self.cell_direction, is_neighbor=is_neighbor)
|
|
127
99
|
|
|
128
100
|
def solve_and_print(self, verbose: bool = True):
|
|
129
|
-
tic = time.time()
|
|
130
101
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
131
|
-
assignment: dict[
|
|
132
|
-
for (pos,
|
|
133
|
-
assignment[
|
|
102
|
+
assignment: dict[Pos, str] = defaultdict(str)
|
|
103
|
+
for (pos, direction), var in board.cell_direction.items():
|
|
104
|
+
assignment[pos] += direction.name[0] if solver.BooleanValue(var) else ''
|
|
134
105
|
return SingleSolution(assignment=assignment)
|
|
135
106
|
def callback(single_res: SingleSolution):
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
min_y = min(pos.y, neighbor.y)
|
|
145
|
-
dx = abs(pos.x - neighbor.x)
|
|
146
|
-
dy = abs(pos.y - neighbor.y)
|
|
147
|
-
if min_x == self.H - 1: # only way to get right
|
|
148
|
-
res[min_y][min_x - 1] += 'R'
|
|
149
|
-
elif min_y == self.V - 1: # only way to get down
|
|
150
|
-
res[min_y - 1][min_x] += 'D'
|
|
151
|
-
elif dx == 1:
|
|
152
|
-
res[min_y][min_x] += 'U'
|
|
153
|
-
elif dy == 1:
|
|
154
|
-
res[min_y][min_x] += 'L'
|
|
155
|
-
else:
|
|
156
|
-
raise ValueError(f'Invalid position: {pos} and {neighbor}')
|
|
157
|
-
print(combined_function(self.V - 1, self.H - 1, cell_flags=lambda r, c: res[r, c], center_char=lambda r, c: '.'))
|
|
107
|
+
print("Solution found")
|
|
108
|
+
output_board = np.full((self.V, self.H), '', dtype=object)
|
|
109
|
+
for pos in get_all_pos(self.V, self.H):
|
|
110
|
+
if get_char(self.board, pos)[-1] in ['B', 'W']: # if the main board has a white or black pearl, put it in the output
|
|
111
|
+
set_char(output_board, pos, get_char(self.board, pos))
|
|
112
|
+
if not single_res.assignment[pos].strip(): # if the cell does not the line through it, put a dot
|
|
113
|
+
set_char(output_board, pos, '.')
|
|
114
|
+
print(combined_function(self.V, self.H, show_grid=False, special_content=lambda r, c: single_res.assignment[get_pos(x=c, y=r)], center_char=lambda r, c: output_board[r, c]))
|
|
158
115
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
File without changes
|
|
File without changes
|