multi-puzzle-solver 1.1.8__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.
Files changed (106) hide show
  1. multi_puzzle_solver-1.1.8.dist-info/METADATA +4326 -0
  2. multi_puzzle_solver-1.1.8.dist-info/RECORD +106 -0
  3. multi_puzzle_solver-1.1.8.dist-info/WHEEL +5 -0
  4. multi_puzzle_solver-1.1.8.dist-info/top_level.txt +1 -0
  5. puzzle_solver/__init__.py +184 -0
  6. puzzle_solver/core/utils.py +298 -0
  7. puzzle_solver/core/utils_ortools.py +333 -0
  8. puzzle_solver/core/utils_visualizer.py +575 -0
  9. puzzle_solver/puzzles/abc_view/abc_view.py +75 -0
  10. puzzle_solver/puzzles/aquarium/aquarium.py +97 -0
  11. puzzle_solver/puzzles/area_51/area_51.py +159 -0
  12. puzzle_solver/puzzles/battleships/battleships.py +139 -0
  13. puzzle_solver/puzzles/binairo/binairo.py +98 -0
  14. puzzle_solver/puzzles/binairo/binairo_plus.py +7 -0
  15. puzzle_solver/puzzles/black_box/black_box.py +243 -0
  16. puzzle_solver/puzzles/branches/branches.py +64 -0
  17. puzzle_solver/puzzles/bridges/bridges.py +104 -0
  18. puzzle_solver/puzzles/chess_range/chess_melee.py +6 -0
  19. puzzle_solver/puzzles/chess_range/chess_range.py +406 -0
  20. puzzle_solver/puzzles/chess_range/chess_solo.py +9 -0
  21. puzzle_solver/puzzles/chess_sequence/chess_sequence.py +262 -0
  22. puzzle_solver/puzzles/circle_9/circle_9.py +44 -0
  23. puzzle_solver/puzzles/clouds/clouds.py +81 -0
  24. puzzle_solver/puzzles/connect_the_dots/connect_the_dots.py +50 -0
  25. puzzle_solver/puzzles/cow_and_cactus/cow_and_cactus.py +66 -0
  26. puzzle_solver/puzzles/dominosa/dominosa.py +67 -0
  27. puzzle_solver/puzzles/filling/filling.py +94 -0
  28. puzzle_solver/puzzles/flip/flip.py +64 -0
  29. puzzle_solver/puzzles/flood_it/flood_it.py +174 -0
  30. puzzle_solver/puzzles/flood_it/parse_map/parse_map.py +197 -0
  31. puzzle_solver/puzzles/galaxies/galaxies.py +110 -0
  32. puzzle_solver/puzzles/galaxies/parse_map/parse_map.py +216 -0
  33. puzzle_solver/puzzles/guess/guess.py +232 -0
  34. puzzle_solver/puzzles/heyawake/heyawake.py +152 -0
  35. puzzle_solver/puzzles/hidden_stars/hidden_stars.py +52 -0
  36. puzzle_solver/puzzles/hidoku/hidoku.py +59 -0
  37. puzzle_solver/puzzles/inertia/inertia.py +121 -0
  38. puzzle_solver/puzzles/inertia/parse_map/parse_map.py +207 -0
  39. puzzle_solver/puzzles/inertia/tsp.py +400 -0
  40. puzzle_solver/puzzles/kakurasu/kakurasu.py +38 -0
  41. puzzle_solver/puzzles/kakuro/kakuro.py +81 -0
  42. puzzle_solver/puzzles/kakuro/krypto_kakuro.py +95 -0
  43. puzzle_solver/puzzles/keen/keen.py +76 -0
  44. puzzle_solver/puzzles/kropki/kropki.py +94 -0
  45. puzzle_solver/puzzles/light_up/light_up.py +58 -0
  46. puzzle_solver/puzzles/linesweeper/linesweeper.py +71 -0
  47. puzzle_solver/puzzles/link_a_pix/link_a_pix.py +91 -0
  48. puzzle_solver/puzzles/lits/lits.py +138 -0
  49. puzzle_solver/puzzles/magnets/magnets.py +96 -0
  50. puzzle_solver/puzzles/map/map.py +56 -0
  51. puzzle_solver/puzzles/mathema_grids/mathema_grids.py +119 -0
  52. puzzle_solver/puzzles/mathrax/mathrax.py +93 -0
  53. puzzle_solver/puzzles/minesweeper/minesweeper.py +123 -0
  54. puzzle_solver/puzzles/mosaic/mosaic.py +38 -0
  55. puzzle_solver/puzzles/n_queens/n_queens.py +71 -0
  56. puzzle_solver/puzzles/nonograms/nonograms.py +121 -0
  57. puzzle_solver/puzzles/nonograms/nonograms_colored.py +220 -0
  58. puzzle_solver/puzzles/norinori/norinori.py +96 -0
  59. puzzle_solver/puzzles/number_path/number_path.py +76 -0
  60. puzzle_solver/puzzles/numbermaze/numbermaze.py +97 -0
  61. puzzle_solver/puzzles/nurikabe/nurikabe.py +130 -0
  62. puzzle_solver/puzzles/palisade/palisade.py +91 -0
  63. puzzle_solver/puzzles/pearl/pearl.py +107 -0
  64. puzzle_solver/puzzles/pipes/pipes.py +82 -0
  65. puzzle_solver/puzzles/range/range.py +59 -0
  66. puzzle_solver/puzzles/rectangles/rectangles.py +128 -0
  67. puzzle_solver/puzzles/ripple_effect/ripple_effect.py +83 -0
  68. puzzle_solver/puzzles/rooms/rooms.py +75 -0
  69. puzzle_solver/puzzles/schurs_numbers/schurs_numbers.py +73 -0
  70. puzzle_solver/puzzles/shakashaka/shakashaka.py +201 -0
  71. puzzle_solver/puzzles/shingoki/shingoki.py +116 -0
  72. puzzle_solver/puzzles/signpost/signpost.py +93 -0
  73. puzzle_solver/puzzles/singles/singles.py +53 -0
  74. puzzle_solver/puzzles/slant/parse_map/parse_map.py +135 -0
  75. puzzle_solver/puzzles/slant/slant.py +111 -0
  76. puzzle_solver/puzzles/slitherlink/slitherlink.py +130 -0
  77. puzzle_solver/puzzles/snail/snail.py +97 -0
  78. puzzle_solver/puzzles/split_ends/split_ends.py +93 -0
  79. puzzle_solver/puzzles/star_battle/star_battle.py +75 -0
  80. puzzle_solver/puzzles/star_battle/star_battle_shapeless.py +7 -0
  81. puzzle_solver/puzzles/stitches/parse_map/parse_map.py +267 -0
  82. puzzle_solver/puzzles/stitches/stitches.py +96 -0
  83. puzzle_solver/puzzles/sudoku/sudoku.py +267 -0
  84. puzzle_solver/puzzles/suguru/suguru.py +55 -0
  85. puzzle_solver/puzzles/suko/suko.py +54 -0
  86. puzzle_solver/puzzles/tapa/tapa.py +97 -0
  87. puzzle_solver/puzzles/tatami/tatami.py +64 -0
  88. puzzle_solver/puzzles/tents/tents.py +80 -0
  89. puzzle_solver/puzzles/thermometers/thermometers.py +82 -0
  90. puzzle_solver/puzzles/towers/towers.py +89 -0
  91. puzzle_solver/puzzles/tracks/tracks.py +88 -0
  92. puzzle_solver/puzzles/trees_logic/trees_logic.py +48 -0
  93. puzzle_solver/puzzles/troix/dumplings.py +7 -0
  94. puzzle_solver/puzzles/troix/troix.py +75 -0
  95. puzzle_solver/puzzles/twiddle/twiddle.py +112 -0
  96. puzzle_solver/puzzles/undead/undead.py +130 -0
  97. puzzle_solver/puzzles/unequal/unequal.py +128 -0
  98. puzzle_solver/puzzles/unruly/unruly.py +54 -0
  99. puzzle_solver/puzzles/vectors/vectors.py +94 -0
  100. puzzle_solver/puzzles/vermicelli/vermicelli.py +74 -0
  101. puzzle_solver/puzzles/walls/walls.py +52 -0
  102. puzzle_solver/puzzles/yajilin/yajilin.py +87 -0
  103. puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py +172 -0
  104. puzzle_solver/puzzles/yin_yang/yin_yang.py +103 -0
  105. puzzle_solver/utils/etc/parser/board_color_digit.py +497 -0
  106. puzzle_solver/utils/visualizer.py +155 -0
