multi-puzzle-solver 0.9.31__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.31.dist-info → multi_puzzle_solver-1.0.2.dist-info}/METADATA +255 -1
  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 +12 -11
  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.31.dist-info/RECORD +0 -67
  44. {multi_puzzle_solver-0.9.31.dist-info → multi_puzzle_solver-1.0.2.dist-info}/WHEEL +0 -0
  45. {multi_puzzle_solver-0.9.31.dist-info → multi_puzzle_solver-1.0.2.dist-info}/top_level.txt +0 -0
@@ -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}')
@@ -1,4 +1,5 @@
1
1
  from typing import Union, Optional
2
+ from collections import defaultdict
2
3
 
3
4
  import numpy as np
4
5
  from ortools.sat.python import cp_model
@@ -35,32 +36,77 @@ def get_block_pos(i: int, Bv: int, Bh: int) -> list[Pos]:
35
36
 
36
37
 
37
38
  class Board:
38
- def __init__(self, board: np.array, block_size: Optional[tuple[int, int]] = None, sandwich: Optional[dict[str, list[int]]] = None, unique_diagonal: bool = False):
39
+ def __init__(self,
40
+ board: np.array,
41
+ constrain_blocks: bool = True,
42
+ block_size: Optional[tuple[int, int]] = None,
43
+ sandwich: Optional[dict[str, list[int]]] = None,
44
+ unique_diagonal: bool = False,
45
+ jigsaw: Optional[np.array] = None,
46
+ killer: Optional[tuple[np.array, dict[str, int]]] = None,
47
+ ):
48
+ """
49
+ board: 2d array of characters
50
+ constrain_blocks: whether to constrain the blocks. If True, each block must contain all numbers from 1 to 9 exactly once.
51
+ block_size: tuple of block size (vertical, horizontal). If not provided, the block size is the square root of the board size.
52
+ sandwich: dictionary of sandwich clues (side, bottom). If provided, the sum of the values between 1 and 9 for each row and column is equal to the clue.
53
+ unique_diagonal: whether to constrain the 2 diagonals to be unique. If True, each diagonal must contain all numbers from 1 to 9 exactly once.
54
+ killer: tuple of (killer board, killer clues). If provided, the killer board must be a 2d array of ids of the killer blocks. The killer clues must be a dictionary of killer block ids to clues.
55
+ Each numbers in a killer block must be unique and sum to the clue.
56
+ """
39
57
  assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
40
58
  assert board.shape[0] == board.shape[1], 'board must be square'
41
59
  assert all(isinstance(i.item(), str) and len(i.item()) == 1 and (i.item().isalnum() or i.item() == ' ') for i in np.nditer(board)), 'board must contain only alphanumeric characters or space'
42
60
  self.board = board
43
61
  self.V, self.H = board.shape
44
- if block_size is None:
45
- B = np.sqrt(self.V) # block size
46
- assert B.is_integer(), 'board size must be a perfect square or provide block_size'
47
- Bv, Bh = int(B), int(B)
62
+ self.L = max(self.V, self.H)
63
+ self.constrain_blocks = constrain_blocks
64
+ self.unique_diagonal = unique_diagonal
65
+ self.sandwich = None
66
+ self.jigsaw_id_to_pos = None
67
+ self.killer = None
68
+
69
+ if self.constrain_blocks:
70
+ if block_size is None:
71
+ B = np.sqrt(self.V) # block size
72
+ assert B.is_integer(), 'board size must be a perfect square or provide block_size'
73
+ Bv, Bh = int(B), int(B)
74
+ else:
75
+ Bv, Bh = block_size
76
+ assert Bv * Bh == self.V, 'block size must be a factor of board size'
77
+ # can be different in 4x3 for example
78
+ self.Bv = Bv
79
+ self.Bh = Bh
80
+ self.B = Bv * Bh # block count
48
81
  else:
