multi-puzzle-solver 0.9.26__py3-none-any.whl → 0.9.30__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.26.dist-info → multi_puzzle_solver-0.9.30.dist-info}/METADATA +982 -41
- {multi_puzzle_solver-0.9.26.dist-info → multi_puzzle_solver-0.9.30.dist-info}/RECORD +20 -12
- puzzle_solver/__init__.py +8 -1
- puzzle_solver/core/utils.py +0 -153
- puzzle_solver/core/utils_visualizer.py +523 -0
- puzzle_solver/puzzles/binairo/binairo.py +44 -16
- puzzle_solver/puzzles/binairo/binairo_plus.py +7 -0
- puzzle_solver/puzzles/heyawake/heyawake.py +94 -0
- puzzle_solver/puzzles/kakuro/kakuro.py +77 -0
- puzzle_solver/puzzles/nurikabe/nurikabe.py +126 -0
- puzzle_solver/puzzles/palisade/palisade.py +2 -1
- puzzle_solver/puzzles/rectangles/rectangles.py +2 -2
- puzzle_solver/puzzles/shakashaka/shakashaka.py +201 -0
- puzzle_solver/puzzles/shingoki/shingoki.py +158 -0
- puzzle_solver/puzzles/singles/singles.py +14 -40
- puzzle_solver/puzzles/slitherlink/slitherlink.py +2 -1
- puzzle_solver/puzzles/stitches/parse_map/parse_map.py +3 -1
- puzzle_solver/puzzles/tapa/tapa.py +98 -0
- {multi_puzzle_solver-0.9.26.dist-info → multi_puzzle_solver-0.9.30.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.9.26.dist-info → multi_puzzle_solver-0.9.30.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from ortools.sat.python import cp_model
|
|
7
|
+
|
|
8
|
+
from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_neighbors4, get_next_pos, get_char, in_bounds
|
|
9
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, force_connected_component, and_constraint
|
|
10
|
+
from puzzle_solver.core.utils_visualizer import render_grid
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
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]]:
|
|
25
|
+
out = []
|
|
26
|
+
prev_pos = pos
|
|
27
|
+
while True:
|
|
28
|
+
pos = get_next_pos(pos, direction)
|
|
29
|
+
if not in_bounds(pos, V, H):
|
|
30
|
+
break
|
|
31
|
+
out.append((prev_pos, pos))
|
|
32
|
+
prev_pos = pos
|
|
33
|
+
return out
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Board:
|
|
37
|
+
def __init__(self, board: np.array):
|
|
38
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
39
|
+
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
|
+
self.V, self.H = board.shape
|
|
42
|
+
self.board_numbers: dict[Pos, int] = {}
|
|
43
|
+
self.board_colors: dict[Pos, str] = {}
|
|
44
|
+
for pos in get_all_pos(self.V, self.H):
|
|
45
|
+
c = get_char(board, pos)
|
|
46
|
+
if c.strip() == '':
|
|
47
|
+
continue
|
|
48
|
+
self.board_numbers[pos] = int(c[:-1])
|
|
49
|
+
self.board_colors[pos] = c[-1].upper()
|
|
50
|
+
self.model = cp_model.CpModel()
|
|
51
|
+
self.edge_vars: dict[tuple[Pos, Pos], cp_model.IntVar] = {}
|
|
52
|
+
|
|
53
|
+
self.create_vars()
|
|
54
|
+
self.add_all_constraints()
|
|
55
|
+
|
|
56
|
+
def create_vars(self):
|
|
57
|
+
for pos in get_all_pos(self.V, self.H):
|
|
58
|
+
for neighbor in get_neighbors4(pos, self.V, self.H):
|
|
59
|
+
if (neighbor, pos) in self.edge_vars: # already added in opposite direction
|
|
60
|
+
self.edge_vars[(pos, neighbor)] = self.edge_vars[(neighbor, pos)]
|
|
61
|
+
else: # new edge
|
|
62
|
+
self.edge_vars[(pos, neighbor)] = self.model.NewBoolVar(f'{pos}-{neighbor}')
|
|
63
|
+
|
|
64
|
+
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
|
+
for pos in get_all_pos(self.V, self.H):
|
|
75
|
+
if pos not in self.board_numbers:
|
|
76
|
+
continue
|
|
77
|
+
self.enforce_corner_color(pos, self.board_colors[pos])
|
|
78
|
+
self.enforce_corner_number(pos, self.board_numbers[pos])
|
|
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)
|
|
84
|
+
|
|
85
|
+
def enforce_corner_color(self, pos: Pos, pos_color: str):
|
|
86
|
+
assert pos_color in ['W', 'B'], f'Invalid color: {pos_color}'
|
|
87
|
+
pos_r = get_next_pos(pos, Direction.RIGHT)
|
|
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
|
|
95
|
+
if pos_color == 'W': # White circles must be passed through in a straight line
|
|
96
|
+
self.model.Add(var_r == var_l)
|
|
97
|
+
self.model.Add(var_u == var_d)
|
|
98
|
+
elif pos_color == 'B': # Black circles must be turned upon
|
|
99
|
+
self.model.Add(var_r == 0).OnlyEnforceIf([var_l])
|
|
100
|
+
self.model.Add(var_l == 0).OnlyEnforceIf([var_r])
|
|
101
|
+
self.model.Add(var_u == 0).OnlyEnforceIf([var_d])
|
|
102
|
+
self.model.Add(var_d == 0).OnlyEnforceIf([var_u])
|
|
103
|
+
else:
|
|
104
|
+
raise ValueError(f'Invalid color: {pos_color}')
|
|
105
|
+
|
|
106
|
+
def enforce_corner_number(self, pos: Pos, pos_number: int):
|
|
107
|
+
# The numbers in the circles show the sum of the lengths of the 2 straight lines going out of that circle.
|
|
108
|
+
# Build visibility chains per direction (exclude self)
|
|
109
|
+
vis_vars: list[cp_model.IntVar] = []
|
|
110
|
+
for direction in Direction:
|
|
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}]")
|
|
118
|
+
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
|
+
self.model.Add(sum(vis_vars) == pos_number)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def solve_and_print(self, verbose: bool = True):
|
|
129
|
+
tic = time.time()
|
|
130
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
131
|
+
assignment: dict[tuple[Pos, Pos], int] = {}
|
|
132
|
+
for (pos, neighbor), var in board.edge_vars.items():
|
|
133
|
+
assignment[(pos, neighbor)] = solver.Value(var)
|
|
134
|
+
return SingleSolution(assignment=assignment)
|
|
135
|
+
def callback(single_res: SingleSolution):
|
|
136
|
+
nonlocal tic
|
|
137
|
+
print(f"Solution found in {time.time() - tic:.2f} seconds")
|
|
138
|
+
tic = time.time()
|
|
139
|
+
res = np.full((self.V - 1, self.H - 1), ' ', dtype=object)
|
|
140
|
+
for (pos, neighbor), v in single_res.assignment.items():
|
|
141
|
+
if v == 0:
|
|
142
|
+
continue
|
|
143
|
+
min_x = min(pos.x, neighbor.x)
|
|
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(render_grid(res, center_char='.'))
|
|
158
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -1,8 +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_char,
|
|
4
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_neighbors4, get_all_pos_to_idx_dict, get_row_pos, get_col_pos, get_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 render_shaded_grid
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class Board:
|
|
@@ -14,62 +15,35 @@ class Board:
|
|
|
14
15
|
self.H = board.shape[1]
|
|
15
16
|
self.N = self.V * self.H
|
|
16
17
|
self.idx_of: dict[Pos, int] = get_all_pos_to_idx_dict(self.V, self.H)
|
|
17
|
-
|
|
18
18
|
self.model = cp_model.CpModel()
|
|
19
|
-
self.B = {}
|
|
20
|
-
self.W = {}
|
|
19
|
+
self.B = {}
|
|
20
|
+
self.W = {}
|
|
21
21
|
self.Num = {} # value of squares (Num = N + idx if black, else board[pos])
|
|
22
|
-
|
|
23
22
|
self.create_vars()
|
|
24
23
|
self.add_all_constraints()
|
|
25
24
|
|
|
26
25
|
def create_vars(self):
|
|
27
26
|
for pos in get_all_pos(self.V, self.H):
|
|
28
27
|
self.B[pos] = self.model.NewBoolVar(f'B:{pos}')
|
|
29
|
-
self.W[pos] = self.
|
|
30
|
-
# either black or white
|
|
31
|
-
self.model.AddExactlyOne([self.B[pos], self.W[pos]])
|
|
28
|
+
self.W[pos] = self.B[pos].Not()
|
|
32
29
|
self.Num[pos] = self.model.NewIntVar(0, 2*self.N, f'{pos}')
|
|
33
30
|
self.model.Add(self.Num[pos] == self.N + self.idx_of[pos]).OnlyEnforceIf(self.B[pos])
|
|
34
|
-
self.model.Add(self.Num[pos] == int(get_char(self.board, pos))).OnlyEnforceIf(self.
|
|
31
|
+
self.model.Add(self.Num[pos] == int(get_char(self.board, pos))).OnlyEnforceIf(self.W[pos])
|
|
35
32
|
|
|
36
33
|
def add_all_constraints(self):
|
|
37
|
-
self.
|
|
38
|
-
self.no_number_appears_twice()
|
|
39
|
-
self.force_connected_component()
|
|
40
|
-
|
|
41
|
-
def no_adjacent_blacks(self):
|
|
42
|
-
# no two black squares are adjacent
|
|
43
|
-
for pos in get_all_pos(self.V, self.H):
|
|
34
|
+
for pos in get_all_pos(self.V, self.H): # no two black squares are adjacent
|
|
44
35
|
for neighbor in get_neighbors4(pos, self.V, self.H):
|
|
45
36
|
self.model.Add(self.B[pos] + self.B[neighbor] <= 1)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# no number appears twice in any
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
self.model.AddAllDifferent(var_list)
|
|
52
|
-
for col in range(self.H):
|
|
53
|
-
var_list = [self.Num[pos] for pos in get_col_pos(col, self.V)]
|
|
54
|
-
self.model.AddAllDifferent(var_list)
|
|
55
|
-
|
|
56
|
-
def force_connected_component(self):
|
|
57
|
-
force_connected_component(self.model, self.W)
|
|
58
|
-
|
|
59
|
-
|
|
37
|
+
for row in range(self.V): # no number appears twice in any row (numbers are ignored if black)
|
|
38
|
+
self.model.AddAllDifferent([self.Num[pos] for pos in get_row_pos(row, self.H)])
|
|
39
|
+
for col in range(self.H): # no number appears twice in any column (numbers are ignored if black)
|
|
40
|
+
self.model.AddAllDifferent([self.Num[pos] for pos in get_col_pos(col, self.V)])
|
|
41
|
+
force_connected_component(self.model, self.W) # all white squares must be a single connected component
|
|
60
42
|
|
|
61
43
|
def solve_and_print(self, verbose: bool = True):
|
|
62
44
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
63
|
-
assignment:
|
|
64
|
-
for pos, var in board.B.items():
|
|
65
|
-
assignment[pos] = solver.value(var)
|
|
66
|
-
return SingleSolution(assignment=assignment)
|
|
45
|
+
return SingleSolution(assignment={pos: 1 if solver.Value(val) == 1 else 0 for pos, val in board.B.items()})
|
|
67
46
|
def callback(single_res: SingleSolution):
|
|
68
47
|
print("Solution found")
|
|
69
|
-
|
|
70
|
-
for pos in get_all_pos(self.V, self.H):
|
|
71
|
-
c = get_char(self.board, pos)
|
|
72
|
-
c = 'B' if single_res.assignment[pos] == 1 else ' '
|
|
73
|
-
set_char(res, pos, c)
|
|
74
|
-
print(res)
|
|
48
|
+
print(render_shaded_grid(self.V, self.H, lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1, empty_text=lambda r, c: self.board[r, c]))
|
|
75
49
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -2,8 +2,9 @@ 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,
|
|
5
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, Direction, get_row_pos, get_col_pos, get_next_pos, in_bounds, get_opposite_direction
|
|
6
6
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
7
|
+
from puzzle_solver.core.utils_visualizer import render_grid
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
CellBorder = tuple[Pos, Direction]
|
|
@@ -242,4 +242,6 @@ if __name__ == '__main__':
|
|
|
242
242
|
# main(Path(__file__).parent / 'input_output' / 'LITS_MDoxNzksNzY3.png')
|
|
243
243
|
# main(Path(__file__).parent / 'input_output' / 'lits_OTo3LDMwNiwwMTU=.png')
|
|
244
244
|
# main(Path(__file__).parent / 'input_output' / 'norinori_501d93110d6b4b818c268378973afbf268f96cfa8d7b4.png')
|
|
245
|
-
main(Path(__file__).parent / 'input_output' / 'norinori_OTo0LDc0Miw5MTU.png')
|
|
245
|
+
# main(Path(__file__).parent / 'input_output' / 'norinori_OTo0LDc0Miw5MTU.png')
|
|
246
|
+
# main(Path(__file__).parent / 'input_output' / 'heyawake_MDoxNiwxNDQ=.png')
|
|
247
|
+
main(Path(__file__).parent / 'input_output' / 'heyawake_MTQ6ODQ4LDEzOQ==.png')
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
from itertools import combinations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from ortools.sat.python import cp_model
|
|
6
|
+
|
|
7
|
+
from puzzle_solver.core.utils import Direction8, Pos, get_all_pos, set_char, get_char, in_bounds, Direction, get_next_pos, get_pos
|
|
8
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import render_shaded_grid
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def rotated_assignments_N_nums(Xs: tuple[int, ...], target_length: int = 8) -> set[tuple[bool, ...]]:
|
|
13
|
+
""" Given Xs = [X1, X2, ..., Xm] (each Xi >= 1), build all unique length-`target_length`
|
|
14
|
+
boolean lists of the form: [ True*X1, False*N1, True*X2, False*N2, ..., True*Xm, False*Nm ]
|
|
15
|
+
where each Ni >= 1 and sum(Xs) + sum(Ni) = target_length,
|
|
16
|
+
including all `target_length` wrap-around rotations, de-duplicated.
|
|
17
|
+
"""
|
|
18
|
+
assert len(Xs) >= 1, "Xs must have at least one block length."
|
|
19
|
+
assert all(x >= 1 for x in Xs), "All Xi must be >= 1."
|
|
20
|
+
assert sum(Xs) + len(Xs) <= target_length, f"sum(Xs) + len(Xs) <= target_length required; got {sum(Xs)} + {len(Xs)} > {target_length}"
|
|
21
|
+
num_zero_blocks = len(Xs)
|
|
22
|
+
total_zeros = target_length - sum(Xs)
|
|
23
|
+
seen: set[tuple[bool, ...]] = set()
|
|
24
|
+
for cut_positions in combinations(range(1, total_zeros), num_zero_blocks - 1):
|
|
25
|
+
cut_positions = (*cut_positions, total_zeros)
|
|
26
|
+
Ns = [cut_positions[0]] # length of zero blocks
|
|
27
|
+
for i in range(1, len(cut_positions)):
|
|
28
|
+
Ns.append(cut_positions[i] - cut_positions[i - 1])
|
|
29
|
+
base: list[bool] = []
|
|
30
|
+
for x, n in zip(Xs, Ns):
|
|
31
|
+
base.extend([True] * x)
|
|
32
|
+
base.extend([False] * n)
|
|
33
|
+
for dx in range(target_length): # all rotations (wrap-around)
|
|
34
|
+
rot = tuple(base[dx:] + base[:dx])
|
|
35
|
+
seen.add(rot)
|
|
36
|
+
return seen
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Board:
|
|
40
|
+
def __init__(self, board: np.array, separator: str = '/'):
|
|
41
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
42
|
+
assert all(all(str(c).strip().isdecimal() or str(c).strip() == '' for c in cell.item().split(separator)) for cell in np.nditer(board)), 'board must contain only digits and separator'
|
|
43
|
+
self.V, self.H = board.shape
|
|
44
|
+
self.board = board
|
|
45
|
+
self.separator = separator
|
|
46
|
+
self.model = cp_model.CpModel()
|
|
47
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
48
|
+
self.create_vars()
|
|
49
|
+
self.add_all_constraints()
|
|
50
|
+
|
|
51
|
+
def create_vars(self):
|
|
52
|
+
for pos in get_all_pos(self.V, self.H):
|
|
53
|
+
self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
|
|
54
|
+
|
|
55
|
+
def add_all_constraints(self):
|
|
56
|
+
# 2x2 blacks are not allowed
|
|
57
|
+
for pos in get_all_pos(self.V, self.H):
|
|
58
|
+
tl = pos
|
|
59
|
+
tr = get_next_pos(pos, Direction.RIGHT)
|
|
60
|
+
bl = get_next_pos(pos, Direction.DOWN)
|
|
61
|
+
br = get_next_pos(bl, Direction.RIGHT)
|
|
62
|
+
if any(not in_bounds(p, self.V, self.H) for p in [tl, tr, bl, br]):
|
|
63
|
+
continue
|
|
64
|
+
self.model.AddBoolOr([self.model_vars[tl].Not(), self.model_vars[tr].Not(), self.model_vars[bl].Not(), self.model_vars[br].Not()])
|
|
65
|
+
for pos in get_all_pos(self.V, self.H):
|
|
66
|
+
c = get_char(self.board, pos)
|
|
67
|
+
if c.strip() == '':
|
|
68
|
+
continue
|
|
69
|
+
clue = tuple(int(x.strip()) for x in c.split(self.separator))
|
|
70
|
+
self.model.Add(self.model_vars[pos] == 0) # clue cannot be black
|
|
71
|
+
self.enforce_clue(pos, clue) # each clue must be satisfied
|
|
72
|
+
# all blacks are connected
|
|
73
|
+
force_connected_component(self.model, self.model_vars)
|
|
74
|
+
|
|
75
|
+
def enforce_clue(self, pos: Pos, clue: Union[int, tuple[int, int]]):
|
|
76
|
+
neighbors = []
|
|
77
|
+
for direction in [Direction8.UP, Direction8.UP_RIGHT, Direction8.RIGHT, Direction8.DOWN_RIGHT, Direction8.DOWN, Direction8.DOWN_LEFT, Direction8.LEFT, Direction8.UP_LEFT]:
|
|
78
|
+
n = get_next_pos(pos, direction)
|
|
79
|
+
neighbors.append(self.model_vars[n] if in_bounds(n, self.V, self.H) else self.model.NewConstant(False))
|
|
80
|
+
valid_assignments = rotated_assignments_N_nums(Xs=clue)
|
|
81
|
+
self.model.AddAllowedAssignments(neighbors, valid_assignments)
|
|
82
|
+
|
|
83
|
+
def solve_and_print(self, verbose: bool = True):
|
|
84
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
85
|
+
assignment: dict[Pos, int] = {}
|
|
86
|
+
for pos, var in board.model_vars.items():
|
|
87
|
+
assignment[pos] = solver.Value(var)
|
|
88
|
+
return SingleSolution(assignment=assignment)
|
|
89
|
+
def callback(single_res: SingleSolution):
|
|
90
|
+
print("Solution found")
|
|
91
|
+
board_justified = np.full((self.V, self.H), ' ', dtype=object)
|
|
92
|
+
for pos in get_all_pos(self.V, self.H):
|
|
93
|
+
c = get_char(self.board, pos).strip()
|
|
94
|
+
if len(c) > 3:
|
|
95
|
+
c = '...'
|
|
96
|
+
set_char(board_justified, pos, ' ' * (2 - len(c)) + c)
|
|
97
|
+
print(render_shaded_grid(self.V, self.H, lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1, empty_text=lambda r, c: str(board_justified[r, c])))
|
|
98
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=5)
|
|
File without changes
|
|
File without changes
|