multi-puzzle-solver 0.9.30__py3-none-any.whl → 1.0.2__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.

Files changed (45) hide show
  1. {multi_puzzle_solver-0.9.30.dist-info → multi_puzzle_solver-1.0.2.dist-info}/METADATA +331 -76
  2. multi_puzzle_solver-1.0.2.dist-info/RECORD +69 -0
  3. puzzle_solver/__init__.py +58 -1
  4. puzzle_solver/core/utils_ortools.py +8 -6
  5. puzzle_solver/core/utils_visualizer.py +23 -41
  6. puzzle_solver/puzzles/binairo/binairo.py +4 -4
  7. puzzle_solver/puzzles/black_box/black_box.py +5 -11
  8. puzzle_solver/puzzles/bridges/bridges.py +1 -1
  9. puzzle_solver/puzzles/chess_range/chess_range.py +3 -3
  10. puzzle_solver/puzzles/chess_range/chess_solo.py +1 -1
  11. puzzle_solver/puzzles/filling/filling.py +3 -3
  12. puzzle_solver/puzzles/flood_it/flood_it.py +174 -0
  13. puzzle_solver/puzzles/flood_it/parse_map/parse_map.py +198 -0
  14. puzzle_solver/puzzles/galaxies/galaxies.py +1 -1
  15. puzzle_solver/puzzles/galaxies/parse_map/parse_map.py +3 -3
  16. puzzle_solver/puzzles/guess/guess.py +1 -1
  17. puzzle_solver/puzzles/heyawake/heyawake.py +3 -3
  18. puzzle_solver/puzzles/inertia/inertia.py +1 -1
  19. puzzle_solver/puzzles/inertia/parse_map/parse_map.py +13 -10
  20. puzzle_solver/puzzles/inertia/tsp.py +5 -7
  21. puzzle_solver/puzzles/kakuro/kakuro.py +1 -1
  22. puzzle_solver/puzzles/keen/keen.py +2 -2
  23. puzzle_solver/puzzles/minesweeper/minesweeper.py +2 -3
  24. puzzle_solver/puzzles/nonograms/nonograms.py +3 -3
  25. puzzle_solver/puzzles/norinori/norinori.py +2 -2
  26. puzzle_solver/puzzles/nurikabe/nurikabe.py +2 -2
  27. puzzle_solver/puzzles/range/range.py +1 -1
  28. puzzle_solver/puzzles/rectangles/rectangles.py +2 -6
  29. puzzle_solver/puzzles/shingoki/shingoki.py +1 -1
  30. puzzle_solver/puzzles/signpost/signpost.py +2 -2
  31. puzzle_solver/puzzles/slant/parse_map/parse_map.py +7 -5
  32. puzzle_solver/puzzles/slitherlink/slitherlink.py +1 -1
  33. puzzle_solver/puzzles/stitches/parse_map/parse_map.py +6 -5
  34. puzzle_solver/puzzles/stitches/stitches.py +1 -1
  35. puzzle_solver/puzzles/sudoku/sudoku.py +91 -20
  36. puzzle_solver/puzzles/tents/tents.py +2 -2
  37. puzzle_solver/puzzles/thermometers/thermometers.py +1 -1
  38. puzzle_solver/puzzles/towers/towers.py +1 -1
  39. puzzle_solver/puzzles/undead/undead.py +1 -1
  40. puzzle_solver/puzzles/unruly/unruly.py +1 -1
  41. puzzle_solver/puzzles/yin_yang/yin_yang.py +1 -1
  42. puzzle_solver/utils/visualizer.py +1 -1
  43. multi_puzzle_solver-0.9.30.dist-info/RECORD +0 -67
  44. {multi_puzzle_solver-0.9.30.dist-info → multi_puzzle_solver-1.0.2.dist-info}/WHEEL +0 -0
  45. {multi_puzzle_solver-0.9.30.dist-info → multi_puzzle_solver-1.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,198 @@
1
+ """
2
+ This file is a simple helper that parses the images from https://www.chiark.greenend.org.uk 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
+ # import json
7
+ from pathlib import Path
8
+ import numpy as np
9
+ cv = None
10
+ Image = None
11
+
12
+
13
+ def extract_lines(bw):
14
+ # Create the images that will use to extract the horizontal and vertical lines
15
+ horizontal = np.copy(bw)
16
+ vertical = np.copy(bw)
17
+
18
+ cols = horizontal.shape[1]
19
+ horizontal_size = cols // 20
20
+ # Create structure element for extracting horizontal lines through morphology operations
21
+ horizontalStructure = cv.getStructuringElement(cv.MORPH_RECT, (horizontal_size, 1))
22
+ horizontal = cv.erode(horizontal, horizontalStructure)
23
+ horizontal = cv.dilate(horizontal, horizontalStructure)
24
+ horizontal_means = np.mean(horizontal, axis=1)
25
+ horizontal_cutoff = np.percentile(horizontal_means, 50)
26
+ # location where the horizontal lines are
27
+ horizontal_idx = np.where(horizontal_means > horizontal_cutoff)[0]
28
+ # print(f"horizontal_idx: {horizontal_idx}")
29
+ # height = len(horizontal_idx)
30
+ # show_wait_destroy("horizontal", horizontal) # this has the horizontal lines
31
+
32
+ rows = vertical.shape[0]
33
+ verticalsize = rows // 20
34
+ # Create structure element for extracting vertical lines through morphology operations
35
+ verticalStructure = cv.getStructuringElement(cv.MORPH_RECT, (1, verticalsize))
36
+ vertical = cv.erode(vertical, verticalStructure)
37
+ vertical = cv.dilate(vertical, verticalStructure)
38
+ vertical_means = np.mean(vertical, axis=0)
39
+ vertical_cutoff = np.percentile(vertical_means, 50)
40
+ vertical_idx = np.where(vertical_means > vertical_cutoff)[0]
41
+ # print(f"vertical_idx: {vertical_idx}")
42
+ # width = len(vertical_idx)
43
+ # print(f"height: {height}, width: {width}")
44
+ # print(f"vertical_means: {vertical_means}")
45
+ # show_wait_destroy("vertical", vertical) # this has the vertical lines
46
+
47
+ vertical = cv.bitwise_not(vertical)
48
+ # show_wait_destroy("vertical_bit", vertical)
49
+
50
+ return horizontal_idx, vertical_idx
51
+
52
+ def show_wait_destroy(winname, img):
53
+ cv.imshow(winname, img)
54
+ cv.moveWindow(winname, 500, 0)
55
+ cv.waitKey(0)
56
+ cv.destroyWindow(winname)
57
+
58
+
59
+ def mean_consecutives(arr: np.ndarray) -> np.ndarray:
60
+ """if a sequence of values is consecutive, then average the values"""
61
+ sums = []
62
+ counts = []
63
+ for i in range(len(arr)):
64
+ if i == 0:
65
+ sums.append(arr[i])
66
+ counts.append(1)
67
+ elif arr[i] == arr[i-1] + 1:
68
+ sums[-1] += arr[i]
69
+ counts[-1] += 1
70
+ else:
71
+ sums.append(arr[i])
72
+ counts.append(1)
73
+ return np.array(sums) // np.array(counts)
74
+
75
+ def main(image):
76
+ global Image
77
+ global cv
78
+ import matplotlib.pyplot as plt
79
+ from PIL import Image as Image_module
80
+ import cv2 as cv_module
81
+ Image = Image_module
82
+ cv = cv_module
83
+
84
+
85
+ image_path = Path(image)
86
+ output_path = image_path.parent / (image_path.stem + '.json')
87
+ src = cv.imread(image, cv.IMREAD_COLOR)
88
+ assert src is not None, f'Error opening image: {image}'
89
+ if len(src.shape) != 2:
90
+ gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
91
+ else:
92
+ gray = src
93
+ # now the image is in grayscale
94
+
95
+ # Apply adaptiveThreshold at the bitwise_not of gray, notice the ~ symbol
96
+ gray = cv.bitwise_not(gray)
97
+ bw = cv.adaptiveThreshold(gray.copy(), 255, cv.ADAPTIVE_THRESH_MEAN_C, \
98
+ cv.THRESH_BINARY, 15, -2)
99
+ # show_wait_destroy("binary", bw)
100
+
101
+ # show_wait_destroy("src", src)
102
+ horizontal_idx, vertical_idx = extract_lines(bw)
103
+ horizontal_idx = mean_consecutives(horizontal_idx)
104
+ vertical_idx = mean_consecutives(vertical_idx)
105
+ median_vertical_dist = np.median(np.diff(vertical_idx))
106
+ median_horizontal_dist = np.median(np.diff(horizontal_idx))
107
+ print(f"median_vertical_dist: {median_vertical_dist}, median_horizontal_dist: {median_horizontal_dist}")
108
+ height = len(horizontal_idx)
109
+ width = len(vertical_idx)
110
+ print(f"height: {height}, width: {width}")
111
+ print(f"horizontal_idx: {horizontal_idx}")
112
+ print(f"vertical_idx: {vertical_idx}")
113
+ output_rgb = {}
114
+ j_idx = 0
115
+ for j in range(height - 1):
116
+ i_idx = 0
117
+ for i in range(width - 1):
118
+ hidx1, hidx2 = horizontal_idx[j], horizontal_idx[j+1]
119
+ vidx1, vidx2 = vertical_idx[i], vertical_idx[i+1]
120
+ hidx1 = max(0, hidx1 - 2)
121
+ hidx2 = min(src.shape[0], hidx2 + 4)
122
+ vidx1 = max(0, vidx1 - 2)
123
+ vidx2 = min(src.shape[1], vidx2 + 4)
124
+ if (hidx2 - hidx1) < median_horizontal_dist * 0.5 or (vidx2 - vidx1) < median_vertical_dist * 0.5:
125
+ continue
126
+ cell = src[hidx1:hidx2, vidx1:vidx2]
127
+ mid_x = cell.shape[1] // 2
128
+ mid_y = cell.shape[0] // 2
129
+ print(f"mid_x: {mid_x}, mid_y: {mid_y}")
130
+ cell_50_percent = cell[int(mid_y*0.5):int(mid_y*1.5), int(mid_x*0.5):int(mid_x*1.5)]
131
+ # show_wait_destroy(f"cell_{i_idx}_{j_idx}", cell_50_percent)
132
+ output_rgb[j_idx, i_idx] = cell_50_percent.mean(axis=(0, 1))
133
+ print(f"output_rgb[{j_idx}, {i_idx}]: {output_rgb[j_idx, i_idx]}")
134
+ i_idx += 1
135
+ j_idx += 1
136
+
137
+ colors_to_cluster = cluster_colors(output_rgb)
138
+ width = max(pos[1] for pos in output_rgb.keys()) + 1
139
+ height = max(pos[0] for pos in output_rgb.keys()) + 1
140
+ out = np.zeros((height, width), dtype=object)
141
+ print(colors_to_cluster)
142
+ for pos, cluster_id in colors_to_cluster.items():
143
+ out[pos[0], pos[1]] = cluster_id
144
+ print('Shape of out:', out.shape)
145
+
146
+ with open(output_path, 'w') as f:
147
+ f.write('[\n')
148
+ for i, row in enumerate(out):
149
+ f.write(' ' + str(row.tolist()).replace("'", '"'))
150
+ if i != len(out) - 1:
151
+ f.write(',')
152
+ f.write('\n')
153
+ f.write(']')
154
+ print('output json: ', output_path)
155
+
156
+ def euclidean_distance(a: tuple[int, int, int], b: tuple[int, int, int]) -> int:
157
+ return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2) ** 0.5
158
+
159
+ KNOWN_COLORS = {
160
+ (0, 0, 255): 'Red',
161
+ (0, 255, 0): 'Green',
162
+ (255, 77, 51): 'Blue',
163
+ (0, 255, 255): 'Yellow',
164
+ (255, 153, 255): 'Pink',
165
+ (0, 128, 255): 'Orange',
166
+ (255, 204, 102): 'Cyan',
167
+ (179, 255, 179): 'Washed Green',
168
+ (77, 77, 128): 'Brown',
169
+ (179, 0, 128): 'Purple',
170
+ }
171
+
172
+ def cluster_colors(rgb: dict[tuple[int, int], tuple[int, int, int]]) -> dict[tuple[int, int, int], int]:
173
+ MIN_DIST = 10 # if distance between two colors is less than this, then they are the same color
174
+ colors_to_cluster = KNOWN_COLORS.copy()
175
+ for pos, color in rgb.items():
176
+ color = tuple(color)
177
+ if color in colors_to_cluster:
178
+ continue
179
+ for existing_color, existing_cluster_id in colors_to_cluster.items():
180
+ if euclidean_distance(color, existing_color) < MIN_DIST:
181
+ colors_to_cluster[color] = existing_cluster_id
182
+ break
183
+ else:
184
+ new_name = str(', '.join(str(int(c)) for c in color))
185
+ print('WARNING: new color found:', new_name, 'at pos:', pos)
186
+ colors_to_cluster[color] = new_name
187
+ pos_to_cluster = {pos: colors_to_cluster[tuple(color)] for pos, color in rgb.items()}
188
+ return pos_to_cluster
189
+
190
+
191
+ if __name__ == '__main__':
192
+ # to run this script and visualize the output, in the root run:
193
+ # python .\src\puzzle_solver\puzzles\flood_it\parse_map\parse_map.py | python .\src\puzzle_solver\utils\visualizer.py --read_stdin
194
+ # main(Path(__file__).parent / 'input_output' / 'flood.html#12x12c10m5%23637467359431429.png')
195
+ # main(Path(__file__).parent / 'input_output' / 'flood.html#12x12c6m5%23132018455881870.png')
196
+ # main(Path(__file__).parent / 'input_output' / 'flood.html#12x12c6m0%23668276603006993.png')
197
+ # main(Path(__file__).parent / 'input_output' / 'flood.html#20x20c8m0%23991967486182787.png')flood.html#20x20c4m0%23690338575695152
198
+ main(Path(__file__).parent / 'input_output' / 'flood.html#20x20c4m0%23690338575695152.png')
@@ -85,7 +85,7 @@ class Board:
85
85
  self.model.AddExactlyOne(pos_vars)
86
86
  for galaxy_idx, v in self.pos_to_galaxy[pos].items():
87
87
  galaxy_vars.setdefault(galaxy_idx, {})[pos] = v
88
- for galaxy_idx, pos_vars in galaxy_vars.items():
88
+ for pos_vars in galaxy_vars.values():
89
89
  force_connected_component(self.model, pos_vars)
90
90
 
91
91
 
@@ -1,5 +1,5 @@
1
1
  """
2
- This file is a simple helper that parses the images from https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/galaxies.html and converts them to a json file.
2
+ This file is a simple helper that parses the images from https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/galaxies.html and converts them to a json file.
3
3
  Look at the ./input_output/ directory for examples of input images and output json files.
4
4
  The output json is used in the test_solve.py file to test the solver.
5
5
  """
@@ -25,7 +25,7 @@ def extract_lines(bw):
25
25
  # location where the horizontal lines are
26
26
  horizontal_idx = np.where(horizontal_means > horizontal_cutoff)[0]
27
27
  # print(f"horizontal_idx: {horizontal_idx}")
28
- height = len(horizontal_idx)
28
+ # height = len(horizontal_idx)
29
29
  # show_wait_destroy("horizontal", horizontal) # this has the horizontal lines
30
30
 
31
31
  rows = vertical.shape[0]
@@ -38,7 +38,7 @@ def extract_lines(bw):
38
38
  vertical_cutoff = np.percentile(vertical_means, 50)
39
39
  vertical_idx = np.where(vertical_means > vertical_cutoff)[0]
40
40
  # print(f"vertical_idx: {vertical_idx}")
41
- width = len(vertical_idx)
41
+ # width = len(vertical_idx)
42
42
  # print(f"height: {height}, width: {width}")
43
43
  # print(f"vertical_means: {vertical_means}")
44
44
  # show_wait_destroy("vertical", vertical) # this has the vertical lines
@@ -5,7 +5,7 @@ import numpy as np
5
5
 
6
6
 
7
7
  class Board:
8
- def __init__(self, num_pegs: int = 4, all_colors: list[str] = ['R', 'Y', 'G', 'B', 'O', 'P'], show_warnings: bool = True, show_progress: bool = False):
8
+ def __init__(self, num_pegs: int = 4, all_colors: tuple[str] = ('R', 'Y', 'G', 'B', 'O', 'P'), show_warnings: bool = True, show_progress: bool = False):
9
9
  assert num_pegs >= 1, 'num_pegs must be at least 1'
10
10
  assert len(all_colors) == len(set(all_colors)), 'all_colors must contain only unique colors'
11
11
  self.previous_guesses = []
@@ -1,12 +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_neighbors4, get_pos, set_char, get_char
4
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_neighbors4, get_pos, get_char
5
5
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
6
6
  from puzzle_solver.core.utils_visualizer import render_shaded_grid
7
7
 
8
+
8
9
  def return_3_consecutives(int_list: list[int]) -> list[tuple[int, int]]:
9
- """Given a list of integers (mostly with duplicates), return every consecutive sequence of 3 integer changes.
10
+ """Given a list of integers (mostly with duplicates), return every consecutive sequence of 3 integer changes.
10
11
  i.e. return a list of (begin_idx, end_idx) tuples where for each r=int_list[begin_idx:end_idx] we have r[0]!=r[1] and r[-2]!=r[-1] and len(r)>=3"""
11
12
  out = []
12
13
  change_indices = [i for i in range(len(int_list) - 1) if int_list[i] != int_list[i+1]]
@@ -18,7 +19,6 @@ def return_3_consecutives(int_list: list[int]) -> list[tuple[int, int]]:
18
19
  continue
19
20
  out.append((begin_idx, end_idx))
20
21
  return out
21
-
22
22
 
23
23
  class Board:
24
24
  def __init__(self, board: np.array, region_to_clue: dict[str, int]):
@@ -118,4 +118,4 @@ def solve_optimal_walk(
118
118
  seed: int = 0,
119
119
  verbose: bool = False
120
120
  ) -> list[tuple[Pos, Pos]]:
121
- return tsp.solve_optimal_walk(start_pos, edges, gems_to_edges, restarts=restarts, time_limit_ms=time_limit_ms, seed=seed, verbose=verbose)
121
+ return tsp.solve_optimal_walk(start_pos, edges, gems_to_edges, restarts=restarts, time_limit_ms=time_limit_ms, seed=seed, verbose=verbose)
@@ -1,25 +1,24 @@
1
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.
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
3
  Look at the ./input_output/ directory for examples of input images and output json files.
4
4
  The output json is used in the test_solve.py file to test the solver.
5
5
  """
6
6
  from pathlib import Path
7
7
  import numpy as np
8
- import numpy as np
9
8
  cv = None
10
9
  Image = None
11
10
 
12
11
  def load_cell_templates(p: Path) -> dict[str, dict]:
13
- img = Image.open(p)
12
+ # img = Image.open(p)
14
13
  src = cv.imread(p, cv.IMREAD_COLOR)
15
- rgb = np.asarray(img).astype(np.float32) / 255.0
14
+ # rgb = np.asarray(img).astype(np.float32) / 255.0
16
15
  if len(src.shape) != 2:
17
16
  gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
18
17
  else:
19
18
  gray = src
20
19
  gray = cv.bitwise_not(gray)
21
- bw = cv.adaptiveThreshold(gray.copy(), 255, cv.ADAPTIVE_THRESH_MEAN_C, \
22
- cv.THRESH_BINARY, 15, -2)
20
+ # bw = cv.adaptiveThreshold(gray.copy(), 255, cv.ADAPTIVE_THRESH_MEAN_C, \
21
+ # cv.THRESH_BINARY, 15, -2)
23
22
  return {"gray": gray}
24
23
 
25
24
 
@@ -53,10 +52,14 @@ def get_distance_robust(cell: np.ndarray, template: np.ndarray, max_shift: int =
53
52
  for dy in range(-max_shift, max_shift + 1):
54
53
  for dx in range(-max_shift, max_shift + 1):
55
54
  # compute overlapping slices for this shift
56
- y0a = max(0, dy); y1a = H + min(0, dy)
57
- x0a = max(0, dx); x1a = W + min(0, dx)
58
- y0b = max(0, -dy); y1b = H + min(0, -dy)
59
- x0b = max(0, -dx); x1b = W + min(0, -dx)
55
+ y0a = max(0, dy)
56
+ y1a = H + min(0, dy)
57
+ x0a = max(0, dx)
58
+ x1a = W + min(0, dx)
59
+ y0b = max(0, -dy)
60
+ y1b = H + min(0, -dy)
61
+ x0b = max(0, -dx)
62
+ x1b = W + min(0, -dx)
60
63
 
61
64
  if y1a <= y0a or x1a <= x0a: # no overlap
62
65
  continue
@@ -197,7 +197,7 @@ def solve_optimal_walk(
197
197
  changed = False
198
198
  i = 0
199
199
  while i + 3 < len(ns):
200
- u, v, w, z = ns[i], ns[i+1], ns[i+2], ns[i+3]
200
+ u, v, w = ns[i], ns[i+1], ns[i+2]
201
201
  if w == u: # u->v, v->u
202
202
  before_edges = walk_edges(ns[:i+1])
203
203
  removed_edges = [(u, v), (v, u)]
@@ -235,7 +235,7 @@ def solve_optimal_walk(
235
235
  for i in range(N_no_depot):
236
236
  gi = state_group[i]
237
237
  for j in range(N_no_depot):
238
- if i == j:
238
+ if i == j:
239
239
  continue
240
240
  gj = state_group[j]
241
241
  if gi != gj:
@@ -244,9 +244,9 @@ def solve_optimal_walk(
244
244
  # ring + shift
245
245
  INF = 10**12
246
246
  succ_in_cluster: Dict[int, int] = {}
247
- for g, order in cluster_orders.items():
247
+ for order in cluster_orders.values():
248
248
  k = len(order)
249
- if k == 0:
249
+ if k == 0:
250
250
  continue
251
251
  pred = {}
252
252
  for idx, v in enumerate(order):
@@ -327,7 +327,6 @@ def solve_optimal_walk(
327
327
 
328
328
  best_nodes = None
329
329
  best_cost = float('inf')
330
- best_reps = None
331
330
 
332
331
  # initial deterministic order as a baseline
333
332
  def shuffled_cluster_orders():
@@ -339,7 +338,7 @@ def solve_optimal_walk(
339
338
  return orders
340
339
 
341
340
  attempts = max(1, restarts)
342
- for attempt in range(attempts):
341
+ for _ in range(attempts):
343
342
  cluster_orders = shuffled_cluster_orders()
344
343
  for meta in meta_list:
345
344
  # print('solve once')
@@ -382,7 +381,6 @@ def solve_optimal_walk(
382
381
  if cost < best_cost:
383
382
  best_cost = cost
384
383
  best_nodes = nodes_seq
385
- best_reps = reps
386
384
 
387
385
  if best_nodes is None:
388
386
  raise RuntimeError("No solution found.")
@@ -4,7 +4,7 @@ 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 Direction, Pos, get_all_pos, get_next_pos, get_pos, in_bounds, set_char, get_char, get_neighbors8
7
+ from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_next_pos, get_pos, in_bounds, get_char
8
8
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
9
9
  from puzzle_solver.core.utils_visualizer import render_shaded_grid
10
10
 
@@ -75,9 +75,9 @@ class Board:
75
75
  for col in range(self.N):
76
76
  col_vars = [self.model_vars[pos] for pos in get_col_pos(col, self.N)]
77
77
  self.model.AddAllDifferent(col_vars)
78
-
78
+
79
79
  def constrain_block_results(self):
80
- # The digits in each block can be combined to form the number stated in the clue, using the arithmetic operation given in the clue. That is:
80
+ # The digits in each block can be combined to form the number stated in the clue, using the arithmetic operation given in the clue. That is:
81
81
  for block, (op, result) in self.block_results.items():
82
82
  block_vars = [self.model_vars[p] for p in self.get_block_pos(block)]
83
83
  add_opcode_constraint(self.model, block_vars, op, result)
@@ -103,7 +103,7 @@ def give_next_guess(board: np.array, mine_count: Optional[int] = None, verbose:
103
103
  print(new_garuneed_mine_positions)
104
104
  print('-'*10)
105
105
  if len(wrong_flag_positions) > 0:
106
- print(f"WARNING | "*4 + "WARNING")
106
+ print("WARNING | "*4 + "WARNING")
107
107
  print(f"Found {len(wrong_flag_positions)} wrong flag positions")
108
108
  print(wrong_flag_positions)
109
109
  print('-'*10)
@@ -120,5 +120,4 @@ def print_board(board: np.array, safe_positions: set[Pos], new_garuneed_mine_pos
120
120
  set_char(res, pos, 'M')
121
121
  elif get_char(board, pos) == 'F' and pos not in wrong_flag_positions:
122
122
  set_char(res, pos, 'F')
123
-
124
- print(res)
123
+ print(res)
@@ -7,8 +7,8 @@ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
7
7
 
8
8
  class Board:
9
9
  def __init__(self, top: list[list[int]], side: list[list[int]]):
10
- assert all(isinstance(i, int) for l in top for i in l), 'top must be a list of lists of integers'
11
- assert all(isinstance(i, int) for l in side for i in l), 'side must be a list of lists of integers'
10
+ assert all(isinstance(i, int) for line in top for i in line), 'top must be a list of lists of integers'
11
+ assert all(isinstance(i, int) for line in side for i in line), 'side must be a list of lists of integers'
12
12
  self.top = top
13
13
  self.side = side
14
14
  self.V = len(side)
@@ -63,7 +63,7 @@ class Board:
63
63
  # Start variables for each run. This is the most critical variable for the problem.
64
64
  starts = []
65
65
  self.extra_vars[f"{ns}_starts"] = starts
66
- for i, c in enumerate(clues):
66
+ for i in range(len(clues)):
67
67
  s = self.model.NewIntVar(0, L, f"{ns}_s[{i}]")
68
68
  starts.append(s)
69
69
  # Enforce order and >=1 blank between consecutive runs.
@@ -3,8 +3,8 @@ 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, Shape, get_all_pos, get_char, set_char, polyominoes, in_bounds, get_next_pos, Direction
7
- from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, and_constraint
6
+ from puzzle_solver.core.utils import Pos, Shape, get_all_pos, get_char, set_char, in_bounds, get_next_pos, Direction
7
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
8
8
 
9
9
 
10
10
  @dataclass
@@ -3,7 +3,7 @@ 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_neighbors4, get_pos, in_bounds, set_char, get_char, polyominoes, Shape, Direction, get_next_pos
6
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_neighbors4, get_pos, in_bounds, get_char, polyominoes, Shape, Direction, get_next_pos
7
7
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
8
8
  from puzzle_solver.core.utils_visualizer import render_shaded_grid
9
9
 
@@ -62,7 +62,7 @@ class Board:
62
62
  assert len(hint_shapes) > 0, f'no shapes found for hint {hint_pos} with value {hint_value}'
63
63
  self.model.AddExactlyOne([s.is_active for s in hint_shapes])
64
64
  self.shapes_on_board.extend(hint_shapes)
65
-
65
+
66
66
  # if no shape is active on the spot then it must be black
67
67
  for pos in self.get_all_legal_pos():
68
68
  shapes_here = [s for s in self.shapes_on_board if pos in s.body]
@@ -2,7 +2,7 @@ import numpy as np
2
2
  from ortools.sat.python import cp_model
3
3
 
4
4
  from puzzle_solver.core.utils import Pos, get_all_pos, set_char, get_neighbors4, in_bounds, Direction, get_next_pos, get_char
5
- from puzzle_solver.core.utils_ortools import and_constraint, or_constraint, generic_solve_all, SingleSolution, force_connected_component
5
+ from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, SingleSolution, force_connected_component
6
6
 
7
7
 
8
8
  def get_ray(pos: Pos, V: int, H: int, direction: Direction) -> list[Pos]:
@@ -97,7 +97,7 @@ class Board:
97
97
  def solve_and_print(self, verbose: bool = True):
98
98
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
99
99
  assignment: dict[Pos, int] = {}
100
- for (i, rectangle) in enumerate(self.rectangles):
100
+ for rectangle in self.rectangles:
101
101
  if solver.Value(rectangle.active) == 1:
102
102
  for pos in rectangle.body:
103
103
  assignment[pos] = f'id{rectangle.clue_id}:N={rectangle.N}:{rectangle.height}x{rectangle.width}'
@@ -121,10 +121,6 @@ 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('[')
125
- # for row in id_board:
126
- # print(' ', row.tolist(), end=',\n')
127
- # print(' ])')
128
- print(render_grid(res, center_char=self.board))
124
+ print(render_grid(res, center_char=lambda r, c: self.board[r, c] if self.board[r, c] != ' ' else ' '))
129
125
 
130
126
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -76,7 +76,7 @@ class Board:
76
76
  continue
77
77
  self.enforce_corner_color(pos, self.board_colors[pos])
78
78
  self.enforce_corner_number(pos, self.board_numbers[pos])
79
-
79
+
80
80
  # enforce single connected component
81
81
  def is_neighbor(edge1: tuple[Pos, Pos], edge2: tuple[Pos, Pos]) -> bool:
82
82
  return any(c1 == c2 for c1 in edge1 for c2 in edge2)
@@ -65,7 +65,7 @@ class Board:
65
65
  continue
66
66
  direction = CHAR_TO_DIRECTION8[c]
67
67
  self.constrain_plus_one(pos, direction)
68
-
68
+
69
69
  def constrain_plus_one(self, pos: Pos, direction: Direction8):
70
70
  beam_res = beam(pos, self.V, self.H, direction)
71
71
  is_eq_list = []
@@ -75,7 +75,7 @@ class Board:
75
75
  self.model.Add(self.model_vars[p] != self.model_vars[pos] + 1).OnlyEnforceIf(aux.Not())
76
76
  is_eq_list.append(aux)
77
77
  self.model.Add(lxp.Sum(is_eq_list) == 1)
78
-
78
+
79
79
  def solve_and_print(self, verbose: bool = True):
80
80
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
81
81
  assignment: dict[Pos, str] = {}
@@ -1,10 +1,10 @@
1
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.
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
3
  Look at the ./input_output/ directory for examples of input images and output json files.
4
4
  The output json is used in the test_solve.py file to test the solver.
5
5
  """
6
6
 
7
- import json, itertools
7
+ import itertools
8
8
  from pathlib import Path
9
9
  import numpy as np
10
10
  cv = None
@@ -37,9 +37,11 @@ def mean_consecutives(arr):
37
37
  sums, counts = [arr[0]], [1]
38
38
  for k in arr[1:]:
39
39
  if k == sums[-1] + counts[-1]:
40
- sums[-1] += k; counts[-1] += 1
40
+ sums[-1] += k
41
+ counts[-1] += 1
41
42
  else:
42
- sums.append(k); counts.append(1)
43
+ sums.append(k)
44
+ counts.append(1)
43
45
  return np.array(sums)//np.array(counts)
44
46
 
45
47
  def main(img_path):
@@ -90,7 +92,7 @@ def main(img_path):
90
92
 
91
93
  # Build KD-like search by grid proximity
92
94
  tol = int(cell*0.5) # max distance from an intersection to accept a circle
93
- for (cx, cy, r) in detected:
95
+ for (cx, cy, _) in detected:
94
96
  # find nearest indices
95
97
  j = int(np.argmin(np.abs(h_idx - cy)))
96
98
  i = int(np.argmin(np.abs(v_idx - cx)))
@@ -56,7 +56,7 @@ class Board:
56
56
  next_pos = get_next_pos(pos, direction)
57
57
  if in_bounds(next_pos, self.V, self.H):
58
58
  self.cell_borders[(next_pos, get_opposite_direction(direction))] = var
59
-
59
+
60
60
  def add_corner_vars(self, cell_border: CellBorder, var: cp_model.IntVar):
61
61
  """
62
62
  An edge always belongs to two corners. Note that the cell xi,yi has the 4 corners (xi,yi), (xi+1,yi), (xi,yi+1), (xi+1,yi+1). (memorize these 4 coordinates or the function wont make sense)
@@ -1,5 +1,5 @@
1
1
  """
2
- This file is a simple helper that parses the images from https://www.puzzle-stitches.com/ and converts them to a json file.
2
+ This file is a simple helper that parses the images from https://www.puzzle-stitches.com/ and converts them to a json file.
3
3
  Look at the ./input_output/ directory for examples of input images and output json files.
4
4
  The output json is used in the test_solve.py file to test the solver.
5
5
  """
@@ -26,7 +26,7 @@ def extract_lines(bw):
26
26
  # location where the horizontal lines are
27
27
  horizontal_idx = np.where(horizontal_means > horizontal_cutoff)[0]
28
28
  # print(f"horizontal_idx: {horizontal_idx}")
29
- height = len(horizontal_idx)
29
+ # height = len(horizontal_idx)
30
30
  # show_wait_destroy("horizontal", horizontal) # this has the horizontal lines
31
31
 
32
32
  rows = vertical.shape[0]
@@ -39,7 +39,7 @@ def extract_lines(bw):
39
39
  vertical_cutoff = np.percentile(vertical_means, 50)
40
40
  vertical_idx = np.where(vertical_means > vertical_cutoff)[0]
41
41
  # print(f"vertical_idx: {vertical_idx}")
42
- width = len(vertical_idx)
42
+ # width = len(vertical_idx)
43
43
  # print(f"height: {height}, width: {width}")
44
44
  # print(f"vertical_means: {vertical_means}")
45
45
  # show_wait_destroy("vertical", vertical) # this has the vertical lines
@@ -126,7 +126,6 @@ def main(image):
126
126
  print(f"vertical_idx: {vertical_idx}")
127
127
  arr = np.zeros((height - 1, width - 1), dtype=object)
128
128
  output = {'top': arr.copy(), 'left': arr.copy(), 'right': arr.copy(), 'bottom': arr.copy()}
129
- target = 200_000
130
129
  hists = {'top': {}, 'left': {}, 'right': {}, 'bottom': {}}
131
130
  for j in range(height - 1):
132
131
  for i in range(width - 1):
@@ -244,4 +243,6 @@ if __name__ == '__main__':
244
243
  # main(Path(__file__).parent / 'input_output' / 'norinori_501d93110d6b4b818c268378973afbf268f96cfa8d7b4.png')
245
244
  # main(Path(__file__).parent / 'input_output' / 'norinori_OTo0LDc0Miw5MTU.png')
246
245
  # main(Path(__file__).parent / 'input_output' / 'heyawake_MDoxNiwxNDQ=.png')
247
- main(Path(__file__).parent / 'input_output' / 'heyawake_MTQ6ODQ4LDEzOQ==.png')
246
+ # main(Path(__file__).parent / 'input_output' / 'heyawake_MTQ6ODQ4LDEzOQ==.png')
247
+ main(Path(__file__).parent / 'input_output' / 'sudoku_jigsaw.png')
248
+
@@ -77,7 +77,7 @@ class Board:
77
77
  # print(f'{pos}:{direction} must == {neighbor}:{opposite_direction}')
78
78
 
79
79
  # all blocks connected exactly N times (N usually 1 but can be 2 or 3)
80
- for (block_i, block_j), connections in self.block_neighbors.items():
80
+ for connections in self.block_neighbors.values():
81
81
  is_connected_list = []
82
82
  for pos_a, direction_a, pos_b, direction_b in connections:
83
83
  v = self.model.NewBoolVar(f'{pos_a}:{direction_a}->{pos_b}:{direction_b}')