49
- Bv, Bh = block_size
50
- assert Bv * Bh == self.V, 'block size must be a factor of board size'
51
- # can be different in 4x3 for example
52
- self.Bv = Bv
53
- self.Bh = Bh
54
- self.B = Bv * Bh # block count
82
+ assert block_size is None, 'cannot set block size if blocks are not constrained'
83
+
84
+ if jigsaw is not None:
85
+ if self.constrain_blocks is not None:
86
+ print('Warning: jigsaw and blocks are both constrained, are you sure you want to do this?')
87
+ assert jigsaw.ndim == 2, f'jigsaw must be 2d, got {jigsaw.ndim}'
88
+ assert jigsaw.shape[0] == self.V and jigsaw.shape[1] == self.H, 'jigsaw must be the same size as the board'
89
+ assert all(isinstance(i.item(), str) and i.item().isdecimal() for i in np.nditer(jigsaw)), 'jigsaw must contain only digits or space'
90
+ self.jigsaw_id_to_pos: dict[int, list[Pos]] = defaultdict(list)
91
+ for pos in get_all_pos(self.V, self.H):
92
+ v = get_char(jigsaw, pos)
93
+ if v.isdecimal():
94
+ self.jigsaw_id_to_pos[int(v)].append(pos)
95
+ assert all(len(pos_list) <= self.L for pos_list in self.jigsaw_id_to_pos.values()), 'jigsaw areas cannot be larger than the number of digits'
96
+
55
97
  if sandwich is not None:
56
98
  assert set(sandwich.keys()) == set(['side', 'bottom']), 'sandwich must contain only side and bottom'
57
99
  assert len(sandwich['side']) == self.H, 'side must be equal to board width'
58
100
  assert len(sandwich['bottom']) == self.V, 'bottom must be equal to board height'
59
101
  self.sandwich = sandwich
60
- else:
61
- self.sandwich = None
62
- self.unique_diagonal = unique_diagonal
63
-
102
+
103
+ if killer is not None:
104
+ assert killer[0].ndim == 2, f'killer board must be 2d, got {killer[0].ndim}'
105
+ assert killer[0].shape[0] == self.V and killer[0].shape[1] == self.H, 'killer board must be the same size as the board'
106
+ assert all(isinstance(i.item(), str) and i.item().isdecimal() for i in np.nditer(killer[0])), 'killer board must contain only digits or space'
107
+ assert set(killer[1].keys()).issubset(set(killer[0].flatten())), f'killer clues must contain all killer block ids, {set(killer[0].flatten()) - set(killer[1].keys())}'
108
+ self.killer = killer
109
+
64
110
  self.model = cp_model.CpModel()
65
111
  self.model_vars: dict[Pos, cp_model.IntVar] = {}
66
112
 
@@ -69,7 +115,7 @@ class Board:
69
115
 
70
116
  def create_vars(self):
71
117
  for pos in get_all_pos(self.V, self.H):
72
- self.model_vars[pos] = self.model.NewIntVar(1, self.B, f'{pos}')
118
+ self.model_vars[pos] = self.model.NewIntVar(1, self.L, f'{pos}')
73
119
 
74
120
  def add_all_constraints(self):
75
121
  # some squares are already filled
@@ -86,16 +132,21 @@ class Board:
86
132
  for col in range(self.H):
87
133
  col_vars = [self.model_vars[pos] for pos in get_col_pos(col, V=self.V)]
88
134
  self.model.AddAllDifferent(col_vars)
89
- # each block
90
- for block_i in range(self.B):
91
- block_vars = [self.model_vars[p] for p in get_block_pos(block_i, Bv=self.Bv, Bh=self.Bh)]
92
- self.model.AddAllDifferent(block_vars)
135
+ if self.constrain_blocks: # each block must contain all numbers from 1 to 9 exactly once
136
+ for block_i in range(self.B):
137
+ block_vars = [self.model_vars[p] for p in get_block_pos(block_i, Bv=self.Bv, Bh=self.Bh)]
138
+ self.model.AddAllDifferent(block_vars)
93
139
  if self.sandwich is not None:
94
140
  self.add_sandwich_constraints()
95
141
  if self.unique_diagonal:
96
142
  self.add_unique_diagonal_constraints()
143
+ if self.jigsaw_id_to_pos is not None:
144
+ self.add_jigsaw_constraints()
145
+ if self.killer is not None:
146
+ self.add_killer_constraints()
97
147
 
98
148
  def add_sandwich_constraints(self):
149
+ """Sandwich constraints, enforce that the sum of the values between 1 and 9 for each row and column is equal to the clue."""
99
150
  for c, clue in enumerate(self.sandwich['bottom']):
100
151
  if clue is None or int(clue) < 0:
101
152
  continue
