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
@@ -1,14 +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_char, set_char, get_neighbors4, get_row_pos, get_col_pos
4
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_neighbors4, get_row_pos, get_col_pos, get_pos
5
5
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
6
+ from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
6
7
 
7
8
 
8
- def _sanity_check(board: np.array):
9
- # percolation check
10
- V = board.shape[0]
11
- H = board.shape[1]
9
+ def _sanity_check(board: np.array): # percolation check
10
+ V, H = board.shape
12
11
  visited: set[Pos] = set()
13
12
  finished_islands: set[int] = set()
14
13
  def dfs(pos: Pos, target_i: int):
@@ -28,15 +27,13 @@ def _sanity_check(board: np.array):
28
27
  assert current_i not in finished_islands, f'island {current_i} already finished'
29
28
  dfs(pos, current_i)
30
29
  finished_islands.add(current_i)
31
-
32
30
  assert len(finished_islands) == len(set(board.flatten())), 'board is not connected'
33
31
 
34
32
  class Board:
35
33
  def __init__(self, board: np.array, top: np.array, side: np.array):
36
34
  assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
37
35
  _sanity_check(board)
38
- self.V = board.shape[0]
39
- self.H = board.shape[1]
36
+ self.V, self.H = board.shape
40
37
  assert top.ndim == 1 and top.shape[0] == self.H, 'top must be a 1d array of length board width'
41
38
  assert side.ndim == 1 and side.shape[0] == self.V, 'side must be a 1d array of length board height'
42
39
  assert all((str(c.item()).isdecimal() for c in np.nditer(board))), 'board must contain only digits'
@@ -69,15 +66,12 @@ class Board:
69
66
  for aq_i in self.aquarium_numbers:
70
67
  for pos in self.aquariums[aq_i]:
71
68
  self.model.Add(self.is_aquarium_here[pos.y, aq_i] == 1).OnlyEnforceIf(self.model_vars[pos])
72
-
73
69
  # aquarium always start from the bottom
74
70
  for aq_i in self.aquarium_numbers:
75
71
  for row in self.aquariums_exist_in_row[aq_i]:
76
- # (row + 1) is below (row)
77
- if row + 1 not in self.aquariums_exist_in_row[aq_i]:
72
+ if row + 1 not in self.aquariums_exist_in_row[aq_i]: # (row + 1) is below (row) thus currently (row) is the bottom of the aquarium
78
73
  continue
79
74
  self.model.Add(self.is_aquarium_here[row + 1, aq_i] == 1).OnlyEnforceIf(self.is_aquarium_here[row, aq_i])
80
-
81
75
  for row in range(self.V):
82
76
  for aq_i in self.aquarium_numbers:
83
77
  aq_i_row_pos = [pos for pos in self.aquariums[aq_i] if pos.y == row]
@@ -88,7 +82,6 @@ class Board:
88
82
  if len(aq_i_row_pos) > 0:
89
83
  self.model.Add(sum([self.model_vars[pos] for pos in aq_i_row_pos]) == len(aq_i_row_pos)).OnlyEnforceIf(self.is_aquarium_here[row, aq_i])
90
84
  self.model.Add(sum([self.model_vars[pos] for pos in aq_i_row_pos]) == 0).OnlyEnforceIf(self.is_aquarium_here[row, aq_i].Not())
91
-
92
85
  # force the top and side constraints
93
86
  for col in range(self.H):
94
87
  self.model.Add(sum([self.model_vars[pos] for pos in get_col_pos(col, self.V)]) == self.top[col])
@@ -97,16 +90,8 @@ class Board:
97
90
 
98
91
  def solve_and_print(self, verbose: bool = True):
99
92
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
100
- assignment: dict[Pos, int] = {}
101
- for pos, var in board.model_vars.items():
102
- assignment[pos] = solver.value(var)
103
- return SingleSolution(assignment=assignment)
93
+ return SingleSolution(assignment={pos: solver.value(board.model_vars[pos]) for pos in board.model_vars.keys()})
104
94
  def callback(single_res: SingleSolution):
105
95
  print("Solution found")
