multi-puzzle-solver 1.0.4__py3-none-any.whl → 1.0.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of multi-puzzle-solver might be problematic. Click here for more details.

Files changed (42) hide show
  1. {multi_puzzle_solver-1.0.4.dist-info → multi_puzzle_solver-1.0.7.dist-info}/METADATA +1075 -556
  2. multi_puzzle_solver-1.0.7.dist-info/RECORD +74 -0
  3. puzzle_solver/__init__.py +5 -1
  4. puzzle_solver/core/utils.py +17 -1
  5. puzzle_solver/core/utils_visualizer.py +257 -201
  6. puzzle_solver/puzzles/abc_view/abc_view.py +75 -0
  7. puzzle_solver/puzzles/aquarium/aquarium.py +8 -23
  8. puzzle_solver/puzzles/battleships/battleships.py +39 -53
  9. puzzle_solver/puzzles/binairo/binairo.py +2 -2
  10. puzzle_solver/puzzles/black_box/black_box.py +6 -70
  11. puzzle_solver/puzzles/connect_the_dots/connect_the_dots.py +4 -2
  12. puzzle_solver/puzzles/filling/filling.py +11 -34
  13. puzzle_solver/puzzles/galaxies/galaxies.py +4 -2
  14. puzzle_solver/puzzles/heyawake/heyawake.py +72 -14
  15. puzzle_solver/puzzles/kakurasu/kakurasu.py +5 -13
  16. puzzle_solver/puzzles/kakuro/kakuro.py +6 -2
  17. puzzle_solver/puzzles/lits/lits.py +4 -2
  18. puzzle_solver/puzzles/mosaic/mosaic.py +8 -18
  19. puzzle_solver/puzzles/nonograms/nonograms.py +80 -85
  20. puzzle_solver/puzzles/nonograms/nonograms_colored.py +221 -0
  21. puzzle_solver/puzzles/norinori/norinori.py +5 -12
  22. puzzle_solver/puzzles/nurikabe/nurikabe.py +6 -2
  23. puzzle_solver/puzzles/palisade/palisade.py +8 -22
  24. puzzle_solver/puzzles/pearl/pearl.py +15 -27
  25. puzzle_solver/puzzles/pipes/pipes.py +2 -1
  26. puzzle_solver/puzzles/range/range.py +19 -55
  27. puzzle_solver/puzzles/rectangles/rectangles.py +4 -2
  28. puzzle_solver/puzzles/shingoki/shingoki.py +62 -105
  29. puzzle_solver/puzzles/singles/singles.py +6 -2
  30. puzzle_solver/puzzles/slant/slant.py +13 -19
  31. puzzle_solver/puzzles/slitherlink/slitherlink.py +2 -2
  32. puzzle_solver/puzzles/star_battle/star_battle.py +5 -2
  33. puzzle_solver/puzzles/stitches/stitches.py +8 -21
  34. puzzle_solver/puzzles/sudoku/sudoku.py +5 -11
  35. puzzle_solver/puzzles/tapa/tapa.py +6 -2
  36. puzzle_solver/puzzles/tents/tents.py +50 -80
  37. puzzle_solver/puzzles/tracks/tracks.py +19 -66
  38. puzzle_solver/puzzles/unruly/unruly.py +17 -49
  39. puzzle_solver/puzzles/yin_yang/yin_yang.py +3 -10
  40. multi_puzzle_solver-1.0.4.dist-info/RECORD +0 -72
  41. {multi_puzzle_solver-1.0.4.dist-info → multi_puzzle_solver-1.0.7.dist-info}/WHEEL +0 -0
  42. {multi_puzzle_solver-1.0.4.dist-info → multi_puzzle_solver-1.0.7.dist-info}/top_level.txt +0 -0
@@ -4,8 +4,9 @@ from dataclasses import dataclass
4
4
  import numpy as np
5
5
  from ortools.sat.python import cp_model
6
6
 
7
- from puzzle_solver.core.utils import Pos, get_all_pos, set_char, in_bounds, get_pos
7
+ from puzzle_solver.core.utils import Pos, get_all_pos, in_bounds, get_pos
8
8
  from puzzle_solver.core.utils_ortools import force_no_loops, generic_solve_all, SingleSolution
9
+ from puzzle_solver.core.utils_visualizer import combined_function
9
10
 
10
11
 
11
12
  @dataclass(frozen=True)
@@ -13,10 +14,10 @@ class Node:
13
14
  """The grid is represented as a graph of cells connected to corners."""
14
15
  node_type: Union[Literal["Cell"], Literal["Corner"]]
15
16
  pos: Pos
16
- slant: Union[Literal["//"], Literal["\\"], None]
17
+ slant: Union[Literal["/"], Literal["\\"], None]
17
18
 
