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
|
@@ -10,26 +10,21 @@ from puzzle_solver.core.utils_visualizer import combined_function
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class Board:
|
|
13
|
-
def __init__(self, board: np.array, arith_rows: Optional[np.array] = None, arith_cols: Optional[np.array] = None, force_unique: bool = True):
|
|
13
|
+
def __init__(self, board: np.array, arith_rows: Optional[np.array] = None, arith_cols: Optional[np.array] = None, force_unique: bool = True, disallow_three: bool = True):
|
|
14
14
|
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
15
|
+
assert board.shape[0] % 2 == 0 and board.shape[1] % 2 == 0, f'board must have even number of rows and columns, got {board.shape[0]}x{board.shape[1]}'
|
|
15
16
|
assert all(c.item() in [' ', 'B', 'W'] for c in np.nditer(board)), 'board must contain only space or B'
|
|
17
|
+
assert arith_rows is None or all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_rows)), 'arith_rows must contain only space, x, or ='
|
|
18
|
+
assert arith_cols is None or all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_cols)), 'arith_cols must contain only space, x, or ='
|
|
16
19
|
self.board = board
|
|
17
20
|
self.V, self.H = board.shape
|
|
18
|
-
if arith_rows is not None:
|
|
19
|
-
assert arith_rows.ndim == 2, f'arith_rows must be 2d, got {arith_rows.ndim}'
|
|
20
|
-
assert arith_rows.shape == (self.V, self.H-1), f'arith_rows must be one column less than board, got {arith_rows.shape} for {board.shape}'
|
|
21
|
-
assert all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_rows)), 'arith_rows must contain only space, x, or ='
|
|
22
|
-
if arith_cols is not None:
|
|
23
|
-
assert arith_cols.ndim == 2, f'arith_cols must be 2d, got {arith_cols.ndim}'
|
|
24
|
-
assert arith_cols.shape == (self.V-1, self.H), f'arith_cols must be one column and row less than board, got {arith_cols.shape} for {board.shape}'
|
|
25
|
-
assert all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_cols)), 'arith_cols must contain only space, x, or ='
|
|
26
21
|
self.arith_rows = arith_rows
|
|
27
22
|
self.arith_cols = arith_cols
|
|
28
23
|
self.force_unique = force_unique
|
|
24
|
+
self.disallow_three = disallow_three
|
|
29
25
|
|
|
30
26
|
self.model = cp_model.CpModel()
|
|
31
27
|
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
32
|
-
|
|
33
28
|
self.create_vars()
|
|
34
29
|
self.add_all_constraints()
|
|
35
30
|
|
|
@@ -39,11 +34,9 @@ class Board:
|
|
|
39
34
|
|
|
40
35
|
def add_all_constraints(self):
|
|
41
36
|
for pos in get_all_pos(self.V, self.H): # force clues
|
|
42
|
-
c = get_char(self.board, pos)
|
|
43
|
-
if c
|
|
44
|
-
self.model.Add(self.model_vars[pos] ==
|
|
45
|
-
elif c == 'W':
|
|
46
|
-
self.model.Add(self.model_vars[pos] == 0)
|
|
37
|
+
c = get_char(self.board, pos).strip()
|
|
38
|
+
if c:
|
|
39
|
+
self.model.Add(self.model_vars[pos] == (c == 'B'))
|
|
47
40
|
# 1. Each row and each column must contain an equal number of white and black circles.
|
|
48
41
|
for row in range(self.V):
|
|
49
42
|
row_vars = [self.model_vars[pos] for pos in get_row_pos(row, self.H)]
|
|
@@ -51,69 +44,48 @@ class Board:
|
|
|
51
44
|
for col in range(self.H):
|
|
52
45
|
col_vars = [self.model_vars[pos] for pos in get_col_pos(col, self.V)]
|
|
53
46
|
self.model.Add(lxp.sum(col_vars) == len(col_vars) // 2)
|
|
54
|
-
# 2.
|
|
55
|
-
|
|
56
|
-
self.
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
# 2. No three consecutive cells of the same color
|
|
48
|
+
if self.disallow_three:
|
|
49
|
+
for pos in get_all_pos(self.V, self.H):
|
|
50
|
+
self.disallow_three_in_a_row(pos, Direction.RIGHT)
|
|
51
|
+
self.disallow_three_in_a_row(pos, Direction.DOWN)
|
|
59
52
|
# 3. Each row and column is unique.
|
|
60
53
|
if self.force_unique:
|
|
61
|
-
# a list per row
|
|
62
54
|
self.force_unique_double_list([[self.model_vars[pos] for pos in get_row_pos(row, self.H)] for row in range(self.V)])
|
|
63
|
-
# a list per column
|
|
64
55
|
self.force_unique_double_list([[self.model_vars[pos] for pos in get_col_pos(col, self.V)] for col in range(self.H)])
|
|
65
|
-
|
|
66
56
|
# if arithmetic is provided, add constraints for it
|
|
67
57
|
if self.arith_rows is not None:
|
|
68
|
-
|
|
69
|
-
for pos in get_all_pos(self.V, self.H-1):
|
|
70
|
-
c = get_char(self.arith_rows, pos)
|
|
71
|
-
if c == 'x':
|
|
72
|
-
self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, Direction.RIGHT)])
|
|
73
|
-
elif c == '=':
|
|
74
|
-
self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, Direction.RIGHT)])
|
|
58
|
+
self.force_arithmetic(self.arith_rows, Direction.RIGHT, self.V, self.H-1)
|
|
75
59
|
if self.arith_cols is not None:
|
|
76
|
-
|
|
77
|
-
for pos in get_all_pos(self.V-1, self.H):
|
|
78
|
-
c = get_char(self.arith_cols, pos)
|
|
79
|
-
if c == 'x':
|
|
80
|
-
self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, Direction.DOWN)])
|
|
81
|
-
elif c == '=':
|
|
82
|
-
self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, Direction.DOWN)])
|
|
83
|
-
|
|
60
|
+
self.force_arithmetic(self.arith_cols, Direction.DOWN, self.V-1, self.H)
|
|
84
61
|
|
|
85
62
|
def disallow_three_in_a_row(self, p1: Pos, direction: Direction):
|
|
86
63
|
p2 = get_next_pos(p1, direction)
|
|
87
64
|
p3 = get_next_pos(p2, direction)
|
|
88
|
-
if
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
self.model_vars[p1],
|
|
92
|
-
self.model_vars[p2],
|
|
93
|
-
self.model_vars[p3],
|
|
94
|
-
])
|
|
95
|
-
self.model.AddBoolOr([
|
|
96
|
-
self.model_vars[p1].Not(),
|
|
97
|
-
self.model_vars[p2].Not(),
|
|
98
|
-
self.model_vars[p3].Not(),
|
|
99
|
-
])
|
|
65
|
+
if all(in_bounds(p, self.V, self.H) for p in [p1, p2, p3]):
|
|
66
|
+
self.model.AddBoolOr([self.model_vars[p1], self.model_vars[p2], self.model_vars[p3]])
|
|
67
|
+
self.model.AddBoolOr([self.model_vars[p1].Not(), self.model_vars[p2].Not(), self.model_vars[p3].Not()])
|
|
100
68
|
|
|
101
69
|
def force_unique_double_list(self, model_vars: list[list[cp_model.IntVar]]):
|
|
102
|
-
if not model_vars or len(model_vars) < 2:
|
|
103
|
-
return
|
|
104
70
|
m = len(model_vars[0])
|
|
105
|
-
assert m <= 61, f
|
|
106
|
-
|
|
71
|
+
assert m <= 61, f'Too many cells for binary encoding in int64: m={m}, model_vars={model_vars}'
|
|
107
72
|
codes = []
|
|
108
|
-
pow2 = [
|
|
73
|
+
pow2 = [2**k for k in range(m)]
|
|
109
74
|
for i, line in enumerate(model_vars):
|
|
110
|
-
code = self.model.NewIntVar(0,
|
|
111
|
-
# Sum 2^k * r[k] == code
|
|
112
|
-
self.model.Add(code == sum(pow2[k] * line[k] for k in range(m)))
|
|
75
|
+
code = self.model.NewIntVar(0, 2**m, f"code_{i}")
|
|
76
|
+
self.model.Add(code == lxp.weighted_sum(line, pow2)) # Sum 2^k * r[k] == code
|
|
113
77
|
codes.append(code)
|
|
114
|
-
|
|
115
78
|
self.model.AddAllDifferent(codes)
|
|
116
79
|
|
|
80
|
+
def force_arithmetic(self, arith_board: np.array, direction: Direction, V: int, H: int):
|
|
81
|
+
assert arith_board.shape == (V, H), f'arith_board going {direction} expected shape {V}x{H}, got {arith_board.shape}'
|
|
82
|
+
for pos in get_all_pos(V, H):
|
|
83
|
+
c = get_char(arith_board, pos).strip()
|
|
84
|
+
if c == 'x':
|
|
85
|
+
self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, direction)])
|
|
86
|
+
elif c == '=':
|
|
87
|
+
self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, direction)])
|
|
88
|
+
|
|
117
89
|
def solve_and_print(self, verbose: bool = True):
|
|
118
90
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
119
91
|
assignment: dict[Pos, int] = {}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ortools.sat.python import cp_model
|
|
4
|
+
from ortools.util.python import sorted_interval_list
|
|
5
|
+
|
|
6
|
+
from puzzle_solver.core.utils import Direction, Pos, get_char, get_next_pos, get_row_pos, get_col_pos, in_bounds, set_char
|
|
7
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
8
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class var_with_bounds:
|
|
13
|
+
var: cp_model.IntVar
|
|
14
|
+
min_value: int
|
|
15
|
+
max_value: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _div_bounds(a_min: int, a_max: int, b_min: int, b_max: int) -> tuple[int, int]:
|
|
19
|
+
assert not (b_min == 0 and b_max == 0), "Denominator interval cannot be [0, 0]."
|
|
20
|
+
denoms = [b_min, b_max]
|
|
21
|
+
if 0 in denoms:
|
|
22
|
+
denoms.remove(0)
|
|
23
|
+
if b_min <= -1:
|
|
24
|
+
denoms += [-1]
|
|
25
|
+
if b_max >= 1:
|
|
26
|
+
denoms += [1]
|
|
27
|
+
candidates = [a_min // d for d in denoms] + [a_max // d for d in denoms]
|
|
28
|
+
return min(candidates), max(candidates)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Board:
|
|
32
|
+
def __init__(self, board: np.array, digits: list[int]):
|
|
33
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
34
|
+
self.board = board
|
|
35
|
+
self.V, self.H = board.shape
|
|
36
|
+
assert self.V >= 3 and self.V % 2 == 1, f'board must have at least 3 rows and an odd number of rows. Got {self.V} rows.'
|
|
37
|
+
assert self.H >= 3 and self.H % 2 == 1, f'board must have at least 3 columns and an odd number of columns. Got {self.H} columns.'
|
|
38
|
+
self.digits = digits
|
|
39
|
+
self.domain_values = sorted_interval_list.Domain.FromValues(self.digits)
|
|
40
|
+
self.domain_values_no_zero = sorted_interval_list.Domain.FromValues([d for d in self.digits if d != 0])
|
|
41
|
+
|
|
42
|
+
self.model = cp_model.CpModel()
|
|
43
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
44
|
+
self.create_vars()
|
|
45
|
+
assert len(self.model_vars) == len(self.digits), f'len(model_vars) != len(digits), {len(self.model_vars)} != {len(self.digits)}'
|
|
46
|
+
self.model.AddAllDifferent(list(self.model_vars.values()))
|
|
47
|
+
|
|
48
|
+
def create_vars(self):
|
|
49
|
+
for row in range(0, self.V-2, 2):
|
|
50
|
+
line_pos = [pos for pos in get_row_pos(row, self.H)]
|
|
51
|
+
self.parse_line(line_pos)
|
|
52
|
+
for col in range(0, self.H-2, 2):
|
|
53
|
+
line_pos = [pos for pos in get_col_pos(col, self.V)]
|
|
54
|
+
self.parse_line(line_pos)
|
|
55
|
+
|
|
56
|
+
def parse_line(self, line_pos: list[Pos]) -> list[int]:
|
|
57
|
+
last_num = get_char(self.board, line_pos[-1])
|
|
58
|
+
equal_sign = get_char(self.board, line_pos[-2])
|
|
59
|
+
assert equal_sign == '=', f'last element of line must be =, got {equal_sign}'
|
|
60
|
+
line_pos = line_pos[:-2]
|
|
61
|
+
operators = [get_char(self.board, pos) for pos in line_pos[1::2]]
|
|
62
|
+
assert all(c.strip() in ['+', '-', '*', '/'] for c in operators), f'even indices of line must be operators, got {operators}'
|
|
63
|
+
digits_pos = line_pos[::2]
|
|
64
|
+
running_var = self.get_var(digits_pos[0], fixed=get_char(self.board, digits_pos[0]))
|
|
65
|
+
for pos, operator in zip(digits_pos[1:], operators):
|
|
66
|
+
running_var = self.apply_operator(operator, running_var, self.get_var(pos, fixed=get_char(self.board, pos)))
|
|
67
|
+
self.model.Add(running_var.var == int(last_num))
|
|
68
|
+
return running_var
|
|
69
|
+
|
|
70
|
+
def get_var(self, pos: Pos, fixed: str) -> var_with_bounds:
|
|
71
|
+
if pos not in self.model_vars:
|
|
72
|
+
domain = self.domain_values_no_zero if self.might_be_denominator(pos) else self.domain_values
|
|
73
|
+
self.model_vars[pos] = self.model.NewIntVarFromDomain(domain, f'{pos}')
|
|
74
|
+
if fixed.strip():
|
|
75
|
+
self.model.Add(self.model_vars[pos] == int(fixed))
|
|
76
|
+
return var_with_bounds(var=self.model_vars[pos], min_value=min(self.digits), max_value=max(self.digits))
|
|
77
|
+
|
|
78
|
+
def might_be_denominator(self, pos: Pos) -> bool:
|
|
79
|
+
"Important since if the variable might be a denominator and the domain includes 0 then ortools immediately sets the model as INVALID"
|
|
80
|
+
above_pos = get_next_pos(pos, Direction.UP)
|
|
81
|
+
left_pos = get_next_pos(pos, Direction.LEFT)
|
|
82
|
+
above_operator = get_char(self.board, above_pos) if in_bounds(above_pos, self.V, self.H) else None
|
|
83
|
+
left_operator = get_char(self.board, left_pos) if in_bounds(left_pos, self.V, self.H) else None
|
|
84
|
+
return above_operator == '/' or left_operator == '/'
|
|
85
|
+
|
|
86
|
+
def apply_operator(self, operator: str, a: var_with_bounds, b: var_with_bounds) -> var_with_bounds:
|
|
87
|
+
assert operator in ['+', '-', '*', '/'], f'invalid operator: {operator}'
|
|
88
|
+
if operator == "+":
|
|
89
|
+
lo = a.min_value + b.min_value
|
|
90
|
+
hi = a.max_value + b.max_value
|
|
91
|
+
res = self.model.NewIntVar(lo, hi, "sum")
|
|
92
|
+
self.model.Add(res == a.var + b.var)
|
|
93
|
+
elif operator == "-":
|
|
94
|
+
lo = a.min_value - b.max_value
|
|
95
|
+
hi = a.max_value - b.min_value
|
|
96
|
+
res = self.model.NewIntVar(lo, hi, "diff")
|
|
97
|
+
self.model.Add(res == a.var - b.var)
|
|
98
|
+
elif operator == "*":
|
|
99
|
+
cands = [a.min_value*b.min_value, a.min_value*b.max_value, a.max_value*b.min_value, a.max_value*b.max_value]
|
|
100
|
+
lo, hi = min(cands), max(cands)
|
|
101
|
+
res = self.model.NewIntVar(lo, hi, "prod")
|
|
102
|
+
self.model.AddMultiplicationEquality(res, [a.var, b.var])
|
|
103
|
+
elif operator == "/":
|
|
104
|
+
self.model.Add(b.var != 0)
|
|
105
|
+
lo, hi = _div_bounds(a.min_value, a.max_value, b.min_value, b.max_value)
|
|
106
|
+
res = self.model.NewIntVar(lo, hi, "quot")
|
|
107
|
+
self.model.AddDivisionEquality(res, a.var, b.var)
|
|
108
|
+
return var_with_bounds(res, lo, hi)
|
|
109
|
+
|
|
110
|
+
def solve_and_print(self, verbose: bool = True):
|
|
111
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
112
|
+
return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
|
|
113
|
+
def callback(single_res: SingleSolution):
|
|
114
|
+
print("Solution found")
|
|
115
|
+
output_board = self.board.copy()
|
|
116
|
+
for pos, var in single_res.assignment.items():
|
|
117
|
+
set_char(output_board, pos, str(var))
|
|
118
|
+
print(combined_function(self.V, self.H, center_char=lambda r, c: str(output_board[r, c])))
|
|
119
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|