106
- res = np.full((self.V, self.H), ' ', dtype=str)
107
- for pos, val in single_res.assignment.items():
108
- c = str(val)
109
- set_char(res, pos, c)
110
- print(res)
111
-
96
+ print(combined_function(self.V, self.H, cell_flags=id_board_to_wall_fn(self.board), center_char=lambda r, c: 'O' if single_res.assignment[get_pos(x=c, y=r)] == 1 else ''))
112
97
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=99)
@@ -1,11 +1,15 @@
1
- from dataclasses import dataclass, field
1
+ from dataclasses import dataclass
2
+ from collections import defaultdict
2
3
  from typing import Optional
3
4
 
4
5
  import numpy as np
5
6
  from ortools.sat.python import cp_model
7
+ from ortools.sat.python.cp_model import LinearExpr as lxp
8
+
9
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_neighbors8, get_row_pos, get_col_pos, get_pos, in_bounds
10
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
11
+ from puzzle_solver.core.utils_visualizer import combined_function
6
12
 
7
- from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_neighbors8, set_char, get_row_pos, get_col_pos, get_pos, in_bounds
8
- from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, or_constraint
9
13
 
10
14
  @dataclass
11
15
  class Ship:
@@ -14,17 +18,16 @@ class Ship:
14
18
  top_left_pos: Pos
15
19
  body: set[Pos]
16
20
  water: set[Pos]
17
- mid_body: set[Pos] = field(default_factory=set)
18
- top_tip: Optional[Pos] = field(default=None)
19
- bottom_tip: Optional[Pos] = field(default=None)
20
- left_tip: Optional[Pos] = field(default=None)
21
- right_tip: Optional[Pos] = field(default=None)
21
+ mid_body: set[Pos]
22
+ top_tip: Optional[Pos]
23
+ bottom_tip: Optional[Pos]
24
+ left_tip: Optional[Pos]
25
+ right_tip: Optional[Pos]
22
26
 
23
27
  class Board:
24
28
  def __init__(self, board: np.array, top: np.array, side: np.array, ship_counts: dict[int, int]):
25
29
  assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
26
- self.V = board.shape[0]
27
- self.H = board.shape[1]
30
+ self.V, self.H = board.shape
28
31
  assert top.ndim == 1 and top.shape[0] == self.H, 'top must be a 1d array of length board width'
29
32
  assert side.ndim == 1 and side.shape[0] == self.V, 'side must be a 1d array of length board height'
30
33
  assert all((str(c.item()) in [' ', 'W', 'O', 'S', 'U', 'D', 'L', 'R'] for c in np.nditer(board))), 'board must contain only spaces, W, O, S, U, D, L, R'
@@ -46,7 +49,6 @@ class Board:
46
49
  self.model_vars[pos] = self.model.NewBoolVar(f'{pos}:is_ship')
47
50
 
48
51
  def get_ship(self, pos: Pos, length: int, orientation: str) -> Optional[Ship]:
49
- assert orientation in ['horizontal', 'vertical'], 'orientation must be horizontal or vertical'
50
52
  if length == 1:
51
53
  body = {pos}
52
54
  top_tip = None
@@ -59,28 +61,22 @@ class Board:
59
61
  bottom_tip = None
60
62
  left_tip = pos
61
63
  right_tip = get_pos(x=pos.x + length - 1, y=pos.y)
62
- else:
64
+ elif orientation == 'vertical':
63
65
  body = set(get_pos(x=pos.x, y=y) for y in range(pos.y, pos.y + length))
64
66
  left_tip = None
65
67
  right_tip = None
66
68
  top_tip = pos
67
69
  bottom_tip = get_pos(x=pos.x, y=pos.y + length - 1)
70
+ else:
71
+ raise ValueError(f'invalid orientation: {orientation}')
68
72
  if any(not in_bounds(p, self.V, self.H) for p in body):
69
73
  return None
70
- water = set(p for pos in body for p in get_neighbors8(pos, self.V, self.H))
71
- water -= body
74
+ water = set(p for pos in body for p in get_neighbors8(pos, self.V, self.H)) - body
72
75
  mid_body = body - {top_tip, bottom_tip, left_tip, right_tip} if length > 1 else set()