18
19
  def get_neighbors(self, board_nodes: dict[tuple[str, Pos, Optional[str]], "Node"]) -> list["Node"]:
19
- if self.node_type == "Cell" and self.slant == "//":
20
+ if self.node_type == "Cell" and self.slant == "/":
20
21
  n1 = board_nodes[("Corner", get_pos(self.pos.x+1, self.pos.y), None)]
21
22
  n2 = board_nodes[("Corner", get_pos(self.pos.x, self.pos.y+1), None)]
22
23
  return [n1, n2]
@@ -27,8 +28,8 @@ class Node:
27
28
  elif self.node_type == "Corner":
28
29
  # 4 cells, 2 cells per slant
29
30
  n1 = ("Cell", get_pos(self.pos.x-1, self.pos.y-1), "\\")
30
- n2 = ("Cell", get_pos(self.pos.x, self.pos.y-1), "//")
31
- n3 = ("Cell", get_pos(self.pos.x-1, self.pos.y), "//")
31
+ n2 = ("Cell", get_pos(self.pos.x, self.pos.y-1), "/")
32
+ n3 = ("Cell", get_pos(self.pos.x-1, self.pos.y), "/")
32
33
  n4 = ("Cell", get_pos(self.pos.x, self.pos.y), "\\")
33
34
  return {board_nodes[n] for n in [n1, n2, n3, n4] if n in board_nodes}
34
35
 
@@ -61,9 +62,9 @@ class Board:
61
62
 
62
63
  def create_vars(self):
63
64
  for pos in get_all_pos(self.V, self.H):
64
- self.model_vars[(pos, '//')] = self.model.NewBoolVar(f'{pos}://')
65
+ self.model_vars[(pos, '/')] = self.model.NewBoolVar(f'{pos}:/')
65
66
  self.model_vars[(pos, '\\')] = self.model.NewBoolVar(f'{pos}:\\')
66
- self.model.AddExactlyOne([self.model_vars[(pos, '//')], self.model_vars[(pos, '\\')]])
67
+ self.model.AddExactlyOne([self.model_vars[(pos, '/')], self.model_vars[(pos, '\\')]])
67
68
  for (pos, slant), v in self.model_vars.items():
68
69
  self.nodes[Node(node_type="Cell", pos=pos, slant=slant)] = v
69
70
  for pos in get_all_pos(self.V + 1, self.H + 1):
@@ -76,8 +77,8 @@ class Board:
76
77
  # when pos is (xi, yi) then it gets a +1 contribution for each:
77
78
  # - cell (xi-1, yi-1) is a "\\"
78
79
  # - cell (xi, yi) is a "\\"
79
- # - cell (xi, yi-1) is a "//"
80
- # - cell (xi-1, yi) is a "//"
80
+ # - cell (xi, yi-1) is a "/"
81
+ # - cell (xi-1, yi) is a "/"
81
82
  xi, yi = pos.x, pos.y
82
83
  tl_pos = get_pos(xi-1, yi-1)
83
84
  br_pos = get_pos(xi, yi)
@@ -85,8 +86,8 @@ class Board:
85
86
  bl_pos = get_pos(xi-1, yi)
86
87
  tl_var = self.model_vars[(tl_pos, '\\')] if in_bounds(tl_pos, self.V, self.H) else 0
87
88
  br_var = self.model_vars[(br_pos, '\\')] if in_bounds(br_pos, self.V, self.H) else 0
88
- tr_var = self.model_vars[(tr_pos, '//')] if in_bounds(tr_pos, self.V, self.H) else 0
89
- bl_var = self.model_vars[(bl_pos, '//')] if in_bounds(bl_pos, self.V, self.H) else 0
89
+ tr_var = self.model_vars[(tr_pos, '/')] if in_bounds(tr_pos, self.V, self.H) else 0
90
+ bl_var = self.model_vars[(bl_pos, '/')] if in_bounds(bl_pos, self.V, self.H) else 0
90
91
  self.model.Add(sum([tl_var, tr_var, bl_var, br_var]) == number)
91
92
  board_nodes = {(node.node_type, node.pos, node.slant): node for node in self.nodes.keys()}
92
93
  self.neighbor_dict = {node: node.get_neighbors(board_nodes) for node in self.nodes.keys()}
@@ -106,12 +107,5 @@ class Board:
106
107
  return SingleSolution(assignment=assignment)
107
108
  def callback(single_res: SingleSolution):
108
109
  print("Solution found")
