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.
- puzzlekit/.DS_Store +0 -0
- puzzlekit/__init__.py +12 -0
- puzzlekit/__pycache__/__init__.cpython-310.pyc +0 -0
- puzzlekit/core/__init__.py +0 -0
- puzzlekit/core/__pycache__/__init__.cpython-310.pyc +0 -0
- puzzlekit/core/__pycache__/direction.cpython-310.pyc +0 -0
- puzzlekit/core/__pycache__/displayer.cpython-310.pyc +0 -0
- puzzlekit/core/__pycache__/grid.cpython-310.pyc +0 -0
- puzzlekit/core/__pycache__/position.cpython-310.pyc +0 -0
- puzzlekit/core/__pycache__/regionsgrid.cpython-310.pyc +0 -0
- puzzlekit/core/__pycache__/result.cpython-310.pyc +0 -0
- puzzlekit/core/__pycache__/solver.cpython-310.pyc +0 -0
- puzzlekit/core/direction.py +107 -0
- puzzlekit/core/grid.py +197 -0
- puzzlekit/core/position.py +173 -0
- puzzlekit/core/regionsgrid.py +51 -0
- puzzlekit/core/result.py +49 -0
- puzzlekit/core/solver.py +211 -0
- puzzlekit/parsers/__init__.py +4 -0
- puzzlekit/parsers/__pycache__/__init__.cpython-310.pyc +0 -0
- puzzlekit/parsers/__pycache__/common.cpython-310.pyc +0 -0
- puzzlekit/parsers/__pycache__/registry.cpython-310.pyc +0 -0
- puzzlekit/parsers/common.py +422 -0
- puzzlekit/parsers/registry.py +87 -0
- puzzlekit/solvers/__init__.py +169 -0
- puzzlekit/solvers/__pycache__/__init__.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/abc_end_view.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/akari.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/balance_loop.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/binairo.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/bosanowa.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/buraitoraito.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/butterfly_sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/clueless_1_sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/clueless_2_sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/country_road.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/detour.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/dominos.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/double_back.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/entry_exit.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/eulero.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/even_odd_sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/fobidoshi.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/fuzili.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/fuzuli.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/gappy.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/gattai_8_sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/grand_tour.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/hakyuu.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/heyawake.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/hitori.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/jigsaw_sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/kakurasu.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/kakuro.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/killer_sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/kuroshuto.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/linesweeper.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/magnetic.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/masyu.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/minesweeper.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/mosaic.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/munraito.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/nondango.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/nonogram.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/norinori.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/one_to_x.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/patchwork.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/pfeilzahlen.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/pills.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/registry.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/renban.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/samurai_sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/shikaku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/shogun_sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/simple_loop.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/slitherlink.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/sohei_sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/square_o.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/starbattle.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/str8t.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/suguru.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/sumo_sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/tenner_grid.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/tent.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/terra_x.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/thermometer.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/tile_paint.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/windmill_sudoku.cpython-310.pyc +0 -0
- puzzlekit/solvers/__pycache__/yajilin.cpython-310.pyc +0 -0
- puzzlekit/solvers/abc_end_view.py +145 -0
- puzzlekit/solvers/akari.py +96 -0
- puzzlekit/solvers/balance_loop.py +184 -0
- puzzlekit/solvers/binairo.py +89 -0
- puzzlekit/solvers/bosanowa.py +62 -0
- puzzlekit/solvers/buraitoraito.py +54 -0
- puzzlekit/solvers/butterfly_sudoku.py +68 -0
- puzzlekit/solvers/clueless_1_sudoku.py +81 -0
- puzzlekit/solvers/clueless_2_sudoku.py +89 -0
- puzzlekit/solvers/country_road.py +108 -0
- puzzlekit/solvers/detour.py +111 -0
- puzzlekit/solvers/dominos.py +84 -0
- puzzlekit/solvers/double_back.py +95 -0
- puzzlekit/solvers/entry_exit.py +92 -0
- puzzlekit/solvers/eulero.py +74 -0
- puzzlekit/solvers/even_odd_sudoku.py +72 -0
- puzzlekit/solvers/fobidoshi.py +75 -0
- puzzlekit/solvers/fuzuli.py +161 -0
- puzzlekit/solvers/gappy.py +106 -0
- puzzlekit/solvers/gattai_8_sudoku.py +65 -0
- puzzlekit/solvers/grand_tour.py +104 -0
- puzzlekit/solvers/hakyuu.py +111 -0
- puzzlekit/solvers/heyawake.py +125 -0
- puzzlekit/solvers/hitori.py +105 -0
- puzzlekit/solvers/jigsaw_sudoku.py +59 -0
- puzzlekit/solvers/kakurasu.py +53 -0
- puzzlekit/solvers/kakuro.py +75 -0
- puzzlekit/solvers/killer_sudoku.py +73 -0
- puzzlekit/solvers/kuroshuto.py +85 -0
- puzzlekit/solvers/linesweeper.py +85 -0
- puzzlekit/solvers/magnetic.py +118 -0
- puzzlekit/solvers/masyu.py +192 -0
- puzzlekit/solvers/minesweeper.py +59 -0
- puzzlekit/solvers/mosaic.py +44 -0
- puzzlekit/solvers/munraito.py +188 -0
- puzzlekit/solvers/nondango.py +75 -0
- puzzlekit/solvers/nonogram.py +224 -0
- puzzlekit/solvers/norinori.py +55 -0
- puzzlekit/solvers/one_to_x.py +69 -0
- puzzlekit/solvers/patchwork.py +73 -0
- puzzlekit/solvers/pfeilzahlen.py +133 -0
- puzzlekit/solvers/pills.py +99 -0
- puzzlekit/solvers/renban.py +83 -0
- puzzlekit/solvers/samurai_sudoku.py +66 -0
- puzzlekit/solvers/shikaku.py +87 -0
- puzzlekit/solvers/shogun_sudoku.py +66 -0
- puzzlekit/solvers/simple_loop.py +82 -0
- puzzlekit/solvers/slitherlink.py +120 -0
- puzzlekit/solvers/sohei_sudoku.py +66 -0
- puzzlekit/solvers/square_o.py +62 -0
- puzzlekit/solvers/starbattle.py +86 -0
- puzzlekit/solvers/str8t.py +159 -0
- puzzlekit/solvers/sudoku.py +62 -0
- puzzlekit/solvers/suguru.py +59 -0
- puzzlekit/solvers/sumo_sudoku.py +74 -0
- puzzlekit/solvers/tenner_grid.py +60 -0
- puzzlekit/solvers/tent.py +70 -0
- puzzlekit/solvers/terra_x.py +63 -0
- puzzlekit/solvers/thermometer.py +94 -0
- puzzlekit/solvers/tile_paint.py +60 -0
- puzzlekit/solvers/windmill_sudoku.py +67 -0
- puzzlekit/solvers/yajilin.py +126 -0
- puzzlekit/utils/__init__.py +0 -0
- puzzlekit/utils/__pycache__/__init__.cpython-310.pyc +0 -0
- puzzlekit/utils/__pycache__/name_utils.cpython-310.pyc +0 -0
- puzzlekit/utils/__pycache__/ortools_utils.cpython-310.pyc +0 -0
- puzzlekit/utils/__pycache__/puzzle_math.cpython-310.pyc +0 -0
- puzzlekit/utils/file_loader.py +20 -0
- puzzlekit/utils/name_utils.py +29 -0
- puzzlekit/utils/ortools_utils.py +230 -0
- puzzlekit/utils/puzzle_math.py +63 -0
- puzzlekit/verifiers/__init__.py +74 -0
- puzzlekit/verifiers/__pycache__/__init__.cpython-310.pyc +0 -0
- puzzlekit/verifiers/__pycache__/common.cpython-310.pyc +0 -0
- puzzlekit/verifiers/common.py +93 -0
- puzzlekit/viz/__init__.py +124 -0
- puzzlekit/viz/__pycache__/__init__.cpython-310.pyc +0 -0
- puzzlekit/viz/__pycache__/base.cpython-310.pyc +0 -0
- puzzlekit/viz/__pycache__/drawers.cpython-310.pyc +0 -0
- puzzlekit/viz/base.py +209 -0
- puzzlekit/viz/drawers.py +263 -0
- puzzlekit-0.1.0.dist-info/METADATA +257 -0
- puzzlekit-0.1.0.dist-info/RECORD +176 -0
- puzzlekit-0.1.0.dist-info/WHEEL +5 -0
- puzzlekit-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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"]
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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 {}
|
puzzlekit/core/result.py
ADDED
|
@@ -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)
|