@@ -113,6 +164,26 @@ class Board:
113
164
  anti_diagonal_vars = [self.model_vars[get_pos(x=i, y=self.V-i-1)] for i in range(min(self.V, self.H))]
114
165
  self.model.AddAllDifferent(anti_diagonal_vars)
115
166
 
167
+ def add_jigsaw_constraints(self):
168
+ """All digits in one jigsaw area must be unique."""
169
+ for pos_list in self.jigsaw_id_to_pos.values():
170
+ self.model.AddAllDifferent([self.model_vars[p] for p in pos_list])
171
+
172
+ def add_killer_constraints(self):
173
+ """Killer constraints, enforce that the sum of the values in each killer block is equal to the clue and all numbers in a block are unique."""
174
+ killer_board, killer_clues = self.killer
175
+ # change clue keys to ints
176
+ killer_clues = {int(k): v for k, v in killer_clues.items()}
177
+ killer_id_to_pos = defaultdict(list)
178
+ for pos in get_all_pos(self.V, self.H):
179
+ v = get_char(killer_board, pos)
180
+ if v.isdecimal():
181
+ killer_id_to_pos[int(v)].append(pos)
182
+ for killer_id, pos_list in killer_id_to_pos.items():
183
+ self.model.AddAllDifferent([self.model_vars[p] for p in pos_list])
184
+ clue = killer_clues[killer_id]
185
+ self.model.Add(sum([self.model_vars[p] for p in pos_list]) == clue)
186
+
116
187
  def solve_and_print(self, verbose: bool = True):
117
188
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
118
189
  assignment: dict[Pos, int] = {}
@@ -45,14 +45,14 @@ class Board:
45
45
  if get_char(self.board, neighbour) != ' ':
46
46
  continue
47
47
  self.model.Add(self.is_tent[neighbour] == 0).OnlyEnforceIf(self.is_tent[pos])
48
- # - the number of tents in each row and column matches the numbers around the edge of the grid
48
+ # - the number of tents in each row and column matches the numbers around the edge of the grid
49
49
  for row in range(self.N):
50
50
  row_vars = [self.is_tent[pos] for pos in get_row_pos(row, self.N)]
51
51
  self.model.Add(lxp.sum(row_vars) == self.sides['side'][row])
52
52
  for col in range(self.N):
53
53
  col_vars = [self.is_tent[pos] for pos in get_col_pos(col, self.N)]
54
54
  self.model.Add(lxp.sum(col_vars) == self.sides['top'][col])
55
- # - it is possible to match tents to trees so that each tree is orthogonally adjacent to its own tent (but may also be adjacent to other tents).
55
+ # - it is possible to match tents to trees so that each tree is orthogonally adjacent to its own tent (but may also be adjacent to other tents).
56
56
  # for each tree, one of the following must be true:
57
57
  # a tent on its left has direction RIGHT
58
58
  # a tent on its right has direction LEFT
@@ -47,7 +47,7 @@ class Board:
47
47
  def create_vars(self):
48
48
  for pos in get_all_pos(self.V, self.H):
49
49
  self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
50
-
50
+
51
51
  def add_all_constraints(self):
52
52
  visited: set[Pos] = set()
53
53
  for cur_pos in self.tip:
@@ -120,7 +120,7 @@ class Board:
120
120
  # create a single bool which decides if I can see it or not
121
121
  res = self.model.NewBoolVar(name)
122
122
  self.model.AddBoolAnd(lits).OnlyEnforceIf(res)
123
- self.model.AddBoolOr([res] + [l.Not() for l in lits])
123
+ self.model.AddBoolOr([res] + [lit.Not() for lit in lits])
124
124
  return res
125
125
 
126
126
  def solve_and_print(self, verbose: bool = True):
@@ -131,7 +131,7 @@ class Board:
131
131
  pos = get_pos(x=i, y=self.N-1)
132
132
  beam_result = beam(self.board, pos, Direction.UP)
133
133
  self.model.add(self.get_var(beam_result) == ground)
134
-
134
+
135
135
  if self.monster_count is not None:
136
136
  for monster, count in self.monster_count.items():
137
137
  if count == -1:
@@ -49,7 +49,7 @@ class Board:
49
49
  continue
50
50
  v = 1 if c == 'B' else 0