109
- res = np.full((self.V, self.H), ' ', dtype=object)
110
- for pos in get_all_pos(self.V, self.H):
111
- set_char(res, pos, '/' if single_res.assignment[pos] == '//' else '\\')
112
- print('[')
113
- for row in range(self.V):
114
- line = ' [ ' + ' '.join(res[row].tolist()) + ' ]'
115
- print(line)
116
- print(']')
110
+ print(combined_function(self.V, self.H, center_char=lambda r, c: single_res.assignment[get_pos(x=c, y=r)]))
117
111
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -4,7 +4,7 @@ from ortools.sat.python import cp_model
4
4
 
5
5
  from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, Direction, get_row_pos, get_col_pos, get_next_pos, in_bounds, get_opposite_direction
6
6
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
7
- from puzzle_solver.core.utils_visualizer import render_grid
7
+ from puzzle_solver.core.utils_visualizer import combined_function
8
8
 
9
9
 
10
10
  CellBorder = tuple[Pos, Direction]
@@ -126,5 +126,5 @@ class Board:
126
126
  set_char(res, pos, c)
127
127
  # replace " " with "·"
128
128
  board = np.where(self.board == ' ', '·', self.board)
129
- print(render_grid(cell_flags=res, center_char=board))
129
+ print(combined_function(self.V, self.H, cell_flags=lambda r, c: res[r, c], center_char=lambda r, c: board[r, c]))
130
130
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=999)
@@ -3,7 +3,7 @@ from ortools.sat.python import cp_model
3
3
 
4
4
  from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_neighbors8, get_row_pos, get_col_pos
5
5
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
6
- from puzzle_solver.core.utils_visualizer import id_board_to_wall_board, render_grid
6
+ from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
7
7
 
8
8
 
9
9
  class Board:
@@ -70,5 +70,8 @@ class Board:
70
70
  set_char(res, pos, ' ')
71
71
  else:
72
72
  set_char(res, pos, '.')
73
- print(render_grid(id_board_to_wall_board(self.board), center_char=lambda r, c: res[r][c]))
73
+ print(combined_function(self.V, self.H,
74
+ cell_flags=id_board_to_wall_fn(self.board),
75
+ center_char=lambda r, c: res[r][c]
76
+ ))
74
77
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -3,8 +3,9 @@ from typing import Union
3
3
  import numpy as np
4
4
  from ortools.sat.python import cp_model
5
5
 
6
- from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_next_pos, Direction, get_row_pos, get_col_pos, in_bounds, get_opposite_direction
6
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_next_pos, Direction, get_row_pos, get_col_pos, in_bounds, get_opposite_direction, get_pos
7
7
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, and_constraint
8
+ from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
8
9
 
9
10
 
10
11
  class Board:
@@ -40,10 +41,6 @@ class Board:
40
41
  self.block_neighbors.setdefault((block_i, block_j), set()).add((pos, direction, neighbor, opposite_direction))
41
42
  self.valid_stitches.add((pos, neighbor))
42
43
  self.valid_stitches.add((neighbor, pos))
43
- # for pair in self.block_neighbors.keys():
44
- # print(pair, self.block_neighbors[pair])
45
- # print('top empties', self.top_empties)
46
- # print('side empties', self.side_empties)
47
44
 
48
45
  self.model = cp_model.CpModel()
49
46
  self.model_vars: dict[tuple[Pos, Union[Direction, None]], cp_model.IntVar] = {}
@@ -62,19 +59,15 @@ class Board:
62
59
  state = [self.model_vars[(pos, direction)] for direction in Direction]
63
60
  state.append(self.model_vars[(pos, None)])
64
61
  self.model.AddExactlyOne(state)
65
- # print('ONLY 1 DIRECTION. only one', state)
66
62
  # If a position points at X (and this is a valid pair) then X has to point at me
67
63
  for pos in get_all_pos(self.V, self.H):
68
64
  for direction in Direction:
69
65
  neighbor = get_next_pos(pos, direction)
70
- if not in_bounds(neighbor, self.V, self.H) or (pos, neighbor) not in self.valid_stitches:
71
- # this is not a valid stitch
66
+ if not in_bounds(neighbor, self.V, self.H) or (pos, neighbor) not in self.valid_stitches: # this is not a valid stitch
72
67
  self.model.Add(self.model_vars[(pos, direction)] == 0)
73
- # print(f'Pos {pos} cant be {direction}')
74
68
  continue
75
69
  opposite_direction = get_opposite_direction(direction)
76
70
  self.model.Add(self.model_vars[(pos, direction)] == self.model_vars[(neighbor, opposite_direction)])
77
- # print(f'{pos}:{direction} must == {neighbor}:{opposite_direction}')
78
71
 
79
72
  # all blocks connected exactly N times (N usually 1 but can be 2 or 3)
80
73
  for connections in self.block_neighbors.values():
