puzzlekit 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.
Files changed (176) hide show
  1. puzzlekit/.DS_Store +0 -0
  2. puzzlekit/__init__.py +12 -0
  3. puzzlekit/__pycache__/__init__.cpython-310.pyc +0 -0
  4. puzzlekit/core/__init__.py +0 -0
  5. puzzlekit/core/__pycache__/__init__.cpython-310.pyc +0 -0
  6. puzzlekit/core/__pycache__/direction.cpython-310.pyc +0 -0
  7. puzzlekit/core/__pycache__/displayer.cpython-310.pyc +0 -0
  8. puzzlekit/core/__pycache__/grid.cpython-310.pyc +0 -0
  9. puzzlekit/core/__pycache__/position.cpython-310.pyc +0 -0
  10. puzzlekit/core/__pycache__/regionsgrid.cpython-310.pyc +0 -0
  11. puzzlekit/core/__pycache__/result.cpython-310.pyc +0 -0
  12. puzzlekit/core/__pycache__/solver.cpython-310.pyc +0 -0
  13. puzzlekit/core/direction.py +107 -0
  14. puzzlekit/core/grid.py +197 -0
  15. puzzlekit/core/position.py +173 -0
  16. puzzlekit/core/regionsgrid.py +51 -0
  17. puzzlekit/core/result.py +49 -0
  18. puzzlekit/core/solver.py +211 -0
  19. puzzlekit/parsers/__init__.py +4 -0
  20. puzzlekit/parsers/__pycache__/__init__.cpython-310.pyc +0 -0
  21. puzzlekit/parsers/__pycache__/common.cpython-310.pyc +0 -0
  22. puzzlekit/parsers/__pycache__/registry.cpython-310.pyc +0 -0
  23. puzzlekit/parsers/common.py +422 -0
  24. puzzlekit/parsers/registry.py +87 -0
  25. puzzlekit/solvers/__init__.py +169 -0
  26. puzzlekit/solvers/__pycache__/__init__.cpython-310.pyc +0 -0
  27. puzzlekit/solvers/__pycache__/abc_end_view.cpython-310.pyc +0 -0
  28. puzzlekit/solvers/__pycache__/akari.cpython-310.pyc +0 -0
  29. puzzlekit/solvers/__pycache__/balance_loop.cpython-310.pyc +0 -0
  30. puzzlekit/solvers/__pycache__/binairo.cpython-310.pyc +0 -0
  31. puzzlekit/solvers/__pycache__/bosanowa.cpython-310.pyc +0 -0
  32. puzzlekit/solvers/__pycache__/buraitoraito.cpython-310.pyc +0 -0
  33. puzzlekit/solvers/__pycache__/butterfly_sudoku.cpython-310.pyc +0 -0
  34. puzzlekit/solvers/__pycache__/clueless_1_sudoku.cpython-310.pyc +0 -0
  35. puzzlekit/solvers/__pycache__/clueless_2_sudoku.cpython-310.pyc +0 -0
  36. puzzlekit/solvers/__pycache__/country_road.cpython-310.pyc +0 -0
  37. puzzlekit/solvers/__pycache__/detour.cpython-310.pyc +0 -0
  38. puzzlekit/solvers/__pycache__/dominos.cpython-310.pyc +0 -0
  39. puzzlekit/solvers/__pycache__/double_back.cpython-310.pyc +0 -0
  40. puzzlekit/solvers/__pycache__/entry_exit.cpython-310.pyc +0 -0
  41. puzzlekit/solvers/__pycache__/eulero.cpython-310.pyc +0 -0
  42. puzzlekit/solvers/__pycache__/even_odd_sudoku.cpython-310.pyc +0 -0
  43. puzzlekit/solvers/__pycache__/fobidoshi.cpython-310.pyc +0 -0
  44. puzzlekit/solvers/__pycache__/fuzili.cpython-310.pyc +0 -0
  45. puzzlekit/solvers/__pycache__/fuzuli.cpython-310.pyc +0 -0
  46. puzzlekit/solvers/__pycache__/gappy.cpython-310.pyc +0 -0
  47. puzzlekit/solvers/__pycache__/gattai_8_sudoku.cpython-310.pyc +0 -0
  48. puzzlekit/solvers/__pycache__/grand_tour.cpython-310.pyc +0 -0
  49. puzzlekit/solvers/__pycache__/hakyuu.cpython-310.pyc +0 -0
  50. puzzlekit/solvers/__pycache__/heyawake.cpython-310.pyc +0 -0
  51. puzzlekit/solvers/__pycache__/hitori.cpython-310.pyc +0 -0
  52. puzzlekit/solvers/__pycache__/jigsaw_sudoku.cpython-310.pyc +0 -0
  53. puzzlekit/solvers/__pycache__/kakurasu.cpython-310.pyc +0 -0
  54. puzzlekit/solvers/__pycache__/kakuro.cpython-310.pyc +0 -0
  55. puzzlekit/solvers/__pycache__/killer_sudoku.cpython-310.pyc +0 -0
  56. puzzlekit/solvers/__pycache__/kuroshuto.cpython-310.pyc +0 -0
  57. puzzlekit/solvers/__pycache__/linesweeper.cpython-310.pyc +0 -0
  58. puzzlekit/solvers/__pycache__/magnetic.cpython-310.pyc +0 -0
  59. puzzlekit/solvers/__pycache__/masyu.cpython-310.pyc +0 -0
  60. puzzlekit/solvers/__pycache__/minesweeper.cpython-310.pyc +0 -0
  61. puzzlekit/solvers/__pycache__/mosaic.cpython-310.pyc +0 -0
  62. puzzlekit/solvers/__pycache__/munraito.cpython-310.pyc +0 -0
  63. puzzlekit/solvers/__pycache__/nondango.cpython-310.pyc +0 -0
  64. puzzlekit/solvers/__pycache__/nonogram.cpython-310.pyc +0 -0
  65. puzzlekit/solvers/__pycache__/norinori.cpython-310.pyc +0 -0
  66. puzzlekit/solvers/__pycache__/one_to_x.cpython-310.pyc +0 -0
  67. puzzlekit/solvers/__pycache__/patchwork.cpython-310.pyc +0 -0
  68. puzzlekit/solvers/__pycache__/pfeilzahlen.cpython-310.pyc +0 -0
  69. puzzlekit/solvers/__pycache__/pills.cpython-310.pyc +0 -0
  70. puzzlekit/solvers/__pycache__/registry.cpython-310.pyc +0 -0
  71. puzzlekit/solvers/__pycache__/renban.cpython-310.pyc +0 -0
  72. puzzlekit/solvers/__pycache__/samurai_sudoku.cpython-310.pyc +0 -0
  73. puzzlekit/solvers/__pycache__/shikaku.cpython-310.pyc +0 -0
  74. puzzlekit/solvers/__pycache__/shogun_sudoku.cpython-310.pyc +0 -0
  75. puzzlekit/solvers/__pycache__/simple_loop.cpython-310.pyc +0 -0
  76. puzzlekit/solvers/__pycache__/slitherlink.cpython-310.pyc +0 -0
  77. puzzlekit/solvers/__pycache__/sohei_sudoku.cpython-310.pyc +0 -0
  78. puzzlekit/solvers/__pycache__/square_o.cpython-310.pyc +0 -0
  79. puzzlekit/solvers/__pycache__/starbattle.cpython-310.pyc +0 -0
  80. puzzlekit/solvers/__pycache__/str8t.cpython-310.pyc +0 -0
  81. puzzlekit/solvers/__pycache__/sudoku.cpython-310.pyc +0 -0
  82. puzzlekit/solvers/__pycache__/suguru.cpython-310.pyc +0 -0
  83. puzzlekit/solvers/__pycache__/sumo_sudoku.cpython-310.pyc +0 -0
  84. puzzlekit/solvers/__pycache__/tenner_grid.cpython-310.pyc +0 -0
  85. puzzlekit/solvers/__pycache__/tent.cpython-310.pyc +0 -0
  86. puzzlekit/solvers/__pycache__/terra_x.cpython-310.pyc +0 -0
  87. puzzlekit/solvers/__pycache__/thermometer.cpython-310.pyc +0 -0
  88. puzzlekit/solvers/__pycache__/tile_paint.cpython-310.pyc +0 -0
  89. puzzlekit/solvers/__pycache__/windmill_sudoku.cpython-310.pyc +0 -0
  90. puzzlekit/solvers/__pycache__/yajilin.cpython-310.pyc +0 -0
  91. puzzlekit/solvers/abc_end_view.py +145 -0
  92. puzzlekit/solvers/akari.py +96 -0
  93. puzzlekit/solvers/balance_loop.py +184 -0
  94. puzzlekit/solvers/binairo.py +89 -0
  95. puzzlekit/solvers/bosanowa.py +62 -0
  96. puzzlekit/solvers/buraitoraito.py +54 -0
  97. puzzlekit/solvers/butterfly_sudoku.py +68 -0
  98. puzzlekit/solvers/clueless_1_sudoku.py +81 -0
  99. puzzlekit/solvers/clueless_2_sudoku.py +89 -0
  100. puzzlekit/solvers/country_road.py +108 -0
  101. puzzlekit/solvers/detour.py +111 -0
  102. puzzlekit/solvers/dominos.py +84 -0
  103. puzzlekit/solvers/double_back.py +95 -0
  104. puzzlekit/solvers/entry_exit.py +92 -0
  105. puzzlekit/solvers/eulero.py +74 -0
  106. puzzlekit/solvers/even_odd_sudoku.py +72 -0
  107. puzzlekit/solvers/fobidoshi.py +75 -0
  108. puzzlekit/solvers/fuzuli.py +161 -0
  109. puzzlekit/solvers/gappy.py +106 -0
  110. puzzlekit/solvers/gattai_8_sudoku.py +65 -0
  111. puzzlekit/solvers/grand_tour.py +104 -0
  112. puzzlekit/solvers/hakyuu.py +111 -0
  113. puzzlekit/solvers/heyawake.py +125 -0
  114. puzzlekit/solvers/hitori.py +105 -0
  115. puzzlekit/solvers/jigsaw_sudoku.py +59 -0
  116. puzzlekit/solvers/kakurasu.py +53 -0
  117. puzzlekit/solvers/kakuro.py +75 -0
  118. puzzlekit/solvers/killer_sudoku.py +73 -0
  119. puzzlekit/solvers/kuroshuto.py +85 -0
  120. puzzlekit/solvers/linesweeper.py +85 -0
  121. puzzlekit/solvers/magnetic.py +118 -0
  122. puzzlekit/solvers/masyu.py +192 -0
  123. puzzlekit/solvers/minesweeper.py +59 -0
  124. puzzlekit/solvers/mosaic.py +44 -0
  125. puzzlekit/solvers/munraito.py +188 -0
  126. puzzlekit/solvers/nondango.py +75 -0
  127. puzzlekit/solvers/nonogram.py +224 -0
  128. puzzlekit/solvers/norinori.py +55 -0
  129. puzzlekit/solvers/one_to_x.py +69 -0
  130. puzzlekit/solvers/patchwork.py +73 -0
  131. puzzlekit/solvers/pfeilzahlen.py +133 -0
  132. puzzlekit/solvers/pills.py +99 -0
  133. puzzlekit/solvers/renban.py +83 -0
  134. puzzlekit/solvers/samurai_sudoku.py +66 -0
  135. puzzlekit/solvers/shikaku.py +87 -0
  136. puzzlekit/solvers/shogun_sudoku.py +66 -0
  137. puzzlekit/solvers/simple_loop.py +82 -0
  138. puzzlekit/solvers/slitherlink.py +120 -0
  139. puzzlekit/solvers/sohei_sudoku.py +66 -0
  140. puzzlekit/solvers/square_o.py +62 -0
  141. puzzlekit/solvers/starbattle.py +86 -0
  142. puzzlekit/solvers/str8t.py +159 -0
  143. puzzlekit/solvers/sudoku.py +62 -0
  144. puzzlekit/solvers/suguru.py +59 -0
  145. puzzlekit/solvers/sumo_sudoku.py +74 -0
  146. puzzlekit/solvers/tenner_grid.py +60 -0
  147. puzzlekit/solvers/tent.py +70 -0
  148. puzzlekit/solvers/terra_x.py +63 -0
  149. puzzlekit/solvers/thermometer.py +94 -0
  150. puzzlekit/solvers/tile_paint.py +60 -0
  151. puzzlekit/solvers/windmill_sudoku.py +67 -0
  152. puzzlekit/solvers/yajilin.py +126 -0
  153. puzzlekit/utils/__init__.py +0 -0
  154. puzzlekit/utils/__pycache__/__init__.cpython-310.pyc +0 -0
  155. puzzlekit/utils/__pycache__/name_utils.cpython-310.pyc +0 -0
  156. puzzlekit/utils/__pycache__/ortools_utils.cpython-310.pyc +0 -0
  157. puzzlekit/utils/__pycache__/puzzle_math.cpython-310.pyc +0 -0
  158. puzzlekit/utils/file_loader.py +20 -0
  159. puzzlekit/utils/name_utils.py +29 -0
  160. puzzlekit/utils/ortools_utils.py +230 -0
  161. puzzlekit/utils/puzzle_math.py +63 -0
  162. puzzlekit/verifiers/__init__.py +74 -0
  163. puzzlekit/verifiers/__pycache__/__init__.cpython-310.pyc +0 -0
  164. puzzlekit/verifiers/__pycache__/common.cpython-310.pyc +0 -0
  165. puzzlekit/verifiers/common.py +93 -0
  166. puzzlekit/viz/__init__.py +124 -0
  167. puzzlekit/viz/__pycache__/__init__.cpython-310.pyc +0 -0
  168. puzzlekit/viz/__pycache__/base.cpython-310.pyc +0 -0
  169. puzzlekit/viz/__pycache__/drawers.cpython-310.pyc +0 -0
  170. puzzlekit/viz/base.py +209 -0
  171. puzzlekit/viz/drawers.py +263 -0
  172. puzzlekit-0.1.0.dist-info/METADATA +257 -0
  173. puzzlekit-0.1.0.dist-info/RECORD +176 -0
  174. puzzlekit-0.1.0.dist-info/WHEEL +5 -0
  175. puzzlekit-0.1.0.dist-info/licenses/LICENSE +21 -0
  176. puzzlekit-0.1.0.dist-info/top_level.txt +1 -0