73
76
  return Ship(
74
- is_active=self.model.NewBoolVar(f'{pos}:is_active'),
75
- length=length,
76
- top_left_pos=pos,
77
- body=body,
78
- water=water,
79
- mid_body=mid_body,
80
- top_tip=top_tip,
81
- bottom_tip=bottom_tip,
82
- left_tip=left_tip,
83
- right_tip=right_tip,
77
+ is_active=self.model.NewBoolVar(f'{pos}:is_active'), length=length,
78
+ top_left_pos=pos, body=body, mid_body=mid_body, water=water,
79
+ top_tip=top_tip, bottom_tip=bottom_tip, left_tip=left_tip, right_tip=right_tip,
84
80
  )
85
81
 
86
82
  def init_shipyard(self):
@@ -94,60 +90,50 @@ class Board:
94
90
  self.shipyard.append(ship)
95
91
 
96
92
  def add_all_constraints(self):
97
- # ship and cells linked
93
+ # if a ship is active then all its body is active and all its water is inactive
94
+ pos_to_ships: dict[Pos, list[Ship]] = defaultdict(list)
98
95
  for ship in self.shipyard:
99
96
  for pos in ship.body:
100
97
  self.model.Add(self.model_vars[pos] == 1).OnlyEnforceIf(ship.is_active)
98
+ pos_to_ships[pos].append(ship)
101
99
  for pos in ship.water:
102
100
  self.model.Add(self.model_vars[pos] == 0).OnlyEnforceIf(ship.is_active)
103
- # constrain the cell to be an OR of all the ships that can be placed at that position
101
+ # if a pos is active then exactly one ship can be placed at that position
104
102
  for pos in get_all_pos(self.V, self.H):
105
- or_constraint(self.model, self.model_vars[pos], [ship.is_active for ship in self.shipyard if pos in ship.body])
103
+ self.model.Add(lxp.Sum([ship.is_active for ship in pos_to_ships[pos]]) == 1).OnlyEnforceIf(self.model_vars[pos])
106
104
  # force ship counts
107
105
  for length, count in self.ship_counts.items():
108
- self.constrain_ship_counts([ship for ship in self.shipyard if ship.length == length], count)
109
- # force initial board placement
106
+ self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if ship.length == length]) == count)
107
+ # force the initial board placement
110
108
  for pos in get_all_pos(self.V, self.H):
111
109
  c = get_char(self.board, pos)
112
110
  if c == 'S': # single-length ship
113
- self.constrain_ship_counts([ship for ship in self.shipyard if ship.length == 1 and ship.top_left_pos == pos], 1)
111
+ self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if ship.length == 1 and ship.top_left_pos == pos]) == 1)
114
112
  elif c == 'W': # water
115
113
  self.model.Add(self.model_vars[pos] == 0)
116
114
  elif c == 'O': # mid-body of a ship
117
- self.constrain_ship_counts([ship for ship in self.shipyard if pos in ship.mid_body], 1)
115
+ self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if pos in ship.mid_body]) == 1)
118
116
  elif c == 'U': # top tip of a ship
119
- self.constrain_ship_counts([ship for ship in self.shipyard if ship.top_tip == pos], 1)
117
+ self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if ship.top_tip == pos]) == 1)
120
118
  elif c == 'D': # bottom tip of a ship
121
- self.constrain_ship_counts([ship for ship in self.shipyard if ship.bottom_tip == pos], 1)
119
+ self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if ship.bottom_tip == pos]) == 1)
122
120
  elif c == 'L': # left tip of a ship
123
- self.constrain_ship_counts([ship for ship in self.shipyard if ship.left_tip == pos], 1)
121
+ self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if ship.left_tip == pos]) == 1)
124
122
  elif c == 'R': # right tip of a ship
125
- self.constrain_ship_counts([ship for ship in self.shipyard if ship.right_tip == pos], 1)
123
+ self.model.Add(lxp.Sum([ship.is_active for ship in self.shipyard if ship.right_tip == pos]) == 1)
126
124
  elif c == ' ': # empty cell
127
125
  pass
128
126
  else:
129
127
  raise ValueError(f'invalid character: {c}')