@@ -93,17 +86,11 @@ class Board:
93
86
 
94
87
  def solve_and_print(self, verbose: bool = True):
95
88
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
96
- assignment: dict[Pos, str] = {}
97
- for (pos, direction), var in board.model_vars.items():
98
- if solver.value(var) == 1:
99
- assignment[pos] = direction.name[0] if direction is not None else ' '
100
- return SingleSolution(assignment=assignment)
89
+ return SingleSolution(assignment={pos: direction.name[0] if direction is not None else ' ' for (pos, direction), var in board.model_vars.items() if solver.Value(var) == 1 and direction is not None})
101
90
  def callback(single_res: SingleSolution):
102
91
  print("Solution found")
103
- res = np.full((self.V, self.H), ' ', dtype=object)
104
- for pos in get_all_pos(self.V, self.H):
105
- c = get_char(self.board, pos)
106
- c = single_res.assignment[pos]
107
- set_char(res, pos, c)
108
- print(res)
92
+ print(combined_function(self.V, self.H,
93
+ cell_flags=id_board_to_wall_fn(self.board),
94
+ special_content=lambda r, c: single_res.assignment.get(get_pos(x=c, y=r), ''),
95
+ center_char=lambda r, c: 'O' if get_pos(x=c, y=r) in single_res.assignment else '.'))
109
96
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=9)
@@ -6,6 +6,7 @@ from ortools.sat.python import cp_model
6
6
 
7
7
  from puzzle_solver.core.utils import Pos, get_pos, get_all_pos, get_char, set_char, get_row_pos, get_col_pos
8
8
  from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, or_constraint, SingleSolution
9
+ from puzzle_solver.core.utils_visualizer import combined_function
9
10
 
10
11
 
11
12
  def get_value(board: np.array, pos: Pos) -> Union[int, str]:
@@ -82,7 +83,7 @@ class Board:
82
83
  assert block_size is None, 'cannot set block size if blocks are not constrained'
83
84
 
84
85
  if jigsaw is not None:
85
- if self.constrain_blocks is not None:
86
+ if self.constrain_blocks:
86
87
  print('Warning: jigsaw and blocks are both constrained, are you sure you want to do this?')
87
88
  assert jigsaw.ndim == 2, f'jigsaw must be 2d, got {jigsaw.ndim}'
88
89
  assert jigsaw.shape[0] == self.V and jigsaw.shape[1] == self.H, 'jigsaw must be the same size as the board'
@@ -186,18 +187,11 @@ class Board:
186
187
 
187
188
  def solve_and_print(self, verbose: bool = True):
188
189
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
189
- assignment: dict[Pos, int] = {}
190
- for pos, var in board.model_vars.items():
191
- assignment[pos] = solver.value(var)
192
- return SingleSolution(assignment=assignment)
190
+ return SingleSolution(assignment={pos: solver.value(var) for pos, var in board.model_vars.items()})
193
191
  def callback(single_res: SingleSolution):
194
192
  print("Solution found")
195
- res = np.full((self.V, self.H), ' ', dtype=object)
196
- for pos in get_all_pos(self.V, self.H):
197
- c = get_value(self.board, pos)
198
- c = single_res.assignment[pos]
199
- set_value(res, pos, c)
200
- print(res)
193
+ val_arr = np.array([[single_res.assignment[get_pos(x=c, y=r)] for c in range(self.H)] for r in range(self.V)])
194
+ print(combined_function(self.V, self.H, center_char=lambda r, c: val_arr[r, c] if val_arr[r, c] < 10 else chr(val_arr[r, c] - 10 + ord('a'))))
201
195
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
202
196
 
203
197
 
@@ -6,7 +6,7 @@ from ortools.sat.python import cp_model
6
6
 
7
7
  from puzzle_solver.core.utils import Direction8, Pos, get_all_pos, set_char, get_char, in_bounds, Direction, get_next_pos, get_pos
8
8
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
9
- from puzzle_solver.core.utils_visualizer import render_shaded_grid
9
+ from puzzle_solver.core.utils_visualizer import combined_function
10
10
 
11
11
 
12
12
  def rotated_assignments_N_nums(Xs: tuple[int, ...], target_length: int = 8) -> set[tuple[bool, ...]]:
@@ -94,5 +94,9 @@ class Board:
94
94
  if len(c) > 3:
95
95
  c = '...'
96
96
  set_char(board_justified, pos, ' ' * (2 - len(c)) + c)
