multi-puzzle-solver 0.1.0__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 (31) hide show
  1. multi_puzzle_solver-0.1.0.dist-info/METADATA +1897 -0
  2. multi_puzzle_solver-0.1.0.dist-info/RECORD +31 -0
  3. multi_puzzle_solver-0.1.0.dist-info/WHEEL +5 -0
  4. multi_puzzle_solver-0.1.0.dist-info/top_level.txt +1 -0
  5. puzzle_solver/__init__.py +26 -0
  6. puzzle_solver/core/utils.py +127 -0
  7. puzzle_solver/core/utils_ortools.py +78 -0
  8. puzzle_solver/puzzles/bridges/bridges.py +106 -0
  9. puzzle_solver/puzzles/dominosa/dominosa.py +136 -0
  10. puzzle_solver/puzzles/filling/filling.py +192 -0
  11. puzzle_solver/puzzles/guess/guess.py +231 -0
  12. puzzle_solver/puzzles/inertia/inertia.py +122 -0
  13. puzzle_solver/puzzles/inertia/parse_map/parse_map.py +204 -0
  14. puzzle_solver/puzzles/inertia/tsp.py +398 -0
  15. puzzle_solver/puzzles/keen/keen.py +99 -0
  16. puzzle_solver/puzzles/light_up/light_up.py +95 -0
  17. puzzle_solver/puzzles/magnets/magnets.py +117 -0
  18. puzzle_solver/puzzles/map/map.py +56 -0
  19. puzzle_solver/puzzles/minesweeper/minesweeper.py +110 -0
  20. puzzle_solver/puzzles/mosaic/mosaic.py +48 -0
  21. puzzle_solver/puzzles/nonograms/nonograms.py +126 -0
  22. puzzle_solver/puzzles/pearl/pearl.py +151 -0
  23. puzzle_solver/puzzles/range/range.py +154 -0
  24. puzzle_solver/puzzles/signpost/signpost.py +95 -0
  25. puzzle_solver/puzzles/singles/singles.py +116 -0
  26. puzzle_solver/puzzles/sudoku/sudoku.py +90 -0
  27. puzzle_solver/puzzles/tents/tents.py +110 -0
  28. puzzle_solver/puzzles/towers/towers.py +139 -0
  29. puzzle_solver/puzzles/tracks/tracks.py +170 -0
  30. puzzle_solver/puzzles/undead/undead.py +168 -0
  31. puzzle_solver/puzzles/unruly/unruly.py +86 -0
