multi-puzzle-solver 1.0.6__py3-none-any.whl → 1.0.8__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.8.dist-info}/METADATA +421 -265
- {multi_puzzle_solver-1.0.6.dist-info → multi_puzzle_solver-1.0.8.dist-info}/RECORD +13 -11
- puzzle_solver/__init__.py +5 -1
- puzzle_solver/core/utils_visualizer.py +565 -561
- puzzle_solver/puzzles/abc_view/abc_view.py +75 -0
- puzzle_solver/puzzles/heyawake/heyawake.py +67 -13
- puzzle_solver/puzzles/mathema_grids/mathema_grids.py +119 -0
- puzzle_solver/puzzles/nonograms/nonograms_colored.py +220 -221
- puzzle_solver/puzzles/palisade/palisade.py +91 -106
- puzzle_solver/puzzles/shingoki/shingoki.py +61 -104
- puzzle_solver/puzzles/tracks/tracks.py +1 -1
- {multi_puzzle_solver-1.0.6.dist-info → multi_puzzle_solver-1.0.8.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-1.0.6.dist-info → multi_puzzle_solver-1.0.8.dist-info}/top_level.txt +0 -0
|
@@ -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)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ortools.sat.python import cp_model
|
|
4
|
+
from ortools.util.python import sorted_interval_list
|
|
5
|
+
|
|
6
|
+
from puzzle_solver.core.utils import Direction, Pos, get_char, get_next_pos, get_row_pos, get_col_pos, in_bounds, set_char
|
|
7
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
8
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class var_with_bounds:
|
|
13
|
+
var: cp_model.IntVar
|
|
14
|
+
min_value: int
|
|
15
|
+
max_value: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _div_bounds(a_min: int, a_max: int, b_min: int, b_max: int) -> tuple[int, int]:
|
|
19
|
+
assert not (b_min == 0 and b_max == 0), "Denominator interval cannot be [0, 0]."
|
|
20
|
+
denoms = [b_min, b_max]
|
|
21
|
+
if 0 in denoms:
|
|
22
|
+
denoms.remove(0)
|
|
23
|
+
if b_min <= -1:
|
|
24
|
+
denoms += [-1]
|
|
25
|
+
if b_max >= 1:
|
|
26
|
+
denoms += [1]
|
|
27
|
+
candidates = [a_min // d for d in denoms] + [a_max // d for d in denoms]
|
|
28
|
+
return min(candidates), max(candidates)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Board:
|
|
32
|
+
def __init__(self, board: np.array, digits: list[int]):
|
|
33
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
34
|
+
self.board = board
|
|
35
|
+
self.V, self.H = board.shape
|
|
36
|
+
assert self.V >= 3 and self.V % 2 == 1, f'board must have at least 3 rows and an odd number of rows. Got {self.V} rows.'
|
|
37
|
+
assert self.H >= 3 and self.H % 2 == 1, f'board must have at least 3 columns and an odd number of columns. Got {self.H} columns.'
|
|
38
|
+
self.digits = digits
|
|
39
|
+
self.domain_values = sorted_interval_list.Domain.FromValues(self.digits)
|
|
40
|
+
self.domain_values_no_zero = sorted_interval_list.Domain.FromValues([d for d in self.digits if d != 0])
|
|
41
|
+
|
|
42
|
+
self.model = cp_model.CpModel()
|
|
43
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
44
|
+
self.create_vars()
|
|
45
|
+
assert len(self.model_vars) == len(self.digits), f'len(model_vars) != len(digits), {len(self.model_vars)} != {len(self.digits)}'
|
|
46
|
+
self.model.AddAllDifferent(list(self.model_vars.values()))
|
|
47
|
+
|
|
48
|
+
def create_vars(self):
|
|
49
|
+
for row in range(0, self.V-2, 2):
|
|
50
|
+
line_pos = [pos for pos in get_row_pos(row, self.H)]
|
|
51
|
+
self.parse_line(line_pos)
|
|
52
|
+
for col in range(0, self.H-2, 2):
|
|
53
|
+
line_pos = [pos for pos in get_col_pos(col, self.V)]
|
|
54
|
+
self.parse_line(line_pos)
|
|
55
|
+
|
|
56
|
+
def parse_line(self, line_pos: list[Pos]) -> list[int]:
|
|
57
|
+
last_num = get_char(self.board, line_pos[-1])
|
|
58
|
+
equal_sign = get_char(self.board, line_pos[-2])
|
|
59
|
+
assert equal_sign == '=', f'last element of line must be =, got {equal_sign}'
|
|
60
|
+
line_pos = line_pos[:-2]
|
|
61
|
+
operators = [get_char(self.board, pos) for pos in line_pos[1::2]]
|
|
62
|
+
assert all(c.strip() in ['+', '-', '*', '/'] for c in operators), f'even indices of line must be operators, got {operators}'
|
|
63
|
+
digits_pos = line_pos[::2]
|
|
64
|
+
running_var = self.get_var(digits_pos[0], fixed=get_char(self.board, digits_pos[0]))
|
|
65
|
+
for pos, operator in zip(digits_pos[1:], operators):
|
|
66
|
+
running_var = self.apply_operator(operator, running_var, self.get_var(pos, fixed=get_char(self.board, pos)))
|
|
67
|
+
self.model.Add(running_var.var == int(last_num))
|
|
68
|
+
return running_var
|
|
69
|
+
|
|
70
|
+
def get_var(self, pos: Pos, fixed: str) -> var_with_bounds:
|
|
71
|
+
if pos not in self.model_vars:
|
|
72
|
+
domain = self.domain_values_no_zero if self.might_be_denominator(pos) else self.domain_values
|
|
73
|
+
self.model_vars[pos] = self.model.NewIntVarFromDomain(domain, f'{pos}')
|
|
74
|
+
if fixed.strip():
|
|
75
|
+
self.model.Add(self.model_vars[pos] == int(fixed))
|
|
76
|
+
return var_with_bounds(var=self.model_vars[pos], min_value=min(self.digits), max_value=max(self.digits))
|
|
77
|
+
|
|
78
|
+
def might_be_denominator(self, pos: Pos) -> bool:
|
|
79
|
+
"Important since if the variable might be a denominator and the domain includes 0 then ortools immediately sets the model as INVALID"
|
|
80
|
+
above_pos = get_next_pos(pos, Direction.UP)
|
|
81
|
+
left_pos = get_next_pos(pos, Direction.LEFT)
|
|
82
|
+
above_operator = get_char(self.board, above_pos) if in_bounds(above_pos, self.V, self.H) else None
|
|
83
|
+
left_operator = get_char(self.board, left_pos) if in_bounds(left_pos, self.V, self.H) else None
|
|
84
|
+
return above_operator == '/' or left_operator == '/'
|
|
85
|
+
|
|
86
|
+
def apply_operator(self, operator: str, a: var_with_bounds, b: var_with_bounds) -> var_with_bounds:
|
|
87
|
+
assert operator in ['+', '-', '*', '/'], f'invalid operator: {operator}'
|
|
88
|
+
if operator == "+":
|
|
89
|
+
lo = a.min_value + b.min_value
|
|
90
|
+
hi = a.max_value + b.max_value
|
|
91
|
+
res = self.model.NewIntVar(lo, hi, "sum")
|
|
92
|
+
self.model.Add(res == a.var + b.var)
|
|
93
|
+
elif operator == "-":
|
|
94
|
+
lo = a.min_value - b.max_value
|
|
95
|
+
hi = a.max_value - b.min_value
|
|
96
|
+
res = self.model.NewIntVar(lo, hi, "diff")
|
|
97
|
+
self.model.Add(res == a.var - b.var)
|
|
98
|
+
elif operator == "*":
|
|
99
|
+
cands = [a.min_value*b.min_value, a.min_value*b.max_value, a.max_value*b.min_value, a.max_value*b.max_value]
|
|
100
|
+
lo, hi = min(cands), max(cands)
|
|
101
|
+
res = self.model.NewIntVar(lo, hi, "prod")
|
|
102
|
+
self.model.AddMultiplicationEquality(res, [a.var, b.var])
|
|
103
|
+
elif operator == "/":
|
|
104
|
+
self.model.Add(b.var != 0)
|
|
105
|
+
lo, hi = _div_bounds(a.min_value, a.max_value, b.min_value, b.max_value)
|
|
106
|
+
res = self.model.NewIntVar(lo, hi, "quot")
|
|
107
|
+
self.model.AddDivisionEquality(res, a.var, b.var)
|
|
108
|
+
return var_with_bounds(res, lo, hi)
|
|
109
|
+
|
|
110
|
+
def solve_and_print(self, verbose: bool = True):
|
|
111
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
112
|
+
return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
|
|
113
|
+
def callback(single_res: SingleSolution):
|
|
114
|
+
print("Solution found")
|
|
115
|
+
output_board = self.board.copy()
|
|
116
|
+
for pos, var in single_res.assignment.items():
|
|
117
|
+
set_char(output_board, pos, str(var))
|
|
118
|
+
print(combined_function(self.V, self.H, center_char=lambda r, c: str(output_board[r, c])))
|
|
119
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|