97
- print(render_shaded_grid(self.V, self.H, lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1, empty_text=lambda r, c: str(board_justified[r, c])))
97
+ print(combined_function(self.V, self.H,
98
+ is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1,
99
+ center_char=lambda r, c: str(board_justified[r, c]),
100
+ text_on_shaded_cells=False
101
+ ))
98
102
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=5)
@@ -4,107 +4,77 @@ 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 Pos, get_all_pos, get_char, set_char, get_neighbors8, get_next_pos, Direction, get_row_pos, get_col_pos
7
+ from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_char, get_neighbors8, get_next_pos, get_row_pos, get_col_pos, get_opposite_direction, get_pos
8
8
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
9
+ from puzzle_solver.core.utils_visualizer import combined_function
9
10
 
10
11
 
11
12
  class Board:
12
- def __init__(self, board: np.array, sides: dict[str, np.array]):
13
+ def __init__(self, board: np.array, side: np.array, top: np.array):
13
14
  assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
14
- assert board.shape[0] == board.shape[1], 'board must be square'
15
- assert len(sides) == 2, '2 sides must be provided'
16
- assert set(sides.keys()) == set(['top', 'side'])
17
- assert all(s.ndim == 1 and s.shape[0] == board.shape[0] for s in sides.values()), 'all sides must be equal to board size'
15
+ assert side.ndim == 1 and side.shape[0] == board.shape[0], 'side must be 1d and equal to board size'
16
+ assert top.ndim == 1 and top.shape[0] == board.shape[1], 'top must be 1d and equal to board size'
18
17
  assert all(c.item() in [' ', 'T'] for c in np.nditer(board)), 'board must contain only space or T'
19
18
  self.board = board
20
- self.N = board.shape[0]
21
- self.star_positions: set[Pos] = {pos for pos in get_all_pos(self.N) if get_char(self.board, pos) == ' '}
22
- self.tree_positions: set[Pos] = {pos for pos in get_all_pos(self.N) if get_char(self.board, pos) == 'T'}
19
+ self.V, self.H = board.shape
20
+ self.side = side
21
+ self.top = top
22
+ self.non_tree_positions: set[Pos] = {pos for pos in get_all_pos(self.V, self.H) if get_char(self.board, pos) == ' '}
23
+ self.tree_positions: set[Pos] = {pos for pos in get_all_pos(self.V, self.H) if get_char(self.board, pos) == 'T'}
24
+
23
25
  self.model = cp_model.CpModel()
24
- self.is_tent = defaultdict(int)
25
- self.tent_direction = defaultdict(int)
26
- self.sides = sides
26
+ self.is_tent: dict[Pos, cp_model.IntVar] = defaultdict(int)
27
+ self.tent_direction: dict[tuple[Pos, Direction], cp_model.IntVar] = defaultdict(int)
27
28
  self.create_vars()
28
29
  self.add_all_constraints()
29
30
 
30
31
  def create_vars(self):
31
- for pos in self.star_positions:
32
- is_tent = self.model.NewBoolVar(f'{pos}:is_tent')
33
- tent_direction = self.model.NewIntVar(0, 4, f'{pos}:tent_direction')
34
- self.model.Add(tent_direction == 0).OnlyEnforceIf(is_tent.Not())
35
- self.model.Add(tent_direction > 0).OnlyEnforceIf(is_tent)
36
- self.is_tent[pos] = is_tent
37
- self.tent_direction[pos] = tent_direction
32
+ for pos in self.non_tree_positions:
33
+ self.is_tent[pos] = self.model.NewBoolVar(f'{pos}:is_tent')
34
+ for pos in self.tree_positions:
35
+ for direction in Direction:
36
+ tent_pos = get_next_pos(pos, direction)
37
+ if tent_pos not in self.is_tent:
38
+ continue
39
+ opposite_direction = get_opposite_direction(direction)
40
+ tent_direction = self.model.NewBoolVar(f'{pos}:{direction}')
41
+ self.model.Add(tent_direction == 0).OnlyEnforceIf(self.is_tent[tent_pos].Not())
42
+ self.tent_direction[(pos, direction)] = tent_direction
43
+ self.tent_direction[(tent_pos, opposite_direction)] = tent_direction
38
44
 
39
45
  def add_all_constraints(self):
40
46
  # - There are exactly as many tents as trees.
41
- self.model.Add(lxp.sum([self.is_tent[pos] for pos in self.star_positions]) == len(self.tree_positions))
47
+ self.model.Add(lxp.sum([self.is_tent[pos] for pos in self.non_tree_positions]) == len(self.tree_positions))
42
48
  # - no two tents are adjacent horizontally, vertically or diagonally
43
- for pos in self.star_positions:
44
- for neighbour in get_neighbors8(pos, V=self.N, H=self.N, include_self=False):
45
- if get_char(self.board, neighbour) != ' ':
46
- continue
49
+ for pos in self.non_tree_positions:
50
+ for neighbour in get_neighbors8(pos, V=self.V, H=self.H, include_self=False):
47
51
  self.model.Add(self.is_tent[neighbour] == 0).OnlyEnforceIf(self.is_tent[pos])
