multi-puzzle-solver 1.0.7__py3-none-any.whl → 1.0.9__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.
- {multi_puzzle_solver-1.0.7.dist-info → multi_puzzle_solver-1.0.9.dist-info}/METADATA +94 -9
- {multi_puzzle_solver-1.0.7.dist-info → multi_puzzle_solver-1.0.9.dist-info}/RECORD +11 -10
- puzzle_solver/__init__.py +3 -1
- puzzle_solver/core/utils_visualizer.py +565 -561
- puzzle_solver/puzzles/binairo/binairo.py +31 -59
- puzzle_solver/puzzles/mathema_grids/mathema_grids.py +119 -0
- puzzle_solver/puzzles/nonograms/nonograms_colored.py +220 -221
- puzzle_solver/puzzles/palisade/palisade.py +91 -91
- puzzle_solver/puzzles/tracks/tracks.py +1 -1
- {multi_puzzle_solver-1.0.7.dist-info → multi_puzzle_solver-1.0.9.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-1.0.7.dist-info → multi_puzzle_solver-1.0.9.dist-info}/top_level.txt +0 -0
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from collections import defaultdict
|
|
3
|
-
|
|
4
|
-
import numpy as np
|
|
5
|
-
from ortools.sat.python import cp_model
|
|
6
|
-
|
|
7
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, get_pos,
|
|
8
|
-
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
9
|
-
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Shape = frozenset[Pos] # a shape on the 2d board is just a set of positions
|
|
13
|
-
|
|
14
|
-
@dataclass(frozen=True)
|
|
15
|
-
class ShapeOnBoard:
|
|
16
|
-
is_active: cp_model.IntVar
|
|
17
|
-
shape: Shape
|
|
18
|
-
shape_id: int
|
|
19
|
-
body: set[Pos]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_valid_translations(shape: Shape, board: np.array) -> set[Pos]:
|
|
23
|
-
# give a shape and a board, return all valid translations of the shape that are fully contained in the board AND consistent with the clues on the board
|
|
24
|
-
shape_list = list(shape)
|
|
25
|
-
shape_borders = [] # will contain the number of borders for each pos in the shape; this has to be consistent with the clues on the board
|
|
26
|
-
for pos in shape_list:
|
|
27
|
-
v = 0
|
|
28
|
-
for direction in Direction:
|
|
29
|
-
next_pos = get_next_pos(pos, direction)
|
|
30
|
-
if not in_bounds(next_pos, board.shape[0], board.shape[1]) or next_pos not in shape:
|
|
31
|
-
v += 1
|
|
32
|
-
shape_borders.append(v)
|
|
33
|
-
shape_list = [(p.x, p.y) for p in shape_list]
|
|
34
|
-
# min x/y is always 0
|
|
35
|
-
max_x = max(p[0] for p in shape_list)
|
|
36
|
-
max_y = max(p[1] for p in shape_list)
|
|
37
|
-
for dy in range(0, board.shape[0] - max_y):
|
|
38
|
-
for dx in range(0, board.shape[1] - max_x):
|
|
39
|
-
body = tuple((p[0] + dx, p[1] + dy) for p in shape_list)
|
|
40
|
-
for i, p in enumerate(body):
|
|
41
|
-
c = board[p[1], p[0]]
|
|
42
|
-
if c != ' ' and c != str(shape_borders[i]): # there is a clue and it doesn't match my translated shape, skip
|
|
43
|
-
break
|
|
44
|
-
else:
|
|
45
|
-
yield frozenset(get_pos(x=p[0], y=p[1]) for p in body)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class Board:
|
|
49
|
-
def __init__(self, board: np.array, region_size: int):
|
|
50
|
-
assert region_size >= 1 and isinstance(region_size, int), 'region_size must be an integer greater than or equal to 1'
|
|
51
|
-
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
52
|
-
assert all((c.item() == ' ') or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or digits'
|
|
53
|
-
self.board = board
|
|
54
|
-
self.V, self.H = board.shape
|
|
55
|
-
self.region_size = region_size
|
|
56
|
-
self.region_count = (self.V * self.H) // self.region_size
|
|
57
|
-
assert self.region_count * self.region_size == self.V * self.H, f'region_size must be a factor of the board size, got {self.region_size} and {self.region_count}'
|
|
58
|
-
self.polyominoes = polyominoes(self.region_size)
|
|
59
|
-
|
|
60
|
-
self.model = cp_model.CpModel()
|
|
61
|
-
self.shapes_on_board: list[ShapeOnBoard] = [] # will contain every possible shape on the board based on polyomino degrees
|
|
62
|
-
self.pos_to_shapes: dict[Pos, set[ShapeOnBoard]] = defaultdict(set)
|
|
63
|
-
self.create_vars()
|
|
64
|
-
self.add_all_constraints()
|
|
65
|
-
|
|
66
|
-
def create_vars(self):
|
|
67
|
-
for shape in self.polyominoes:
|
|
68
|
-
for body in get_valid_translations(shape, self.board):
|
|
69
|
-
uid = len(self.shapes_on_board)
|
|
70
|
-
shape_on_board = ShapeOnBoard(
|
|
71
|
-
is_active=self.model.NewBoolVar(f'{uid}:is_active'),
|
|
72
|
-
shape=shape, shape_id=uid, body=body
|
|
73
|
-
)
|
|
74
|
-
self.shapes_on_board.append(shape_on_board)
|
|
75
|
-
for pos in body:
|
|
76
|
-
self.pos_to_shapes[pos].add(shape_on_board)
|
|
77
|
-
|
|
78
|
-
def add_all_constraints(self):
|
|
79
|
-
for pos in get_all_pos(self.V, self.H): # each position has exactly one shape active
|
|
80
|
-
self.model.AddExactlyOne(shape.is_active for shape in self.pos_to_shapes[pos])
|
|
81
|
-
|
|
82
|
-
def solve_and_print(self, verbose: bool = True):
|
|
83
|
-
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
84
|
-
active_shapes = [shape for shape in board.shapes_on_board if solver.Value(shape.is_active) == 1]
|
|
85
|
-
return SingleSolution(assignment={pos: shape.shape_id for shape in active_shapes for pos in shape.body})
|
|
86
|
-
def callback(single_res: SingleSolution):
|
|
87
|
-
print("Solution found")
|
|
88
|
-
print(combined_function(self.V, self.H,
|
|
89
|
-
cell_flags=id_board_to_wall_fn(np.array([[single_res.assignment[get_pos(x=c, y=r)] for c in range(self.H)] for r in range(self.V)])),
|
|
90
|
-
center_char=lambda r, c: self.board[r, c] if self.board[r, c] != ' ' else '·'))
|
|
91
|
-
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from ortools.sat.python import cp_model
|
|
6
|
+
|
|
7
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_pos, in_bounds, get_next_pos, Direction, polyominoes
|
|
8
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
Shape = frozenset[Pos] # a shape on the 2d board is just a set of positions
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ShapeOnBoard:
|
|
16
|
+
is_active: cp_model.IntVar
|
|
17
|
+
shape: Shape
|
|
18
|
+
shape_id: int
|
|
19
|
+
body: set[Pos]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_valid_translations(shape: Shape, board: np.array) -> set[Pos]:
|
|
23
|
+
# give a shape and a board, return all valid translations of the shape that are fully contained in the board AND consistent with the clues on the board
|
|
24
|
+
shape_list = list(shape)
|
|
25
|
+
shape_borders = [] # will contain the number of borders for each pos in the shape; this has to be consistent with the clues on the board
|
|
26
|
+
for pos in shape_list:
|
|
27
|
+
v = 0
|
|
28
|
+
for direction in Direction:
|
|
29
|
+
next_pos = get_next_pos(pos, direction)
|
|
30
|
+
if not in_bounds(next_pos, board.shape[0], board.shape[1]) or next_pos not in shape:
|
|
31
|
+
v += 1
|
|
32
|
+
shape_borders.append(v)
|
|
33
|
+
shape_list = [(p.x, p.y) for p in shape_list]
|
|
34
|
+
# min x/y is always 0
|
|
35
|
+
max_x = max(p[0] for p in shape_list)
|
|
36
|
+
max_y = max(p[1] for p in shape_list)
|
|
37
|
+
for dy in range(0, board.shape[0] - max_y):
|
|
38
|
+
for dx in range(0, board.shape[1] - max_x):
|
|
39
|
+
body = tuple((p[0] + dx, p[1] + dy) for p in shape_list)
|
|
40
|
+
for i, p in enumerate(body):
|
|
41
|
+
c = board[p[1], p[0]]
|
|
42
|
+
if c != ' ' and c != str(shape_borders[i]): # there is a clue and it doesn't match my translated shape, skip
|
|
43
|
+
break
|
|
44
|
+
else:
|
|
45
|
+
yield frozenset(get_pos(x=p[0], y=p[1]) for p in body)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Board:
|
|
49
|
+
def __init__(self, board: np.array, region_size: int):
|
|
50
|
+
assert region_size >= 1 and isinstance(region_size, int), 'region_size must be an integer greater than or equal to 1'
|
|
51
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
52
|
+
assert all((c.item() == ' ') or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or digits'
|
|
53
|
+
self.board = board
|
|
54
|
+
self.V, self.H = board.shape
|
|
55
|
+
self.region_size = region_size
|
|
56
|
+
self.region_count = (self.V * self.H) // self.region_size
|
|
57
|
+
assert self.region_count * self.region_size == self.V * self.H, f'region_size must be a factor of the board size, got {self.region_size} and {self.region_count}'
|
|
58
|
+
self.polyominoes = polyominoes(self.region_size)
|
|
59
|
+
|
|
60
|
+
self.model = cp_model.CpModel()
|
|
61
|
+
self.shapes_on_board: list[ShapeOnBoard] = [] # will contain every possible shape on the board based on polyomino degrees
|
|
62
|
+
self.pos_to_shapes: dict[Pos, set[ShapeOnBoard]] = defaultdict(set)
|
|
63
|
+
self.create_vars()
|
|
64
|
+
self.add_all_constraints()
|
|
65
|
+
|
|
66
|
+
def create_vars(self):
|
|
67
|
+
for shape in self.polyominoes:
|
|
68
|
+
for body in get_valid_translations(shape, self.board):
|
|
69
|
+
uid = len(self.shapes_on_board)
|
|
70
|
+
shape_on_board = ShapeOnBoard(
|
|
71
|
+
is_active=self.model.NewBoolVar(f'{uid}:is_active'),
|
|
72
|
+
shape=shape, shape_id=uid, body=body
|
|
73
|
+
)
|
|
74
|
+
self.shapes_on_board.append(shape_on_board)
|
|
75
|
+
for pos in body:
|
|
76
|
+
self.pos_to_shapes[pos].add(shape_on_board)
|
|
77
|
+
|
|
78
|
+
def add_all_constraints(self):
|
|
79
|
+
for pos in get_all_pos(self.V, self.H): # each position has exactly one shape active
|
|
80
|
+
self.model.AddExactlyOne(shape.is_active for shape in self.pos_to_shapes[pos])
|
|
81
|
+
|
|
82
|
+
def solve_and_print(self, verbose: bool = True):
|
|
83
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
84
|
+
active_shapes = [shape for shape in board.shapes_on_board if solver.Value(shape.is_active) == 1]
|
|
85
|
+
return SingleSolution(assignment={pos: shape.shape_id for shape in active_shapes for pos in shape.body})
|
|
86
|
+
def callback(single_res: SingleSolution):
|
|
87
|
+
print("Solution found")
|
|
88
|
+
print(combined_function(self.V, self.H,
|
|
89
|
+
cell_flags=id_board_to_wall_fn(np.array([[single_res.assignment[get_pos(x=c, y=r)] for c in range(self.H)] for r in range(self.V)])),
|
|
90
|
+
center_char=lambda r, c: self.board[r, c] if self.board[r, c] != ' ' else '·'))
|
|
91
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -2,7 +2,7 @@ from collections import defaultdict
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
from ortools.sat.python import cp_model
|
|
4
4
|
|
|
5
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, Direction,
|
|
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
6
|
from puzzle_solver.core.utils_ortools import force_connected_component, generic_solve_all, SingleSolution
|
|
7
7
|
from puzzle_solver.core.utils_visualizer import combined_function
|
|
8
8
|
|
|
File without changes
|
|
File without changes
|