puzzlekit/.DS_Store ADDED
Binary file
puzzlekit/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ from typing import Dict, Any
2
+ from puzzlekit.solvers import get_solver_class
3
+
4
+ def solver(puzzle_type: str, data: Dict[str, Any] = None, **kwargs) -> Any:
5
+
6
+ init_params = (data or {}).copy()
7
+ init_params.update(kwargs)
8
+
9
+ SolverClass = get_solver_class(puzzle_type)
10
+ return SolverClass(**init_params)
11
+
12
+ __all__ = ["solver"]
File without changes
@@ -0,0 +1,107 @@
1
+ class Direction:
2
+ def __init__(self, value):
3
+ if isinstance(value, str):
4
+ if value == 'down':
5
+ self._value = Direction._DOWN
6
+ return
7
+ if value == 'right':
8
+ self._value = Direction._RIGHT
9
+ return
10
+ if value == 'up':
11
+ self._value = Direction._UP
12
+ return
13
+ if value == 'left':
14
+ self._value = Direction._LEFT
15
+ return
16
+ else:
17
+ raise ValueError(f"Unknown direction {value}")
18
+ if value not in Direction._orthogonal_directions_values() and value != Direction._NONE:
19
+ raise ValueError(f"Unknown direction {value}")
20
+ self._value = value
21
+
22
+ @staticmethod
23
+ def orthogonals():
24
+ return [Direction.up(), Direction.left(), Direction.down(), Direction.right()]
25
+
26
+ @staticmethod
27
+ def _orthogonal_directions_values():
28
+ return [Direction._UP, Direction._LEFT, Direction._DOWN, Direction._RIGHT]
29
+
30
+ @staticmethod
31
+ def up():
32
+ return Direction(Direction._UP)
33
+
34
+ @staticmethod
35
+ def down():
36
+ return Direction(Direction._DOWN)
37
+
38
+ @staticmethod
39
+ def right():
40
+ return Direction(Direction._RIGHT)
41
+
42
+ @staticmethod
43
+ def left():
44
+ return Direction(Direction._LEFT)
45
+
46
+ @staticmethod
47
+ def none():
48
+ return Direction(Direction._NONE)
49
+
50
+ @property
51
+ def opposite(self):
52
+ if self.value == Direction._DOWN:
53
+ return Direction.up()
54
+ if self.value == Direction._UP:
55
+ return Direction.down()
56
+ if self.value == Direction._RIGHT:
57
+ return Direction.left()
58
+ if self.value == Direction._LEFT:
59
+ return Direction.right()
60
+ return Direction(Direction._NONE)
61
+
62
+ @property
63
+ def value(self):
64
+ return self._value
65
+
66
+ def __str__(self):
67
+ if self._value == Direction._DOWN:
68
+ return 'D'
69
+ if self._value == Direction._RIGHT:
70
+ return 'R'
71
+ if self._value == Direction._UP:
72
+ return 'U'
73
+ if self._value == Direction._LEFT:
74
+ return 'L'
75
+ return 'X'
76
+
77
+ # def __str__(self):
78
+ # if self._value == Direction._DOWN:
79
+ # return '⊓'
80
+ # if self._value == Direction._RIGHT:
81
+ # return '⊏'
82
+ # if self._value == Direction._UP:
83
+ # return '⊔'
84
+ # if self._value == Direction._LEFT:
85
+ # return '⊐'
86
+ # return 'x'
87
+
88
+ def __eq__(self, other):
89
+ if isinstance(other, Direction):
90
+ return self.value == other.value
91
+ if isinstance(other, str):
92
+ return self.__str__() == other
93
+ if isinstance(other, int):
94
+ return self.value == other
95
+ return False
96
+
97
+ def __repr__(self):
98
+ return self.__str__()
99
+
100
+ def __hash__(self):
101
+ return hash(self._value)
102
+
103
+ _NONE = 0
104
+ _DOWN = 1
105
+ _RIGHT = 2
106
+ _UP = 3
107
+ _LEFT = 4
puzzlekit/core/grid.py ADDED
@@ -0,0 +1,197 @@
1
+ from collections import defaultdict
2
+ from typing import Generic, TypeVar, FrozenSet, Generator, Any
3
+ from puzzlekit.core.position import Position
4
+
5
+ T = TypeVar("T")
6
+
7
+ class Grid(Generic[T]):
8
+ def __init__(self, matrix: list[list[T]]):
9
+ self._matrix = matrix if matrix is not None else []
10
+ try:
11
+ self.num_rows = len(self._matrix)
12
+ self.num_cols = len(self._matrix[0]) if self.num_rows > 0 else 0
13
+ except (TypeError, IndexError):
14
+ self.num_cols = 0
15
+ self._walls : set[FrozenSet[Position]] = set()
16
+
17
+ def __getitem__(self, key) -> T:
18
+ if isinstance(key, Position):
19
+ return self._matrix[key.r][key.c]
20
+ if isinstance(key, tuple):
21
+ return self._matrix[key[0]][key[1]]
22
+ return self._matrix[key]
23
+
24
+ def __eq__(self, other):
25
+ if not issubclass(type(other), Grid):
26
+ return False
27
+ if all(isinstance(cell, bool) for cell in self._matrix):
28
+ return all(value == other.value(position) for position, value in self)
29
+ return self.matrix == other.matrix
30
+
31
+ def __hash__(self):
32
+ return hash(str(self._matrix))
33
+
34
+ def __repr__(self) -> str:
35
+ if self.is_empty():
36
+ return "Grid.empty()"
37
+ return "\n".join(" ".join(str(cell) for cell in row) for row in self._matrix)
38
+
39
+ def __contains__(self, item: Position | T) -> bool:
40
+ if item is None:
41
+ return False
42
+ return 0 <= item.r < self.num_rows and 0 <= item.c < self.num_cols
43
+
44
+ def __iter__(self) -> Generator[tuple[Position, T | Any], None, None]:
45
+ for r, row in enumerate(self._matrix):
46
+ for c, cell in enumerate(row):
47
+ yield Position(r, c), cell
48
+
49
+ @property
50
+ def matrix(self):
51
+ return self._matrix
52
+
53
+ @property
54
+ def walls(self):
55
+ return self._walls
56
+
57
+ @staticmethod
58
+ def empty() -> 'Grid':
59
+ return Grid([[]])
60
+
61
+ def is_empty(self):
62
+ return self == Grid.empty()
63
+
64
+ def value(self, r, c = None):
65
+ if isinstance(r, Position):
66
+ # in case directly input Position
67
+ return self._matrix[r.r][r.c]
68
+ return self._matrix[r][c]
69
+
70
+ # def get_regions(self) -> dict[T, frozenset[Position]]:
71
+ # regions = defaultdict(set)
72
+ # for r in range(self.num_rows):
73
+ # for c in range(self.num_cols):
74
+ # if self._matrix[r][c] not in regions:
75
+ # regions[self._matrix[r][c]] = set()
76
+ # regions[self._matrix[r][c]].add(Position(r, c))
77
+ # return {key: frozenset(value) for key, value in regions.items()} if regions else {}
78
+
79
+ def set_value(self, position: Position, value):
80
+ self._matrix[position.r][position.c] = value
81
+
82
+ def get_index_from_position(self, position: Position) -> int:
83
+ return position.r * self.num_cols + position.c
84
+
85
+ def get_position_from_index(self, index: int) -> Position:
86
+ return Position(index // self.num_cols, index % self.num_rows)
87
+
88
+ def _depth_first_search(self, position: Position, value, mode = "orthogonal", visited = None) -> set[Position]:
89
+ if visited is None:
90
+ visited = set()
91
+ if (self.value(position) != value) or (position in visited):
92
+ return visited
93
+ visited.add(position)
94
+ directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] if mode != 'diagonal' else [(1, 1), (1, -1), (-1, 1), (-1, -1)]
95
+ for dr, dc in directions:
96
+ if 0 <= position.r + dr < self.num_rows and 0 <= position.c + dc < self.num_cols and (position.r + dr, position.c + dc) not in visited:
97
+ current_position = position + Position(dr, dc)
98
+ if self.value(current_position) == value:
99
+ new_visited = self._depth_first_search(current_position, value, mode, visited)
100
+ if new_visited != visited:
101
+ return new_visited
102
+ return visited
103
+
104
+ # define neighbor
105
+ def neighbor_up(self, position: Position) -> Position:
106
+ return position.up if position.up in self and {position, position.up} not in self._walls else None
107
+
108
+ def neighbor_down(self, position: Position) -> Position:
109
+ return position.down if position.down in self and {position, position.down} not in self._walls else None
110
+
111
+ def neighbor_left(self, position: Position) -> Position:
112
+ return position.left if position.left in self and {position, position.left} not in self._walls else None
113
+
114
+ def neighbor_right(self, position: Position) -> Position:
115
+ return position.right if position.right in self and {position, position.right} not in self._walls else None
116
+
117
+ def neighbor_right(self, position: Position) -> Position:
118
+ return position.right if position.right in self and {position, position.right} not in self._walls else None
119
+
120
+ def neighbor_up_left(self, position: Position) -> Position:
121
+ return position.up_left if position.up_left in self else None # check if wall is not between position and position.up_left ?
122
+
123
+ def neighbor_up_right(self, position: Position) -> Position:
124
+ return position.up_right if position.up_right in self else None # check if wall is not between position and position.up_right ?
125
+
126
+ def neighbor_down_left(self, position: Position) -> Position:
127
+ return position.down_left if position.down_left in self else None # check if wall is not between position and position.down_left ?
128
+
129
+ def neighbor_down_right(self, position: Position) -> Position:
130
+ return position.down_right if position.down_right in self else None # check if wall is not between position and position.down_right ?
131
+
132
+ def get_neighbors(self, position: Position, mode = "orthogonal"):
133
+ if mode == 'diagonal_only':
134
+ return {self.neighbor_up_left(position), self.neighbor_up_right(position), self.neighbor_down_left(position), self.neighbor_down_right(position)} - {None}
135
+
136
+ orthogonal_neighbors = {self.neighbor_up(position), self.neighbor_down(position), self.neighbor_left(position), self.neighbor_right(position)} - {None}
137
+
138
+ if mode == 'orthogonal':
139
+ return orthogonal_neighbors
140
+
141
+ if mode == 'diagonal' or mode == 'all':
142
+ diagonal_neighbors = {self.neighbor_up_left(position), self.neighbor_up_right(position), self.neighbor_down_left(position), self.neighbor_down_right(position)} - {None}
143
+ return orthogonal_neighbors | diagonal_neighbors
144
+
145
+ raise ValueError(f"Invalid mode: {mode}")
146
+
147
+ def get_line_of_sight(self, position : Position, mode = "orthogonal", end = None):
148
+ directions_orthogonal = [(-1, 0), (1, 0), (0, 1), (0, -1)]
149
+ directions_diagonal = [(-1, -1), (-1, 1), (1, -1), (1, 1)]
150
+ directions = []
151
+ if mode == "orthogonal":
152
+ directions = directions_orthogonal
153
+ elif mode == "diagonal_only":
154
+ directions = directions_diagonal
155
+ elif mode == "diagonal" or mode == "all":
156
+ directions = directions_orthogonal + directions_diagonal
157
+ else:
158
+ raise ValueError(f"Invalid mode: {mode}")
159
+
160
+ line_of_sight = set()
161
+
162
+ if end is None:
163
+ end = set()
164
+ for (x_, y_) in directions:
165
+ i, j = position.r, position.c
166
+ while 0 <= i + x_ < self.num_rows and 0 <= j + y_ < self.num_cols and (i + x_, j + y_) not in end:
167
+ line_of_sight.add(Position(i + x_, j + y_))
168
+ i += x_
169
+ j += y_
170
+
171
+ return line_of_sight - {None}
172
+
173
+
174
+ def is_bijective(self, other):
175
+ if not issubclass(type(other), Grid):
176
+ return False
177
+ mapping_1_to_2 = {}
178
+ mapping_2_to_1 = {}
179
+ r2, c2 = other.num_rows, other.num_cols
180
+ if self.num_rows != r2 or self.num_cols != c2:
181
+ return False
182
+ for i in range(self.num_rows):
183
+ for j in range(self.num_cols):
184
+ elem1 = self.value(i, j)
185
+ elem2 = other.value(i, j)
186
+ if elem1 in mapping_1_to_2:
187
+ if mapping_1_to_2[elem1] != elem2:
188
+ return False
189
+ else:
190
+ mapping_1_to_2[elem1] = elem2
191
+ if elem2 in mapping_2_to_1:
192
+ if mapping_2_to_1[elem2] != elem1:
193
+ return False
194
+ else:
195
+ mapping_2_to_1[elem2] = elem1
196
+ return True
197
+
@@ -0,0 +1,173 @@
1
+ import math
2
+ from puzzlekit.core.direction import Direction
3
+ class Position:
4
+ def __init__(self, r, c):
5
+ self.r = r
6
+ self.c = c
7
+
8
+ def neighbors(self, mode='orthogonal') -> list['Position']:
9
+ if mode == 'orthogonal':
10
+ return [self.up, self.left, self.down, self.right, ]
11
+ if mode == 'diagonal':
12
+ return [self.up, self.up_left, self.left, self.down_left, self.down, self.down_right, self.right, self.up_right]
13
+ raise ValueError(f"Unknown mode {mode}")
14
+
15
+ def direction_to(self, other: 'Position') -> Direction:
16
+ if other is None or self == other:
17
+ return Direction(Direction.none())
18
+ if self.r == other.r:
19
+ if self.c < other.c:
20
+ return Direction.right()
21
+ return Direction.left()
22
+ if self.c == other.c:
23
+ if self.r < other.r:
24
+ return Direction.down()
25
+ return Direction.up()
26
+ return Direction(Direction.none())
27
+
28
+ def direction_from(self, other: 'Position') -> Direction:
29
+ return other.direction_to(self)
30
+
31
+ def distance_to(self, other: 'Position') -> float:
32
+ if self.r == other.r:
33
+ return abs(self.c - other.c)
34
+ if self.c == other.c:
35
+ return abs(self.r - other.r)
36
+ return math.sqrt(math.pow(self.r - other.r, 2) + math.pow(self.c - other.c, 2))
37
+
38
+ def after(self, direction: Direction, count=1) -> 'Position':
39
+ if direction == Direction.down():
40
+ return Position(self.r + count, self.c)
41
+ if direction == Direction.right():
42
+ return Position(self.r, self.c + count)
43
+ if direction == Direction.up():
44
+ return Position(self.r - count, self.c)
45
+ if direction == Direction.left():
46
+ return Position(self.r, self.c - count)
47
+ return self
48
+
49
+ def before(self, direction, count=1) -> 'Position':
50
+ return self.after(direction.opposite, count)
51
+
52
+ @property
53
+ def left(self):
54
+ return Position(self.r, self.c - 1)
55
+
56
+ @property
57
+ def right(self):
58
+ return Position(self.r, self.c + 1)
59
+
60
+ @property
61
+ def up(self):
62
+ return Position(self.r - 1, self.c)
63
+
64
+ @property
65
+ def down(self):
66
+ return Position(self.r + 1, self.c)
67
+
68
+ @property
69
+ def up_left(self):
70
+ return Position(self.r - 1, self.c - 1)
71
+
72
+ @property
73
+ def up_right(self):
74
+ return Position(self.r - 1, self.c + 1)
75
+
76
+ @property
77
+ def down_left(self):
78
+ return Position(self.r + 1, self.c - 1)
79
+
80
+ @property
81
+ def down_right(self):
82
+ return Position(self.r + 1, self.c + 1)
83
+
84
+ def all_positions_between(self, position: 'Position') -> list['Position']:
85
+ if self.r == position.r:
86
+ return [Position(self.r, c) for c in range(min(self.c, position.c) + 1, max(self.c, position.c))]
87
+ if self.c == position.c:
88
+ return [Position(r, self.c) for r in range(min(self.r, position.r) + 1, max(self.r, position.r))]
89
+ return []
90
+
91
+ def all_positions_and_bounds_between(self, position: 'Position') -> list['Position']:
92
+ if self.r == position.r:
93
+ return [Position(self.r, c) for c in range(min(self.c, position.c), max(self.c, position.c) + 1)]
94
+ if self.c == position.c:
95
+ return [Position(r, self.c) for r in range(min(self.r, position.r), max(self.r, position.r) + 1)]
96
+ return []
97
+
98
+ def symmetric(self, position, to_int=True) -> 'Position':
99
+ if to_int:
100
+ return Position(int(2 * position.r - self.r), int(2 * position.c - self.c))
101
+ return Position(2 * position.r - self.r, 2 * position.c - self.c)
102
+
103
+ def is_on_row(self):
104
+ return self.r == math.floor(self.r)
105
+
106
+ def is_on_column(self):
107
+ return self.c == math.floor(self.c)
108
+
109
+ def __floor__(self):
110
+ return Position(math.floor(self.r), math.floor(self.c))
111
+
112
+ def __ceil__(self):
113
+ return Position(math.ceil(self.r), math.ceil(self.c))
114
+
115
+ def __eq__(self, other):
116
+ return isinstance(other, Position) and self.r == other.r and self.c == other.c
117
+
118
+ def __hash__(self):
119
+ return hash((self.r, self.c))
120
+
121
+ def __str__(self):
122
+ return f'({self.r}, {self.c})'
123
+
124
+ def __repr__(self):
125
+ return f'Position{self.__str__()}'
126
+
127
+ def __add__(self, other):
128
+ return Position(self.r + other.r, self.c + other.c)
129
+
130
+ def __sub__(self, other):
131
+ return Position(self.r - other.r, self.c - other.c)
132
+
133
+ def __mul__(self, other):
134
+ return Position(self.r * other, self.c * other)
135
+
136
+ def __truediv__(self, other):
137
+ return Position(self.r / other, self.c / other)
138
+
139
+ def __floordiv__(self, other):
140
+ return Position(self.r // other, self.c // other)
141
+
142
+ def __mod__(self, other):
143
+ return Position(self.r % other, self.c % other)
144
+
145
+ def __lt__(self, other):
146
+ return self.r < other.r or (self.r == other.r and self.c < other.c)
147
+
148
+ def __le__(self, other):
149
+ return self.r <= other.r or (self.r == other.r and self.c <= other.c)
150
+
151
+ def __gt__(self, other):
152
+ return self.r > other.r or (self.r == other.r and self.c > other.c)
153
+
154
+ def __ge__(self, other):
155
+ return self.r >= other.r or (self.r == other.r and self.c >= other.c)
156
+
157
+ def __getitem__(self, item):
158
+ return self.r if item == 0 else self.c
159
+
160
+ def __setitem__(self, key, value):
161
+ if key == 0:
162
+ self.r = value
163
+ else:
164
+ self.c = value
165
+
166
+ def __iter__(self):
167
+ return iter([self.r, self.c])
168
+
169
+ def __neg__(self):
170
+ return Position(-self.r, -self.c)
171
+
172
+ def __len__(self): # get air of rectangle from (0, 0) to self
173
+ return abs(self.r) + abs(self.c) + 1
@@ -0,0 +1,51 @@
1
+ from typing import Generic, TypeVar, FrozenSet, Generator, Any, List, Set
2
+ from puzzlekit.core.grid import Grid
3
+ from puzzlekit.core.position import Position
4
+ from collections import defaultdict
5
+
6
+ T = TypeVar("T")
7
+
8
+ class RegionsGrid(Grid):
9
+ def __init__(self, matrix: list[list[T]]):
10
+ super().__init__(matrix)
11
+ self._matrix = matrix
12
+ self.num_rows = len(matrix)
13
+ self.num_cols = len(matrix[0])
14
+ self._walls : set[FrozenSet[Position]] = set()
15
+ self._helper_grid = Grid(matrix)
16
+ self.regions, self.pos_to_regions, self.region_borders = self._get_regions()
17
+ self.num_regions = len(self.regions) if self.regions else 0
18
+
19
+ @staticmethod
20
+ def from_grid(grid: Grid):
21
+ return RegionsGrid(grid.matrix)
22
+
23
+ def _get_regions(self) -> dict[T, frozenset[Position]]:
24
+ regions = defaultdict(set)
25
+ pos_to_regions = dict()
26
+ region_borders = dict() # record the border of current region
27
+ for r in range(self.num_rows):
28
+ for c in range(self.num_cols):
29
+ curr_region = self._matrix[r][c]
30
+ curr_position = Position(r, c)
31
+ if curr_region not in regions:
32
+ regions[curr_region] = set()
33
+ region_borders[curr_region] = set()
34
+
35
+ regions[curr_region].add(Position(r, c))
36
+ pos_to_regions[r, c] = curr_region
37
+ for nbr in self._helper_grid.get_neighbors(curr_position, "orthogonal"):
38
+ if self._matrix[nbr.r][nbr.c] != curr_region:
39
+ if nbr == curr_position.up:
40
+ region_borders[curr_region].add((nbr, curr_position))
41
+ elif nbr == curr_position.left:
42
+ region_borders[curr_region].add((nbr, curr_position))
43
+ elif nbr == curr_position.right:
44
+ region_borders[curr_region].add((curr_position, nbr))
45
+ elif nbr == curr_position.down:
46
+ region_borders[curr_region].add((curr_position, nbr))
47
+ # if str(curr_region) == "13":
48
+ # print(nbr, (r, c))
49
+ return {key: frozenset(value) for key, value in regions.items()} if regions else {}, \
50
+ pos_to_regions if pos_to_regions else {}, \
51
+ {key: frozenset(value) for key, value in region_borders.items()} if region_borders else {}
@@ -0,0 +1,49 @@
1
+ import time
2
+ from typing import Dict, Any, Optional
3
+ from puzzlekit.core.grid import Grid
4
+ from puzzlekit.viz import visualize
5
+
6
+ class PuzzleResult:
7
+ def __init__(self,
8
+ puzzle_type: str,
9
+ puzzle_data: Dict[str, Any],
10
+ solution_data: Dict[str, Any]):
11
+ self.puzzle_type = puzzle_type
12
+ self.puzzle_data = puzzle_data # Original meta data (clues, rows, etc.)
13
+ self.solution_data = solution_data # Solution data (status, solution_grid, cpu_time, build_time, etc.)
14
+
15
+ # save additional data
16
+ self.sol_grid = self.solution_data.get('solution_grid', None)
17
+
18
+ @property
19
+ def is_solved(self) -> bool:
20
+ return self.solution_data.get("status") in ["Optimal", "Feasible"]
21
+
22
+ def __repr__(self) -> str:
23
+ header = f"[{self.puzzle_type.title()}] \nStatus: {self.solution_data['status']} \nCPU time {self.solution_data['cpu_time']:.4f} s \nBuild time {self.solution_data['build_time']:.4f} s"
24
+ if self.is_solved and self.sol_grid:
25
+ return f"{header}\n{self.sol_grid}"
26
+ return header
27
+
28
+ def _call_visualizer(self, show: bool, save_path: Optional[str], auto_close_sec: float = 0):
29
+ try:
30
+ # print(self.puzzle_data,)
31
+ visualize(
32
+ puzzle_type = self.puzzle_type,
33
+ solution_grid = self.sol_grid,
34
+ puzzle_data = self.puzzle_data,
35
+ title=f"{self.puzzle_type.title()} Result",
36
+ show=show,
37
+ save_path=save_path,
38
+ auto_close_sec=auto_close_sec
39
+ )
40
+ except NotImplementedError:
41
+ print(f"Visualizer for {self.puzzle_type} is not implemented yet.")
42
+ except Exception as e:
43
+ print(f"Visualization failed: {e}")
44
+
45
+ def show(self, auto_close_sec: float = 0, block: bool = True):
46
+ self._call_visualizer(show=True, save_path=None, auto_close_sec=auto_close_sec)
47
+
48
+ def save(self, path: str):
49
+ self._call_visualizer(show=False, save_path=path)