48
52
  # - the number of tents in each row and column matches the numbers around the edge of the grid
49
- for row in range(self.N):
50
- row_vars = [self.is_tent[pos] for pos in get_row_pos(row, self.N)]
51
- self.model.Add(lxp.sum(row_vars) == self.sides['side'][row])
52
- for col in range(self.N):
53
- col_vars = [self.is_tent[pos] for pos in get_col_pos(col, self.N)]
54
- self.model.Add(lxp.sum(col_vars) == self.sides['top'][col])
53
+ for row in range(self.V):
54
+ if self.side[row] == -1:
55
+ continue
56
+ row_vars = [self.is_tent[pos] for pos in get_row_pos(row, H=self.H)]
57
+ self.model.Add(lxp.sum(row_vars) == self.side[row])
58
+ for col in range(self.H):
59
+ if self.top[col] == -1:
60
+ continue
61
+ col_vars = [self.is_tent[pos] for pos in get_col_pos(col, V=self.V)]
62
+ self.model.Add(lxp.sum(col_vars) == self.top[col])
55
63
  # - 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
- # for each tree, one of the following must be true:
57
- # a tent on its left has direction RIGHT
58
- # a tent on its right has direction LEFT
59
- # a tent on its top has direction DOWN
60
- # a tent on its bottom has direction UP
61
- for tree in self.tree_positions:
62
- self.add_tree_constraints(tree)
63
-
64
- def add_tree_constraints(self, tree_pos: Pos):
65
- left_pos = get_next_pos(tree_pos, Direction.LEFT)
66
- right_pos = get_next_pos(tree_pos, Direction.RIGHT)
67
- top_pos = get_next_pos(tree_pos, Direction.UP)
68
- bottom_pos = get_next_pos(tree_pos, Direction.DOWN)
69
- var_list = []
70
- if left_pos in self.star_positions:
71
- aux = self.model.NewBoolVar(f'{tree_pos}:left')
72
- self.model.Add(self.tent_direction[left_pos] == Direction.RIGHT.value).OnlyEnforceIf(aux)
73
- self.model.Add(self.tent_direction[left_pos] != Direction.RIGHT.value).OnlyEnforceIf(aux.Not())
74
- var_list.append(aux)
75
- if right_pos in self.star_positions:
76
- aux = self.model.NewBoolVar(f'{tree_pos}:right')
77
- self.model.Add(self.tent_direction[right_pos] == Direction.LEFT.value).OnlyEnforceIf(aux)
78
- self.model.Add(self.tent_direction[right_pos] != Direction.LEFT.value).OnlyEnforceIf(aux.Not())
79
- var_list.append(aux)
80
- if top_pos in self.star_positions:
81
- aux = self.model.NewBoolVar(f'{tree_pos}:top')
82
- self.model.Add(self.tent_direction[top_pos] == Direction.DOWN.value).OnlyEnforceIf(aux)
83
- self.model.Add(self.tent_direction[top_pos] != Direction.DOWN.value).OnlyEnforceIf(aux.Not())
84
- var_list.append(aux)
85
- if bottom_pos in self.star_positions:
86
- aux = self.model.NewBoolVar(f'{tree_pos}:bottom')
87
- self.model.Add(self.tent_direction[bottom_pos] == Direction.UP.value).OnlyEnforceIf(aux)
88
- self.model.Add(self.tent_direction[bottom_pos] != Direction.UP.value).OnlyEnforceIf(aux.Not())
89
- var_list.append(aux)
90
- self.model.AddBoolOr(var_list)
64
+ # each tent is pointing exactly once at a tree
65
+ for pos in self.non_tree_positions:
66
+ var_list = [self.tent_direction[(pos, direction)] for direction in Direction]
67
+ self.model.Add(lxp.sum(var_list) == 1).OnlyEnforceIf(self.is_tent[pos])
68
+ self.model.Add(lxp.sum(var_list) == 0).OnlyEnforceIf(self.is_tent[pos].Not())
69
+ # each tree is pointed at by exactly one tent
70
+ for pos in self.tree_positions:
71
+ var_list = [self.tent_direction[(pos, direction)] for direction in Direction]
72
+ self.model.Add(lxp.sum(var_list) == 1)
91
73
 
92
74
  def solve_and_print(self, verbose: bool = True):
93
75
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
94
- assignment: dict[Pos, int] = {}
95
- for pos, var in board.is_tent.items():
96
- if isinstance(var, int):
97
- continue
98
- assignment[pos] = solver.value(var)
99
- return SingleSolution(assignment=assignment)
76
+ return SingleSolution(assignment={pos: solver.value(var) for pos, var in board.is_tent.items() if not isinstance(var, int)})
100
77
  def callback(single_res: SingleSolution):