130
- # force the top and side counts
131
- for row in range(self.V):
132
- self.model.Add(sum([self.model_vars[p] for p in get_row_pos(row, self.H)]) == self.side[row])
133
- for col in range(self.H):
134
- self.model.Add(sum([self.model_vars[p] for p in get_col_pos(col, self.V)]) == self.top[col])
135
-
136
- def constrain_ship_counts(self, ships: list[Ship], count: int):
137
- self.model.Add(sum([ship.is_active for ship in ships]) == count)
128
+ for row in range(self.V): # force the top counts
129
+ self.model.Add(lxp.Sum([self.model_vars[p] for p in get_row_pos(row, self.H)]) == self.side[row])
130
+ for col in range(self.H): # force the side counts
131
+ self.model.Add(lxp.Sum([self.model_vars[p] for p in get_col_pos(col, self.V)]) == self.top[col])
138
132
 
139
133
  def solve_and_print(self, verbose: bool = True):
140
134
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
141
- assignment: dict[Pos, int] = {}
142
- for pos, var in board.model_vars.items():
143
- assignment[pos] = solver.Value(var)
144
- return SingleSolution(assignment=assignment)
135
+ return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
145
136
  def callback(single_res: SingleSolution):
146
137
  print("Solution found")
147
- res = np.full((self.V, self.H), ' ', dtype=str)
148
- for pos, val in single_res.assignment.items():
149
- c = 'S' if val == 1 else ' '
150
- set_char(res, pos, c)
151
- print(res)
152
-
138
+ print(combined_function(self.V, self.H, is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1))
153
139
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -6,7 +6,7 @@ from ortools.sat.python.cp_model import LinearExpr as lxp
6
6
 
7
7
  from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_next_pos, get_pos, in_bounds, get_char, get_row_pos, get_col_pos
8
8
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
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
  class Board:
@@ -122,5 +122,5 @@ class Board:
122
122
  return SingleSolution(assignment=assignment)
123
123
  def callback(single_res: SingleSolution):
124
124
  print("Solution found")
125
- print(render_shaded_grid(self.V, self.H, lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1))
125
+ print(combined_function(self.V, self.H, is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1))
126
126
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -1,24 +1,10 @@
1
- from dataclasses import dataclass
2
- from typing import Optional, Union
3
- import json
1
+ from typing import Optional
4
2
 
5
- import numpy as np
6
3
  from ortools.sat.python import cp_model
7
4
 
8
- from puzzle_solver.core.utils import Pos, get_all_pos, get_row_pos, get_col_pos, in_bounds, get_opposite_direction, get_next_pos, Direction
9
- from puzzle_solver.core.utils_ortools import generic_solve_all
10
-
11
-
12
- @dataclass(frozen=True)
13
- class SingleSolution:
14
- assignment: dict[Pos, Union[str, int]]
15
- beam_assignments: dict[Pos, list[tuple[int, Pos, Direction]]]
16
-
17
- def get_hashable_solution(self) -> str:
18
- result = []
19
- for pos, v in self.assignment.items():
20
- result.append((pos.x, pos.y, v))
21
- return json.dumps(result, sort_keys=True)
5
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_pos, get_row_pos, get_col_pos, in_bounds, get_opposite_direction, get_next_pos, Direction
6
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
7
+ from puzzle_solver.core.utils_visualizer import combined_function
22
8
 
23
9
 
24
10
  class Board:
@@ -33,18 +19,12 @@ class Board:
33
19
  else:
34
20
  self.T = max_travel_steps
35
21
  self.ball_count = ball_count
36
-
37
22
  # top and bottom entry cells are at -1 and V
38
23
  self.top_cells = set(get_row_pos(row_idx=-1, H=self.H))
39
24
  self.bottom_cells = set(get_row_pos(row_idx=self.V, H=self.H))
40
25
  # left and right entry cells are at -1 and H
41
26
  self.left_cells = set(get_col_pos(col_idx=-1, V=self.V))
42
27
  self.right_cells = set(get_col_pos(col_idx=self.H, V=self.V))
43
- # self.top_cells = set([Pos(x=1, y=-1)])
44
- # self.bottom_cells = set()
45
- # self.left_cells = set()
46
- # self.right_cells = set()
47
- # print(self.top_cells)
48
28
 
49
29
  self.top_values = top
50
30
  self.right_values = right
@@ -115,12 +95,9 @@ class Board:
115
95
  # beam at t=0 is present at beam_id and facing direction
