multi-puzzle-solver 1.0.4__py3-none-any.whl → 1.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of multi-puzzle-solver might be problematic. Click here for more details.
- {multi_puzzle_solver-1.0.4.dist-info → multi_puzzle_solver-1.0.7.dist-info}/METADATA +1075 -556
- multi_puzzle_solver-1.0.7.dist-info/RECORD +74 -0
- puzzle_solver/__init__.py +5 -1
- puzzle_solver/core/utils.py +17 -1
- puzzle_solver/core/utils_visualizer.py +257 -201
- puzzle_solver/puzzles/abc_view/abc_view.py +75 -0
- puzzle_solver/puzzles/aquarium/aquarium.py +8 -23
- puzzle_solver/puzzles/battleships/battleships.py +39 -53
- puzzle_solver/puzzles/binairo/binairo.py +2 -2
- puzzle_solver/puzzles/black_box/black_box.py +6 -70
- puzzle_solver/puzzles/connect_the_dots/connect_the_dots.py +4 -2
- puzzle_solver/puzzles/filling/filling.py +11 -34
- puzzle_solver/puzzles/galaxies/galaxies.py +4 -2
- puzzle_solver/puzzles/heyawake/heyawake.py +72 -14
- puzzle_solver/puzzles/kakurasu/kakurasu.py +5 -13
- puzzle_solver/puzzles/kakuro/kakuro.py +6 -2
- puzzle_solver/puzzles/lits/lits.py +4 -2
- puzzle_solver/puzzles/mosaic/mosaic.py +8 -18
- puzzle_solver/puzzles/nonograms/nonograms.py +80 -85
- puzzle_solver/puzzles/nonograms/nonograms_colored.py +221 -0
- puzzle_solver/puzzles/norinori/norinori.py +5 -12
- puzzle_solver/puzzles/nurikabe/nurikabe.py +6 -2
- puzzle_solver/puzzles/palisade/palisade.py +8 -22
- puzzle_solver/puzzles/pearl/pearl.py +15 -27
- puzzle_solver/puzzles/pipes/pipes.py +2 -1
- puzzle_solver/puzzles/range/range.py +19 -55
- puzzle_solver/puzzles/rectangles/rectangles.py +4 -2
- puzzle_solver/puzzles/shingoki/shingoki.py +62 -105
- puzzle_solver/puzzles/singles/singles.py +6 -2
- puzzle_solver/puzzles/slant/slant.py +13 -19
- puzzle_solver/puzzles/slitherlink/slitherlink.py +2 -2
- puzzle_solver/puzzles/star_battle/star_battle.py +5 -2
- puzzle_solver/puzzles/stitches/stitches.py +8 -21
- puzzle_solver/puzzles/sudoku/sudoku.py +5 -11
- puzzle_solver/puzzles/tapa/tapa.py +6 -2
- puzzle_solver/puzzles/tents/tents.py +50 -80
- puzzle_solver/puzzles/tracks/tracks.py +19 -66
- puzzle_solver/puzzles/unruly/unruly.py +17 -49
- puzzle_solver/puzzles/yin_yang/yin_yang.py +3 -10
- multi_puzzle_solver-1.0.4.dist-info/RECORD +0 -72
- {multi_puzzle_solver-1.0.4.dist-info → multi_puzzle_solver-1.0.7.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-1.0.4.dist-info → multi_puzzle_solver-1.0.7.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,13 @@
|
|
|
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_row_pos, get_col_pos, get_pos
|
|
5
5
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
6
|
+
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
def _sanity_check(board: np.array):
|
|
9
|
-
|
|
10
|
-
V = board.shape[0]
|
|
11
|
-
H = board.shape[1]
|
|
9
|
+
def _sanity_check(board: np.array): # percolation check
|
|
10
|
+
V, H = board.shape
|
|
12
11
|
visited: set[Pos] = set()
|
|
13
12
|
finished_islands: set[int] = set()
|
|
14
13
|
def dfs(pos: Pos, target_i: int):
|
|
@@ -28,15 +27,13 @@ def _sanity_check(board: np.array):
|
|
|
28
27
|
assert current_i not in finished_islands, f'island {current_i} already finished'
|
|
29
28
|
dfs(pos, current_i)
|
|
30
29
|
finished_islands.add(current_i)
|
|
31
|
-
|
|
32
30
|
assert len(finished_islands) == len(set(board.flatten())), 'board is not connected'
|
|
33
31
|
|
|
34
32
|
class Board:
|
|
35
33
|
def __init__(self, board: np.array, top: np.array, side: np.array):
|
|
36
34
|
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
37
35
|
_sanity_check(board)
|
|
38
|
-
self.V = board.shape
|
|
39
|
-
self.H = board.shape[1]
|
|
36
|
+
self.V, self.H = board.shape
|
|
40
37
|
assert top.ndim == 1 and top.shape[0] == self.H, 'top must be a 1d array of length board width'
|
|
41
38
|
assert side.ndim == 1 and side.shape[0] == self.V, 'side must be a 1d array of length board height'
|
|
42
39
|
assert all((str(c.item()).isdecimal() for c in np.nditer(board))), 'board must contain only digits'
|
|
@@ -69,15 +66,12 @@ class Board:
|
|
|
69
66
|
for aq_i in self.aquarium_numbers:
|
|
70
67
|
for pos in self.aquariums[aq_i]:
|
|
71
68
|
self.model.Add(self.is_aquarium_here[pos.y, aq_i] == 1).OnlyEnforceIf(self.model_vars[pos])
|
|
72
|
-
|
|
73
69
|
# aquarium always start from the bottom
|
|
74
70
|
for aq_i in self.aquarium_numbers:
|
|
75
71
|
for row in self.aquariums_exist_in_row[aq_i]:
|
|
76
|
-
# (row + 1) is below (row)
|
|
77
|
-
if row + 1 not in self.aquariums_exist_in_row[aq_i]:
|
|
72
|
+
if row + 1 not in self.aquariums_exist_in_row[aq_i]: # (row + 1) is below (row) thus currently (row) is the bottom of the aquarium
|
|
78
73
|
continue
|
|
79
74
|
self.model.Add(self.is_aquarium_here[row + 1, aq_i] == 1).OnlyEnforceIf(self.is_aquarium_here[row, aq_i])
|
|
80
|
-
|
|
81
75
|
for row in range(self.V):
|
|
82
76
|
for aq_i in self.aquarium_numbers:
|
|
83
77
|
aq_i_row_pos = [pos for pos in self.aquariums[aq_i] if pos.y == row]
|
|
@@ -88,7 +82,6 @@ class Board:
|
|
|
88
82
|
if len(aq_i_row_pos) > 0:
|
|
89
83
|
self.model.Add(sum([self.model_vars[pos] for pos in aq_i_row_pos]) == len(aq_i_row_pos)).OnlyEnforceIf(self.is_aquarium_here[row, aq_i])
|
|
90
84
|
self.model.Add(sum([self.model_vars[pos] for pos in aq_i_row_pos]) == 0).OnlyEnforceIf(self.is_aquarium_here[row, aq_i].Not())
|
|
91
|
-
|
|
92
85
|
# force the top and side constraints
|
|
93
86
|
for col in range(self.H):
|
|
94
87
|
self.model.Add(sum([self.model_vars[pos] for pos in get_col_pos(col, self.V)]) == self.top[col])
|
|
@@ -97,16 +90,8 @@ class Board:
|
|
|
97
90
|
|
|
98
91
|
def solve_and_print(self, verbose: bool = True):
|
|
99
92
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
100
|
-
assignment:
|
|
101
|
-
for pos, var in board.model_vars.items():
|
|
102
|
-
assignment[pos] = solver.value(var)
|
|
103
|
-
return SingleSolution(assignment=assignment)
|
|
93
|
+
return SingleSolution(assignment={pos: solver.value(board.model_vars[pos]) for pos in board.model_vars.keys()})
|
|
104
94
|
def callback(single_res: SingleSolution):
|
|
105
95
|
print("Solution found")
|
|
106
|
-
|
|
107
|
-
for pos, val in single_res.assignment.items():
|
|
108
|
-
c = str(val)
|
|
109
|
-
set_char(res, pos, c)
|
|
110
|
-
print(res)
|
|
111
|
-
|
|
96
|
+
print(combined_function(self.V, self.H, cell_flags=id_board_to_wall_fn(self.board), center_char=lambda r, c: 'O' if single_res.assignment[get_pos(x=c, y=r)] == 1 else ''))
|
|
112
97
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=99)
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from collections import defaultdict
|
|
2
3
|
from typing import Optional
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
5
6
|
from ortools.sat.python import cp_model
|
|
7
|
+
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
8
|
+
|
|
9
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_neighbors8, get_row_pos, get_col_pos, get_pos, in_bounds
|
|
10
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
11
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
6
12
|
|
|
7
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_neighbors8, set_char, get_row_pos, get_col_pos, get_pos, in_bounds
|
|
8
|
-
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, or_constraint
|
|
9
13
|
|
|
10
14
|
@dataclass
|
|
11
15
|
class Ship:
|
|
@@ -14,17 +18,16 @@ class Ship:
|
|
|
14
18
|
top_left_pos: Pos
|
|
15
19
|
body: set[Pos]
|
|
16
20
|
water: set[Pos]
|
|
17
|
-
mid_body: set[Pos]
|
|
18
|
-
top_tip: Optional[Pos]
|
|
19
|
-
bottom_tip: Optional[Pos]
|
|
20
|
-
left_tip: Optional[Pos]
|
|
21
|
-
right_tip: Optional[Pos]
|
|
21
|
+
mid_body: set[Pos]
|
|
22
|
+
top_tip: Optional[Pos]
|
|
23
|
+
bottom_tip: Optional[Pos]
|
|
24
|
+
left_tip: Optional[Pos]
|
|
25
|
+
right_tip: Optional[Pos]
|
|
22
26
|
|
|
23
27
|
class Board:
|
|
24
28
|
def __init__(self, board: np.array, top: np.array, side: np.array, ship_counts: dict[int, int]):
|
|
25
29
|
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
26
|
-
self.V = board.shape
|
|
27
|
-
self.H = board.shape[1]
|
|
30
|
+
self.V, self.H = board.shape
|
|
28
31
|
assert top.ndim == 1 and top.shape[0] == self.H, 'top must be a 1d array of length board width'
|
|
29
32
|
assert side.ndim == 1 and side.shape[0] == self.V, 'side must be a 1d array of length board height'
|
|
30
33
|
assert all((str(c.item()) in [' ', 'W', 'O', 'S', 'U', 'D', 'L', 'R'] for c in np.nditer(board))), 'board must contain only spaces, W, O, S, U, D, L, R'
|
|
@@ -46,7 +49,6 @@ class Board:
|
|
|
46
49
|
self.model_vars[pos] = self.model.NewBoolVar(f'{pos}:is_ship')
|
|
47
50
|
|
|
48
51
|
def get_ship(self, pos: Pos, length: int, orientation: str) -> Optional[Ship]:
|
|
49
|
-
assert orientation in ['horizontal', 'vertical'], 'orientation must be horizontal or vertical'
|
|
50
52
|
if length == 1:
|
|
51
53
|
body = {pos}
|
|
52
54
|
top_tip = None
|
|
@@ -59,28 +61,22 @@ class Board:
|
|
|
59
61
|
bottom_tip = None
|
|
60
62
|
left_tip = pos
|
|
61
63
|
right_tip = get_pos(x=pos.x + length - 1, y=pos.y)
|
|
62
|
-
|
|
64
|
+
elif orientation == 'vertical':
|
|
63
65
|
body = set(get_pos(x=pos.x, y=y) for y in range(pos.y, pos.y + length))
|
|
64
66
|
left_tip = None
|
|
65
67
|
right_tip = None
|
|
66
68
|
top_tip = pos
|
|
67
69
|
bottom_tip = get_pos(x=pos.x, y=pos.y + length - 1)
|
|
70
|
+
else:
|
|
71
|
+
raise ValueError(f'invalid orientation: {orientation}')
|
|
68
72
|
if any(not in_bounds(p, self.V, self.H) for p in body):
|
|
69
73
|
return None
|
|
70
|
-
water = set(p for pos in body for p in get_neighbors8(pos, self.V, self.H))
|
|
71
|
-
water -= body
|
|
74
|
+
water = set(p for pos in body for p in get_neighbors8(pos, self.V, self.H)) - body
|
|
72
75
|
mid_body = body - {top_tip, bottom_tip, left_tip, right_tip} if length > 1 else set()
|
|
73
76
|
return Ship(
|
|
74
|
-
is_active=self.model.NewBoolVar(f'{pos}:is_active'),
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
body=body,
|
|
78
|
-
water=water,
|
|
79
|
-
mid_body=mid_body,
|
|
80
|
-
top_tip=top_tip,
|
|
81
|
-
bottom_tip=bottom_tip,
|
|
82
|
-
left_tip=left_tip,
|
|
83
|
-
right_tip=right_tip,
|
|
77
|
+
is_active=self.model.NewBoolVar(f'{pos}:is_active'), length=length,
|
|
78
|
+
top_left_pos=pos, body=body, mid_body=mid_body, water=water,
|
|
79
|
+
top_tip=top_tip, bottom_tip=bottom_tip, left_tip=left_tip, right_tip=right_tip,
|
|
84
80
|
)
|
|
85
81
|
|
|
86
82
|
def init_shipyard(self):
|
|
@@ -94,60 +90,50 @@ class Board:
|
|
|
94
90
|
self.shipyard.append(ship)
|
|
95
91
|
|
|
96
92
|
def add_all_constraints(self):
|
|
97
|
-
# ship and
|
|
93
|
+
# if a ship is active then all its body is active and all its water is inactive
|
|
94
|
+
pos_to_ships: dict[Pos, list[Ship]] = defaultdict(list)
|
|
98
95
|
for ship in self.shipyard:
|
|
99
96
|
for pos in ship.body:
|
|
100
97
|
self.model.Add(self.model_vars[pos] == 1).OnlyEnforceIf(ship.is_active)
|
|
98
|
+
pos_to_ships[pos].append(ship)
|
|
101
99
|
for pos in ship.water:
|
|
102
100
|
self.model.Add(self.model_vars[pos] == 0).OnlyEnforceIf(ship.is_active)
|
|
103
|
-
#
|
|
101
|
+
# if a pos is active then exactly one ship can be placed at that position
|
|
104
102
|
for pos in get_all_pos(self.V, self.H):
|
|
105
|
-
|
|
103
|
+
self.model.Add(lxp.Sum([ship.is_active for ship in pos_to_ships[pos]]) == 1).OnlyEnforceIf(self.model_vars[pos])
|
|
106
104
|
# force ship counts
|
|
107
105
|
for length, count in self.ship_counts.items():
|
|
108
|
-
self.
|
|
109
|
-
# force initial board placement
|
|
106
|
+
self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if ship.length == length]) == count)
|
|
107
|
+
# force the initial board placement
|
|
110
108
|
for pos in get_all_pos(self.V, self.H):
|
|
111
109
|
c = get_char(self.board, pos)
|
|
112
110
|
if c == 'S': # single-length ship
|
|
113
|
-
self.
|
|
111
|
+
self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if ship.length == 1 and ship.top_left_pos == pos]) == 1)
|
|
114
112
|
elif c == 'W': # water
|
|
115
113
|
self.model.Add(self.model_vars[pos] == 0)
|
|
116
114
|
elif c == 'O': # mid-body of a ship
|
|
117
|
-
self.
|
|
115
|
+
self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if pos in ship.mid_body]) == 1)
|
|
118
116
|
elif c == 'U': # top tip of a ship
|
|
119
|
-
self.
|
|
117
|
+
self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if ship.top_tip == pos]) == 1)
|
|
120
118
|
elif c == 'D': # bottom tip of a ship
|
|
121
|
-
self.
|
|
119
|
+
self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if ship.bottom_tip == pos]) == 1)
|
|
122
120
|
elif c == 'L': # left tip of a ship
|
|
123
|
-
self.
|
|
121
|
+
self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if ship.left_tip == pos]) == 1)
|
|
124
122
|
elif c == 'R': # right tip of a ship
|
|
125
|
-
self.
|
|
123
|
+
self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if ship.right_tip == pos]) == 1)
|
|
126
124
|
elif c == ' ': # empty cell
|
|
127
125
|
pass
|
|
128
126
|
else:
|
|
129
127
|
raise ValueError(f'invalid character: {c}')
|
|
130
|
-
# force the top
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
self.model.Add(sum([self.model_vars[p] for p in get_col_pos(col, self.V)]) == self.top[col])
|
|
135
|
-
|
|
136
|
-
def constrain_ship_counts(self, ships: list[Ship], count: int):
|
|
137
|
-
self.model.Add(sum([ship.is_active for ship in ships]) == count)
|
|
128
|
+
for row in range(self.V): # force the top counts
|
|
129
|
+
self.model.Add(lxp.Sum([self.model_vars[p] for p in get_row_pos(row, self.H)]) == self.side[row])
|
|
130
|
+
for col in range(self.H): # force the side counts
|
|
131
|
+
self.model.Add(lxp.Sum([self.model_vars[p] for p in get_col_pos(col, self.V)]) == self.top[col])
|
|
138
132
|
|
|
139
133
|
def solve_and_print(self, verbose: bool = True):
|
|
140
134
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
141
|
-
assignment:
|
|
142
|
-
for pos, var in board.model_vars.items():
|
|
143
|
-
assignment[pos] = solver.Value(var)
|
|
144
|
-
return SingleSolution(assignment=assignment)
|
|
135
|
+
return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
|
|
145
136
|
def callback(single_res: SingleSolution):
|
|
146
137
|
print("Solution found")
|
|
147
|
-
|
|
148
|
-
for pos, val in single_res.assignment.items():
|
|
149
|
-
c = 'S' if val == 1 else ' '
|
|
150
|
-
set_char(res, pos, c)
|
|
151
|
-
print(res)
|
|
152
|
-
|
|
138
|
+
print(combined_function(self.V, self.H, is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1))
|
|
153
139
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -6,7 +6,7 @@ from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
|
6
6
|
|
|
7
7
|
from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_next_pos, get_pos, in_bounds, get_char, get_row_pos, get_col_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
|
class Board:
|
|
@@ -122,5 +122,5 @@ class Board:
|
|
|
122
122
|
return SingleSolution(assignment=assignment)
|
|
123
123
|
def callback(single_res: SingleSolution):
|
|
124
124
|
print("Solution found")
|
|
125
|
-
print(
|
|
125
|
+
print(combined_function(self.V, self.H, is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1))
|
|
126
126
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -1,24 +1,10 @@
|
|
|
1
|
-
from
|
|
2
|
-
from typing import Optional, Union
|
|
3
|
-
import json
|
|
1
|
+
from typing import Optional
|
|
4
2
|
|
|
5
|
-
import numpy as np
|
|
6
3
|
from ortools.sat.python import cp_model
|
|
7
4
|
|
|
8
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, get_row_pos, get_col_pos, in_bounds, get_opposite_direction, get_next_pos, Direction
|
|
9
|
-
from puzzle_solver.core.utils_ortools import generic_solve_all
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@dataclass(frozen=True)
|
|
13
|
-
class SingleSolution:
|
|
14
|
-
assignment: dict[Pos, Union[str, int]]
|
|
15
|
-
beam_assignments: dict[Pos, list[tuple[int, Pos, Direction]]]
|
|
16
|
-
|
|
17
|
-
def get_hashable_solution(self) -> str:
|
|
18
|
-
result = []
|
|
19
|
-
for pos, v in self.assignment.items():
|
|
20
|
-
result.append((pos.x, pos.y, v))
|
|
21
|
-
return json.dumps(result, sort_keys=True)
|
|
5
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_pos, get_row_pos, get_col_pos, in_bounds, get_opposite_direction, get_next_pos, Direction
|
|
6
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
7
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
22
8
|
|
|
23
9
|
|
|
24
10
|
class Board:
|
|
@@ -33,18 +19,12 @@ class Board:
|
|
|
33
19
|
else:
|
|
34
20
|
self.T = max_travel_steps
|
|
35
21
|
self.ball_count = ball_count
|
|
36
|
-
|
|
37
22
|
# top and bottom entry cells are at -1 and V
|
|
38
23
|
self.top_cells = set(get_row_pos(row_idx=-1, H=self.H))
|
|
39
24
|
self.bottom_cells = set(get_row_pos(row_idx=self.V, H=self.H))
|
|
40
25
|
# left and right entry cells are at -1 and H
|
|
41
26
|
self.left_cells = set(get_col_pos(col_idx=-1, V=self.V))
|
|
42
27
|
self.right_cells = set(get_col_pos(col_idx=self.H, V=self.V))
|
|
43
|
-
# self.top_cells = set([Pos(x=1, y=-1)])
|
|
44
|
-
# self.bottom_cells = set()
|
|
45
|
-
# self.left_cells = set()
|
|
46
|
-
# self.right_cells = set()
|
|
47
|
-
# print(self.top_cells)
|
|
48
28
|
|
|
49
29
|
self.top_values = top
|
|
50
30
|
self.right_values = right
|
|
@@ -115,12 +95,9 @@ class Board:
|
|
|
115
95
|
# beam at t=0 is present at beam_id and facing direction
|
|
116
96
|
self.model.Add(self.beam_states[(beam_id, 0, beam_id, direction)] == 1)
|
|
117
97
|
for p in self.get_all_pos_extended():
|
|
118
|
-
# print(f'beam can be at {p}')
|
|
119
98
|
for direction in Direction:
|
|
120
99
|
if (p, direction) != (beam_id, direction):
|
|
121
100
|
self.model.Add(self.beam_states[(beam_id, 0, p, direction)] == 0)
|
|
122
|
-
# for p in self.get_all_pos_extended():
|
|
123
|
-
# print(f'beam can be at {p}')
|
|
124
101
|
|
|
125
102
|
|
|
126
103
|
def constrain_beam_movement(self):
|
|
@@ -128,24 +105,18 @@ class Board:
|
|
|
128
105
|
for entry_pos in self.beam_states_at_t[t].keys():
|
|
129
106
|
next_state_dict = self.beam_states_at_t[t][entry_pos]
|
|
130
107
|
self.model.AddExactlyOne(list(next_state_dict.values()))
|
|
131
|
-
# print('add exactly one constraint for beam id', entry_pos, 'at time', t, next_state_dict.keys(), '\n')
|
|
132
108
|
if t == self.T - 1:
|
|
133
109
|
continue
|
|
134
110
|
for (cell, direction), prev_state in next_state_dict.items():
|
|
135
|
-
# print(f'for beam id {entry_pos}, time {t}\nif its at {cell} facing {direction}')
|
|
136
111
|
self.constrain_next_beam_state(entry_pos, t+1, cell, direction, prev_state)
|
|
137
112
|
|
|
138
113
|
|
|
139
114
|
def constrain_next_beam_state(self, entry_pos: Pos, t: int, cur_pos: Pos, direction: Direction, prev_state: cp_model.IntVar):
|
|
140
|
-
# print(f"constraining next beam state for {entry_pos}, {t}, {cur_pos}, {direction}")
|
|
141
115
|
if cur_pos == "HIT": # a beam that was "HIT" stays "HIT"
|
|
142
|
-
# print(f' HIT -> stays HIT')
|
|
143
116
|
self.model.Add(self.beam_states[(entry_pos, t, "HIT", "HIT")] == 1).OnlyEnforceIf(prev_state)
|
|
144
117
|
return
|
|
145
|
-
# if a beam is outside the board AND it is not facing the board -> it maintains its state
|
|
146
118
|
pos_ahead = get_next_pos(cur_pos, direction)
|
|
147
119
|
if not in_bounds(pos_ahead, self.V, self.H) and not in_bounds(cur_pos, self.V, self.H):
|
|
148
|
-
# print(f' OUTSIDE BOARD -> beam stays in the same state')
|
|
149
120
|
self.model.Add(self.beam_states[(entry_pos, t, cur_pos, direction)] == 1).OnlyEnforceIf(prev_state)
|
|
150
121
|
return
|
|
151
122
|
|
|
@@ -200,16 +171,6 @@ class Board:
|
|
|
200
171
|
if not in_bounds(pos_reflected, self.V, self.H):
|
|
201
172
|
pos_reflected = cur_pos
|
|
202
173
|
|
|
203
|
-
# debug_states = {
|
|
204
|
-
# 'if ball head': (entry_pos, t, "HIT", "HIT"),
|
|
205
|
-
# 'if ball in front-left': (entry_pos, t, get_next_pos(cur_pos, direction_right), direction_right),
|
|
206
|
-
# 'if ball in front-right': (entry_pos, t, get_next_pos(cur_pos, direction_left), direction_left),
|
|
207
|
-
# 'if ball in front-left and front-right': (entry_pos, t, get_next_pos(cur_pos, reflected), reflected),
|
|
208
|
-
# 'if no ball ahead': (entry_pos, t, pos_ahead, direction),
|
|
209
|
-
# }
|
|
210
|
-
# for k,v in debug_states.items():
|
|
211
|
-
# print(f' {k} -> {v}')
|
|
212
|
-
|
|
213
174
|
# ball head-on -> beam is "HIT"
|
|
214
175
|
self.model.Add(self.beam_states[(entry_pos, t, "HIT", "HIT")] == 1).OnlyEnforceIf([
|
|
215
176
|
ball_ahead,
|
|
@@ -273,35 +234,10 @@ class Board:
|
|
|
273
234
|
for reflect in reflects:
|
|
274
235
|
self.model.AddExactlyOne([self.beam_states[(reflect, self.T-1, reflect, direction)] for direction in Direction])
|
|
275
236
|
|
|
276
|
-
|
|
277
237
|
def solve_and_print(self, verbose: bool = True):
|
|
278
|
-
count = 0
|
|
279
238
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
280
|
-
|
|
281
|
-
count += 1
|
|
282
|
-
# if count > 99:
|
|
283
|
-
# import sys
|
|
284
|
-
# sys.exit(0)
|
|
285
|
-
assignment = {}
|
|
286
|
-
for pos in get_all_pos(self.V, self.H):
|
|
287
|
-
assignment[pos] = solver.value(self.ball_states[pos])
|
|
288
|
-
beam_assignments = {}
|
|
289
|
-
# for (entry_pos, t, cell, direction), v in self.beam_states.items():
|
|
290
|
-
# if entry_pos not in beam_assignments:
|
|
291
|
-
# beam_assignments[entry_pos] = []
|
|
292
|
-
# if solver.value(v): # for every beam it can only be present in one state at a time
|
|
293
|
-
# beam_assignments[entry_pos].append((t, cell, direction))
|
|
294
|
-
# for k,v in beam_assignments.items():
|
|
295
|
-
# beam_assignments[k] = sorted(v, key=lambda x: x[0])
|
|
296
|
-
# print(k, beam_assignments[k], '\n')
|
|
297
|
-
# print(beam_assignments)
|
|
298
|
-
return SingleSolution(assignment=assignment, beam_assignments=beam_assignments)
|
|
299
|
-
|
|
239
|
+
return SingleSolution(assignment={pos: solver.Value(self.ball_states[pos]) for pos in get_all_pos(self.V, self.H)})
|
|
300
240
|
def callback(single_res: SingleSolution):
|
|
301
241
|
print("Solution found")
|
|
302
|
-
|
|
303
|
-
for pos in get_all_pos(self.V, self.H):
|
|
304
|
-
ball_state = 'O' if single_res.assignment[pos] else ' '
|
|
305
|
-
res[pos.y][pos.x] = ball_state
|
|
306
|
-
print(res)
|
|
242
|
+
print(combined_function(self.V, self.H, center_char=lambda r, c: 'O' if single_res.assignment[get_pos(x=c, y=r)] else ''))
|
|
307
243
|
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, set_char, get_char
|
|
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, id_board_to_wall_fn
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class Board:
|
|
@@ -44,5 +44,7 @@ class Board:
|
|
|
44
44
|
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
45
45
|
for pos in get_all_pos(self.V, self.H):
|
|
46
46
|
set_char(res, pos, single_res.assignment[pos])
|
|
47
|
-
print(
|
|
47
|
+
print(combined_function(self.V, self.H,
|
|
48
|
+
cell_flags=id_board_to_wall_fn(res),
|
|
49
|
+
center_char=lambda r, c: res[r][c]))
|
|
48
50
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -3,8 +3,9 @@ from dataclasses import dataclass
|
|
|
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_pos, polyominoes, in_bounds, get_next_pos, Direction
|
|
7
7
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
8
|
+
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@dataclass
|
|
@@ -23,8 +24,6 @@ class Board:
|
|
|
23
24
|
assert all((c == ' ') or (str(c).isdecimal() and 0 <= int(c) <= 9) for c in np.nditer(board)), "board must contain space or digits 0..9"
|
|
24
25
|
self.digits = digits
|
|
25
26
|
self.polyominoes = {d: polyominoes(d) for d in self.digits}
|
|
26
|
-
# len_shapes = sum(len(shapes) for shapes in self.polyominoes.values())
|
|
27
|
-
# print(f'total number of shapes: {len_shapes}')
|
|
28
27
|
|
|
29
28
|
self.model = cp_model.CpModel()
|
|
30
29
|
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
@@ -33,7 +32,7 @@ class Board:
|
|
|
33
32
|
self.forced_pos: dict[Pos, int] = {}
|
|
34
33
|
|
|
35
34
|
self.create_vars()
|
|
36
|
-
self.
|
|
35
|
+
self.force_hints()
|
|
37
36
|
self.init_polyominoes_on_board()
|
|
38
37
|
self.add_all_constraints()
|
|
39
38
|
|
|
@@ -42,8 +41,7 @@ class Board:
|
|
|
42
41
|
for d in self.digits:
|
|
43
42
|
self.model_vars[(d,pos)] = self.model.NewBoolVar(f'{d}:{pos}')
|
|
44
43
|
|
|
45
|
-
def
|
|
46
|
-
# force numbers already on the board
|
|
44
|
+
def force_hints(self):
|
|
47
45
|
for pos in get_all_pos(self.V, self.H):
|
|
48
46
|
c = get_char(self.board, pos)
|
|
49
47
|
if c.isdecimal():
|
|
@@ -51,7 +49,6 @@ class Board:
|
|
|
51
49
|
self.forced_pos[pos] = int(c)
|
|
52
50
|
|
|
53
51
|
def init_polyominoes_on_board(self):
|
|
54
|
-
# total_count = 0
|
|
55
52
|
for d in self.digits: # all digits
|
|
56
53
|
digit_count = 0
|
|
57
54
|
for pos in get_all_pos(self.V, self.H): # translate by shape
|
|
@@ -73,45 +70,25 @@ class Board:
|
|
|
73
70
|
for p in body:
|
|
74
71
|
self.body_loc_to_shape[(d,p)].append(shape_on_board)
|
|
75
72
|
digit_count += 1
|
|
76
|
-
# total_count += 1
|
|
77
|
-
# if total_count % 1000 == 0:
|
|
78
|
-
# print(f'{total_count} shapes on board')
|
|
79
|
-
# print(f'total number of shapes on board: {total_count}')
|
|
80
73
|
|
|
81
74
|
def add_all_constraints(self):
|
|
82
75
|
for pos in get_all_pos(self.V, self.H):
|
|
83
|
-
# exactly one digit is active at every position
|
|
84
|
-
self.model.AddExactlyOne(self.
|
|
85
|
-
|
|
86
|
-
self.model.AddExactlyOne(s.is_active for d in self.digits for s in self.body_loc_to_shape[(d,pos)])
|
|
87
|
-
# if a shape is active then all its body is active
|
|
88
|
-
|
|
89
|
-
for s_list in self.body_loc_to_shape.values():
|
|
76
|
+
self.model.AddExactlyOne(self.model_vars[(d,pos)] for d in self.digits) # exactly one digit is active at every position
|
|
77
|
+
self.model.AddExactlyOne(s.is_active for d in self.digits for s in self.body_loc_to_shape[(d,pos)]) # exactly one shape is active at that position
|
|
78
|
+
for s_list in self.body_loc_to_shape.values(): # if a shape is active then all its body is active
|
|
90
79
|
for s in s_list:
|
|
91
80
|
for p in s.body:
|
|
92
81
|
self.model.Add(self.model_vars[(s.N,p)] == 1).OnlyEnforceIf(s.is_active)
|
|
93
|
-
|
|
94
|
-
# same shape cannot touch
|
|
95
|
-
for d, s_list in self.digit_to_shapes.items():
|
|
82
|
+
for d, s_list in self.digit_to_shapes.items(): # same shape cannot touch each other
|
|
96
83
|
for s in s_list:
|
|
97
84
|
for disallow_pos in s.disallow_same_shape:
|
|
98
85
|
self.model.Add(self.model_vars[(d,disallow_pos)] == 0).OnlyEnforceIf(s.is_active)
|
|
99
86
|
|
|
100
87
|
def solve_and_print(self, verbose: bool = True):
|
|
101
88
|
def board_to_solution(board: "Board", solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
102
|
-
assignment:
|
|
103
|
-
for pos in get_all_pos(self.V, self.H):
|
|
104
|
-
for d in self.digits:
|
|
105
|
-
if solver.Value(self.model_vars[(d,pos)]) == 1:
|
|
106
|
-
assignment[pos] = d
|
|
107
|
-
break
|
|
108
|
-
return SingleSolution(assignment=assignment)
|
|
89
|
+
return SingleSolution(assignment={pos: d for pos in get_all_pos(self.V, self.H) for d in self.digits if solver.Value(self.model_vars[(d,pos)]) == 1})
|
|
109
90
|
def callback(single_res: SingleSolution):
|
|
110
91
|
print("Solution found")
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
c = get_char(self.board, pos)
|
|
114
|
-
c = single_res.assignment[pos]
|
|
115
|
-
set_char(res, pos, c)
|
|
116
|
-
print(res)
|
|
92
|
+
res_arr = np.array([[single_res.assignment[get_pos(x=c, y=r)] for c in range(self.H)] for r in range(self.V)])
|
|
93
|
+
print(combined_function(self.V, self.H, cell_flags=id_board_to_wall_fn(res_arr), center_char=lambda r, c: res_arr[r, c]))
|
|
117
94
|
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, set_char, Direction, get_next_pos, in_bounds, get_opposite_direction, 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, id_board_to_wall_fn
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def parse_numpy(galaxies: np.ndarray) -> list[tuple[Pos, ...]]:
|
|
@@ -104,5 +104,7 @@ class Board:
|
|
|
104
104
|
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
105
105
|
for pos in get_all_pos(self.V, self.H):
|
|
106
106
|
set_char(res, pos, single_res.assignment[pos])
|
|
107
|
-
print(
|
|
107
|
+
print(combined_function(self.V, self.H,
|
|
108
|
+
cell_flags=id_board_to_wall_fn(res),
|
|
109
|
+
center_char=lambda r, c: '.' if (Pos(x=c, y=r) in self.prelocated_positions) else ' '))
|
|
108
110
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|