101
78
  print("Solution found")
102
- res = np.full((self.N, self.N), ' ', dtype=object)
103
- for pos in get_all_pos(self.N):
104
- c = get_char(self.board, pos)
105
- if c == ' ':
106
- c = single_res.assignment[pos]
107
- c = 'E' if c == 1 else ' '
108
- set_char(res, pos, c)
109
- print(res)
110
- return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
79
+ print(combined_function(self.V, self.H, center_char=lambda r, c: ('|' if self.board[r][c].strip() else ('▲' if single_res.assignment[get_pos(c, r)] == 1 else ' '))))
80
+ return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=5)
@@ -2,8 +2,9 @@ from collections import defaultdict
2
2
  import numpy as np
3
3
  from ortools.sat.python import cp_model
4
4
 
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
5
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, Direction, in_bounds, get_next_pos, get_row_pos, get_col_pos, get_opposite_direction, get_pos
6
6
  from puzzle_solver.core.utils_ortools import force_connected_component, generic_solve_all, SingleSolution
7
+ from puzzle_solver.core.utils_visualizer import combined_function
7
8
 
8
9
 
9
10
  class Board:
@@ -11,8 +12,7 @@ class Board:
11
12
  assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
12
13
  assert all((len(c.item()) == 2) and all(ch in [' ', 'U', 'L', 'D', 'R'] for ch in c.item()) for c in np.nditer(board)), 'board must contain only digits or space'
13
14
  self.board = board
14
- self.V = board.shape[0]
15
- self.H = board.shape[1]
15
+ self.V, self.H = board.shape
16
16
  self.side = side
17
17
  self.top = top
18
18
  self.first_col_start_pos = [p for p in get_col_pos(0, self.V) if 'L' in get_char(self.board, p)]
@@ -33,75 +33,45 @@ class Board:
33
33
  for pos in get_all_pos(self.V, self.H):
34
34
  self.cell_active[pos] = self.model.NewBoolVar(f'{pos}')
35
35
  for direction in Direction:
36
- self.cell_direction[(pos, direction)] = self.model.NewBoolVar(f'{pos}:{direction}')
36
+ next_pos = get_next_pos(pos, direction)
37
+ opposite_direction = get_opposite_direction(direction)
38
+ if (next_pos, opposite_direction) in self.cell_direction:
39
+ self.cell_direction[(pos, direction)] = self.cell_direction[(next_pos, opposite_direction)]
40
+ else:
41
+ self.cell_direction[(pos, direction)] = self.model.NewBoolVar(f'{pos}:{direction}')
37
42
 
38
43
  def add_all_constraints(self):
39
- self.force_hints()
40
- self.force_sides()
41
- self.force_0_or_2_active()
42
- self.force_direction_constraints()
43
- self.force_connected_component()
44
-
45
-
46
- def force_hints(self):
47
44
  # force the already given hints
45
+ str_to_direction = {'U': Direction.UP, 'L': Direction.LEFT, 'D': Direction.DOWN, 'R': Direction.RIGHT}
48
46
  for pos in get_all_pos(self.V, self.H):
49
- c = get_char(self.board, pos)
50
- if 'U' in c:
51
- self.model.Add(self.cell_direction[(pos, Direction.UP)] == 1)
52
- if 'L' in c:
53
- self.model.Add(self.cell_direction[(pos, Direction.LEFT)] == 1)
54
- if 'D' in c:
55
- self.model.Add(self.cell_direction[(pos, Direction.DOWN)] == 1)
56
- if 'R' in c:
57
- self.model.Add(self.cell_direction[(pos, Direction.RIGHT)] == 1)
47
+ for char in get_char(self.board, pos).strip():
48
+ self.model.Add(self.cell_direction[(pos, str_to_direction[char])] == 1)
58
49
 
59
- def force_sides(self):
60
50
  # force the already given sides
61
51
  for i in range(self.V):
62
52
  self.model.Add(sum([self.cell_active[pos] for pos in get_row_pos(i, self.H)]) == self.side[i])
63
53
  for i in range(self.H):
64
54
  self.model.Add(sum([self.cell_active[pos] for pos in get_col_pos(i, self.V)]) == self.top[i])
65
55
 
66
- def force_0_or_2_active(self):
67
56
  # cell active means exactly 2 directions are active, cell not active means no directions are active
68
57
  for pos in get_all_pos(self.V, self.H):
69
58
  s = sum([self.cell_direction[(pos, direction)] for direction in Direction])