116
96
  self.model.Add(self.beam_states[(beam_id, 0, beam_id, direction)] == 1)
117
97
  for p in self.get_all_pos_extended():
118
- # print(f'beam can be at {p}')
119
98
  for direction in Direction:
120
99
  if (p, direction) != (beam_id, direction):
121
100
  self.model.Add(self.beam_states[(beam_id, 0, p, direction)] == 0)
122
- # for p in self.get_all_pos_extended():
123
- # print(f'beam can be at {p}')
124
101
 
125
102
 
126
103
  def constrain_beam_movement(self):
@@ -128,24 +105,18 @@ class Board:
128
105
  for entry_pos in self.beam_states_at_t[t].keys():
129
106
  next_state_dict = self.beam_states_at_t[t][entry_pos]
130
107
  self.model.AddExactlyOne(list(next_state_dict.values()))
131
- # print('add exactly one constraint for beam id', entry_pos, 'at time', t, next_state_dict.keys(), '\n')
132
108
  if t == self.T - 1:
133
109
  continue
134
110
  for (cell, direction), prev_state in next_state_dict.items():
135
- # print(f'for beam id {entry_pos}, time {t}\nif its at {cell} facing {direction}')
136
111
  self.constrain_next_beam_state(entry_pos, t+1, cell, direction, prev_state)
137
112
 
138
113
 
139
114
  def constrain_next_beam_state(self, entry_pos: Pos, t: int, cur_pos: Pos, direction: Direction, prev_state: cp_model.IntVar):
140
- # print(f"constraining next beam state for {entry_pos}, {t}, {cur_pos}, {direction}")
141
115
  if cur_pos == "HIT": # a beam that was "HIT" stays "HIT"
142
- # print(f' HIT -> stays HIT')
143
116
  self.model.Add(self.beam_states[(entry_pos, t, "HIT", "HIT")] == 1).OnlyEnforceIf(prev_state)
144
117
  return
145
- # if a beam is outside the board AND it is not facing the board -> it maintains its state
146
118
  pos_ahead = get_next_pos(cur_pos, direction)
147
119
  if not in_bounds(pos_ahead, self.V, self.H) and not in_bounds(cur_pos, self.V, self.H):
148
- # print(f' OUTSIDE BOARD -> beam stays in the same state')
149
120
  self.model.Add(self.beam_states[(entry_pos, t, cur_pos, direction)] == 1).OnlyEnforceIf(prev_state)
150
121
  return
151
122
 
@@ -200,16 +171,6 @@ class Board:
200
171
  if not in_bounds(pos_reflected, self.V, self.H):
201
172
  pos_reflected = cur_pos
202
173
 
203
- # debug_states = {
204
- # 'if ball head': (entry_pos, t, "HIT", "HIT"),
205
- # 'if ball in front-left': (entry_pos, t, get_next_pos(cur_pos, direction_right), direction_right),
206
- # 'if ball in front-right': (entry_pos, t, get_next_pos(cur_pos, direction_left), direction_left),
207
- # 'if ball in front-left and front-right': (entry_pos, t, get_next_pos(cur_pos, reflected), reflected),
208
- # 'if no ball ahead': (entry_pos, t, pos_ahead, direction),
209
- # }
210
- # for k,v in debug_states.items():
211
- # print(f' {k} -> {v}')
212
-
213
174
  # ball head-on -> beam is "HIT"