51
51
  self.model.Add(self.model_vars[pos] == v)
52
- # no three consecutive squares, horizontally or vertically, are the same colour
52
+ # no three consecutive squares, horizontally or vertically, are the same colour
53
53
  for pos in get_all_pos(self.V, self.H):
54
54
  horiz, vert = get_3_consecutive_horiz_and_vert(pos, self.V, self.H)
55
55
  if len(horiz) == 3:
@@ -48,7 +48,7 @@ class Board:
48
48
  continue
49
49
  self.model.AddBoolOr([self.B[tl], self.B[tr], self.B[bl], self.B[br]])
50
50
  self.model.AddBoolOr([self.B[tl].Not(), self.B[tr].Not(), self.B[bl].Not(), self.B[br].Not()])
51
-
51
+
52
52
  def disallow_checkers(self):
53
53
  # from https://ralphwaldo.github.io/yinyang_summary.html
54
54
  for pos in get_all_pos(self.V, self.H): # disallow (WB/BW) and (BW/WB)
@@ -152,4 +152,4 @@ if __name__ == '__main__':
152
152
  print('max i:', max(nums))
153
153
  if all(str(c).isdigit() for c in nums):
154
154
  print('skipped:', set(range(int(max(nums)) + 1)) - set(int(i) for i in nums))