70
59
  self.model.Add(s == 2).OnlyEnforceIf(self.cell_active[pos])
71
60
  self.model.Add(s == 0).OnlyEnforceIf(self.cell_active[pos].Not())
72
61
 
73
- def force_direction_constraints(self):
74
- # X having right means the cell to its right has left and so on for all directions
75
- for pos in get_all_pos(self.V, self.H):
76
- right_pos = get_next_pos(pos, Direction.RIGHT)
77
- if in_bounds(right_pos, self.V, self.H):
78
- self.model.Add(self.cell_direction[(pos, Direction.RIGHT)] == self.cell_direction[(right_pos, Direction.LEFT)])
79
- down_pos = get_next_pos(pos, Direction.DOWN)
80
- if in_bounds(down_pos, self.V, self.H):
81
- self.model.Add(self.cell_direction[(pos, Direction.DOWN)] == self.cell_direction[(down_pos, Direction.UP)])
82
- left_pos = get_next_pos(pos, Direction.LEFT)
83
- if in_bounds(left_pos, self.V, self.H):
84
- self.model.Add(self.cell_direction[(pos, Direction.LEFT)] == self.cell_direction[(left_pos, Direction.RIGHT)])
85
- top_pos = get_next_pos(pos, Direction.UP)
86
- if in_bounds(top_pos, self.V, self.H):
87
- self.model.Add(self.cell_direction[(pos, Direction.UP)] == self.cell_direction[(top_pos, Direction.DOWN)])
88
-
89
- # first column cant have L unless it is the start position
90
- for pos in get_col_pos(0, self.V):
62
+ # force borders
63
+ for pos in get_col_pos(0, self.V): # first column cant have L unless it is the start position
91
64
  if pos != self.first_col_start_pos:
92
65
  self.model.Add(self.cell_direction[(pos, Direction.LEFT)] == 0)
93
- # last column cant have R
94
- for pos in get_col_pos(self.H - 1, self.V):
66
+ for pos in get_col_pos(self.H - 1, self.V): # last column cant have R
95
67
  self.model.Add(self.cell_direction[(pos, Direction.RIGHT)] == 0)
96
- # last row cant have D unless it is the end position
97
- for pos in get_row_pos(self.V - 1, self.H):
68
+ for pos in get_row_pos(self.V - 1, self.H): # last row cant have D unless it is the end position
98
69
  if pos != self.last_row_end_pos:
99
70
  self.model.Add(self.cell_direction[(pos, Direction.DOWN)] == 0)
100
- # first row cant have U
101
- for pos in get_row_pos(0, self.H):
71
+ for pos in get_row_pos(0, self.H): # first row cant have U
102
72
  self.model.Add(self.cell_direction[(pos, Direction.UP)] == 0)
103
73
 
104
- def force_connected_component(self):
74
+ # force single connected component
105
75
  def is_neighbor(pd1: tuple[Pos, Direction], pd2: tuple[Pos, Direction]) -> bool:
106
76
  p1, d1 = pd1
107
77
  p2, d2 = pd2
@@ -112,30 +82,13 @@ class Board:
112
82
  return False
113
83
  force_connected_component(self.model, self.cell_direction, is_neighbor=is_neighbor)
114
84
 
115
-
116
-
117
-
118
-
119
85
  def solve_and_print(self, verbose: bool = True):
120
86
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
121
87
  assignment: dict[Pos, str] = defaultdict(str)
122
88
  for (pos, direction), var in board.cell_direction.items():
123
89
  assignment[pos] += direction.name[0] if solver.BooleanValue(var) else ''
124
- for pos in get_all_pos(self.V, self.H):
125
- if len(assignment[pos]) == 0:
126
- assignment[pos] = ' '
127
- else:
128
- assignment[pos] = ''.join(sorted(assignment[pos]))
129
90
  return SingleSolution(assignment=assignment)
130
91
  def callback(single_res: SingleSolution):
131
92
  print("Solution found")
132
- print(single_res.assignment)
133
- res = np.full((self.V, self.H), ' ', dtype=object)
134
- pretty_dict = {'DU': '┃ ', 'LR': '━━', 'DL': '━┒', 'DR': '┏━', 'RU': '┗━', 'LU': '━┛', ' ': ' '}
135
- for pos in get_all_pos(self.V, self.H):
136
- c = get_char(self.board, pos)
137
- c = single_res.assignment[pos]
138
- c = pretty_dict[c]
139
- set_char(res, pos, c)
140
- print(res)
93
+ print(combined_function(self.V, self.H, show_grid=False, special_content=lambda r, c: single_res.assignment[get_pos(x=c, y=r)].strip(), center_char=lambda r, c: '.', text_on_shaded_cells=False))
141
94
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=20)