214
175
  self.model.Add(self.beam_states[(entry_pos, t, "HIT", "HIT")] == 1).OnlyEnforceIf([
215
176
  ball_ahead,
@@ -273,35 +234,10 @@ class Board:
273
234
  for reflect in reflects:
274
235
  self.model.AddExactlyOne([self.beam_states[(reflect, self.T-1, reflect, direction)] for direction in Direction])
275
236
 
276
-
277
237
  def solve_and_print(self, verbose: bool = True):
278
- count = 0
279
238
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
280
- nonlocal count
281
- count += 1
282
- # if count > 99:
283
- # import sys
284
- # sys.exit(0)
285
- assignment = {}
286
- for pos in get_all_pos(self.V, self.H):
287
- assignment[pos] = solver.value(self.ball_states[pos])
288
- beam_assignments = {}
289
- # for (entry_pos, t, cell, direction), v in self.beam_states.items():
290
- # if entry_pos not in beam_assignments:
291
- # beam_assignments[entry_pos] = []
292
- # if solver.value(v): # for every beam it can only be present in one state at a time
293
- # beam_assignments[entry_pos].append((t, cell, direction))
294
- # for k,v in beam_assignments.items():
295
- # beam_assignments[k] = sorted(v, key=lambda x: x[0])
296
- # print(k, beam_assignments[k], '\n')
297
- # print(beam_assignments)
298
- return SingleSolution(assignment=assignment, beam_assignments=beam_assignments)
299
-
239
+ return SingleSolution(assignment={pos: solver.Value(self.ball_states[pos]) for pos in get_all_pos(self.V, self.H)})
300
240
  def callback(single_res: SingleSolution):
301
241
  print("Solution found")
302
- res = np.full((self.V, self.H), ' ', dtype=object)
303
- for pos in get_all_pos(self.V, self.H):
304
- ball_state = 'O' if single_res.assignment[pos] else ' '
305
- res[pos.y][pos.x] = ball_state
306
- print(res)
242
+ print(combined_function(self.V, self.H, center_char=lambda r, c: 'O' if single_res.assignment[get_pos(x=c, y=r)] else ''))
307
243
  generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -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, set_char, get_char
5
5
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
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:
@@ -44,5 +44,7 @@ class Board:
44
44
  res = np.full((self.V, self.H), ' ', dtype=object)
45
45
  for pos in get_all_pos(self.V, self.H):
46
46
  set_char(res, pos, single_res.assignment[pos])
47
- print(render_grid(id_board_to_wall_board(res), center_char=lambda r, c: res[r][c]))
47
+ print(combined_function(self.V, self.H,
48
+ cell_flags=id_board_to_wall_fn(res),
49
+ center_char=lambda r, c: res[r][c]))
48
50
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -3,8 +3,9 @@ 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_char, set_char, polyominoes, in_bounds, get_next_pos, Direction
6
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_pos, polyominoes, in_bounds, get_next_pos, Direction
7
7
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
8
+ from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
8
9
 
9
10
 
10
11
  @dataclass
@@ -23,8 +24,6 @@ class Board:
23
24
  assert all((c == ' ') or (str(c).isdecimal() and 0 <= int(c) <= 9) for c in np.nditer(board)), "board must contain space or digits 0..9"
24
25
  self.digits = digits
25
26
  self.polyominoes = {d: polyominoes(d) for d in self.digits}
26
- # len_shapes = sum(len(shapes) for shapes in self.polyominoes.values())
27
- # print(f'total number of shapes: {len_shapes}')
28
27
 
29
28
  self.model = cp_model.CpModel()
30
29
  self.model_vars: dict[Pos, cp_model.IntVar] = {}
@@ -33,7 +32,7 @@ class Board:
33
32
  self.forced_pos: dict[Pos, int] = {}
34
33
 
35
34
  self.create_vars()
36
- self.constrain_numbers_on_board()
35
+ self.force_hints()
37
36
  self.init_polyominoes_on_board()
38
37
  self.add_all_constraints()
39
38
 
@@ -42,8 +41,7 @@ class Board:
42
41
  for d in self.digits:
43
42
  self.model_vars[(d,pos)] = self.model.NewBoolVar(f'{d}:{pos}')
44
43
 
45
- def constrain_numbers_on_board(self):
46
- # force numbers already on the board
44
+ def force_hints(self):
47
45
  for pos in get_all_pos(self.V, self.H):
48
46
  c = get_char(self.board, pos)
49
47
  if c.isdecimal():
@@ -51,7 +49,6 @@ class Board:
51
49
  self.forced_pos[pos] = int(c)
52
50
 
53
51
  def init_polyominoes_on_board(self):
54
- # total_count = 0
55
52
  for d in self.digits: # all digits
56
53
  digit_count = 0
57
54
  for pos in get_all_pos(self.V, self.H): # translate by shape
@@ -73,45 +70,25 @@ class Board:
73
70
  for p in body:
74
71
  self.body_loc_to_shape[(d,p)].append(shape_on_board)
75
72
  digit_count += 1
76
- # total_count += 1
77
- # if total_count % 1000 == 0:
78
- # print(f'{total_count} shapes on board')
79
- # print(f'total number of shapes on board: {total_count}')
80
73
 
81
74
  def add_all_constraints(self):
82
75
  for pos in get_all_pos(self.V, self.H):
83
- # exactly one digit is active at every position
84
- self.model.AddExactlyOne(self.model_vars[(d,pos)] for d in self.digits)
85
- # exactly one shape is active at that position
86
- self.model.AddExactlyOne(s.is_active for d in self.digits for s in self.body_loc_to_shape[(d,pos)])
87
- # if a shape is active then all its body is active
88
-
89
- for s_list in self.body_loc_to_shape.values():
76
+ self.model.AddExactlyOne(self.model_vars[(d,pos)] for d in self.digits) # exactly one digit is active at every position
77
+ self.model.AddExactlyOne(s.is_active for d in self.digits for s in self.body_loc_to_shape[(d,pos)]) # exactly one shape is active at that position
78
+ for s_list in self.body_loc_to_shape.values(): # if a shape is active then all its body is active
90
79
  for s in s_list:
91
80
  for p in s.body:
92
81
  self.model.Add(self.model_vars[(s.N,p)] == 1).OnlyEnforceIf(s.is_active)
93
-
94
- # same shape cannot touch
95
- for d, s_list in self.digit_to_shapes.items():
82
+ for d, s_list in self.digit_to_shapes.items(): # same shape cannot touch each other
96
83
  for s in s_list:
97
84
  for disallow_pos in s.disallow_same_shape:
98
85
  self.model.Add(self.model_vars[(d,disallow_pos)] == 0).OnlyEnforceIf(s.is_active)
99
86
 
100
87
  def solve_and_print(self, verbose: bool = True):
101
88
  def board_to_solution(board: "Board", solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
102
- assignment: dict[Pos, int] = {}
103
- for pos in get_all_pos(self.V, self.H):
104
- for d in self.digits:
105
- if solver.Value(self.model_vars[(d,pos)]) == 1:
106
- assignment[pos] = d
107
- break
108
- return SingleSolution(assignment=assignment)
89
+ return SingleSolution(assignment={pos: d for pos in get_all_pos(self.V, self.H) for d in self.digits if solver.Value(self.model_vars[(d,pos)]) == 1})
109
90
  def callback(single_res: SingleSolution):
110
91
  print("Solution found")
111
- res = np.full((self.V, self.H), ' ', dtype=object)
112
- for pos in get_all_pos(self.V, self.H):
113
- c = get_char(self.board, pos)
114
- c = single_res.assignment[pos]
115
- set_char(res, pos, c)
116
- print(res)
92
+ res_arr = np.array([[single_res.assignment[get_pos(x=c, y=r)] for c in range(self.H)] for r in range(self.V)])
93
+ print(combined_function(self.V, self.H, cell_flags=id_board_to_wall_fn(res_arr), center_char=lambda r, c: res_arr[r, c]))
117
94
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -6,7 +6,7 @@ from ortools.sat.python import cp_model
6
6
 
7
7
  from puzzle_solver.core.utils import Pos, get_all_pos, set_char, Direction, get_next_pos, in_bounds, get_opposite_direction, 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 id_board_to_wall_board, render_grid
9
+ from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
10
10
 
11
11
 
12
12
  def parse_numpy(galaxies: np.ndarray) -> list[tuple[Pos, ...]]:
@@ -104,5 +104,7 @@ class Board:
104
104
  res = np.full((self.V, self.H), ' ', dtype=object)
105
105
  for pos in get_all_pos(self.V, self.H):
106
106
  set_char(res, pos, single_res.assignment[pos])
107
- print(render_grid(id_board_to_wall_board(res), center_char=lambda r, c: '.' if (Pos(x=c, y=r) in self.prelocated_positions) else ' '))
107
+ print(combined_function(self.V, self.H,
108
+ cell_flags=id_board_to_wall_fn(res),
109
+ center_char=lambda r, c: '.' if (Pos(x=c, y=r) in self.prelocated_positions) else ' '))
108
110
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)