@@ -0,0 +1,82 @@
1
+ from typing import Optional
2
+
3
+ import numpy as np
4
+ from ortools.sat.python import cp_model
5
+
6
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, Direction, get_opposite_direction, get_next_pos, in_bounds, get_row_pos, get_col_pos
7
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
8
+
9
+ def get_direction(pos: Pos, board: np.array) -> Optional[Direction]:
10
+ if get_char(board, pos) == 'R':
11
+ return Direction.RIGHT
12
+ elif get_char(board, pos) == 'D':
13
+ return Direction.DOWN
14
+ elif get_char(board, pos) == 'U':
15
+ return Direction.UP
16
+ elif get_char(board, pos) == 'L':
17
+ return Direction.LEFT
18
+ return None
19
+
20
+ def move_backward(pos: Pos, board: np.array) -> Pos:
21
+ for direction in Direction:
22
+ opposite_direction = get_opposite_direction(direction)
23
+ neighbor = get_next_pos(pos, direction)
24
+ if in_bounds(neighbor, board.shape[0], board.shape[1]) and get_direction(neighbor, board) == opposite_direction: # the neighbor is pointing to me
25
+ return neighbor
26
+ return None
27
+
28
+ class Board:
29
+ def __init__(self, board: np.array, top: np.array, side: np.array):
30
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
31
+ self.V = board.shape[0]
32
+ self.H = board.shape[1]
33
+ assert top.ndim == 1 and top.shape[0] == self.H, 'top must be a 1d array of length board width'
34
+ assert side.ndim == 1 and side.shape[0] == self.V, 'side must be a 1d array of length board height'
35
+ assert all((c in ['R', 'D', 'U', 'X', 'L']) for c in np.nditer(board)), 'board must contain only valid characters: R, D, U, X, L'
36
+ self.board = board
37
+ self.top = top
38
+ self.side = side
39
+ self.tip: set[Pos] = {pos for pos in get_all_pos(self.V, self.H) if get_char(self.board, pos) == 'X'}
40
+
41
+ self.model = cp_model.CpModel()
42
+ self.model_vars: dict[Pos, cp_model.IntVar] = {}
43
+
44
+ self.create_vars()
45
+ self.add_all_constraints()
46
+
47
+ def create_vars(self):
48
+ for pos in get_all_pos(self.V, self.H):
49
+ self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
50
+
51
+ def add_all_constraints(self):
52
+ visited: set[Pos] = set()
53
+ for cur_pos in self.tip:
54
+ visited.add(cur_pos)
55
+ while cur_pos is not None:
56
+ backward_pos = move_backward(cur_pos, self.board)
57
+ if backward_pos is None:
58
+ break
59
+ self.model.Add(self.model_vars[backward_pos] == 1).OnlyEnforceIf(self.model_vars[cur_pos])
60
+ cur_pos = backward_pos
61
+ visited.add(cur_pos)
62
+ assert len(visited) == self.V * self.H, f'all positions must be visited, got {len(visited)}. missing {set(get_all_pos(self.V, self.H)) - visited}'
63
+ for row in range(self.V):
64
+ self.model.Add(sum([self.model_vars[pos] for pos in get_row_pos(row, self.H)]) == self.side[row])
65
+ for col in range(self.H):
66
+ self.model.Add(sum([self.model_vars[pos] for pos in get_col_pos(col, self.V)]) == self.top[col])
67
+
68
+ def solve_and_print(self, verbose: bool = True):
69
+ def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
70
+ assignment: dict[Pos, int] = {}
71
+ for pos, var in board.model_vars.items():
72
+ assignment[pos] = solver.value(var)
73
+ return SingleSolution(assignment=assignment)
74
+ def callback(single_res: SingleSolution):
75
+ print("Solution found")
76
+ res = np.full((self.V, self.H), ' ', dtype=object)
77
+ for pos in get_all_pos(self.V, self.H):
78
+ c = get_char(self.board, pos)
79
+ c = 'X' if single_res.assignment[pos] == 1 else ' '
80
+ set_char(res, pos, c)
81
+ print(res)
82
+ return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -0,0 +1,89 @@
1
+ import numpy as np
2
+ from ortools.sat.python import cp_model
3
+ from ortools.sat.python.cp_model import LinearExpr as lxp
4
+
5
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_pos, get_row_pos, get_col_pos
6
+ from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, SingleSolution
7
+
8
+
9
+ def bool_from_greater_than(model, a, b, name):
10
+ res = model.NewBoolVar(name)
11
+ model.add(a > b).OnlyEnforceIf(res)
12
+ model.add(a <= b).OnlyEnforceIf(res.Not())
13
+ return res
14
+
15
+
16
+ class Board:
17
+ def __init__(self, board: np.array, top: np.array, bottom: np.array, right: np.array, left: np.array):
18
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
19
+ assert all((c == ' ') or str(c).isdecimal() for c in np.nditer(board)), 'board must contain space or digits'
20
+ self.board = board
21
+ self.V, self.H = board.shape
22
+ assert top.shape == (self.H,) and bottom.shape == (self.H,) and right.shape == (self.V,) and left.shape == (self.V,), 'top, bottom, right, and left must be 1d arrays of length board width and height'
23
+ self.top = top
24
+ self.bottom = bottom
25
+ self.right = right
26
+ self.left = left
27
+ self.model = cp_model.CpModel()
28
+ self.model_vars: dict[Pos, cp_model.IntVar] = {}
29
+
30
+ self.create_vars()
31
+ self.add_all_constraints()
32
+
33
+ def create_vars(self):
34
+ for pos in get_all_pos(self.V, self.H):
35
+ self.model_vars[pos] = self.model.NewIntVar(1, max(self.V, self.H), f'{pos}')
36
+
37
+ def add_all_constraints(self):
38
+ for pos in get_all_pos(self.V, self.H): # force board clues
39
+ v = get_char(self.board, pos)
40
+ if str(v).isdecimal():
41
+ self.model.Add(self.model_vars[pos] == int(v))
42
+ for row_i in range(self.V): # all different for rows
43
+ self.model.AddAllDifferent([self.model_vars[pos] for pos in get_row_pos(row_i, self.H)])
44
+ for col_i in range(self.H): # all different for cols
45
+ self.model.AddAllDifferent([self.model_vars[pos] for pos in get_col_pos(col_i, self.V)])
46
+ for x in range(self.H): # top
47
+ self.tower_constraints(real=self.top[x], pos_list=[get_pos(x=x, y=y) for y in range(self.V)], name=f'top:{x}')
48
+ for x in range(self.H): # bottom
49
+ self.tower_constraints(real=self.bottom[x], pos_list=[get_pos(x=x, y=y) for y in range(self.V-1, -1, -1)], name=f'bottom:{x}')
50
+ for y in range(self.V): # left
51
+ self.tower_constraints(real=self.left[y], pos_list=[get_pos(x=x, y=y) for x in range(self.H)], name=f'left:{y}')
52
+ for y in range(self.H): # right
53
+ self.tower_constraints(real=self.right[y], pos_list=[get_pos(x=x, y=y) for x in range(self.V-1, -1, -1)], name=f'right:{y}')
54
+
55
+ def tower_constraints(self, real: int, pos_list: list[Pos], name: str):
56
+ if real == -1:
57
+ return
58
+ can_see_variables = []
59
+ previous_towers: list[cp_model.IntVar] = []
60
+ for pos in pos_list:
61
+ current_tower = self.model_vars[pos]
62
+ can_see_variables.append(self.can_see_tower(previous_towers, current_tower, f'{name}:{pos}'))
63
+ previous_towers.append(current_tower)
64
+ self.model.add(lxp.sum(can_see_variables) == real)
65
+
66
+ def can_see_tower(self, blocks: list[cp_model.IntVar], tower: cp_model.IntVar, name: str) -> cp_model.IntVar:
67
+ """Returns a boolean variable of whether a position BEFORE the blocks can see the "tower" parameter."""
68
+ if len(blocks) == 0:
69
+ return True
70
+ # I can see "tower" if it's larger that all the blocks before it, lits is a list of [(tower > b0), (tower > b1), ..., (tower > bi)]
71
+ res = self.model.NewBoolVar(name)
72
+ and_constraint(self.model, target=res, cs=[bool_from_greater_than(self.model, tower, block, f'{name}:lits:{block}') for block in blocks])
73
+ return res
74
+
75
+ def solve_and_print(self, verbose: bool = True):
76
+ def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
77
+ assignment: dict[Pos, int] = {}
78
+ for pos, var in board.model_vars.items():
79
+ assignment[pos] = solver.value(var)
80
+ return SingleSolution(assignment=assignment)
81
+ def callback(single_res: SingleSolution):
82
+ print("Solution found")
83
+ res = np.full((self.V, self.H), ' ', dtype=object)
84
+ for pos in get_all_pos(self.V, self.H):
85
+ c = get_char(self.board, pos)
86
+ c = single_res.assignment[pos]
87
+ set_char(res, pos, c)
88
+ print(res)
89
+ return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -0,0 +1,88 @@
1
+ from collections import defaultdict
2
+ import numpy as np
3
+ from ortools.sat.python import cp_model
4
+
5
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, Direction, get_next_pos, get_row_pos, get_col_pos, get_opposite_direction, get_pos
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
8
+
9
+
10
+ class Board:
11
+ def __init__(self, board: np.array, side: np.array, top: np.array):
12
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
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'
14
+ self.board = board
15
+ self.V, self.H = board.shape
16
+ self.side = side
17
+ self.top = top
18
+
19
+ self.model = cp_model.CpModel()
20
+ self.cell_active: dict[Pos, cp_model.IntVar] = {}
21
+ self.cell_direction: dict[tuple[Pos, Direction], cp_model.IntVar] = {}
22
+
23
+ self.create_vars()
24
+ self.add_all_constraints()
25
+
26
+ def create_vars(self):
27
+ for pos in get_all_pos(self.V, self.H):
28
+ self.cell_active[pos] = self.model.NewBoolVar(f'{pos}')
29
+ for direction in Direction:
30
+ next_pos = get_next_pos(pos, direction)
31
+ opposite_direction = get_opposite_direction(direction)
32
+ if (next_pos, opposite_direction) in self.cell_direction:
33
+ self.cell_direction[(pos, direction)] = self.cell_direction[(next_pos, opposite_direction)]
34
+ else:
35
+ self.cell_direction[(pos, direction)] = self.model.NewBoolVar(f'{pos}:{direction}')
36
+
37
+ def add_all_constraints(self):
38
+ # force the already given hints
39
+ str_to_direction = {'U': Direction.UP, 'L': Direction.LEFT, 'D': Direction.DOWN, 'R': Direction.RIGHT}
40
+ for pos in get_all_pos(self.V, self.H):
41
+ for char in get_char(self.board, pos).strip():
42
+ self.model.Add(self.cell_direction[(pos, str_to_direction[char])] == 1)
43
+
44
+ # force the already given sides
45
+ for i in range(self.V):
46
+ self.model.Add(sum([self.cell_active[pos] for pos in get_row_pos(i, self.H)]) == self.side[i])
47
+ for i in range(self.H):
48
+ self.model.Add(sum([self.cell_active[pos] for pos in get_col_pos(i, self.V)]) == self.top[i])
49
+
50
+ # cell active means exactly 2 directions are active, cell not active means no directions are active
51
+ for pos in get_all_pos(self.V, self.H):
52
+ s = sum([self.cell_direction[(pos, direction)] for direction in Direction])
53
+ self.model.Add(s == 2).OnlyEnforceIf(self.cell_active[pos])
54
+ self.model.Add(s == 0).OnlyEnforceIf(self.cell_active[pos].Not())
55
+
56
+ # force borders to only have 2 point outside the board
57
+ pointing_outside = []
58
+ for pos in get_col_pos(0, self.V): # left border
59
+ pointing_outside.append(self.cell_direction[(pos, Direction.LEFT)])
60
+ for pos in get_col_pos(self.H - 1, self.V): # right border
61
+ pointing_outside.append(self.cell_direction[(pos, Direction.RIGHT)])
62
+ for pos in get_row_pos(0, self.H): # top border
63
+ pointing_outside.append(self.cell_direction[(pos, Direction.UP)])
64
+ for pos in get_row_pos(self.V - 1, self.H): # bottom border
65
+ pointing_outside.append(self.cell_direction[(pos, Direction.DOWN)])
66
+ self.model.Add(sum(pointing_outside) == 2)
67
+
68
+ # force single connected component
69
+ def is_neighbor(pd1: tuple[Pos, Direction], pd2: tuple[Pos, Direction]) -> bool:
70
+ p1, d1 = pd1
71
+ p2, d2 = pd2
72
+ if p1 == p2: # same position, different direction, is neighbor
73
+ return True
74
+ if get_next_pos(p1, d1) == p2 and d2 == get_opposite_direction(d1):
75
+ return True
76
+ return False
77
+ force_connected_component(self.model, self.cell_direction, is_neighbor=is_neighbor)
78
+
79
+ def solve_and_print(self, verbose: bool = True):
80
+ def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
81
+ assignment: dict[Pos, str] = defaultdict(str)
82
+ for (pos, direction), var in board.cell_direction.items():
83
+ assignment[pos] += direction.name[0] if solver.BooleanValue(var) else ''
84
+ return SingleSolution(assignment=assignment)
85
+ def callback(single_res: SingleSolution):
86
+ print("Solution found")
87
+ 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))
88
+ return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=20)
@@ -0,0 +1,48 @@
1
+ import numpy as np
2
+ from ortools.sat.python import cp_model
3
+ from ortools.sat.python.cp_model import LinearExpr as lxp
4
+
5
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_neighbors8, get_pos, get_row_pos, get_col_pos
6
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
7
+ from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
8
+
9
+
10
+ class Board:
11
+ def __init__(self, board: np.array):
12
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
13
+ assert all((str(c.item()).isdecimal() for c in np.nditer(board))), 'board must contain only digits'
14
+ self.board = board
15
+ self.V, self.H = board.shape
16
+ self.block_numbers = {int(c.item()) for c in np.nditer(board)}
17
+ self.blocks = {num: [] for num in self.block_numbers}
18
+ for pos in get_all_pos(self.V, self.H):
19
+ self.blocks[int(get_char(board, pos))].append(pos)
20
+
21
+ self.model = cp_model.CpModel()
22
+ self.model_vars: dict[Pos, cp_model.IntVar] = {}
23
+ self.create_vars()
24
+ self.add_all_constraints()
25
+
26
+ def create_vars(self):
27
+ for pos in get_all_pos(self.V, self.H):
28
+ self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
29
+
30
+ def add_all_constraints(self):
31
+ for row in range(self.V): # each row must have exactly one tree
32
+ self.model.Add(lxp.sum([self.model_vars[pos] for pos in get_row_pos(row, self.H)]) == 1)
33
+ for col in range(self.H): # each column must have exactly one tree
34
+ self.model.Add(lxp.sum([self.model_vars[pos] for pos in get_col_pos(col, self.V)]) == 1)
35
+ for block_number in self.block_numbers: # each block must have exactly one tree
36
+ self.model.Add(lxp.sum([self.model_vars[pos] for pos in self.blocks[block_number]]) == 1)
37
+ # trees cannot touch even diagonally
38
+ for pos in get_all_pos(self.V, self.H):
39
+ for neighbor in get_neighbors8(pos, self.V, self.H, include_self=False):
40
+ self.model.Add(self.model_vars[neighbor] == 0).OnlyEnforceIf(self.model_vars[pos])
41
+
42
+ def solve_and_print(self, verbose: bool = True):
43
+ def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
44
+ return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
45
+ def callback(single_res: SingleSolution):
46
+ print("Solution found")
47
+ print(combined_function(self.V, self.H, is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1, cell_flags=id_board_to_wall_fn(self.board)))
48
+ return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -0,0 +1,7 @@
1
+ import numpy as np
2
+ from .troix import Board as TroixBoard
3
+
4
+
5
+ class Board(TroixBoard):
6
+ def __init__(self, board: np.array):
7
+ super().__init__(board=board, illegal_run=None)
@@ -0,0 +1,75 @@
1
+ from typing import Optional
2
+
3
+ import numpy as np
4
+ from ortools.sat.python import cp_model
5
+ from ortools.sat.python.cp_model import LinearExpr as lxp
6
+
7
+ from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_char, get_next_pos, get_pos, get_row_pos, get_col_pos, in_bounds
8
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
9
+ from puzzle_solver.core.utils_visualizer import combined_function
10
+
11
+
12
+ class Board:
13
+ def __init__(self, board: np.array, characters: set[str] = None, illegal_run: Optional[int] = 3, wall_char: Optional[str] = None):
14
+ if characters is None:
15
+ characters = set(c.item() for c in np.nditer(board) if c.item() not in [' ', wall_char])
16
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
17
+ assert all(c.item() in ([' ', wall_char] + list(characters)) for c in np.nditer(board)), 'board must contain only space or characters'
18
+ self.board = board
19
+ self.V, self.H = board.shape
20
+ assert self.V % len(characters) == 0, f'board height must be divisible by number of characters, got {self.V} % {len(characters)} = {self.V % len(characters)}'
21
+ assert self.H % len(characters) == 0, f'board width must be divisible by number of characters, got {self.H} % {len(characters)} = {self.H % len(characters)}'
22
+ self.num_repeats_v = self.V // len(characters)
23
+ self.num_repeats_h = self.H // len(characters)
24
+ self.characters = characters
25
+ self.illegal_run = illegal_run
26
+ self.wall_char = wall_char
27
+
28
+ self.model = cp_model.CpModel()
29
+ self.model_vars: dict[Pos, cp_model.IntVar] = {}
30
+ self.create_vars()
31
+ self.add_all_constraints()
32
+
33
+ def create_vars(self):
34
+ for pos in get_all_pos(self.V, self.H):
35
+ if get_char(self.board, pos) == self.wall_char:
36
+ continue
37
+ for char in self.characters:
38
+ self.model_vars[pos, char] = self.model.NewBoolVar(f'{pos}:{char}')
39
+
40
+ def add_all_constraints(self):
41
+ for pos in get_all_pos(self.V, self.H):
42
+ c = get_char(self.board, pos)
43
+ if c == self.wall_char:
44
+ continue
45
+ self.model.AddExactlyOne([self.model_vars[pos, char] for char in self.characters])
46
+ if c in self.characters:
47
+ self.model.Add(self.model_vars[pos, c] == 1)
48
+ for char in self.characters:
49
+ for row in range(self.V):
50
+ self.model.Add(lxp.Sum([self.model_vars[pos, char] for pos in get_row_pos(row, self.H)]) == self.num_repeats_v)
51
+ for col in range(self.H):
52
+ self.model.Add(lxp.Sum([self.model_vars[pos, char] for pos in get_col_pos(col, self.V)]) == self.num_repeats_h)
53
+ if self.illegal_run is not None:
54
+ for pos in get_all_pos(self.V, self.H):
55
+ self.disallow_run_constraint(pos, Direction.RIGHT)
56
+ self.disallow_run_constraint(pos, Direction.DOWN)
57
+
58
+ def disallow_run_constraint(self, pos: Pos, direction: Direction):
59
+ run = [pos]
60
+ while len(run) < self.illegal_run:
61
+ pos = get_next_pos(pos, direction)
62
+ if not in_bounds(pos, self.V, self.H):
63
+ return
64
+ run.append(pos)
65
+ assert len(run) == self.illegal_run, f'SHOULD NOT HAPPEN: run length != max run, {len(run)} != {self.illegal_run}'
66
+ for char in self.characters:
67
+ self.model.Add(lxp.Sum([self.model_vars[p, char] for p in run]) < self.illegal_run)
68
+
69
+ def solve_and_print(self, verbose: bool = True):
70
+ def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
71
+ return SingleSolution(assignment={pos: char for (pos, char), var in board.model_vars.items() if solver.Value(var) == 1})
72
+ def callback(single_res: SingleSolution):
73
+ print("Solution found")
74
+ print(combined_function(self.V, self.H, center_char=lambda r, c: single_res.assignment.get(get_pos(x=c, y=r), self.board[r, c]), text_on_shaded_cells=False))
75
+ return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -0,0 +1,112 @@
1
+ import time
2
+ import numpy as np
3
+ from ortools.sat.python import cp_model
4
+ from ortools.sat.python.cp_model import LinearExpr as lxp
5
+
6
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_next_pos, Direction
7
+
8
+
9
+ class Board:
10
+ def __init__(self, board: np.array, time_horizon: int = 10):
11
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
12
+ assert all(str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only digits'
13
+ self.board = board
14
+ self.target_state = np.sort(board, axis=None).reshape(board.shape)
15
+ self.V, self.H = board.shape
16
+ self.min_value = int(np.min(board.flatten()))
17
+ self.max_value = int(np.max(board.flatten()))
18
+ self.time_horizon = time_horizon
19
+
20
+ self.model = cp_model.CpModel()
21
+ self.state: dict[tuple[Pos, int], cp_model.IntVar] = {}
22
+ self.decision: dict[int, dict[Pos, cp_model.IntVar]] = {t: {} for t in range(self.time_horizon - 1)}
23
+
24
+ self.create_vars()
25
+ self.add_all_constraints()
26
+ self.minimize_actions()
27
+ self.constrain_final_state()
28
+
29
+ def create_vars(self):
30
+ for pos in get_all_pos(self.V, self.H):
31
+ for t in range(self.time_horizon):
32
+ self.state[pos, t] = self.model.NewIntVar(self.min_value, self.max_value, f'state:{pos}:{t}')
33
+ for t in range(self.time_horizon - 1):
34
+ self.decision[t]['NOOP'] = self.model.NewBoolVar(f'decision:NOOP:{t}')
35
+ for pos in get_all_pos(self.V, self.H):
36
+ if pos.x == self.H - 1 or pos.y == self.V - 1:
37
+ continue
38
+ self.decision[t][pos] = self.model.NewBoolVar(f'decision:{pos}:{t}')
39
+
40
+ def add_all_constraints(self):
41
+ # one action at most every time
42
+ for decision_at_t in self.decision.values():
43
+ self.model.AddExactlyOne(list(decision_at_t.values()))
44
+ # constrain the state at t=0
45
+ for pos in get_all_pos(self.V, self.H):
46
+ self.model.Add(self.state[pos, 0] == get_char(self.board, pos))
47
+ # constrain the state dynamics at t=1..T
48
+ for action_pos in get_all_pos(self.V, self.H):
49
+ if action_pos.x == self.H - 1 or action_pos.y == self.V - 1:
50
+ continue
51
+ self.constrain_state(action_pos)
52
+ # state does not change if NOOP is chosen
53
+ for t in range(1, self.time_horizon):
54
+ noop_var = self.decision[t - 1]['NOOP']
55
+ for pos in get_all_pos(self.V, self.H):
56
+ self.model.Add(self.state[pos, t] == self.state[pos, t - 1]).OnlyEnforceIf(noop_var)
57
+
58
+ def constrain_state(self, action: Pos):
59
+ tl = action
60
+ tr = get_next_pos(tl, Direction.RIGHT)
61
+ bl = get_next_pos(tl, Direction.DOWN)
62
+ br = get_next_pos(tr, Direction.DOWN)
63
+ two_by_two = (tl, tr, br, bl)
64
+ # lock state outside the two by two
65
+ for pos in get_all_pos(self.V, self.H):
66
+ if pos in two_by_two:
67
+ continue
68
+ for t in range(1, self.time_horizon):
69
+ self.model.Add(self.state[pos, t] == self.state[pos, t - 1]).OnlyEnforceIf(self.decision[t - 1][action])
70
+ # rotate clockwise inside the two by two
71
+ clockwise = two_by_two[-1:] + two_by_two[:-1]
72
+ # print('action', action)
73
+ # print('two_by_two', two_by_two)
74
+ # print('clockwise', clockwise)
75
+ for pre_pos, post_pos in zip(clockwise, two_by_two):
76
+ for t in range(1, self.time_horizon):
77
+ # print(f'IF self.decision[{t - 1}][{action}] THEN self.state[{post_pos}, {t}] == self.state[{pre_pos}, {t - 1}]')
78
+ self.model.Add(self.state[post_pos, t] == self.state[pre_pos, t - 1]).OnlyEnforceIf(self.decision[t - 1][action])
79
+
80
+ def constrain_final_state(self):
81
+ final_time = self.time_horizon - 1
82
+ for pos in get_all_pos(self.V, self.H):
83
+ self.model.Add(self.state[pos, final_time] == get_char(self.target_state, pos))
84
+
85
+ def minimize_actions(self):
86
+ flat_decisions = [(var, t+1) for t, tvs in self.decision.items() for pos, var in tvs.items() if pos != 'NOOP']
87
+ self.model.Minimize(lxp.weighted_sum([p[0] for p in flat_decisions], [p[1] for p in flat_decisions]))
88
+
89
+ def solve_and_print(self, verbose: bool = True):
90
+ solver = cp_model.CpSolver()
91
+ tic = time.time()
92
+ solver.solve(self.model)
93
+ assignment: dict[Pos] = [None for _ in range(self.time_horizon - 1)]
94
+ if solver.StatusName() in ['OPTIMAL', 'FEASIBLE']:
95
+ for t, tvs in self.decision.items():
96
+ for pos, var in tvs.items():
97
+ if solver.Value(var) == 1:
98
+ assignment[t] = (pos.x, pos.y) if pos != 'NOOP' else 'NOOP'
99
+ for t in range(self.time_horizon):
100
+ res_at_t = np.full((self.V, self.H), ' ', dtype=object)
101
+ for pos in get_all_pos(self.V, self.H):
102
+ res_at_t[pos.y][pos.x] = solver.Value(self.state[pos, t])
103
+ print(f't={t}')
104
+ print(res_at_t)
105
+ if verbose:
106
+ print("Solution found:", assignment)
107
+ if verbose:
108
+ print("status:", solver.StatusName())
109
+ toc = time.time()
110
+ print(f"Time taken: {toc - tic:.2f} seconds")
111
+ return assignment
112
+
@@ -0,0 +1,130 @@
1
+ from typing import Optional
2
+ from enum import Enum
3
+ from dataclasses import dataclass
4
+
5
+ import numpy as np
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_pos, get_next_pos, in_bounds, get_char, Direction
10
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
11
+ from puzzle_solver.core.utils_visualizer import combined_function
12
+
13
+
14
+ class Monster(Enum):
15
+ VAMPIRE = "VAMPIRE"
16
+ ZOMBIE = "ZOMBIE"
17
+ GHOST = "GHOST"
18
+
19
+
20
+ @dataclass
21
+ class SingleBeamResult:
22
+ position: Pos
23
+ reflect_count: int
24
+
25
+
26
+ def can_see(reflect_count: int, monster: Monster) -> bool:
27
+ if monster == Monster.ZOMBIE:
28
+ return True
29
+ elif monster == Monster.VAMPIRE:
30
+ return reflect_count == 0
31
+ elif monster == Monster.GHOST:
32
+ return reflect_count > 0
33
+
34
+
35
+ def beam(board, start_pos: Pos, direction: Direction) -> list[SingleBeamResult]:
36
+ V, H = board.shape
37
+ cur_result: list[SingleBeamResult] = []
38
+ reflect_count = 0
39
+ cur_pos = start_pos
40
+ while True:
41
+ if not in_bounds(cur_pos, V, H):
42
+ break
43
+ cur_pos_char = get_char(board, cur_pos)
44
+ if cur_pos_char == '//':
45
+ direction = {
46
+ Direction.RIGHT: Direction.UP,
47
+ Direction.UP: Direction.RIGHT,
48
+ Direction.DOWN: Direction.LEFT,
49
+ Direction.LEFT: Direction.DOWN
50
+ }[direction]
51
+ reflect_count += 1
52
+ elif cur_pos_char == '\\':
53
+ direction = {
54
+ Direction.RIGHT: Direction.DOWN,
55
+ Direction.DOWN: Direction.RIGHT,
56
+ Direction.UP: Direction.LEFT,
57
+ Direction.LEFT: Direction.UP
58
+ }[direction]
59
+ reflect_count += 1
60
+ else: # not a mirror
61
+ cur_result.append(SingleBeamResult(cur_pos, reflect_count))
62
+ cur_pos = get_next_pos(cur_pos, direction)
63
+ return cur_result
64
+
65
+
66
+ class Board:
67
+ def __init__(self, board: np.array, sides: dict[str, np.array], monster_count: Optional[dict[Monster, int]] = None):
68
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
69
+ assert set(sides.keys()) == set(['right', 'left', 'top', 'bottom'])
70
+ self.board = board
71
+ self.V, self.H = board.shape
72
+ assert sides['top'].shape == (self.H,) and sides['bottom'].shape == (self.H,) and sides['right'].shape == (self.V,) and sides['left'].shape == (self.V,), 'all sides must be equal to board size'
73
+ self.sides = sides
74
+ self.monster_count = monster_count or {}
75
+
76
+ self.model = cp_model.CpModel()
77
+ self.model_vars: dict[tuple[Pos, str], cp_model.IntVar] = {}
78
+ self.create_vars()
79
+ self.add_all_constraints()
80
+
81
+ def create_vars(self):
82
+ for pos in get_all_pos(self.V, self.H):
83
+ for monster in Monster:
84
+ self.model_vars[(pos, monster)] = self.model.NewBoolVar(f"{pos}_is_{monster}")
85
+
86
+ def add_all_constraints(self):
87
+ for pos in get_all_pos(self.V, self.H):
88
+ if get_char(self.board, pos).strip():
89
+ self.model.Add(lxp.Sum([self.model_vars[(pos, monster)] for monster in Monster]) == 0)
90
+ continue
91
+ self.model.AddExactlyOne([self.model_vars[(pos, monster)] for monster in Monster])
92
+ for i, ground in enumerate(self.sides['top']): # top edge
93
+ if ground == -1:
94
+ continue
95
+ beam_result = beam(self.board, get_pos(x=i, y=0), Direction.DOWN)
96
+ self.model.add(self.get_var(beam_result) == ground)
97
+ for i, ground in enumerate(self.sides['left']): # left edge
98
+ if ground == -1:
99
+ continue
100
+ beam_result = beam(self.board, get_pos(x=0, y=i), Direction.RIGHT)
101
+ self.model.add(self.get_var(beam_result) == ground)
102
+ for i, ground in enumerate(self.sides['right']): # right edge
103
+ if ground == -1:
104
+ continue
105
+ beam_result = beam(self.board, get_pos(x=self.H-1, y=i), Direction.LEFT)
106
+ self.model.add(self.get_var(beam_result) == ground)
107
+ for i, ground in enumerate(self.sides['bottom']): # bottom edge
108
+ if ground == -1:
109
+ continue
110
+ beam_result = beam(self.board, get_pos(x=i, y=self.V-1), Direction.UP)
111
+ self.model.add(self.get_var(beam_result) == ground)
112
+ for monster, count in self.monster_count.items():
113
+ self.model.add(lxp.Sum([self.model_vars.get((pos, monster), 0) for pos in get_all_pos(self.V, self.H)]) == count)
114
+
115
+ def get_var(self, path: list[SingleBeamResult]) -> lxp:
116
+ path_vars = []
117
+ for square in path:
118
+ assert get_char(self.board, square.position).strip() == '', f'square {square.position} is not a star position'
119
+ for monster in Monster:
120
+ if can_see(square.reflect_count, monster):
121
+ path_vars.append(self.model_vars[(square.position, monster)])
122
+ return lxp.Sum(path_vars) if path_vars else 0
123
+
124
+ def solve_and_print(self, verbose: bool = True):
125
+ def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
126
+ return SingleSolution(assignment={pos: monster.name[0] for (pos, monster), var in board.model_vars.items() if solver.BooleanValue(var)})
127
+ def callback(single_res: SingleSolution):
128
+ print("Solution found")
129
+ print(combined_function(self.V, self.H, center_char=lambda r, c: single_res.assignment.get(get_pos(x=c, y=r), self.board[r, c].replace('//', '/')).strip()))
130
+ return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)