@@ -0,0 +1,31 @@
1
+ puzzle_solver/__init__.py,sha256=wc05n-PjDpBTNOH7VyTQCz-vFm6ixcRHqa-DoVc-5RA,1599
2
+ puzzle_solver/core/utils.py,sha256=3LlBDuie_G0uSlzibpQS2ULmEYSZmpJXh1kawj7rjkg,3396
3
+ puzzle_solver/core/utils_ortools.py,sha256=d1YAmNuSIx6pf8pTlcbbk6oXQex2_lBpnyW_Hgiip48,2890
4
+ puzzle_solver/puzzles/bridges/bridges.py,sha256=W03M4UWucgdcQ9freO21sZQYiXV5wu8lxovT67HS8Jo,5429
5
+ puzzle_solver/puzzles/dominosa/dominosa.py,sha256=fPNL7KuoPQNjQwamEu3fGN6iTY3NglJFG7JOgliZ0o0,7182
6
+ puzzle_solver/puzzles/filling/filling.py,sha256=FP7Qdbk1Jh1Y259IXXb6WmLUSn2n-aE99BDKJm1GRrg,9079
7
+ puzzle_solver/puzzles/guess/guess.py,sha256=w8ZE-0MswR1_e7_MX622OiCJ8fGTloRHblYFvSA4yHc,10812
8
+ puzzle_solver/puzzles/inertia/inertia.py,sha256=xHtQ09sI7hqPcs20foz6YVMuYCsw59TZQxTumAKJuBs,5658
9
+ puzzle_solver/puzzles/inertia/tsp.py,sha256=qMyT_W5vBmmaFUB7Cl9rC5xJNdAdQvIJEx6yxlbG9hw,15240
10
+ puzzle_solver/puzzles/inertia/parse_map/parse_map.py,sha256=DCV8oI4lAp4CbRVrxeki51t_uJCx1hcCxxpdka1J8AA,8382
11
+ puzzle_solver/puzzles/keen/keen.py,sha256=isojmF9KaRtDgvZFFyIJWDkM8atv51x285iZ_JBn8VQ,4979
12
+ puzzle_solver/puzzles/light_up/light_up.py,sha256=9p0pFyCnXmrprLgCC7FLQ515tnw23KtqTiUSPGHdKUU,4477
13
+ puzzle_solver/puzzles/magnets/magnets.py,sha256=9p6atYUfkK5G8exNukzVDkfjXe557w8eif8DGtgdQVk,6414
14
+ puzzle_solver/puzzles/map/map.py,sha256=vVq0Dtw8a3HMUGMZfXlGX58mKePfJqLpOEqusxeq1V4,2522
15
+ puzzle_solver/puzzles/minesweeper/minesweeper.py,sha256=ztFOiZF6SV1T8E8d3XqW7wCNNsZyFsJxcibZ7chWr0U,5109
16
+ puzzle_solver/puzzles/mosaic/mosaic.py,sha256=KKHk0fLYQfylujKlT0J9TneTV2cKxKqCvBCD3V_DMR4,2111
17
+ puzzle_solver/puzzles/nonograms/nonograms.py,sha256=rcnsSKptyVklJaWc--xNYqcFmviHTiftKU8pksLoY_4,5996
18
+ puzzle_solver/puzzles/pearl/pearl.py,sha256=vyzZ1T_jcd14RgouDfCHj9AH8gStW7OTtI_nTh2cE_o,8506
19
+ puzzle_solver/puzzles/range/range.py,sha256=IuzWuz3TYL4N-7KejI_f1nZWcIN8IVYkgv6wt-FbY9Y,6933
20
+ puzzle_solver/puzzles/signpost/signpost.py,sha256=K7KCV53Uhn21P-dK7aHuDBt-UQzhPQE_gHX3wcXa0cU,3871
21
+ puzzle_solver/puzzles/singles/singles.py,sha256=FFKrXbHGiMgspZW77n_5hFMnfvvDucNg-naj1Hp2JiI,5598
22
+ puzzle_solver/puzzles/sudoku/sudoku.py,sha256=RWSicDOOpUVGdZq39RIwO9vWIfHwKZXV1FuttmxAcwA,3524
23
+ puzzle_solver/puzzles/tents/tents.py,sha256=StGVhX54Lnj7PT1ep4MpBKaPse5a2Ptwq6pHcItI7jY,6219
24
+ puzzle_solver/puzzles/towers/towers.py,sha256=UFy-uhpQNqAp_3XJl7Tpnt5WkSadX0Mc6a1tz3EaKJo,6319
25
+ puzzle_solver/puzzles/tracks/tracks.py,sha256=vO9bryajvnBXgV-1Gd3naS26wgJd-C3A201w3VzNaRs,9025
26
+ puzzle_solver/puzzles/undead/undead.py,sha256=sXAc_mSQ94qnX0XxGyNL4vbTFyBu13fHEtbSXIdclJk,6570
27
+ puzzle_solver/puzzles/unruly/unruly.py,sha256=tMH9saoga9Y2imqxqryuAz0_TKxnOCrFxSenaIOFySA,3767
28
+ multi_puzzle_solver-0.1.0.dist-info/METADATA,sha256=xQnVqwjGEcXBTqWom5lVcyWpHwHevsCZj7TZL2dlJvg,90635
29
+ multi_puzzle_solver-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
+ multi_puzzle_solver-0.1.0.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
31
+ multi_puzzle_solver-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ puzzle_solver
@@ -0,0 +1,26 @@
1
+ from puzzle_solver.puzzles.bridges import bridges as bridges_solver
2
+ from puzzle_solver.puzzles.dominosa import dominosa as dominosa_solver
3
+ from puzzle_solver.puzzles.filling import filling as filling_solver
4
+ from puzzle_solver.puzzles.guess import guess as guess_solver
5
+ from puzzle_solver.puzzles.inertia import inertia as inertia_solver
6
+ from puzzle_solver.puzzles.keen import keen as keen_solver
7
+ from puzzle_solver.puzzles.light_up import light_up as light_up_solver
8
+ from puzzle_solver.puzzles.magnets import magnets as magnets_solver
9
+ from puzzle_solver.puzzles.map import map as map_solver
10
+ from puzzle_solver.puzzles.minesweeper import minesweeper as minesweeper_solver
11
+ from puzzle_solver.puzzles.mosaic import mosaic as mosaic_solver
12
+ from puzzle_solver.puzzles.nonograms import nonograms as nonograms_solver
13
+ from puzzle_solver.puzzles.pearl import pearl as pearl_solver
14
+ from puzzle_solver.puzzles.range import range as range_solver
15
+ from puzzle_solver.puzzles.signpost import signpost as signpost_solver
16
+ from puzzle_solver.puzzles.singles import singles as singles_solver
17
+ from puzzle_solver.puzzles.sudoku import sudoku as sudoku_solver
18
+ from puzzle_solver.puzzles.tents import tents as tents_solver
19
+ from puzzle_solver.puzzles.towers import towers as towers_solver
20
+ from puzzle_solver.puzzles.tracks import tracks as tracks_solver
21
+ from puzzle_solver.puzzles.undead import undead as undead_solver
22
+ from puzzle_solver.puzzles.unruly import unruly as unruly_solver
23
+
24
+ from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
25
+
26
+ __version__ = '0.1.0'
@@ -0,0 +1,127 @@
1
+ from dataclasses import dataclass
2
+ from typing import Tuple, Iterable, Union
3
+ from enum import Enum
4
+
5
+ import numpy as np
6
+
7
+
8
+ class Direction(Enum):
9
+ UP = 1
10
+ DOWN = 2
11
+ LEFT = 3
12
+ RIGHT = 4
13
+
14
+ class Direction8(Enum):
15
+ UP = 1
16
+ DOWN = 2
17
+ LEFT = 3
18
+ RIGHT = 4
19
+ UP_LEFT = 5
20
+ UP_RIGHT = 6
21
+ DOWN_LEFT = 7
22
+ DOWN_RIGHT = 8
23
+
24
+ @dataclass(frozen=True)
25
+ class Pos:
26
+ x: int
27
+ y: int
28
+
29
+
30
+ def get_pos(x: int, y: int) -> Pos:
31
+ return Pos(x=x, y=y)
32
+
33
+
34
+ def get_next_pos(cur_pos: Pos, direction: Union[Direction, Direction8]) -> Pos:
35
+ delta_x, delta_y = get_deltas(direction)
36
+ return get_pos(cur_pos.x+delta_x, cur_pos.y+delta_y)
37
+
38
+
39
+ def get_neighbors4(pos: Pos, V: int, H: int) -> Iterable[Pos]:
40
+ for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)):
41
+ p2 = get_pos(x=pos.x+dx, y=pos.y+dy)
42
+ if in_bounds(p2, V, H):
43
+ yield p2
44
+
45
+
46
+ def get_neighbors8(pos: Pos, V: int, H: int = None, include_self: bool = False) -> Iterable[Pos]:
47
+ if H is None:
48
+ H = V
49
+ for dx in [-1, 0, 1]:
50
+ for dy in [-1, 0, 1]:
51
+ if not include_self and (dx, dy) == (0, 0):
52
+ continue
53
+ d_pos = get_pos(x=pos.x+dx, y=pos.y+dy)
54
+ if in_bounds(d_pos, V, H):
55
+ yield d_pos
56
+
57
+
58
+ def get_row_pos(row_idx: int, H: int) -> Iterable[Pos]:
59
+ for x in range(H):
60
+ yield get_pos(x=x, y=row_idx)
61
+
62
+
63
+ def get_col_pos(col_idx: int, V: int) -> Iterable[Pos]:
64
+ for y in range(V):
65
+ yield get_pos(x=col_idx, y=y)
66
+
67
+
68
+ def get_all_pos(V, H=None):
69
+ if H is None:
70
+ H = V
71
+ for y in range(V):
72
+ for x in range(H):
73
+ yield get_pos(x=x, y=y)
74
+
75
+
76
+ def get_all_pos_to_idx_dict(V, H=None) -> dict[Pos, int]:
77
+ if H is None:
78
+ H = V
79
+ return {get_pos(x=x, y=y): y*H+x for y in range(V) for x in range(H)}
80
+
81
+
82
+ def get_char(board: np.array, pos: Pos) -> str:
83
+ return board[pos.y][pos.x]
84
+
85
+
86
+ def set_char(board: np.array, pos: Pos, char: str):
87
+ board[pos.y][pos.x] = char
88
+
89
+
90
+ def in_bounds(pos: Pos, V: int, H: int = None) -> bool:
91
+ if H is None:
92
+ H = V
93
+ return 0 <= pos.y < V and 0 <= pos.x < H
94
+
95
+
96
+ def get_opposite_direction(direction: Direction) -> Direction:
97
+ if direction == Direction.RIGHT:
98
+ return Direction.LEFT
99
+ elif direction == Direction.LEFT:
100
+ return Direction.RIGHT
101
+ elif direction == Direction.DOWN:
102
+ return Direction.UP
103
+ elif direction == Direction.UP:
104
+ return Direction.DOWN
105
+ else:
106
+ raise ValueError(f'invalid direction: {direction}')
107
+
108
+
109
+ def get_deltas(direction: Union[Direction, Direction8]) -> Tuple[int, int]:
110
+ if direction == Direction.RIGHT or direction == Direction8.RIGHT:
111
+ return +1, 0
112
+ elif direction == Direction.LEFT or direction == Direction8.LEFT:
113
+ return -1, 0
114
+ elif direction == Direction.DOWN or direction == Direction8.DOWN:
115
+ return 0, +1
116
+ elif direction == Direction.UP or direction == Direction8.UP:
117
+ return 0, -1
118
+ elif direction == Direction8.UP_LEFT:
119
+ return -1, -1
120
+ elif direction == Direction8.UP_RIGHT:
121
+ return +1, -1
122
+ elif direction == Direction8.DOWN_LEFT:
123
+ return -1, +1
124
+ elif direction == Direction8.DOWN_RIGHT:
125
+ return +1, +1
126
+ else:
127
+ raise ValueError(f'invalid direction: {direction}')
@@ -0,0 +1,78 @@
1
+ import time
2
+ import json
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Callable, Any, Union
5
+
6
+ from ortools.sat.python import cp_model
7
+ from ortools.sat.python.cp_model import CpSolverSolutionCallback
8
+
9
+ from puzzle_solver.core.utils import Pos
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class SingleSolution:
14
+ assignment: dict[Pos, Union[str, int]]
15
+
16
+ def get_hashable_solution(self) -> str:
17
+ result = []
18
+ for pos, v in self.assignment.items():
19
+ result.append((pos.x, pos.y, v))
20
+ return json.dumps(result, sort_keys=True)
21
+
22
+
23
+ def and_constraint(model: cp_model.CpModel, target: cp_model.IntVar, cs: list[cp_model.IntVar]):
24
+ for c in cs:
25
+ model.Add(target <= c)
26
+ model.Add(target >= sum(cs) - len(cs) + 1)
27
+
28
+
29
+ def or_constraint(model: cp_model.CpModel, target: cp_model.IntVar, cs: list[cp_model.IntVar]):
30
+ for c in cs:
31
+ model.Add(target >= c)
32
+ model.Add(target <= sum(cs))
33
+
34
+
35
+
36
+ class AllSolutionsCollector(CpSolverSolutionCallback):
37
+ def __init__(self,
38
+ board: Any,
39
+ board_to_solution: Callable[Any, SingleSolution],
40
+ max_solutions: Optional[int] = None,
41
+ callback: Optional[Callable[SingleSolution, None]] = None
42
+ ):
43
+ super().__init__()
44
+ self.board = board
45
+ self.board_to_solution = board_to_solution
46
+ self.max_solutions = max_solutions
47
+ self.callback = callback
48
+ self.solutions = []
49
+ self.unique_solutions = set()
50
+
51
+ def on_solution_callback(self):
52
+ try:
53
+ result = self.board_to_solution(self.board, self)
54
+ result_json = result.get_hashable_solution()
55
+ if result_json in self.unique_solutions:
56
+ return
57
+ self.unique_solutions.add(result_json)
58
+ self.solutions.append(result)
59
+ if self.callback is not None:
60
+ self.callback(result)
61
+ if self.max_solutions is not None and len(self.solutions) >= self.max_solutions:
62
+ self.StopSearch()
63
+ except Exception as e:
64
+ print(e)
65
+ raise e
66
+
67
+ def generic_solve_all(board: Any, board_to_solution: Callable[Any, SingleSolution], max_solutions: Optional[int] = None, callback: Optional[Callable[[SingleSolution], None]] = None, verbose: bool = True) -> list[SingleSolution]:
68
+ solver = cp_model.CpSolver()
69
+ solver.parameters.enumerate_all_solutions = True
70
+ collector = AllSolutionsCollector(board, board_to_solution, max_solutions=max_solutions, callback=callback)
71
+ tic = time.time()
72
+ solver.solve(board.model, collector)
73
+ if verbose:
74
+ print("Solutions found:", len(collector.solutions))
75
+ print("status:", solver.StatusName())
76
+ toc = time.time()
77
+ print(f"Time taken: {toc - tic:.2f} seconds")
78
+ return collector.solutions
@@ -0,0 +1,106 @@
1
+ import json
2
+ from collections import defaultdict
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_char, set_char, get_neighbors8, get_next_pos, Direction, get_row_pos, get_col_pos
10
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
11
+
12
+
13
+ class Board:
14
+ def __init__(self, board: np.array, max_bridges_per_direction: int = 2):
15
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
16
+ assert all(c.item() == ' ' or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only spaces or digits'
17
+ self.board = board
18
+ self.V = board.shape[0]
19
+ self.H = board.shape[1]
20
+ self.max_bridges_per_direction = max_bridges_per_direction
21
+ self.horiz_bridges: set[tuple[Pos, Pos]] = set()
22
+ self.vert_bridges: set[tuple[Pos, Pos]] = set()
23
+
24
+ self.model = cp_model.CpModel()
25
+ self.model_vars: dict[tuple[Pos, Pos], cp_model.IntVar] = {}
26
+ self.is_bridge_active: dict[tuple[Pos, Pos], cp_model.IntVar] = {}
27
+
28
+ self.init_bridges()
29
+ self.create_vars()
30
+ self.add_all_constraints()
31
+
32
+ def init_bridges(self):
33
+ for row_i in range(self.V):
34
+ cells_in_row = [i for i in get_row_pos(row_i, self.H) if get_char(self.board, i) != ' ']
35
+ for cell_i in range(len(cells_in_row) - 1):
36
+ self.horiz_bridges.add((cells_in_row[cell_i], cells_in_row[cell_i + 1]))
37
+ for col_i in range(self.H):
38
+ cells_in_col = [i for i in get_col_pos(col_i, self.V) if get_char(self.board, i) != ' ']
39
+ for cell_i in range(len(cells_in_col) - 1):
40
+ self.vert_bridges.add((cells_in_col[cell_i], cells_in_col[cell_i + 1]))
41
+
42
+ def create_vars(self):
43
+ for bridge in self.horiz_bridges | self.vert_bridges:
44
+ self.model_vars[bridge] = self.model.NewIntVar(0, self.max_bridges_per_direction, f'{bridge}')
45
+ self.is_bridge_active[bridge] = self.model.NewBoolVar(f'{bridge}:is_active')
46
+ self.model.Add(self.model_vars[bridge] == 0).OnlyEnforceIf(self.is_bridge_active[bridge].Not())
47
+ self.model.Add(self.model_vars[bridge] > 0).OnlyEnforceIf(self.is_bridge_active[bridge])
48
+
49
+ def add_all_constraints(self):
50
+ self.constrain_sums()
51
+ self.constrain_no_overlapping_bridges()
52
+
53
+ def constrain_sums(self):
54
+ for pos in get_all_pos(self.V, self.H):
55
+ c = get_char(self.board, pos)
56
+ if c == ' ':
57
+ continue
58
+ all_pos_bridges = [bridge for bridge in self.horiz_bridges if pos in bridge] + [bridge for bridge in self.vert_bridges if pos in bridge]
59
+ self.model.Add(lxp.sum([self.model_vars[bridge] for bridge in all_pos_bridges]) == int(c))
60
+
61
+ def constrain_no_overlapping_bridges(self):
62
+ for horiz_bridge in self.horiz_bridges:
63
+ for vert_bridge in self.vert_bridges:
64
+ if self.is_overlapping(horiz_bridge, vert_bridge):
65
+ self.model.AddImplication(self.is_bridge_active[horiz_bridge], self.is_bridge_active[vert_bridge].Not())
66
+ self.model.AddImplication(self.is_bridge_active[vert_bridge], self.is_bridge_active[horiz_bridge].Not())
67
+
68
+ def is_overlapping(self, horiz_bridge: tuple[Pos, Pos], vert_bridge: tuple[Pos, Pos]) -> bool:
69
+ assert vert_bridge[0].x == vert_bridge[1].x, 'vertical bridge must have constant x'
70
+ assert horiz_bridge[0].y == horiz_bridge[1].y, 'horizontal bridge must have constant y'
71
+ xvert = vert_bridge[0].x
72
+ yvert_min = min(vert_bridge[0].y, vert_bridge[1].y)
73
+ yvert_max = max(vert_bridge[0].y, vert_bridge[1].y)
74
+
75
+ xhoriz_min = min(horiz_bridge[0].x, horiz_bridge[1].x)
76
+ xhoriz_max = max(horiz_bridge[0].x, horiz_bridge[1].x)
77
+ yhoriz = horiz_bridge[0].y
78
+
79
+ # no equals because thats what the puzzle says
80
+ x_contained = xhoriz_min < xvert < xhoriz_max
81
+ y_contained = yvert_min < yhoriz < yvert_max
82
+ return x_contained and y_contained
83
+
84
+ def solve_and_print(self):
85
+ def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
86
+ assignment = defaultdict(lambda: [0, 0, 0, 0])
87
+ for bridge in board.horiz_bridges:
88
+ v = solver.Value(board.model_vars[bridge])
89
+ assignment[bridge[0]][0] += v
90
+ assignment[bridge[1]][1] += v
91
+ for bridge in board.vert_bridges:
92
+ v = solver.Value(board.model_vars[bridge])
93
+ assignment[bridge[0]][2] += v
94
+ assignment[bridge[1]][3] += v
95
+ # convert to tuples
96
+ assignment = {pos: tuple(assignment[pos]) for pos in assignment.keys()}
97
+ return SingleSolution(assignment=assignment)
98
+ def callback(single_res: SingleSolution):
99
+ print("Solution found")
100
+ res = np.full((self.V, self.H), ' ', dtype=object)
101
+ for pos, (h1, h2, v1, v2) in single_res.assignment.items():
102
+ c = str(h1) + str(h2) + str(v1) + str(v2)
103
+ set_char(res, pos, c)
104
+ for row in res:
105
+ print('|' + '|'.join(row) + '|\n')
106
+ return generic_solve_all(self, board_to_solution, callback=callback, max_solutions=20)
@@ -0,0 +1,136 @@
1
+ from collections import defaultdict
2
+
3
+ import numpy as np
4
+ from ortools.sat.python import cp_model
5
+
6
+ from puzzle_solver.core.utils import Pos, get_pos, get_all_pos, get_char, set_char, get_row_pos, get_col_pos, Direction, get_next_pos, in_bounds
7
+ from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, SingleSolution, or_constraint
8
+
9
+
10
+ def get_bool_var(model: cp_model.CpModel, var: cp_model.IntVar, eq_val: int, name: str) -> cp_model.IntVar:
11
+ res = model.NewBoolVar(name)
12
+ model.Add(var == eq_val).OnlyEnforceIf(res)
13
+ model.Add(var != eq_val).OnlyEnforceIf(res.Not())
14
+ return res
15
+
16
+
17
+ class Board:
18
+ def __init__(self, board: np.array):
19
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
20
+ assert all(0 <= int(i.item()) <= 9 for i in np.nditer(board)), 'board must contain only alphanumeric characters or *'
21
+ assert np.min(board) == 0, 'expected board to start from 0'
22
+ self.board = board
23
+ self.max_val = np.max(board)
24
+
25
+ self.V = board.shape[0]
26
+ self.H = board.shape[1]
27
+
28
+ # for every pair, list where to find it on the board and which direction to go
29
+ # no need for left and up directions as the pairs are unordered (so right and down would have already captured both)
30
+ self.pair_to_pos_list: dict[tuple[int, int], list[tuple[Pos, Direction]]] = defaultdict(list)
31
+ for pos in get_all_pos(self.V, self.H):
32
+ right_pos = get_next_pos(pos, Direction.RIGHT)
33
+ if in_bounds(right_pos, self.V, self.H):
34
+ cur_pair = (int(get_char(self.board, pos)), int(get_char(self.board, right_pos)))
35
+ cur_pair = tuple(sorted(cur_pair)) # pairs are unordered
36
+ self.pair_to_pos_list[cur_pair].append((pos, Direction.RIGHT))
37
+ down_pos = get_next_pos(pos, Direction.DOWN)
38
+ if in_bounds(down_pos, self.V, self.H):
39
+ cur_pair = (int(get_char(self.board, pos)), int(get_char(self.board, down_pos)))
40
+ cur_pair = tuple(sorted(cur_pair)) # pairs are unordered
41
+ self.pair_to_pos_list[cur_pair].append((pos, Direction.DOWN))
42
+
43
+ self.model = cp_model.CpModel()
44
+ self.model_vars: dict[Pos, cp_model.IntVar] = {}
45
+ self.pair_vars: dict[tuple[int, int], cp_model.BoolVar] = {}
46
+
47
+ self.create_vars()
48
+ self.add_all_constraints()
49
+
50
+ def create_vars(self):
51
+ for pos in get_all_pos(self.V, self.H):
52
+ self.model_vars[pos] = self.model.NewIntVar(1, 4, f'{pos}') # directions
53
+ # for even pair, boolean variable to indicate if it appears
54
+ for i in range(self.max_val + 1):
55
+ for j in range(i, self.max_val + 1):
56
+ if (i, j) in self.pair_vars:
57
+ print('already in pair_vars')
58
+ continue
59
+ self.pair_vars[(i, j)] = self.model.NewBoolVar(f'{i}_{j}')
60
+
61
+ def add_all_constraints(self):
62
+ # all pairs must be used
63
+ self.all_pairs_used()
64
+ self.constrain_domino_shape()
65
+ self.constrain_pair_activation()
66
+
67
+
68
+ # for pos in get_all_pos(self.V, self.H):
69
+ # # to the right
70
+ # right_pos = get_next_pos(pos, Direction.RIGHT)
71
+ # if not in_bounds(right_pos, self.V, self.H):
72
+ # continue
73
+ # v = self.model.NewBoolVar(f'{pos}:right')
74
+ # and_constraint(self.model, v, [self.model_vars[pos] == Direction.RIGHT.value, self.model_vars[right_pos] == Direction.LEFT.value])
75
+
76
+ def all_pairs_used(self):
77
+ for pair in self.pair_vars:
78
+ self.model.Add(self.pair_vars[pair] == 1)
79
+
80
+ def constrain_domino_shape(self):
81
+ # if X is right then the cell to its right must be left
82
+ # if X is down then the cell to its down must be up
83
+ # if X is left then the cell to its left must be right
84
+ # if X is up then the cell to its up must be down
85
+ for pos in get_all_pos(self.V, self.H):
86
+ right_pos = get_next_pos(pos, Direction.RIGHT)
87
+ if in_bounds(right_pos, self.V, self.H):
88
+ aux = get_bool_var(self.model, self.model_vars[right_pos], Direction.LEFT.value, f'{pos}:right')
89
+ self.model.Add(self.model_vars[pos] == Direction.RIGHT.value).OnlyEnforceIf([aux])
90
+ self.model.Add(self.model_vars[pos] != Direction.RIGHT.value).OnlyEnforceIf([aux.Not()])
91
+ else:
92
+ self.model.Add(self.model_vars[pos] != Direction.RIGHT.value)
93
+ down_pos = get_next_pos(pos, Direction.DOWN)
94
+ if in_bounds(down_pos, self.V, self.H):
95
+ aux = get_bool_var(self.model, self.model_vars[down_pos], Direction.UP.value, f'{pos}:down')
96
+ self.model.Add(self.model_vars[pos] == Direction.DOWN.value).OnlyEnforceIf([aux])
97
+ self.model.Add(self.model_vars[pos] != Direction.DOWN.value).OnlyEnforceIf([aux.Not()])
98
+ else:
99
+ self.model.Add(self.model_vars[pos] != Direction.DOWN.value)
100
+ left_pos = get_next_pos(pos, Direction.LEFT)
101
+ if in_bounds(left_pos, self.V, self.H):
102
+ aux = get_bool_var(self.model, self.model_vars[left_pos], Direction.RIGHT.value, f'{pos}:left')
103
+ self.model.Add(self.model_vars[pos] == Direction.LEFT.value).OnlyEnforceIf([aux])
104
+ self.model.Add(self.model_vars[pos] != Direction.LEFT.value).OnlyEnforceIf([aux.Not()])
105
+ else:
106
+ self.model.Add(self.model_vars[pos] != Direction.LEFT.value)
107
+ top_pos = get_next_pos(pos, Direction.UP)
108
+ if in_bounds(top_pos, self.V, self.H):
109
+ aux = get_bool_var(self.model, self.model_vars[top_pos], Direction.DOWN.value, f'{pos}:top')
110
+ self.model.Add(self.model_vars[pos] == Direction.UP.value).OnlyEnforceIf([aux])
111
+ self.model.Add(self.model_vars[pos] != Direction.UP.value).OnlyEnforceIf([aux.Not()])
112
+ else:
113
+ self.model.Add(self.model_vars[pos] != Direction.UP.value)
114
+
115
+ def constrain_pair_activation(self):
116
+ for pair, pos_list in self.pair_to_pos_list.items():
117
+ aux_list = []
118
+ for pos, direction in pos_list:
119
+ aux_list.append(get_bool_var(self.model, self.model_vars[pos], direction.value, f'{pos}:{direction.name}'))
120
+ or_constraint(self.model, self.pair_vars[pair], aux_list)
121
+
122
+ def solve_and_print(self):
123
+ def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
124
+ assignment: dict[Pos, int] = {}
125
+ for pos, var in board.model_vars.items():
126
+ assignment[pos] = solver.value(var)
127
+ return SingleSolution(assignment=assignment)
128
+ def callback(single_res: SingleSolution):
129
+ print("Solution found")
130
+ res = np.full((self.V, self.H), ' ', dtype=object)
131
+ for pos in get_all_pos(self.V, self.H):
132
+ c = get_char(self.board, pos)
133
+ c = Direction(single_res.assignment[pos]).name[:1]
134
+ set_char(res, pos, c)
135
+ print(res)
136
+ return generic_solve_all(self, board_to_solution, callback=callback)