multi-puzzle-solver 0.9.12__py3-none-any.whl → 0.9.14__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.12.dist-info → multi_puzzle_solver-0.9.14.dist-info}/METADATA +128 -17
- {multi_puzzle_solver-0.9.12.dist-info → multi_puzzle_solver-0.9.14.dist-info}/RECORD +17 -15
- puzzle_solver/__init__.py +3 -2
- puzzle_solver/core/utils.py +228 -127
- puzzle_solver/core/utils_ortools.py +235 -172
- puzzle_solver/puzzles/battleships/battleships.py +1 -0
- puzzle_solver/puzzles/black_box/black_box.py +313 -0
- puzzle_solver/puzzles/filling/filling.py +117 -192
- puzzle_solver/puzzles/inertia/tsp.py +4 -1
- puzzle_solver/puzzles/lits/lits.py +162 -0
- puzzle_solver/puzzles/pearl/pearl.py +12 -44
- puzzle_solver/puzzles/range/range.py +2 -51
- puzzle_solver/puzzles/singles/singles.py +9 -50
- puzzle_solver/puzzles/stitches/parse_map/parse_map.py +212 -212
- puzzle_solver/puzzles/tracks/tracks.py +12 -41
- {multi_puzzle_solver-0.9.12.dist-info → multi_puzzle_solver-0.9.14.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.9.12.dist-info → multi_puzzle_solver-0.9.14.dist-info}/top_level.txt +0 -0
puzzle_solver/core/utils.py
CHANGED
|
@@ -1,127 +1,228 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from typing import Tuple, Iterable, Union
|
|
3
|
-
from enum import Enum
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class Direction(Enum):
|
|
9
|
-
UP = 1
|
|
10
|
-
DOWN = 2
|
|
11
|
-
LEFT = 3
|
|
12
|
-
RIGHT = 4
|
|
13
|
-
|
|
14
|
-
class Direction8(Enum):
|
|
15
|
-
UP = 1
|
|
16
|
-
DOWN = 2
|
|
17
|
-
LEFT = 3
|
|
18
|
-
RIGHT = 4
|
|
19
|
-
UP_LEFT = 5
|
|
20
|
-
UP_RIGHT = 6
|
|
21
|
-
DOWN_LEFT = 7
|
|
22
|
-
DOWN_RIGHT = 8
|
|
23
|
-
|
|
24
|
-
@dataclass(frozen=True, order=True)
|
|
25
|
-
class Pos:
|
|
26
|
-
x: int
|
|
27
|
-
y: int
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return Direction.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
118
|
-
elif direction == Direction8.
|
|
119
|
-
return -1,
|
|
120
|
-
elif direction == Direction8.
|
|
121
|
-
return
|
|
122
|
-
elif direction == Direction8.
|
|
123
|
-
return
|
|
124
|
-
elif direction == Direction8.
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Tuple, Iterable, Union
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Direction(Enum):
|
|
9
|
+
UP = 1
|
|
10
|
+
DOWN = 2
|
|
11
|
+
LEFT = 3
|
|
12
|
+
RIGHT = 4
|
|
13
|
+
|
|
14
|
+
class Direction8(Enum):
|
|
15
|
+
UP = 1
|
|
16
|
+
DOWN = 2
|
|
17
|
+
LEFT = 3
|
|
18
|
+
RIGHT = 4
|
|
19
|
+
UP_LEFT = 5
|
|
20
|
+
UP_RIGHT = 6
|
|
21
|
+
DOWN_LEFT = 7
|
|
22
|
+
DOWN_RIGHT = 8
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, order=True)
|
|
25
|
+
class Pos:
|
|
26
|
+
x: int
|
|
27
|
+
y: int
|
|
28
|
+
|
|
29
|
+
def __add__(self, other: 'Pos') -> 'Pos':
|
|
30
|
+
return get_pos(self.x + other.x, self.y + other.y)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Shape = frozenset[Pos] # a shape on the 2d board is just a set of positions
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_pos(x: int, y: int) -> Pos:
|
|
37
|
+
return Pos(x=x, y=y)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_next_pos(cur_pos: Pos, direction: Union[Direction, Direction8]) -> Pos:
|
|
41
|
+
delta_x, delta_y = get_deltas(direction)
|
|
42
|
+
return get_pos(cur_pos.x+delta_x, cur_pos.y+delta_y)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_neighbors4(pos: Pos, V: int, H: int) -> Iterable[Pos]:
|
|
46
|
+
for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)):
|
|
47
|
+
p2 = get_pos(x=pos.x+dx, y=pos.y+dy)
|
|
48
|
+
if in_bounds(p2, V, H):
|
|
49
|
+
yield p2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_neighbors8(pos: Pos, V: int, H: int = None, include_self: bool = False) -> Iterable[Pos]:
|
|
53
|
+
if H is None:
|
|
54
|
+
H = V
|
|
55
|
+
for dx in [-1, 0, 1]:
|
|
56
|
+
for dy in [-1, 0, 1]:
|
|
57
|
+
if not include_self and (dx, dy) == (0, 0):
|
|
58
|
+
continue
|
|
59
|
+
d_pos = get_pos(x=pos.x+dx, y=pos.y+dy)
|
|
60
|
+
if in_bounds(d_pos, V, H):
|
|
61
|
+
yield d_pos
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_row_pos(row_idx: int, H: int) -> Iterable[Pos]:
|
|
65
|
+
for x in range(H):
|
|
66
|
+
yield get_pos(x=x, y=row_idx)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_col_pos(col_idx: int, V: int) -> Iterable[Pos]:
|
|
70
|
+
for y in range(V):
|
|
71
|
+
yield get_pos(x=col_idx, y=y)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_all_pos(V, H=None):
|
|
75
|
+
if H is None:
|
|
76
|
+
H = V
|
|
77
|
+
for y in range(V):
|
|
78
|
+
for x in range(H):
|
|
79
|
+
yield get_pos(x=x, y=y)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_all_pos_to_idx_dict(V, H=None) -> dict[Pos, int]:
|
|
83
|
+
if H is None:
|
|
84
|
+
H = V
|
|
85
|
+
return {get_pos(x=x, y=y): y*H+x for y in range(V) for x in range(H)}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_char(board: np.array, pos: Pos) -> str:
|
|
89
|
+
return board[pos.y][pos.x]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def set_char(board: np.array, pos: Pos, char: str):
|
|
93
|
+
board[pos.y][pos.x] = char
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def in_bounds(pos: Pos, V: int, H: int = None) -> bool:
|
|
97
|
+
if H is None:
|
|
98
|
+
H = V
|
|
99
|
+
return 0 <= pos.y < V and 0 <= pos.x < H
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_opposite_direction(direction: Direction) -> Direction:
|
|
103
|
+
if direction == Direction.RIGHT:
|
|
104
|
+
return Direction.LEFT
|
|
105
|
+
elif direction == Direction.LEFT:
|
|
106
|
+
return Direction.RIGHT
|
|
107
|
+
elif direction == Direction.DOWN:
|
|
108
|
+
return Direction.UP
|
|
109
|
+
elif direction == Direction.UP:
|
|
110
|
+
return Direction.DOWN
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError(f'invalid direction: {direction}')
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_deltas(direction: Union[Direction, Direction8]) -> Tuple[int, int]:
|
|
116
|
+
if direction == Direction.RIGHT or direction == Direction8.RIGHT:
|
|
117
|
+
return +1, 0
|
|
118
|
+
elif direction == Direction.LEFT or direction == Direction8.LEFT:
|
|
119
|
+
return -1, 0
|
|
120
|
+
elif direction == Direction.DOWN or direction == Direction8.DOWN:
|
|
121
|
+
return 0, +1
|
|
122
|
+
elif direction == Direction.UP or direction == Direction8.UP:
|
|
123
|
+
return 0, -1
|
|
124
|
+
elif direction == Direction8.UP_LEFT:
|
|
125
|
+
return -1, -1
|
|
126
|
+
elif direction == Direction8.UP_RIGHT:
|
|
127
|
+
return +1, -1
|
|
128
|
+
elif direction == Direction8.DOWN_LEFT:
|
|
129
|
+
return -1, +1
|
|
130
|
+
elif direction == Direction8.DOWN_RIGHT:
|
|
131
|
+
return +1, +1
|
|
132
|
+
else:
|
|
133
|
+
raise ValueError(f'invalid direction: {direction}')
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def polyominoes(N):
|
|
137
|
+
"""Generate all polyominoes of size N. Every rotation and reflection is considered different and included in the result.
|
|
138
|
+
Translation is not considered different and is removed from the result (otherwise the result would be infinite).
|
|
139
|
+
|
|
140
|
+
Below is the number of unique polyominoes of size N (not including rotations and reflections) and the lenth of the returned result (which includes all rotations and reflections)
|
|
141
|
+
N name #shapes #results
|
|
142
|
+
1 monomino 1 1
|
|
143
|
+
2 domino 1 2
|
|
144
|
+
3 tromino 2 6
|
|
145
|
+
4 tetromino 5 19
|
|
146
|
+
5 pentomino 12 63
|
|
147
|
+
6 hexomino 35 216
|
|
148
|
+
7 heptomino 108 760
|
|
149
|
+
8 octomino 369 2,725
|
|
150
|
+
9 nonomino 1,285 9,910
|
|
151
|
+
10 decomino 4,655 36,446
|
|
152
|
+
11 undecomino 17,073 135,268
|
|
153
|
+
12 dodecomino 63,600 505,861
|
|
154
|
+
Source: https://en.wikipedia.org/wiki/Polyomino
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
N (int): The size of the polyominoes to generate.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
set[(frozenset[Pos], int)]: A set of all polyominoes of size N (rotated and reflected up to D4 symmetry).
|
|
161
|
+
"""
|
|
162
|
+
assert N >= 1, 'N cannot be less than 1'
|
|
163
|
+
# need a frozenset because regular sets are not hashable
|
|
164
|
+
FastShape = frozenset[Tuple[int, int]]
|
|
165
|
+
shapes: set[FastShape] = {frozenset({(0, 0)})}
|
|
166
|
+
for i in range(1, N):
|
|
167
|
+
next_shapes: set[FastShape] = set()
|
|
168
|
+
directions = ((1,0),(-1,0),(0,1)) if i > 1 else (((1,0),(0,1))) # cannot take left on first step, if confused read: https://louridas.github.io/rwa/assignments/polyominoes/
|
|
169
|
+
for s in shapes:
|
|
170
|
+
# frontier of a single shape: all 4-neighbors of existing cells not already in the shape
|
|
171
|
+
frontier = set()
|
|
172
|
+
for x, y in s:
|
|
173
|
+
# only need to consider 3 directions and neighbors condition is (n.y > 0 or (n.y == 0 and n.x >= 0)) it's obvious if you plot it
|
|
174
|
+
# if confused read: https://louridas.github.io/rwa/assignments/polyominoes/
|
|
175
|
+
for dx, dy in directions:
|
|
176
|
+
n = (x + dx, y + dy)
|
|
177
|
+
if n not in s and (n[1] > 0 or (n[1] == 0 and n[0] >= 0)):
|
|
178
|
+
frontier.add(n)
|
|
179
|
+
for cell in frontier:
|
|
180
|
+
t = s | {cell}
|
|
181
|
+
# normalize by translation only: shift so min x,y is (0,0). This removes translational symmetries.
|
|
182
|
+
minx = min(x for x, y in t)
|
|
183
|
+
miny = min(y for x, y in t)
|
|
184
|
+
t0 = frozenset((x - minx, y - miny) for x, y in t)
|
|
185
|
+
next_shapes.add(t0)
|
|
186
|
+
shapes = next_shapes
|
|
187
|
+
# shapes is now complete, now classify up to D4 symmetry (rotations/reflections), translations ignored
|
|
188
|
+
shapes = {frozenset(Pos(x, y) for x, y in s) for s in shapes} # regular class, not the dirty-fast one
|
|
189
|
+
return shapes
|
|
190
|
+
|
|
191
|
+
def polyominoes_with_shape_id(N):
|
|
192
|
+
"""Refer to polyominoes() for more details. This function returns a set of all polyominoes of size N (rotated and reflected up to D4 symmetry) along with a unique ID for each polyomino that is unique up to D4 symmetry.
|
|
193
|
+
Args:
|
|
194
|
+
N (int): The size of the polyominoes to generate.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
set[(frozenset[Pos], int)]: A set of all polyominoes of size N (rotated and reflected up to D4 symmetry) along with a unique ID for each polyomino that is unique up to D4 symmetry.
|
|
198
|
+
"""
|
|
199
|
+
FastPos = Tuple[int, int]
|
|
200
|
+
FastShape = frozenset[Tuple[int, int]]
|
|
201
|
+
shapes = polyominoes(N)
|
|
202
|
+
shapes = {frozenset((p.x, p.y) for p in s) for s in shapes}
|
|
203
|
+
mats = (
|
|
204
|
+
( 1, 0, 0, 1), # regular
|
|
205
|
+
(-1, 0, 0, 1), # reflect about x
|
|
206
|
+
( 1, 0, 0,-1), # reflect about y
|
|
207
|
+
(-1, 0, 0,-1), # reflect about x and y
|
|
208
|
+
# trnaspose then all 4 above
|
|
209
|
+
( 0, 1, 1, 0), ( 0, 1, -1, 0), ( 0,-1, 1, 0), ( 0,-1, -1, 0),
|
|
210
|
+
)
|
|
211
|
+
# compute canonical representative for each shape (lexicographically smallest normalized transform)
|
|
212
|
+
shape_to_canon: dict[FastShape, tuple[FastPos, ...]] = {}
|
|
213
|
+
for s in shapes:
|
|
214
|
+
reps: list[tuple[FastPos, ...]] = []
|
|
215
|
+
for a, b, c, d in mats:
|
|
216
|
+
pts = {(a*x + b*y, c*x + d*y) for x, y in s}
|
|
217
|
+
minx = min(x for x, y in pts)
|
|
218
|
+
miny = min(y for x, y in pts)
|
|
219
|
+
rep = tuple(sorted((x - minx, y - miny) for x, y in pts))
|
|
220
|
+
reps.append(rep)
|
|
221
|
+
canon = min(reps)
|
|
222
|
+
shape_to_canon[s] = canon
|
|
223
|
+
|
|
224
|
+
canon_set = set(shape_to_canon.values())
|
|
225
|
+
canon_to_id = {canon: i for i, canon in enumerate(sorted(canon_set))}
|
|
226
|
+
result = {(s, canon_to_id[shape_to_canon[s]]) for s in shapes}
|
|
227
|
+
result = {(frozenset(Pos(x, y) for x, y in s), _id) for s, _id in result}
|
|
228
|
+
return result
|