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
|
@@ -1,212 +1,212 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This file is a simple helper that parses the images from https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/inertia.html and converts them to a json file.
|
|
3
|
-
Look at the ./input_output/ directory for examples of input images and output json files.
|
|
4
|
-
The output json is used in the test_solve.py file to test the solver.
|
|
5
|
-
"""
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
import numpy as np
|
|
8
|
-
cv = None
|
|
9
|
-
Image = None
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def extract_lines(bw):
|
|
13
|
-
# Create the images that will use to extract the horizontal and vertical lines
|
|
14
|
-
horizontal = np.copy(bw)
|
|
15
|
-
vertical = np.copy(bw)
|
|
16
|
-
|
|
17
|
-
cols = horizontal.shape[1]
|
|
18
|
-
horizontal_size = cols // 5
|
|
19
|
-
# Create structure element for extracting horizontal lines through morphology operations
|
|
20
|
-
horizontalStructure = cv.getStructuringElement(cv.MORPH_RECT, (horizontal_size, 1))
|
|
21
|
-
horizontal = cv.erode(horizontal, horizontalStructure)
|
|
22
|
-
horizontal = cv.dilate(horizontal, horizontalStructure)
|
|
23
|
-
horizontal_means = np.mean(horizontal, axis=1)
|
|
24
|
-
horizontal_cutoff = np.percentile(horizontal_means, 50)
|
|
25
|
-
# location where the horizontal lines are
|
|
26
|
-
horizontal_idx = np.where(horizontal_means > horizontal_cutoff)[0]
|
|
27
|
-
# print(f"horizontal_idx: {horizontal_idx}")
|
|
28
|
-
height = len(horizontal_idx)
|
|
29
|
-
# show_wait_destroy("horizontal", horizontal) # this has the horizontal lines
|
|
30
|
-
|
|
31
|
-
rows = vertical.shape[0]
|
|
32
|
-
verticalsize = rows // 5
|
|
33
|
-
# Create structure element for extracting vertical lines through morphology operations
|
|
34
|
-
verticalStructure = cv.getStructuringElement(cv.MORPH_RECT, (1, verticalsize))
|
|
35
|
-
vertical = cv.erode(vertical, verticalStructure)
|
|
36
|
-
vertical = cv.dilate(vertical, verticalStructure)
|
|
37
|
-
vertical_means = np.mean(vertical, axis=0)
|
|
38
|
-
vertical_cutoff = np.percentile(vertical_means, 50)
|
|
39
|
-
vertical_idx = np.where(vertical_means > vertical_cutoff)[0]
|
|
40
|
-
# print(f"vertical_idx: {vertical_idx}")
|
|
41
|
-
width = len(vertical_idx)
|
|
42
|
-
# print(f"height: {height}, width: {width}")
|
|
43
|
-
# print(f"vertical_means: {vertical_means}")
|
|
44
|
-
# show_wait_destroy("vertical", vertical) # this has the vertical lines
|
|
45
|
-
|
|
46
|
-
vertical = cv.bitwise_not(vertical)
|
|
47
|
-
# show_wait_destroy("vertical_bit", vertical)
|
|
48
|
-
|
|
49
|
-
return horizontal_idx, vertical_idx
|
|
50
|
-
|
|
51
|
-
def show_wait_destroy(winname, img):
|
|
52
|
-
cv.imshow(winname, img)
|
|
53
|
-
cv.moveWindow(winname, 500, 0)
|
|
54
|
-
cv.waitKey(0)
|
|
55
|
-
cv.destroyWindow(winname)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def mean_consecutives(arr: np.ndarray) -> np.ndarray:
|
|
59
|
-
"""if a sequence of values is consecutive, then average the values"""
|
|
60
|
-
sums = []
|
|
61
|
-
counts = []
|
|
62
|
-
for i in range(len(arr)):
|
|
63
|
-
if i == 0:
|
|
64
|
-
sums.append(arr[i])
|
|
65
|
-
counts.append(1)
|
|
66
|
-
elif arr[i] == arr[i-1] + 1:
|
|
67
|
-
sums[-1] += arr[i]
|
|
68
|
-
counts[-1] += 1
|
|
69
|
-
else:
|
|
70
|
-
sums.append(arr[i])
|
|
71
|
-
counts.append(1)
|
|
72
|
-
return np.array(sums) // np.array(counts)
|
|
73
|
-
|
|
74
|
-
def dfs(x, y, out, output, current_num):
|
|
75
|
-
if x < 0 or x >= out.shape[1] or y < 0 or y >= out.shape[0]:
|
|
76
|
-
return
|
|
77
|
-
if out[y, x] != ' ':
|
|
78
|
-
return
|
|
79
|
-
out[y, x] = current_num
|
|
80
|
-
if output['top'][y, x] == 0:
|
|
81
|
-
dfs(x, y-1, out, output, current_num)
|
|
82
|
-
if output['left'][y, x] == 0:
|
|
83
|
-
dfs(x-1, y, out, output, current_num)
|
|
84
|
-
if output['right'][y, x] == 0:
|
|
85
|
-
dfs(x+1, y, out, output, current_num)
|
|
86
|
-
if output['bottom'][y, x] == 0:
|
|
87
|
-
dfs(x, y+1, out, output, current_num)
|
|
88
|
-
|
|
89
|
-
def main(image):
|
|
90
|
-
global Image
|
|
91
|
-
global cv
|
|
92
|
-
import matplotlib.pyplot as plt
|
|
93
|
-
from PIL import Image as Image_module
|
|
94
|
-
import cv2 as cv_module
|
|
95
|
-
Image = Image_module
|
|
96
|
-
cv = cv_module
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
image_path = Path(image)
|
|
100
|
-
output_path = image_path.parent / (image_path.stem + '.json')
|
|
101
|
-
src = cv.imread(image, cv.IMREAD_COLOR)
|
|
102
|
-
assert src is not None, f'Error opening image: {image}'
|
|
103
|
-
if len(src.shape) != 2:
|
|
104
|
-
gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
|
|
105
|
-
else:
|
|
106
|
-
gray = src
|
|
107
|
-
# now the image is in grayscale
|
|
108
|
-
|
|
109
|
-
# Apply adaptiveThreshold at the bitwise_not of gray, notice the ~ symbol
|
|
110
|
-
gray = cv.bitwise_not(gray)
|
|
111
|
-
bw = cv.adaptiveThreshold(gray.copy(), 255, cv.ADAPTIVE_THRESH_MEAN_C, \
|
|
112
|
-
cv.THRESH_BINARY, 15, -2)
|
|
113
|
-
# show_wait_destroy("binary", bw)
|
|
114
|
-
|
|
115
|
-
# show_wait_destroy("src", src)
|
|
116
|
-
horizontal_idx, vertical_idx = extract_lines(bw)
|
|
117
|
-
horizontal_idx = mean_consecutives(horizontal_idx)
|
|
118
|
-
vertical_idx = mean_consecutives(vertical_idx)
|
|
119
|
-
height = len(horizontal_idx)
|
|
120
|
-
width = len(vertical_idx)
|
|
121
|
-
print(f"height: {height}, width: {width}")
|
|
122
|
-
print(f"horizontal_idx: {horizontal_idx}")
|
|
123
|
-
print(f"vertical_idx: {vertical_idx}")
|
|
124
|
-
arr = np.zeros((height - 1, width - 1), dtype=object)
|
|
125
|
-
output = {'top': arr.copy(), 'left': arr.copy(), 'right': arr.copy(), 'bottom': arr.copy()}
|
|
126
|
-
target = 200_000
|
|
127
|
-
hists = {'top': {}, 'left': {}, 'right': {}, 'bottom': {}}
|
|
128
|
-
for j in range(height - 1):
|
|
129
|
-
for i in range(width - 1):
|
|
130
|
-
hidx1, hidx2 = horizontal_idx[j], horizontal_idx[j+1]
|
|
131
|
-
vidx1, vidx2 = vertical_idx[i], vertical_idx[i+1]
|
|
132
|
-
hidx1 = max(0, hidx1 - 2)
|
|
133
|
-
hidx2 = min(src.shape[0], hidx2 + 4)
|
|
134
|
-
vidx1 = max(0, vidx1 - 2)
|
|
135
|
-
vidx2 = min(src.shape[1], vidx2 + 4)
|
|
136
|
-
cell = src[hidx1:hidx2, vidx1:vidx2]
|
|
137
|
-
mid_x = cell.shape[1] // 2
|
|
138
|
-
mid_y = cell.shape[0] // 2
|
|
139
|
-
# show_wait_destroy(f"cell_{i}_{j}", cell)
|
|
140
|
-
cell = cv.bitwise_not(cell) # invert colors
|
|
141
|
-
top = cell[0:10, mid_y-5:mid_y+5]
|
|
142
|
-
hists['top'][j, i] = np.sum(top)
|
|
143
|
-
left = cell[mid_x-5:mid_x+5, 0:10]
|
|
144
|
-
hists['left'][j, i] = np.sum(left)
|
|
145
|
-
right = cell[mid_x-5:mid_x+5, -10:]
|
|
146
|
-
hists['right'][j, i] = np.sum(right)
|
|
147
|
-
bottom = cell[-10:, mid_y-5:mid_y+5]
|
|
148
|
-
hists['bottom'][j, i] = np.sum(bottom)
|
|
149
|
-
|
|
150
|
-
fig, axs = plt.subplots(2, 2)
|
|
151
|
-
axs[0, 0].hist(list(hists['top'].values()), bins=100)
|
|
152
|
-
axs[0, 0].set_title('Top')
|
|
153
|
-
axs[0, 1].hist(list(hists['left'].values()), bins=100)
|
|
154
|
-
axs[0, 1].set_title('Left')
|
|
155
|
-
axs[1, 0].hist(list(hists['right'].values()), bins=100)
|
|
156
|
-
axs[1, 0].set_title('Right')
|
|
157
|
-
axs[1, 1].hist(list(hists['bottom'].values()), bins=100)
|
|
158
|
-
axs[1, 1].set_title('Bottom')
|
|
159
|
-
target_top = np.mean(list(hists['top'].values()))
|
|
160
|
-
target_left = np.mean(list(hists['left'].values()))
|
|
161
|
-
target_right = np.mean(list(hists['right'].values()))
|
|
162
|
-
target_bottom = np.mean(list(hists['bottom'].values()))
|
|
163
|
-
axs[0, 0].axvline(target_top, color='red')
|
|
164
|
-
axs[0, 1].axvline(target_left, color='red')
|
|
165
|
-
axs[1, 0].axvline(target_right, color='red')
|
|
166
|
-
axs[1, 1].axvline(target_bottom, color='red')
|
|
167
|
-
# plt.show()
|
|
168
|
-
# 1/0
|
|
169
|
-
print(f"target_top: {target_top}, target_left: {target_left}, target_right: {target_right}, target_bottom: {target_bottom}")
|
|
170
|
-
for j in range(height - 1):
|
|
171
|
-
for i in range(width - 1):
|
|
172
|
-
if hists['top'][j, i] > target_top:
|
|
173
|
-
output['top'][j, i] = 1
|
|
174
|
-
if hists['left'][j, i] > target_left:
|
|
175
|
-
output['left'][j, i] = 1
|
|
176
|
-
if hists['right'][j, i] > target_right:
|
|
177
|
-
output['right'][j, i] = 1
|
|
178
|
-
if hists['bottom'][j, i] > target_bottom:
|
|
179
|
-
output['bottom'][j, i] = 1
|
|
180
|
-
print(f"cell_{j}_{i}", end=': ')
|
|
181
|
-
print('T' if output['top'][j, i] else '', end='')
|
|
182
|
-
print('L' if output['left'][j, i] else '', end='')
|
|
183
|
-
print('R' if output['right'][j, i] else '', end='')
|
|
184
|
-
print('B' if output['bottom'][j, i] else '', end='')
|
|
185
|
-
print(' Sums: ', hists['top'][j, i], hists['left'][j, i], hists['right'][j, i], hists['bottom'][j, i])
|
|
186
|
-
|
|
187
|
-
current_count = 0
|
|
188
|
-
out = np.full_like(output['top'], ' ', dtype='U2')
|
|
189
|
-
for j in range(out.shape[0]):
|
|
190
|
-
for i in range(out.shape[1]):
|
|
191
|
-
if out[j, i] == ' ':
|
|
192
|
-
dfs(i, j, out, output, str(current_count).zfill(2))
|
|
193
|
-
current_count += 1
|
|
194
|
-
|
|
195
|
-
with open(output_path, 'w') as f:
|
|
196
|
-
f.write('[\n')
|
|
197
|
-
for i, row in enumerate(out):
|
|
198
|
-
f.write(' ' + str(row.tolist()).replace("'", '"'))
|
|
199
|
-
if i != len(out) - 1:
|
|
200
|
-
f.write(',')
|
|
201
|
-
f.write('\n')
|
|
202
|
-
f.write(']')
|
|
203
|
-
print('output json: ', output_path)
|
|
204
|
-
|
|
205
|
-
if __name__ == '__main__':
|
|
206
|
-
# to run this script and visualize the output, in the root run:
|
|
207
|
-
# python .\src\puzzle_solver\puzzles\stitches\parse_map\parse_map.py | python .\src\puzzle_solver\utils\visualizer.py --read_stdin
|
|
208
|
-
# main(Path(__file__).parent / 'input_output' / 'MTM6OSw4MjEsNDAx.png')
|
|
209
|
-
# main(Path(__file__).parent / 'input_output' / 'weekly_oct_3rd_2025.png')
|
|
210
|
-
# main(Path(__file__).parent / 'input_output' / 'star_battle_67f73ff90cd8cdb4b3e30f56f5261f4968f5dac940bc6.png')
|
|
211
|
-
# main(Path(__file__).parent / 'input_output' / 'LITS_MDoxNzksNzY3.png')
|
|
212
|
-
main(Path(__file__).parent / 'input_output' / 'lits_OTo3LDMwNiwwMTU=.png')
|
|
1
|
+
"""
|
|
2
|
+
This file is a simple helper that parses the images from https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/inertia.html and converts them to a json file.
|
|
3
|
+
Look at the ./input_output/ directory for examples of input images and output json files.
|
|
4
|
+
The output json is used in the test_solve.py file to test the solver.
|
|
5
|
+
"""
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import numpy as np
|
|
8
|
+
cv = None
|
|
9
|
+
Image = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extract_lines(bw):
|
|
13
|
+
# Create the images that will use to extract the horizontal and vertical lines
|
|
14
|
+
horizontal = np.copy(bw)
|
|
15
|
+
vertical = np.copy(bw)
|
|
16
|
+
|
|
17
|
+
cols = horizontal.shape[1]
|
|
18
|
+
horizontal_size = cols // 5
|
|
19
|
+
# Create structure element for extracting horizontal lines through morphology operations
|
|
20
|
+
horizontalStructure = cv.getStructuringElement(cv.MORPH_RECT, (horizontal_size, 1))
|
|
21
|
+
horizontal = cv.erode(horizontal, horizontalStructure)
|
|
22
|
+
horizontal = cv.dilate(horizontal, horizontalStructure)
|
|
23
|
+
horizontal_means = np.mean(horizontal, axis=1)
|
|
24
|
+
horizontal_cutoff = np.percentile(horizontal_means, 50)
|
|
25
|
+
# location where the horizontal lines are
|
|
26
|
+
horizontal_idx = np.where(horizontal_means > horizontal_cutoff)[0]
|
|
27
|
+
# print(f"horizontal_idx: {horizontal_idx}")
|
|
28
|
+
height = len(horizontal_idx)
|
|
29
|
+
# show_wait_destroy("horizontal", horizontal) # this has the horizontal lines
|
|
30
|
+
|
|
31
|
+
rows = vertical.shape[0]
|
|
32
|
+
verticalsize = rows // 5
|
|
33
|
+
# Create structure element for extracting vertical lines through morphology operations
|
|
34
|
+
verticalStructure = cv.getStructuringElement(cv.MORPH_RECT, (1, verticalsize))
|
|
35
|
+
vertical = cv.erode(vertical, verticalStructure)
|
|
36
|
+
vertical = cv.dilate(vertical, verticalStructure)
|
|
37
|
+
vertical_means = np.mean(vertical, axis=0)
|
|
38
|
+
vertical_cutoff = np.percentile(vertical_means, 50)
|
|
39
|
+
vertical_idx = np.where(vertical_means > vertical_cutoff)[0]
|
|
40
|
+
# print(f"vertical_idx: {vertical_idx}")
|
|
41
|
+
width = len(vertical_idx)
|
|
42
|
+
# print(f"height: {height}, width: {width}")
|
|
43
|
+
# print(f"vertical_means: {vertical_means}")
|
|
44
|
+
# show_wait_destroy("vertical", vertical) # this has the vertical lines
|
|
45
|
+
|
|
46
|
+
vertical = cv.bitwise_not(vertical)
|
|
47
|
+
# show_wait_destroy("vertical_bit", vertical)
|
|
48
|
+
|
|
49
|
+
return horizontal_idx, vertical_idx
|
|
50
|
+
|
|
51
|
+
def show_wait_destroy(winname, img):
|
|
52
|
+
cv.imshow(winname, img)
|
|
53
|
+
cv.moveWindow(winname, 500, 0)
|
|
54
|
+
cv.waitKey(0)
|
|
55
|
+
cv.destroyWindow(winname)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def mean_consecutives(arr: np.ndarray) -> np.ndarray:
|
|
59
|
+
"""if a sequence of values is consecutive, then average the values"""
|
|
60
|
+
sums = []
|
|
61
|
+
counts = []
|
|
62
|
+
for i in range(len(arr)):
|
|
63
|
+
if i == 0:
|
|
64
|
+
sums.append(arr[i])
|
|
65
|
+
counts.append(1)
|
|
66
|
+
elif arr[i] == arr[i-1] + 1:
|
|
67
|
+
sums[-1] += arr[i]
|
|
68
|
+
counts[-1] += 1
|
|
69
|
+
else:
|
|
70
|
+
sums.append(arr[i])
|
|
71
|
+
counts.append(1)
|
|
72
|
+
return np.array(sums) // np.array(counts)
|
|
73
|
+
|
|
74
|
+
def dfs(x, y, out, output, current_num):
|
|
75
|
+
if x < 0 or x >= out.shape[1] or y < 0 or y >= out.shape[0]:
|
|
76
|
+
return
|
|
77
|
+
if out[y, x] != ' ':
|
|
78
|
+
return
|
|
79
|
+
out[y, x] = current_num
|
|
80
|
+
if output['top'][y, x] == 0:
|
|
81
|
+
dfs(x, y-1, out, output, current_num)
|
|
82
|
+
if output['left'][y, x] == 0:
|
|
83
|
+
dfs(x-1, y, out, output, current_num)
|
|
84
|
+
if output['right'][y, x] == 0:
|
|
85
|
+
dfs(x+1, y, out, output, current_num)
|
|
86
|
+
if output['bottom'][y, x] == 0:
|
|
87
|
+
dfs(x, y+1, out, output, current_num)
|
|
88
|
+
|
|
89
|
+
def main(image):
|
|
90
|
+
global Image
|
|
91
|
+
global cv
|
|
92
|
+
import matplotlib.pyplot as plt
|
|
93
|
+
from PIL import Image as Image_module
|
|
94
|
+
import cv2 as cv_module
|
|
95
|
+
Image = Image_module
|
|
96
|
+
cv = cv_module
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
image_path = Path(image)
|
|
100
|
+
output_path = image_path.parent / (image_path.stem + '.json')
|
|
101
|
+
src = cv.imread(image, cv.IMREAD_COLOR)
|
|
102
|
+
assert src is not None, f'Error opening image: {image}'
|
|
103
|
+
if len(src.shape) != 2:
|
|
104
|
+
gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
|
|
105
|
+
else:
|
|
106
|
+
gray = src
|
|
107
|
+
# now the image is in grayscale
|
|
108
|
+
|
|
109
|
+
# Apply adaptiveThreshold at the bitwise_not of gray, notice the ~ symbol
|
|
110
|
+
gray = cv.bitwise_not(gray)
|
|
111
|
+
bw = cv.adaptiveThreshold(gray.copy(), 255, cv.ADAPTIVE_THRESH_MEAN_C, \
|
|
112
|
+
cv.THRESH_BINARY, 15, -2)
|
|
113
|
+
# show_wait_destroy("binary", bw)
|
|
114
|
+
|
|
115
|
+
# show_wait_destroy("src", src)
|
|
116
|
+
horizontal_idx, vertical_idx = extract_lines(bw)
|
|
117
|
+
horizontal_idx = mean_consecutives(horizontal_idx)
|
|
118
|
+
vertical_idx = mean_consecutives(vertical_idx)
|
|
119
|
+
height = len(horizontal_idx)
|
|
120
|
+
width = len(vertical_idx)
|
|
121
|
+
print(f"height: {height}, width: {width}")
|
|
122
|
+
print(f"horizontal_idx: {horizontal_idx}")
|
|
123
|
+
print(f"vertical_idx: {vertical_idx}")
|
|
124
|
+
arr = np.zeros((height - 1, width - 1), dtype=object)
|
|
125
|
+
output = {'top': arr.copy(), 'left': arr.copy(), 'right': arr.copy(), 'bottom': arr.copy()}
|
|
126
|
+
target = 200_000
|
|
127
|
+
hists = {'top': {}, 'left': {}, 'right': {}, 'bottom': {}}
|
|
128
|
+
for j in range(height - 1):
|
|
129
|
+
for i in range(width - 1):
|
|
130
|
+
hidx1, hidx2 = horizontal_idx[j], horizontal_idx[j+1]
|
|
131
|
+
vidx1, vidx2 = vertical_idx[i], vertical_idx[i+1]
|
|
132
|
+
hidx1 = max(0, hidx1 - 2)
|
|
133
|
+
hidx2 = min(src.shape[0], hidx2 + 4)
|
|
134
|
+
vidx1 = max(0, vidx1 - 2)
|
|
135
|
+
vidx2 = min(src.shape[1], vidx2 + 4)
|
|
136
|
+
cell = src[hidx1:hidx2, vidx1:vidx2]
|
|
137
|
+
mid_x = cell.shape[1] // 2
|
|
138
|
+
mid_y = cell.shape[0] // 2
|
|
139
|
+
# show_wait_destroy(f"cell_{i}_{j}", cell)
|
|
140
|
+
cell = cv.bitwise_not(cell) # invert colors
|
|
141
|
+
top = cell[0:10, mid_y-5:mid_y+5]
|
|
142
|
+
hists['top'][j, i] = np.sum(top)
|
|
143
|
+
left = cell[mid_x-5:mid_x+5, 0:10]
|
|
144
|
+
hists['left'][j, i] = np.sum(left)
|
|
145
|
+
right = cell[mid_x-5:mid_x+5, -10:]
|
|
146
|
+
hists['right'][j, i] = np.sum(right)
|
|
147
|
+
bottom = cell[-10:, mid_y-5:mid_y+5]
|
|
148
|
+
hists['bottom'][j, i] = np.sum(bottom)
|
|
149
|
+
|
|
150
|
+
fig, axs = plt.subplots(2, 2)
|
|
151
|
+
axs[0, 0].hist(list(hists['top'].values()), bins=100)
|
|
152
|
+
axs[0, 0].set_title('Top')
|
|
153
|
+
axs[0, 1].hist(list(hists['left'].values()), bins=100)
|
|
154
|
+
axs[0, 1].set_title('Left')
|
|
155
|
+
axs[1, 0].hist(list(hists['right'].values()), bins=100)
|
|
156
|
+
axs[1, 0].set_title('Right')
|
|
157
|
+
axs[1, 1].hist(list(hists['bottom'].values()), bins=100)
|
|
158
|
+
axs[1, 1].set_title('Bottom')
|
|
159
|
+
target_top = np.mean(list(hists['top'].values()))
|
|
160
|
+
target_left = np.mean(list(hists['left'].values()))
|
|
161
|
+
target_right = np.mean(list(hists['right'].values()))
|
|
162
|
+
target_bottom = np.mean(list(hists['bottom'].values()))
|
|
163
|
+
axs[0, 0].axvline(target_top, color='red')
|
|
164
|
+
axs[0, 1].axvline(target_left, color='red')
|
|
165
|
+
axs[1, 0].axvline(target_right, color='red')
|
|
166
|
+
axs[1, 1].axvline(target_bottom, color='red')
|
|
167
|
+
# plt.show()
|
|
168
|
+
# 1/0
|
|
169
|
+
print(f"target_top: {target_top}, target_left: {target_left}, target_right: {target_right}, target_bottom: {target_bottom}")
|
|
170
|
+
for j in range(height - 1):
|
|
171
|
+
for i in range(width - 1):
|
|
172
|
+
if hists['top'][j, i] > target_top:
|
|
173
|
+
output['top'][j, i] = 1
|
|
174
|
+
if hists['left'][j, i] > target_left:
|
|
175
|
+
output['left'][j, i] = 1
|
|
176
|
+
if hists['right'][j, i] > target_right:
|
|
177
|
+
output['right'][j, i] = 1
|
|
178
|
+
if hists['bottom'][j, i] > target_bottom:
|
|
179
|
+
output['bottom'][j, i] = 1
|
|
180
|
+
print(f"cell_{j}_{i}", end=': ')
|
|
181
|
+
print('T' if output['top'][j, i] else '', end='')
|
|
182
|
+
print('L' if output['left'][j, i] else '', end='')
|
|
183
|
+
print('R' if output['right'][j, i] else '', end='')
|
|
184
|
+
print('B' if output['bottom'][j, i] else '', end='')
|
|
185
|
+
print(' Sums: ', hists['top'][j, i], hists['left'][j, i], hists['right'][j, i], hists['bottom'][j, i])
|
|
186
|
+
|
|
187
|
+
current_count = 0
|
|
188
|
+
out = np.full_like(output['top'], ' ', dtype='U2')
|
|
189
|
+
for j in range(out.shape[0]):
|
|
190
|
+
for i in range(out.shape[1]):
|
|
191
|
+
if out[j, i] == ' ':
|
|
192
|
+
dfs(i, j, out, output, str(current_count).zfill(2))
|
|
193
|
+
current_count += 1
|
|
194
|
+
|
|
195
|
+
with open(output_path, 'w') as f:
|
|
196
|
+
f.write('[\n')
|
|
197
|
+
for i, row in enumerate(out):
|
|
198
|
+
f.write(' ' + str(row.tolist()).replace("'", '"'))
|
|
199
|
+
if i != len(out) - 1:
|
|
200
|
+
f.write(',')
|
|
201
|
+
f.write('\n')
|
|
202
|
+
f.write(']')
|
|
203
|
+
print('output json: ', output_path)
|
|
204
|
+
|
|
205
|
+
if __name__ == '__main__':
|
|
206
|
+
# to run this script and visualize the output, in the root run:
|
|
207
|
+
# python .\src\puzzle_solver\puzzles\stitches\parse_map\parse_map.py | python .\src\puzzle_solver\utils\visualizer.py --read_stdin
|
|
208
|
+
# main(Path(__file__).parent / 'input_output' / 'MTM6OSw4MjEsNDAx.png')
|
|
209
|
+
# main(Path(__file__).parent / 'input_output' / 'weekly_oct_3rd_2025.png')
|
|
210
|
+
# main(Path(__file__).parent / 'input_output' / 'star_battle_67f73ff90cd8cdb4b3e30f56f5261f4968f5dac940bc6.png')
|
|
211
|
+
# main(Path(__file__).parent / 'input_output' / 'LITS_MDoxNzksNzY3.png')
|
|
212
|
+
main(Path(__file__).parent / 'input_output' / 'lits_OTo3LDMwNiwwMTU=.png')
|
|
@@ -3,7 +3,7 @@ import numpy as np
|
|
|
3
3
|
from ortools.sat.python import cp_model
|
|
4
4
|
|
|
5
5
|
from puzzle_solver.core.utils import Pos, get_all_pos, set_char, get_char, Direction, in_bounds, get_next_pos, get_row_pos, get_col_pos, get_opposite_direction
|
|
6
|
-
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
6
|
+
from puzzle_solver.core.utils_ortools import force_connected_component, generic_solve_all, SingleSolution
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class Board:
|
|
@@ -25,7 +25,6 @@ class Board:
|
|
|
25
25
|
self.model = cp_model.CpModel()
|
|
26
26
|
self.cell_active: dict[Pos, cp_model.IntVar] = {}
|
|
27
27
|
self.cell_direction: dict[tuple[Pos, Direction], cp_model.IntVar] = {}
|
|
28
|
-
self.reach_layers: list[dict[Pos, cp_model.IntVar]] = [] # R_t[p] booleans, t = 0..T
|
|
29
28
|
|
|
30
29
|
self.create_vars()
|
|
31
30
|
self.add_all_constraints()
|
|
@@ -35,19 +34,13 @@ class Board:
|
|
|
35
34
|
self.cell_active[pos] = self.model.NewBoolVar(f'{pos}')
|
|
36
35
|
for direction in Direction:
|
|
37
36
|
self.cell_direction[(pos, direction)] = self.model.NewBoolVar(f'{pos}:{direction}')
|
|
38
|
-
# Percolation layers R_t (monotone flood fill)
|
|
39
|
-
for t in range(self.V * self.H + 1):
|
|
40
|
-
Rt: dict[Pos, cp_model.IntVar] = {}
|
|
41
|
-
for pos in get_all_pos(self.V, self.H):
|
|
42
|
-
Rt[pos] = self.model.NewBoolVar(f"R[{t}][{pos}]")
|
|
43
|
-
self.reach_layers.append(Rt)
|
|
44
37
|
|
|
45
38
|
def add_all_constraints(self):
|
|
46
39
|
self.force_hints()
|
|
47
40
|
self.force_sides()
|
|
48
41
|
self.force_0_or_2_active()
|
|
49
42
|
self.force_direction_constraints()
|
|
50
|
-
self.
|
|
43
|
+
self.force_connected_component()
|
|
51
44
|
|
|
52
45
|
|
|
53
46
|
def force_hints(self):
|
|
@@ -108,38 +101,16 @@ class Board:
|
|
|
108
101
|
for pos in get_row_pos(0, self.H):
|
|
109
102
|
self.model.Add(self.cell_direction[(pos, Direction.UP)] == 0)
|
|
110
103
|
|
|
111
|
-
def
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
self.model
|
|
121
|
-
for pos in get_all_pos(self.V, self.H):
|
|
122
|
-
if pos != self.first_col_start_pos:
|
|
123
|
-
self.model.Add(self.reach_layers[0][pos] == 0)
|
|
124
|
-
|
|
125
|
-
for t in range(1, len(self.reach_layers)):
|
|
126
|
-
Rt_prev = self.reach_layers[t - 1]
|
|
127
|
-
Rt = self.reach_layers[t]
|
|
128
|
-
for p in get_all_pos(self.V, self.H):
|
|
129
|
-
# Rt[p] = Rt_prev[p] | (active[p] & Rt_prev[neighbour #1]) | (active[p] & Rt_prev[neighbour #2]) | ...
|
|
130
|
-
# Create helper (active[p] & Rt_prev[neighbour #X]) for each neighbor q
|
|
131
|
-
neigh_helpers: list[cp_model.IntVar] = []
|
|
132
|
-
for direction in Direction:
|
|
133
|
-
q = get_next_pos(p, direction)
|
|
134
|
-
if not in_bounds(q, self.V, self.H):
|
|
135
|
-
continue
|
|
136
|
-
a = self.model.NewBoolVar(f"A[{t}][{p}]<-({q})")
|
|
137
|
-
and_constraint(self.model, target=a, cs=[self.cell_active[p], Rt_prev[q], self.cell_direction[(q, get_opposite_direction(direction))]])
|
|
138
|
-
neigh_helpers.append(a)
|
|
139
|
-
or_constraint(self.model, target=Rt[p], cs=[Rt_prev[p]] + neigh_helpers)
|
|
140
|
-
# every avtive track must be reachible -> single connected component
|
|
141
|
-
for pos in get_all_pos(self.V, self.H):
|
|
142
|
-
self.model.Add(self.reach_layers[-1][pos] == 1).OnlyEnforceIf(self.cell_active[pos])
|
|
104
|
+
def force_connected_component(self):
|
|
105
|
+
def is_neighbor(pd1: tuple[Pos, Direction], pd2: tuple[Pos, Direction]) -> bool:
|
|
106
|
+
p1, d1 = pd1
|
|
107
|
+
p2, d2 = pd2
|
|
108
|
+
if p1 == p2: # same position, different direction, is neighbor
|
|
109
|
+
return True
|
|
110
|
+
if get_next_pos(p1, d1) == p2 and d2 == get_opposite_direction(d1):
|
|
111
|
+
return True
|
|
112
|
+
return False
|
|
113
|
+
force_connected_component(self.model, self.cell_direction, is_neighbor=is_neighbor)
|
|
143
114
|
|
|
144
115
|
|
|
145
116
|
|
|
File without changes
|
|
File without changes
|