multi-puzzle-solver 1.0.4__py3-none-any.whl → 1.0.6__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.6.dist-info}/METADATA +740 -296
- multi_puzzle_solver-1.0.6.dist-info/RECORD +73 -0
- puzzle_solver/__init__.py +3 -1
- puzzle_solver/core/utils.py +17 -1
- puzzle_solver/core/utils_visualizer.py +257 -201
- 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 +6 -2
- 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 +4 -3
- 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 +2 -2
- 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.6.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-1.0.4.dist-info → multi_puzzle_solver-1.0.6.dist-info}/top_level.txt +0 -0
|
@@ -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,
|
|
4
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_pos, get_neighbors4, in_bounds, Direction, get_next_pos, get_char
|
|
5
5
|
from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, SingleSolution, force_connected_component
|
|
6
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def get_ray(pos: Pos, V: int, H: int, direction: Direction) -> list[Pos]:
|
|
@@ -18,88 +19,51 @@ def get_ray(pos: Pos, V: int, H: int, direction: Direction) -> list[Pos]:
|
|
|
18
19
|
class Board:
|
|
19
20
|
def __init__(self, clues: np.ndarray):
|
|
20
21
|
assert clues.ndim == 2 and clues.shape[0] > 0 and clues.shape[1] > 0, f'clues must be 2d, got {clues.ndim}'
|
|
21
|
-
assert all(
|
|
22
|
-
self.V = clues.shape
|
|
23
|
-
self.H = clues.shape[1]
|
|
22
|
+
assert all(str(i.item()).strip() == '' or str(i.item()).strip().isdecimal() for i in np.nditer(clues)), f'clues must be empty or a decimal number, got {list(np.nditer(clues))}'
|
|
23
|
+
self.V, self.H = clues.shape
|
|
24
24
|
self.clues = clues
|
|
25
|
-
self.model = cp_model.CpModel()
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
self.b: dict[Pos, cp_model.IntVar] = {}
|
|
29
|
-
self.w: dict[Pos, cp_model.IntVar] = {}
|
|
26
|
+
self.model = cp_model.CpModel()
|
|
27
|
+
self.b: dict[Pos, cp_model.IntVar] = {}
|
|
28
|
+
self.w: dict[Pos, cp_model.IntVar] = {}
|
|
30
29
|
|
|
31
30
|
self.create_vars()
|
|
32
31
|
self.add_all_constraints()
|
|
33
32
|
|
|
34
33
|
def create_vars(self):
|
|
35
|
-
# Cell color vars
|
|
36
34
|
for pos in get_all_pos(self.V, self.H):
|
|
37
35
|
self.b[pos] = self.model.NewBoolVar(f"b[{pos}]")
|
|
38
|
-
self.w[pos] = self.
|
|
39
|
-
self.model.AddExactlyOne([self.b[pos], self.w[pos]])
|
|
36
|
+
self.w[pos] = self.b[pos].Not()
|
|
40
37
|
|
|
41
38
|
def add_all_constraints(self):
|
|
42
39
|
self.no_adjacent_blacks()
|
|
43
|
-
self.white_connectivity_percolation()
|
|
44
40
|
self.range_clues()
|
|
41
|
+
force_connected_component(self.model, self.w)
|
|
45
42
|
|
|
46
43
|
def no_adjacent_blacks(self):
|
|
47
|
-
cache = set()
|
|
48
44
|
for p in get_all_pos(self.V, self.H):
|
|
49
45
|
for q in get_neighbors4(p, self.V, self.H):
|
|
50
|
-
if (p, q) in cache:
|
|
51
|
-
continue
|
|
52
|
-
cache.add((p, q))
|
|
53
46
|
self.model.Add(self.b[p] + self.b[q] <= 1)
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
def white_connectivity_percolation(self):
|
|
57
|
-
force_connected_component(self.model, self.w)
|
|
58
|
-
|
|
59
48
|
def range_clues(self):
|
|
60
|
-
# For each numbered cell c with value k
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# - Sum of visible whites = 1 (itself) + sum(chains) == k
|
|
64
|
-
for pos in get_all_pos(self.V, self.H):
|
|
65
|
-
k = get_char(self.clues, pos)
|
|
66
|
-
if k == -1:
|
|
49
|
+
for pos in get_all_pos(self.V, self.H): # For each numbered cell c with value k
|
|
50
|
+
k = str(get_char(self.clues, pos)).strip()
|
|
51
|
+
if not k:
|
|
67
52
|
continue
|
|
68
|
-
|
|
69
|
-
self.model.Add(self.b[pos] == 0)
|
|
70
|
-
|
|
71
|
-
# Build visibility chains per direction (exclude self)
|
|
53
|
+
self.model.Add(self.w[pos] == 1) # Force it white
|
|
72
54
|
vis_vars: list[cp_model.IntVar] = []
|
|
73
|
-
for direction in Direction:
|
|
55
|
+
for direction in Direction: # Build visibility chains in four direction
|
|
74
56
|
ray = get_ray(pos, self.V, self.H, direction) # cells outward
|
|
75
|
-
|
|
76
|
-
continue
|
|
77
|
-
# Chain: v0 = w[ray[0]]; vt = w[ray[t]] & vt-1
|
|
78
|
-
prev = None
|
|
79
|
-
for idx, cell in enumerate(ray):
|
|
57
|
+
for idx in range(len(ray)):
|
|
80
58
|
v = self.model.NewBoolVar(f"vis[{pos}]->({direction.name})[{idx}]")
|
|
59
|
+
and_constraint(self.model, target=v, cs=[self.w[p] for p in ray[:idx+1]])
|
|
81
60
|
vis_vars.append(v)
|
|
82
|
-
|
|
83
|
-
# v0 == w[cell]
|
|
84
|
-
self.model.Add(v == self.w[cell])
|
|
85
|
-
else:
|
|
86
|
-
and_constraint(self.model, target=v, cs=[self.w[cell], prev])
|
|
87
|
-
prev = v
|
|
88
|
-
|
|
89
|
-
# 1 (self) + sum(vis_vars) == k
|
|
90
|
-
self.model.Add(1 + sum(vis_vars) == k)
|
|
61
|
+
self.model.Add(1 + sum(vis_vars) == int(k)) # Sum of visible whites = 1 (itself) + sum(chains) == k
|
|
91
62
|
|
|
92
63
|
def solve_and_print(self, verbose: bool = True):
|
|
93
64
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
94
|
-
assignment:
|
|
95
|
-
for pos, var in board.b.items():
|
|
96
|
-
assignment[pos] = solver.Value(var)
|
|
97
|
-
return SingleSolution(assignment=assignment)
|
|
65
|
+
return SingleSolution(assignment={pos: solver.Value(board.b[pos]) for pos in get_all_pos(board.V, board.H)})
|
|
98
66
|
def callback(single_res: SingleSolution):
|
|
99
67
|
print("Solution:")
|
|
100
|
-
|
|
101
|
-
for pos in get_all_pos(self.V, self.H):
|
|
102
|
-
c = 'B' if single_res.assignment[pos] == 1 else ' '
|
|
103
|
-
set_char(res, pos, c)
|
|
104
|
-
print(res)
|
|
68
|
+
print(combined_function(self.V, self.H, is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1, center_char=lambda r, c: self.clues[r, c].strip(), text_on_shaded_cells=False))
|
|
105
69
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -6,7 +6,7 @@ from ortools.sat.python import cp_model
|
|
|
6
6
|
|
|
7
7
|
from puzzle_solver.core.utils import Pos, get_all_pos, in_bounds, set_char, get_char, Direction, get_next_pos
|
|
8
8
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
9
|
-
from puzzle_solver.core.utils_visualizer import
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def factor_pairs(N: int, upper_limit_i: int, upper_limit_j: int):
|
|
@@ -121,6 +121,8 @@ class Board:
|
|
|
121
121
|
set_char(res, pos, get_char(res, pos) + 'U')
|
|
122
122
|
if bottom_pos not in single_res.assignment or single_res.assignment[bottom_pos] != cur:
|
|
123
123
|
set_char(res, pos, get_char(res, pos) + 'D')
|
|
124
|
-
print(
|
|
124
|
+
print(combined_function(self.V, self.H,
|
|
125
|
+
cell_flags=lambda r, c: res[r, c],
|
|
126
|
+
center_char=lambda r, c: self.board[r, c] if self.board[r, c] != ' ' else ' '))
|
|
125
127
|
|
|
126
128
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -7,7 +7,7 @@ from ortools.sat.python import cp_model
|
|
|
7
7
|
|
|
8
8
|
from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_neighbors4, get_next_pos, get_char, in_bounds
|
|
9
9
|
from puzzle_solver.core.utils_ortools import generic_solve_all, force_connected_component, and_constraint
|
|
10
|
-
from puzzle_solver.core.utils_visualizer import
|
|
10
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@dataclass(frozen=True)
|
|
@@ -154,5 +154,5 @@ class Board:
|
|
|
154
154
|
res[min_y][min_x] += 'L'
|
|
155
155
|
else:
|
|
156
156
|
raise ValueError(f'Invalid position: {pos} and {neighbor}')
|
|
157
|
-
print(
|
|
157
|
+
print(combined_function(self.V - 1, self.H - 1, cell_flags=lambda r, c: res[r, c], center_char=lambda r, c: '.'))
|
|
158
158
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -3,7 +3,7 @@ from ortools.sat.python import cp_model
|
|
|
3
3
|
|
|
4
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
|
|
6
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class Board:
|
|
@@ -45,5 +45,9 @@ class Board:
|
|
|
45
45
|
return SingleSolution(assignment={pos: 1 if solver.Value(val) == 1 else 0 for pos, val in board.B.items()})
|
|
46
46
|
def callback(single_res: SingleSolution):
|
|
47
47
|
print("Solution found")
|
|
48
|
-
print(
|
|
48
|
+
print(combined_function(self.V, self.H,
|
|
49
|
+
is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1,
|
|
50
|
+
center_char=lambda r, c: self.board[r, c],
|
|
51
|
+
text_on_shaded_cells=False
|
|
52
|
+
))
|
|
49
53
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -4,8 +4,9 @@ from dataclasses import dataclass
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from ortools.sat.python import cp_model
|
|
6
6
|
|
|
7
|
-
from puzzle_solver.core.utils import Pos, get_all_pos,
|
|
7
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, in_bounds, get_pos
|
|
8
8
|
from puzzle_solver.core.utils_ortools import force_no_loops, generic_solve_all, SingleSolution
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@dataclass(frozen=True)
|
|
@@ -13,10 +14,10 @@ class Node:
|
|
|
13
14
|
"""The grid is represented as a graph of cells connected to corners."""
|
|
14
15
|
node_type: Union[Literal["Cell"], Literal["Corner"]]
|
|
15
16
|
pos: Pos
|
|
16
|
-
slant: Union[Literal["
|
|
17
|
+
slant: Union[Literal["/"], Literal["\\"], None]
|
|
17
18
|
|
|
18
19
|
def get_neighbors(self, board_nodes: dict[tuple[str, Pos, Optional[str]], "Node"]) -> list["Node"]:
|
|
19
|
-
if self.node_type == "Cell" and self.slant == "
|
|
20
|
+
if self.node_type == "Cell" and self.slant == "/":
|
|
20
21
|
n1 = board_nodes[("Corner", get_pos(self.pos.x+1, self.pos.y), None)]
|
|
21
22
|
n2 = board_nodes[("Corner", get_pos(self.pos.x, self.pos.y+1), None)]
|
|
22
23
|
return [n1, n2]
|
|
@@ -27,8 +28,8 @@ class Node:
|
|
|
27
28
|
elif self.node_type == "Corner":
|
|
28
29
|
# 4 cells, 2 cells per slant
|
|
29
30
|
n1 = ("Cell", get_pos(self.pos.x-1, self.pos.y-1), "\\")
|
|
30
|
-
n2 = ("Cell", get_pos(self.pos.x, self.pos.y-1), "
|
|
31
|
-
n3 = ("Cell", get_pos(self.pos.x-1, self.pos.y), "
|
|
31
|
+
n2 = ("Cell", get_pos(self.pos.x, self.pos.y-1), "/")
|
|
32
|
+
n3 = ("Cell", get_pos(self.pos.x-1, self.pos.y), "/")
|
|
32
33
|
n4 = ("Cell", get_pos(self.pos.x, self.pos.y), "\\")
|
|
33
34
|
return {board_nodes[n] for n in [n1, n2, n3, n4] if n in board_nodes}
|
|
34
35
|
|
|
@@ -61,9 +62,9 @@ class Board:
|
|
|
61
62
|
|
|
62
63
|
def create_vars(self):
|
|
63
64
|
for pos in get_all_pos(self.V, self.H):
|
|
64
|
-
self.model_vars[(pos, '
|
|
65
|
+
self.model_vars[(pos, '/')] = self.model.NewBoolVar(f'{pos}:/')
|
|
65
66
|
self.model_vars[(pos, '\\')] = self.model.NewBoolVar(f'{pos}:\\')
|
|
66
|
-
self.model.AddExactlyOne([self.model_vars[(pos, '
|
|
67
|
+
self.model.AddExactlyOne([self.model_vars[(pos, '/')], self.model_vars[(pos, '\\')]])
|
|
67
68
|
for (pos, slant), v in self.model_vars.items():
|
|
68
69
|
self.nodes[Node(node_type="Cell", pos=pos, slant=slant)] = v
|
|
69
70
|
for pos in get_all_pos(self.V + 1, self.H + 1):
|
|
@@ -76,8 +77,8 @@ class Board:
|
|
|
76
77
|
# when pos is (xi, yi) then it gets a +1 contribution for each:
|
|
77
78
|
# - cell (xi-1, yi-1) is a "\\"
|
|
78
79
|
# - cell (xi, yi) is a "\\"
|
|
79
|
-
# - cell (xi, yi-1) is a "
|
|
80
|
-
# - cell (xi-1, yi) is a "
|
|
80
|
+
# - cell (xi, yi-1) is a "/"
|
|
81
|
+
# - cell (xi-1, yi) is a "/"
|
|
81
82
|
xi, yi = pos.x, pos.y
|
|
82
83
|
tl_pos = get_pos(xi-1, yi-1)
|
|
83
84
|
br_pos = get_pos(xi, yi)
|
|
@@ -85,8 +86,8 @@ class Board:
|
|
|
85
86
|
bl_pos = get_pos(xi-1, yi)
|
|
86
87
|
tl_var = self.model_vars[(tl_pos, '\\')] if in_bounds(tl_pos, self.V, self.H) else 0
|
|
87
88
|
br_var = self.model_vars[(br_pos, '\\')] if in_bounds(br_pos, self.V, self.H) else 0
|
|
88
|
-
tr_var = self.model_vars[(tr_pos, '
|
|
89
|
-
bl_var = self.model_vars[(bl_pos, '
|
|
89
|
+
tr_var = self.model_vars[(tr_pos, '/')] if in_bounds(tr_pos, self.V, self.H) else 0
|
|
90
|
+
bl_var = self.model_vars[(bl_pos, '/')] if in_bounds(bl_pos, self.V, self.H) else 0
|
|
90
91
|
self.model.Add(sum([tl_var, tr_var, bl_var, br_var]) == number)
|
|
91
92
|
board_nodes = {(node.node_type, node.pos, node.slant): node for node in self.nodes.keys()}
|
|
92
93
|
self.neighbor_dict = {node: node.get_neighbors(board_nodes) for node in self.nodes.keys()}
|
|
@@ -106,12 +107,5 @@ class Board:
|
|
|
106
107
|
return SingleSolution(assignment=assignment)
|
|
107
108
|
def callback(single_res: SingleSolution):
|
|
108
109
|
print("Solution found")
|
|
109
|
-
|
|
110
|
-
for pos in get_all_pos(self.V, self.H):
|
|
111
|
-
set_char(res, pos, '/' if single_res.assignment[pos] == '//' else '\\')
|
|
112
|
-
print('[')
|
|
113
|
-
for row in range(self.V):
|
|
114
|
-
line = ' [ ' + ' '.join(res[row].tolist()) + ' ]'
|
|
115
|
-
print(line)
|
|
116
|
-
print(']')
|
|
110
|
+
print(combined_function(self.V, self.H, center_char=lambda r, c: single_res.assignment[get_pos(x=c, y=r)]))
|
|
117
111
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -4,7 +4,7 @@ from ortools.sat.python import cp_model
|
|
|
4
4
|
|
|
5
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
|
|
7
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
CellBorder = tuple[Pos, Direction]
|
|
@@ -126,5 +126,5 @@ class Board:
|
|
|
126
126
|
set_char(res, pos, c)
|
|
127
127
|
# replace " " with "·"
|
|
128
128
|
board = np.where(self.board == ' ', '·', self.board)
|
|
129
|
-
print(
|
|
129
|
+
print(combined_function(self.V, self.H, cell_flags=lambda r, c: res[r, c], center_char=lambda r, c: board[r, c]))
|
|
130
130
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=999)
|
|
@@ -3,7 +3,7 @@ from ortools.sat.python import cp_model
|
|
|
3
3
|
|
|
4
4
|
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_neighbors8, get_row_pos, get_col_pos
|
|
5
5
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
6
|
-
from puzzle_solver.core.utils_visualizer import
|
|
6
|
+
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class Board:
|
|
@@ -70,5 +70,8 @@ class Board:
|
|
|
70
70
|
set_char(res, pos, ' ')
|
|
71
71
|
else:
|
|
72
72
|
set_char(res, pos, '.')
|
|
73
|
-
print(
|
|
73
|
+
print(combined_function(self.V, self.H,
|
|
74
|
+
cell_flags=id_board_to_wall_fn(self.board),
|
|
75
|
+
center_char=lambda r, c: res[r][c]
|
|
76
|
+
))
|
|
74
77
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -3,8 +3,9 @@ from typing import Union
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from ortools.sat.python import cp_model
|
|
5
5
|
|
|
6
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, get_char,
|
|
6
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_next_pos, Direction, get_row_pos, get_col_pos, in_bounds, get_opposite_direction, get_pos
|
|
7
7
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, and_constraint
|
|
8
|
+
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class Board:
|
|
@@ -40,10 +41,6 @@ class Board:
|
|
|
40
41
|
self.block_neighbors.setdefault((block_i, block_j), set()).add((pos, direction, neighbor, opposite_direction))
|
|
41
42
|
self.valid_stitches.add((pos, neighbor))
|
|
42
43
|
self.valid_stitches.add((neighbor, pos))
|
|
43
|
-
# for pair in self.block_neighbors.keys():
|
|
44
|
-
# print(pair, self.block_neighbors[pair])
|
|
45
|
-
# print('top empties', self.top_empties)
|
|
46
|
-
# print('side empties', self.side_empties)
|
|
47
44
|
|
|
48
45
|
self.model = cp_model.CpModel()
|
|
49
46
|
self.model_vars: dict[tuple[Pos, Union[Direction, None]], cp_model.IntVar] = {}
|
|
@@ -62,19 +59,15 @@ class Board:
|
|
|
62
59
|
state = [self.model_vars[(pos, direction)] for direction in Direction]
|
|
63
60
|
state.append(self.model_vars[(pos, None)])
|
|
64
61
|
self.model.AddExactlyOne(state)
|
|
65
|
-
# print('ONLY 1 DIRECTION. only one', state)
|
|
66
62
|
# If a position points at X (and this is a valid pair) then X has to point at me
|
|
67
63
|
for pos in get_all_pos(self.V, self.H):
|
|
68
64
|
for direction in Direction:
|
|
69
65
|
neighbor = get_next_pos(pos, direction)
|
|
70
|
-
if not in_bounds(neighbor, self.V, self.H) or (pos, neighbor) not in self.valid_stitches:
|
|
71
|
-
# this is not a valid stitch
|
|
66
|
+
if not in_bounds(neighbor, self.V, self.H) or (pos, neighbor) not in self.valid_stitches: # this is not a valid stitch
|
|
72
67
|
self.model.Add(self.model_vars[(pos, direction)] == 0)
|
|
73
|
-
# print(f'Pos {pos} cant be {direction}')
|
|
74
68
|
continue
|
|
75
69
|
opposite_direction = get_opposite_direction(direction)
|
|
76
70
|
self.model.Add(self.model_vars[(pos, direction)] == self.model_vars[(neighbor, opposite_direction)])
|
|
77
|
-
# print(f'{pos}:{direction} must == {neighbor}:{opposite_direction}')
|
|
78
71
|
|
|
79
72
|
# all blocks connected exactly N times (N usually 1 but can be 2 or 3)
|
|
80
73
|
for connections in self.block_neighbors.values():
|
|
@@ -93,17 +86,11 @@ class Board:
|
|
|
93
86
|
|
|
94
87
|
def solve_and_print(self, verbose: bool = True):
|
|
95
88
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
96
|
-
assignment:
|
|
97
|
-
for (pos, direction), var in board.model_vars.items():
|
|
98
|
-
if solver.value(var) == 1:
|
|
99
|
-
assignment[pos] = direction.name[0] if direction is not None else ' '
|
|
100
|
-
return SingleSolution(assignment=assignment)
|
|
89
|
+
return SingleSolution(assignment={pos: direction.name[0] if direction is not None else ' ' for (pos, direction), var in board.model_vars.items() if solver.Value(var) == 1 and direction is not None})
|
|
101
90
|
def callback(single_res: SingleSolution):
|
|
102
91
|
print("Solution found")
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
c =
|
|
106
|
-
c = single_res.assignment
|
|
107
|
-
set_char(res, pos, c)
|
|
108
|
-
print(res)
|
|
92
|
+
print(combined_function(self.V, self.H,
|
|
93
|
+
cell_flags=id_board_to_wall_fn(self.board),
|
|
94
|
+
special_content=lambda r, c: single_res.assignment.get(get_pos(x=c, y=r), ''),
|
|
95
|
+
center_char=lambda r, c: 'O' if get_pos(x=c, y=r) in single_res.assignment else '.'))
|
|
109
96
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=9)
|
|
@@ -6,6 +6,7 @@ from ortools.sat.python import cp_model
|
|
|
6
6
|
|
|
7
7
|
from puzzle_solver.core.utils import Pos, get_pos, get_all_pos, get_char, set_char, get_row_pos, get_col_pos
|
|
8
8
|
from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, or_constraint, SingleSolution
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def get_value(board: np.array, pos: Pos) -> Union[int, str]:
|
|
@@ -82,7 +83,7 @@ class Board:
|
|
|
82
83
|
assert block_size is None, 'cannot set block size if blocks are not constrained'
|
|
83
84
|
|
|
84
85
|
if jigsaw is not None:
|
|
85
|
-
if self.constrain_blocks
|
|
86
|
+
if self.constrain_blocks:
|
|
86
87
|
print('Warning: jigsaw and blocks are both constrained, are you sure you want to do this?')
|
|
87
88
|
assert jigsaw.ndim == 2, f'jigsaw must be 2d, got {jigsaw.ndim}'
|
|
88
89
|
assert jigsaw.shape[0] == self.V and jigsaw.shape[1] == self.H, 'jigsaw must be the same size as the board'
|
|
@@ -186,18 +187,11 @@ class Board:
|
|
|
186
187
|
|
|
187
188
|
def solve_and_print(self, verbose: bool = True):
|
|
188
189
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
189
|
-
assignment:
|
|
190
|
-
for pos, var in board.model_vars.items():
|
|
191
|
-
assignment[pos] = solver.value(var)
|
|
192
|
-
return SingleSolution(assignment=assignment)
|
|
190
|
+
return SingleSolution(assignment={pos: solver.value(var) for pos, var in board.model_vars.items()})
|
|
193
191
|
def callback(single_res: SingleSolution):
|
|
194
192
|
print("Solution found")
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
c = get_value(self.board, pos)
|
|
198
|
-
c = single_res.assignment[pos]
|
|
199
|
-
set_value(res, pos, c)
|
|
200
|
-
print(res)
|
|
193
|
+
val_arr = np.array([[single_res.assignment[get_pos(x=c, y=r)] for c in range(self.H)] for r in range(self.V)])
|
|
194
|
+
print(combined_function(self.V, self.H, center_char=lambda r, c: val_arr[r, c] if val_arr[r, c] < 10 else chr(val_arr[r, c] - 10 + ord('a'))))
|
|
201
195
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
202
196
|
|
|
203
197
|
|
|
@@ -6,7 +6,7 @@ from ortools.sat.python import cp_model
|
|
|
6
6
|
|
|
7
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
8
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
9
|
-
from puzzle_solver.core.utils_visualizer import
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def rotated_assignments_N_nums(Xs: tuple[int, ...], target_length: int = 8) -> set[tuple[bool, ...]]:
|
|
@@ -94,5 +94,9 @@ class Board:
|
|
|
94
94
|
if len(c) > 3:
|
|
95
95
|
c = '...'
|
|
96
96
|
set_char(board_justified, pos, ' ' * (2 - len(c)) + c)
|
|
97
|
-
print(
|
|
97
|
+
print(combined_function(self.V, self.H,
|
|
98
|
+
is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1,
|
|
99
|
+
center_char=lambda r, c: str(board_justified[r, c]),
|
|
100
|
+
text_on_shaded_cells=False
|
|
101
|
+
))
|
|
98
102
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=5)
|
|
@@ -4,107 +4,77 @@ import numpy as np
|
|
|
4
4
|
from ortools.sat.python import cp_model
|
|
5
5
|
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
6
6
|
|
|
7
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, get_char,
|
|
7
|
+
from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_char, get_neighbors8, get_next_pos, get_row_pos, get_col_pos, get_opposite_direction, get_pos
|
|
8
8
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class Board:
|
|
12
|
-
def __init__(self, board: np.array,
|
|
13
|
+
def __init__(self, board: np.array, side: np.array, top: np.array):
|
|
13
14
|
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
14
|
-
assert
|
|
15
|
-
assert
|
|
16
|
-
assert set(sides.keys()) == set(['top', 'side'])
|
|
17
|
-
assert all(s.ndim == 1 and s.shape[0] == board.shape[0] for s in sides.values()), 'all sides must be equal to board size'
|
|
15
|
+
assert side.ndim == 1 and side.shape[0] == board.shape[0], 'side must be 1d and equal to board size'
|
|
16
|
+
assert top.ndim == 1 and top.shape[0] == board.shape[1], 'top must be 1d and equal to board size'
|
|
18
17
|
assert all(c.item() in [' ', 'T'] for c in np.nditer(board)), 'board must contain only space or T'
|
|
19
18
|
self.board = board
|
|
20
|
-
self.
|
|
21
|
-
self.
|
|
22
|
-
self.
|
|
19
|
+
self.V, self.H = board.shape
|
|
20
|
+
self.side = side
|
|
21
|
+
self.top = top
|
|
22
|
+
self.non_tree_positions: set[Pos] = {pos for pos in get_all_pos(self.V, self.H) if get_char(self.board, pos) == ' '}
|
|
23
|
+
self.tree_positions: set[Pos] = {pos for pos in get_all_pos(self.V, self.H) if get_char(self.board, pos) == 'T'}
|
|
24
|
+
|
|
23
25
|
self.model = cp_model.CpModel()
|
|
24
|
-
self.is_tent = defaultdict(int)
|
|
25
|
-
self.tent_direction = defaultdict(int)
|
|
26
|
-
self.sides = sides
|
|
26
|
+
self.is_tent: dict[Pos, cp_model.IntVar] = defaultdict(int)
|
|
27
|
+
self.tent_direction: dict[tuple[Pos, Direction], cp_model.IntVar] = defaultdict(int)
|
|
27
28
|
self.create_vars()
|
|
28
29
|
self.add_all_constraints()
|
|
29
30
|
|
|
30
31
|
def create_vars(self):
|
|
31
|
-
for pos in self.
|
|
32
|
-
is_tent = self.model.NewBoolVar(f'{pos}:is_tent')
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
for pos in self.non_tree_positions:
|
|
33
|
+
self.is_tent[pos] = self.model.NewBoolVar(f'{pos}:is_tent')
|
|
34
|
+
for pos in self.tree_positions:
|
|
35
|
+
for direction in Direction:
|
|
36
|
+
tent_pos = get_next_pos(pos, direction)
|
|
37
|
+
if tent_pos not in self.is_tent:
|
|
38
|
+
continue
|
|
39
|
+
opposite_direction = get_opposite_direction(direction)
|
|
40
|
+
tent_direction = self.model.NewBoolVar(f'{pos}:{direction}')
|
|
41
|
+
self.model.Add(tent_direction == 0).OnlyEnforceIf(self.is_tent[tent_pos].Not())
|
|
42
|
+
self.tent_direction[(pos, direction)] = tent_direction
|
|
43
|
+
self.tent_direction[(tent_pos, opposite_direction)] = tent_direction
|
|
38
44
|
|
|
39
45
|
def add_all_constraints(self):
|
|
40
46
|
# - There are exactly as many tents as trees.
|
|
41
|
-
self.model.Add(lxp.sum([self.is_tent[pos] for pos in self.
|
|
47
|
+
self.model.Add(lxp.sum([self.is_tent[pos] for pos in self.non_tree_positions]) == len(self.tree_positions))
|
|
42
48
|
# - no two tents are adjacent horizontally, vertically or diagonally
|
|
43
|
-
for pos in self.
|
|
44
|
-
for neighbour in get_neighbors8(pos, V=self.
|
|
45
|
-
if get_char(self.board, neighbour) != ' ':
|
|
46
|
-
continue
|
|
49
|
+
for pos in self.non_tree_positions:
|
|
50
|
+
for neighbour in get_neighbors8(pos, V=self.V, H=self.H, include_self=False):
|
|
47
51
|
self.model.Add(self.is_tent[neighbour] == 0).OnlyEnforceIf(self.is_tent[pos])
|
|
48
52
|
# - the number of tents in each row and column matches the numbers around the edge of the grid
|
|
49
|
-
for row in range(self.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
for row in range(self.V):
|
|
54
|
+
if self.side[row] == -1:
|
|
55
|
+
continue
|
|
56
|
+
row_vars = [self.is_tent[pos] for pos in get_row_pos(row, H=self.H)]
|
|
57
|
+
self.model.Add(lxp.sum(row_vars) == self.side[row])
|
|
58
|
+
for col in range(self.H):
|
|
59
|
+
if self.top[col] == -1:
|
|
60
|
+
continue
|
|
61
|
+
col_vars = [self.is_tent[pos] for pos in get_col_pos(col, V=self.V)]
|
|
62
|
+
self.model.Add(lxp.sum(col_vars) == self.top[col])
|
|
55
63
|
# - it is possible to match tents to trees so that each tree is orthogonally adjacent to its own tent (but may also be adjacent to other tents).
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
left_pos = get_next_pos(tree_pos, Direction.LEFT)
|
|
66
|
-
right_pos = get_next_pos(tree_pos, Direction.RIGHT)
|
|
67
|
-
top_pos = get_next_pos(tree_pos, Direction.UP)
|
|
68
|
-
bottom_pos = get_next_pos(tree_pos, Direction.DOWN)
|
|
69
|
-
var_list = []
|
|
70
|
-
if left_pos in self.star_positions:
|
|
71
|
-
aux = self.model.NewBoolVar(f'{tree_pos}:left')
|
|
72
|
-
self.model.Add(self.tent_direction[left_pos] == Direction.RIGHT.value).OnlyEnforceIf(aux)
|
|
73
|
-
self.model.Add(self.tent_direction[left_pos] != Direction.RIGHT.value).OnlyEnforceIf(aux.Not())
|
|
74
|
-
var_list.append(aux)
|
|
75
|
-
if right_pos in self.star_positions:
|
|
76
|
-
aux = self.model.NewBoolVar(f'{tree_pos}:right')
|
|
77
|
-
self.model.Add(self.tent_direction[right_pos] == Direction.LEFT.value).OnlyEnforceIf(aux)
|
|
78
|
-
self.model.Add(self.tent_direction[right_pos] != Direction.LEFT.value).OnlyEnforceIf(aux.Not())
|
|
79
|
-
var_list.append(aux)
|
|
80
|
-
if top_pos in self.star_positions:
|
|
81
|
-
aux = self.model.NewBoolVar(f'{tree_pos}:top')
|
|
82
|
-
self.model.Add(self.tent_direction[top_pos] == Direction.DOWN.value).OnlyEnforceIf(aux)
|
|
83
|
-
self.model.Add(self.tent_direction[top_pos] != Direction.DOWN.value).OnlyEnforceIf(aux.Not())
|
|
84
|
-
var_list.append(aux)
|
|
85
|
-
if bottom_pos in self.star_positions:
|
|
86
|
-
aux = self.model.NewBoolVar(f'{tree_pos}:bottom')
|
|
87
|
-
self.model.Add(self.tent_direction[bottom_pos] == Direction.UP.value).OnlyEnforceIf(aux)
|
|
88
|
-
self.model.Add(self.tent_direction[bottom_pos] != Direction.UP.value).OnlyEnforceIf(aux.Not())
|
|
89
|
-
var_list.append(aux)
|
|
90
|
-
self.model.AddBoolOr(var_list)
|
|
64
|
+
# each tent is pointing exactly once at a tree
|
|
65
|
+
for pos in self.non_tree_positions:
|
|
66
|
+
var_list = [self.tent_direction[(pos, direction)] for direction in Direction]
|
|
67
|
+
self.model.Add(lxp.sum(var_list) == 1).OnlyEnforceIf(self.is_tent[pos])
|
|
68
|
+
self.model.Add(lxp.sum(var_list) == 0).OnlyEnforceIf(self.is_tent[pos].Not())
|
|
69
|
+
# each tree is pointed at by exactly one tent
|
|
70
|
+
for pos in self.tree_positions:
|
|
71
|
+
var_list = [self.tent_direction[(pos, direction)] for direction in Direction]
|
|
72
|
+
self.model.Add(lxp.sum(var_list) == 1)
|
|
91
73
|
|
|
92
74
|
def solve_and_print(self, verbose: bool = True):
|
|
93
75
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
94
|
-
assignment:
|
|
95
|
-
for pos, var in board.is_tent.items():
|
|
96
|
-
if isinstance(var, int):
|
|
97
|
-
continue
|
|
98
|
-
assignment[pos] = solver.value(var)
|
|
99
|
-
return SingleSolution(assignment=assignment)
|
|
76
|
+
return SingleSolution(assignment={pos: solver.value(var) for pos, var in board.is_tent.items() if not isinstance(var, int)})
|
|
100
77
|
def callback(single_res: SingleSolution):
|
|
101
78
|
print("Solution found")
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
c = get_char(self.board, pos)
|
|
105
|
-
if c == ' ':
|
|
106
|
-
c = single_res.assignment[pos]
|
|
107
|
-
c = 'E' if c == 1 else ' '
|
|
108
|
-
set_char(res, pos, c)
|
|
109
|
-
print(res)
|
|
110
|
-
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
79
|
+
print(combined_function(self.V, self.H, center_char=lambda r, c: ('|' if self.board[r][c].strip() else ('▲' if single_res.assignment[get_pos(c, r)] == 1 else ' '))))
|
|
80
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=5)
|