155
- render_board_image(board, colors)
155
+ render_board_image(board, colors)
@@ -1,67 +0,0 @@
1
- puzzle_solver/__init__.py,sha256=q8lMGHFeg0ZbUxDApJHDcGcQtpNKPHCh3muRfPwpPN4,3693
2
- puzzle_solver/core/utils.py,sha256=XBW5j-IwtJMPMP-ycmY6SqRCM1NOVl5O6UeoGqNj618,8153
3
- puzzle_solver/core/utils_ortools.py,sha256=_i8cixHOB5XGqqcr-493bOiZgYJidnvxQMEfj--Trns,10278
4
- puzzle_solver/core/utils_visualizer.py,sha256=c4cq_BpwDc1oao6KkghisiawwaIVQpTOIEZtVWIkA3w,20249
5
- puzzle_solver/puzzles/aquarium/aquarium.py,sha256=BUfkAS2d9eG3TdMoe1cOGGeNYgKUebRvn-z9nsC9gvE,5708
6
- puzzle_solver/puzzles/battleships/battleships.py,sha256=RuYCrs4j0vUjlU139NRYYP-uNPAgO0V7hAzbsHrRwD8,7446
7
- puzzle_solver/puzzles/binairo/binairo.py,sha256=4xgYd1ewYIQCqEzsHdgp6hWzyW_TF_2rt6PO8QLFKWU,6838
8
- puzzle_solver/puzzles/binairo/binairo_plus.py,sha256=TvLG3olwANtft3LuCF-y4OofpU9PNa4IXDqgZqsD-g0,267
9
- puzzle_solver/puzzles/black_box/black_box.py,sha256=ZnHDVt6PFS_r1kMNSsbz9hav1hxIrNDUvPyERGPjLjM,15635
10
- puzzle_solver/puzzles/bridges/bridges.py,sha256=15A9uV4xjoqPRo_9CTnoKeGRxS3z2aMF619T1n0dTOQ,5402
11
- puzzle_solver/puzzles/chess_range/chess_melee.py,sha256=D-_Oi8OyxsVe1j3dIKYwRlxgeb3NWLmDWGcv-oclY0c,195
12
- puzzle_solver/puzzles/chess_range/chess_range.py,sha256=uMQGTIwzGskHIhI-tPYjT9a3wHUBIkZ18eXjV9IpUE4,21071
13
- puzzle_solver/puzzles/chess_range/chess_solo.py,sha256=U3v766UsZHx_dC3gxqU90VbjAXn-OlYhtrnnvJYFvrQ,401
14
- puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
15
- puzzle_solver/puzzles/dominosa/dominosa.py,sha256=Nmb7pn8U27QJwGy9F3wo8ylqo2_U51OAo3GN2soaNpc,7195
16
- puzzle_solver/puzzles/filling/filling.py,sha256=vrOIil285_r3IQ0F4c9mUBWMRVlPH4vowog_z1tCGdI,5567
17
- puzzle_solver/puzzles/flip/flip.py,sha256=ZngJLUhRNc7qqo2wtNLdMPx4u9w9JTUge27PmdXyDCw,3985
18
- puzzle_solver/puzzles/galaxies/galaxies.py,sha256=p10lpmW0FjtneFCMEjG1FSiEpQuvD8zZG9FG8zYGoes,5582
19
- puzzle_solver/puzzles/galaxies/parse_map/parse_map.py,sha256=v5TCrdREeOB69s9_QFgPHKA7flG69Im1HVzIdxH0qQc,9355
20
- puzzle_solver/puzzles/guess/guess.py,sha256=sH-NlYhxM3DNbhk4eGde09kgM0KaDvSbLrpHQiwcFGo,10791
21
- puzzle_solver/puzzles/heyawake/heyawake.py,sha256=qMnc_CuHn8K5Rw40tefjueI1pycpHQ7eN1R9Xg5WEuw,5601
22
- puzzle_solver/puzzles/inertia/inertia.py,sha256=gJBahkh69CrSWNscalKEoP1j4X-Q3XpbIBMiG9PUpU0,5657
23
- puzzle_solver/puzzles/inertia/tsp.py,sha256=gobiISHtARA4Elq0jr90p6Yhq11ULjGoqsS-rLFhYcc,15389
24
- puzzle_solver/puzzles/inertia/parse_map/parse_map.py,sha256=A9JQTNqamUdzlwqks0XQp3Hge3mzyTIVK6YtDJvqpL4,8422
25
- puzzle_solver/puzzles/kakurasu/kakurasu.py,sha256=VNGMJnBHDi6WkghLObRLhUvkmrPaGphTTUDMC0TkQvQ,2064
26
- puzzle_solver/puzzles/kakuro/kakuro.py,sha256=Jf0Iilv32EPcaWikX92_vgBOVRp5MAE27aFRmnLotGQ,4374
27
- puzzle_solver/puzzles/keen/keen.py,sha256=tDb6C5S3Q0JAKPsdw-84WQ6PxRADELZHr_BK8FDH-NA,5039
28
- puzzle_solver/puzzles/light_up/light_up.py,sha256=iSA1rjZMFsnI0V0Nxivxox4qZkB7PvUrROSHXcoUXds,4541
29
- puzzle_solver/puzzles/lits/lits.py,sha256=3fPIkhAIUz8JokcfaE_ZM3b0AFEnf5xPzGJ2qnm8SWY,7099
30
- puzzle_solver/puzzles/magnets/magnets.py,sha256=-Wl49JD_PKeq735zQVMQ3XSQX6gdHiY-7PKw-Sh16jw,6474
31
- puzzle_solver/puzzles/map/map.py,sha256=sxc57tapB8Tsgam-yoDitln1o-EB_SbIYvO6WEYy3us,2582
32
- puzzle_solver/puzzles/minesweeper/minesweeper.py,sha256=LiQVOGkWCsc1WtX8CdPgL_WwAcaeUFuoi5_eqH8U2Og,5876
33
- puzzle_solver/puzzles/mosaic/mosaic.py,sha256=QX_nVpVKQg8OfaUcqFk9tKqsDyVqvZc6-XWvfI3YcSw,2175
34
- puzzle_solver/puzzles/nonograms/nonograms.py,sha256=1jmDTOCnmivmBlwtMDyyk3TVqH5IjapzLn7zLQ4qubk,6056
35
- puzzle_solver/puzzles/norinori/norinori.py,sha256=uC8vXAw35xsTmpmTeKqYW7tbcssms9LCcXFBONtV2Ng,4743
36
- puzzle_solver/puzzles/nurikabe/nurikabe.py,sha256=VMJjB9KAKmfBkG1mDT3Jf2I1PZJb--Qx0BicN8xL4eg,6519
37
- puzzle_solver/puzzles/palisade/palisade.py,sha256=T-LXlaLU5OwUQ24QWJWhBUFUktg0qDODTilNmBaXs4I,5014
38
- puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
39
- puzzle_solver/puzzles/range/range.py,sha256=rruvD5ZSaOgvQuX6uGV_Dkr82nSiWZ5kDz03_j7Tt24,4425
40
- puzzle_solver/puzzles/rectangles/rectangles.py,sha256=zaPg3qI9TNxr2iXmNi2kOL8R2RsS9DyQPUTY3ukgYIA,7033
41
- puzzle_solver/puzzles/shakashaka/shakashaka.py,sha256=PRpg_qI7XA3ysAo_g1TRJsT3VwB5Vial2UcFyBOMwKQ,9571
42
- puzzle_solver/puzzles/shingoki/shingoki.py,sha256=uwX1ZIGGDlshMtsZedlgGYE8hDB1ou3h6aBnZEr_l8I,7425
43
- puzzle_solver/puzzles/signpost/signpost.py,sha256=-0_S6ycwzwlUf9-ZhP127Rgo5gMBOHiTM6t08dLLDac,3869
44
- puzzle_solver/puzzles/singles/singles.py,sha256=KKn_Yl-eW874Bl1UmmcqoQ5vhNiO1JbM7fxKczOV5M4,2847
45
- puzzle_solver/puzzles/slant/slant.py,sha256=xF-N4PuXYfx638NP1f1mi6YncIZB4mLtXtdS79XyPbg,6122
46
- puzzle_solver/puzzles/slant/parse_map/parse_map.py,sha256=dxnALSDXe9wU0uSD0QEXnzoh1q801mj1ePTNLtG0n60,4796
47
- puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=e1A_f_3J-QXN9fmt_Nf3FsYnp-TmE9TRKN06Wn4NnAU,7056
48
- puzzle_solver/puzzles/star_battle/star_battle.py,sha256=IX6w4H3sifN01kPPtrAVRCK0Nl_xlXXSHvJKw8K1EuE,3718
49
- puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
50
- puzzle_solver/puzzles/stitches/stitches.py,sha256=iK8t02q43gH3FPbuIDn4dK0sbaOgZOnw8yHNRNvNuIU,6534
51
- puzzle_solver/puzzles/stitches/parse_map/parse_map.py,sha256=f49ZGVBPXjAGgqZnqPab6PcO_DsFDFZnG3uA8b-1d7k,10441
52
- puzzle_solver/puzzles/sudoku/sudoku.py,sha256=SE4TM_gic6Jj0fkDR_NzUJdX2XKyQ8eeOnVAQ011Xbo,8870
53
- puzzle_solver/puzzles/tapa/tapa.py,sha256=TsOQhnEvlC1JxaWiEjQg2KxRXJR49GrN71DsMvPpia8,5337
54
- puzzle_solver/puzzles/tents/tents.py,sha256=iyVK2WXfIT5j_9qqlQg0WmwvixwXlZSsHGK3XA-KpII,6283
55
- puzzle_solver/puzzles/thermometers/thermometers.py,sha256=nsvJZkm7G8FALT27bpaB0lv5E_AWawqmvapQI8QcYXw,4015
56
- puzzle_solver/puzzles/towers/towers.py,sha256=QvL0Pp-Z2ewCeq9ZkNrh8MShKOh-Y52sFBSudve68wk,6496
57
- puzzle_solver/puzzles/tracks/tracks.py,sha256=98xds9SKNqtOLFTRUX_KSMC7XYmZo567LOFeqotVQaM,7237
58
- puzzle_solver/puzzles/undead/undead.py,sha256=IrCUfzQFBem658P5KKqldG7vd2TugTHehcwseCarerM,6604
59
- puzzle_solver/puzzles/unequal/unequal.py,sha256=ExY2XDCrqROCDpRLfHo8uVr1zuli1QvbCdNCiDhlCac,6978
60
- puzzle_solver/puzzles/unruly/unruly.py,sha256=sDF0oKT50G-NshyW2DYrvAgD9q9Ku9ANUyNhGSAu7cQ,3827
61
- puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=WrRdNhmKhIARdGOt_36gpRxRzrfLGv3wl7igBpPFM64,5259
62
- puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
63
- puzzle_solver/utils/visualizer.py,sha256=tsX1yEKwmwXBYuBJpx_oZGe2UUt1g5yV73G3UbtmvtE,6817
64
- multi_puzzle_solver-0.9.31.dist-info/METADATA,sha256=yVpID_W67orWIzxxJ49qwggTacSPLjOrS0KjfUZm31U,336388
65
- multi_puzzle_solver-0.9.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
66
- multi_puzzle_solver-0.9.31.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
67
- multi_puzzle_solver-0.9.31.dist-info/RECORD,,