multi-puzzle-solver 0.9.31__py3-none-any.whl → 1.0.2__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-0.9.31.dist-info → multi_puzzle_solver-1.0.2.dist-info}/METADATA +255 -1
- multi_puzzle_solver-1.0.2.dist-info/RECORD +69 -0
- puzzle_solver/__init__.py +58 -1
- puzzle_solver/core/utils_ortools.py +8 -6
- puzzle_solver/core/utils_visualizer.py +12 -11
- puzzle_solver/puzzles/binairo/binairo.py +4 -4
- puzzle_solver/puzzles/black_box/black_box.py +5 -11
- puzzle_solver/puzzles/bridges/bridges.py +1 -1
- puzzle_solver/puzzles/chess_range/chess_range.py +3 -3
- puzzle_solver/puzzles/chess_range/chess_solo.py +1 -1
- puzzle_solver/puzzles/filling/filling.py +3 -3
- puzzle_solver/puzzles/flood_it/flood_it.py +174 -0
- puzzle_solver/puzzles/flood_it/parse_map/parse_map.py +198 -0
- puzzle_solver/puzzles/galaxies/galaxies.py +1 -1
- puzzle_solver/puzzles/galaxies/parse_map/parse_map.py +3 -3
- puzzle_solver/puzzles/guess/guess.py +1 -1
- puzzle_solver/puzzles/heyawake/heyawake.py +3 -3
- puzzle_solver/puzzles/inertia/inertia.py +1 -1
- puzzle_solver/puzzles/inertia/parse_map/parse_map.py +13 -10
- puzzle_solver/puzzles/inertia/tsp.py +5 -7
- puzzle_solver/puzzles/kakuro/kakuro.py +1 -1
- puzzle_solver/puzzles/keen/keen.py +2 -2
- puzzle_solver/puzzles/minesweeper/minesweeper.py +2 -3
- puzzle_solver/puzzles/nonograms/nonograms.py +3 -3
- puzzle_solver/puzzles/norinori/norinori.py +2 -2
- puzzle_solver/puzzles/nurikabe/nurikabe.py +2 -2
- puzzle_solver/puzzles/range/range.py +1 -1
- puzzle_solver/puzzles/rectangles/rectangles.py +2 -6
- puzzle_solver/puzzles/shingoki/shingoki.py +1 -1
- puzzle_solver/puzzles/signpost/signpost.py +2 -2
- puzzle_solver/puzzles/slant/parse_map/parse_map.py +7 -5
- puzzle_solver/puzzles/slitherlink/slitherlink.py +1 -1
- puzzle_solver/puzzles/stitches/parse_map/parse_map.py +6 -5
- puzzle_solver/puzzles/stitches/stitches.py +1 -1
- puzzle_solver/puzzles/sudoku/sudoku.py +91 -20
- puzzle_solver/puzzles/tents/tents.py +2 -2
- puzzle_solver/puzzles/thermometers/thermometers.py +1 -1
- puzzle_solver/puzzles/towers/towers.py +1 -1
- puzzle_solver/puzzles/undead/undead.py +1 -1
- puzzle_solver/puzzles/unruly/unruly.py +1 -1
- puzzle_solver/puzzles/yin_yang/yin_yang.py +1 -1
- puzzle_solver/utils/visualizer.py +1 -1
- multi_puzzle_solver-0.9.31.dist-info/RECORD +0 -67
- {multi_puzzle_solver-0.9.31.dist-info → multi_puzzle_solver-1.0.2.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.9.31.dist-info → multi_puzzle_solver-1.0.2.dist-info}/top_level.txt +0 -0
|
@@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from ortools.sat.python import cp_model
|
|
5
5
|
|
|
6
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, get_neighbors4, get_pos, in_bounds,
|
|
6
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_neighbors4, get_pos, in_bounds, get_char, polyominoes, Shape, Direction, get_next_pos
|
|
7
7
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
8
8
|
from puzzle_solver.core.utils_visualizer import render_shaded_grid
|
|
9
9
|
|
|
@@ -62,7 +62,7 @@ class Board:
|
|
|
62
62
|
assert len(hint_shapes) > 0, f'no shapes found for hint {hint_pos} with value {hint_value}'
|
|
63
63
|
self.model.AddExactlyOne([s.is_active for s in hint_shapes])
|
|
64
64
|
self.shapes_on_board.extend(hint_shapes)
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
# if no shape is active on the spot then it must be black
|
|
67
67
|
for pos in self.get_all_legal_pos():
|
|
68
68
|
shapes_here = [s for s in self.shapes_on_board if pos in s.body]
|
|
@@ -2,7 +2,7 @@ import numpy as np
|
|
|
2
2
|
from ortools.sat.python import cp_model
|
|
3
3
|
|
|
4
4
|
from puzzle_solver.core.utils import Pos, get_all_pos, set_char, get_neighbors4, in_bounds, Direction, get_next_pos, get_char
|
|
5
|
-
from puzzle_solver.core.utils_ortools import and_constraint,
|
|
5
|
+
from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, SingleSolution, force_connected_component
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def get_ray(pos: Pos, V: int, H: int, direction: Direction) -> list[Pos]:
|
|
@@ -97,7 +97,7 @@ class Board:
|
|
|
97
97
|
def solve_and_print(self, verbose: bool = True):
|
|
98
98
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
99
99
|
assignment: dict[Pos, int] = {}
|
|
100
|
-
for
|
|
100
|
+
for rectangle in self.rectangles:
|
|
101
101
|
if solver.Value(rectangle.active) == 1:
|
|
102
102
|
for pos in rectangle.body:
|
|
103
103
|
assignment[pos] = f'id{rectangle.clue_id}:N={rectangle.N}:{rectangle.height}x{rectangle.width}'
|
|
@@ -121,10 +121,6 @@ class Board:
|
|
|
121
121
|
set_char(res, pos, get_char(res, pos) + 'U')
|
|
122
122
|
if bottom_pos not in single_res.assignment or single_res.assignment[bottom_pos] != cur:
|
|
123
123
|
set_char(res, pos, get_char(res, pos) + 'D')
|
|
124
|
-
|
|
125
|
-
# for row in id_board:
|
|
126
|
-
# print(' ', row.tolist(), end=',\n')
|
|
127
|
-
# print(' ])')
|
|
128
|
-
print(render_grid(res, center_char=self.board))
|
|
124
|
+
print(render_grid(res, center_char=lambda r, c: self.board[r, c] if self.board[r, c] != ' ' else ' '))
|
|
129
125
|
|
|
130
126
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -76,7 +76,7 @@ class Board:
|
|
|
76
76
|
continue
|
|
77
77
|
self.enforce_corner_color(pos, self.board_colors[pos])
|
|
78
78
|
self.enforce_corner_number(pos, self.board_numbers[pos])
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
# enforce single connected component
|
|
81
81
|
def is_neighbor(edge1: tuple[Pos, Pos], edge2: tuple[Pos, Pos]) -> bool:
|
|
82
82
|
return any(c1 == c2 for c1 in edge1 for c2 in edge2)
|
|
@@ -65,7 +65,7 @@ class Board:
|
|
|
65
65
|
continue
|
|
66
66
|
direction = CHAR_TO_DIRECTION8[c]
|
|
67
67
|
self.constrain_plus_one(pos, direction)
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
def constrain_plus_one(self, pos: Pos, direction: Direction8):
|
|
70
70
|
beam_res = beam(pos, self.V, self.H, direction)
|
|
71
71
|
is_eq_list = []
|
|
@@ -75,7 +75,7 @@ class Board:
|
|
|
75
75
|
self.model.Add(self.model_vars[p] != self.model_vars[pos] + 1).OnlyEnforceIf(aux.Not())
|
|
76
76
|
is_eq_list.append(aux)
|
|
77
77
|
self.model.Add(lxp.Sum(is_eq_list) == 1)
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
def solve_and_print(self, verbose: bool = True):
|
|
80
80
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
81
81
|
assignment: dict[Pos, str] = {}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
|
-
This file is a simple helper that parses the images from https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/inertia.html and converts them to a json file.
|
|
2
|
+
This file is a simple helper that parses the images from https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/inertia.html and converts them to a json file.
|
|
3
3
|
Look at the ./input_output/ directory for examples of input images and output json files.
|
|
4
4
|
The output json is used in the test_solve.py file to test the solver.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import itertools
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
import numpy as np
|
|
10
10
|
cv = None
|
|
@@ -37,9 +37,11 @@ def mean_consecutives(arr):
|
|
|
37
37
|
sums, counts = [arr[0]], [1]
|
|
38
38
|
for k in arr[1:]:
|
|
39
39
|
if k == sums[-1] + counts[-1]:
|
|
40
|
-
sums[-1] += k
|
|
40
|
+
sums[-1] += k
|
|
41
|
+
counts[-1] += 1
|
|
41
42
|
else:
|
|
42
|
-
sums.append(k)
|
|
43
|
+
sums.append(k)
|
|
44
|
+
counts.append(1)
|
|
43
45
|
return np.array(sums)//np.array(counts)
|
|
44
46
|
|
|
45
47
|
def main(img_path):
|
|
@@ -90,7 +92,7 @@ def main(img_path):
|
|
|
90
92
|
|
|
91
93
|
# Build KD-like search by grid proximity
|
|
92
94
|
tol = int(cell*0.5) # max distance from an intersection to accept a circle
|
|
93
|
-
for (cx, cy,
|
|
95
|
+
for (cx, cy, _) in detected:
|
|
94
96
|
# find nearest indices
|
|
95
97
|
j = int(np.argmin(np.abs(h_idx - cy)))
|
|
96
98
|
i = int(np.argmin(np.abs(v_idx - cx)))
|
|
@@ -56,7 +56,7 @@ class Board:
|
|
|
56
56
|
next_pos = get_next_pos(pos, direction)
|
|
57
57
|
if in_bounds(next_pos, self.V, self.H):
|
|
58
58
|
self.cell_borders[(next_pos, get_opposite_direction(direction))] = var
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
def add_corner_vars(self, cell_border: CellBorder, var: cp_model.IntVar):
|
|
61
61
|
"""
|
|
62
62
|
An edge always belongs to two corners. Note that the cell xi,yi has the 4 corners (xi,yi), (xi+1,yi), (xi,yi+1), (xi+1,yi+1). (memorize these 4 coordinates or the function wont make sense)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
This file is a simple helper that parses the images from https://www.puzzle-stitches.com/ and converts them to a json file.
|
|
2
|
+
This file is a simple helper that parses the images from https://www.puzzle-stitches.com/ and converts them to a json file.
|
|
3
3
|
Look at the ./input_output/ directory for examples of input images and output json files.
|
|
4
4
|
The output json is used in the test_solve.py file to test the solver.
|
|
5
5
|
"""
|
|
@@ -26,7 +26,7 @@ def extract_lines(bw):
|
|
|
26
26
|
# location where the horizontal lines are
|
|
27
27
|
horizontal_idx = np.where(horizontal_means > horizontal_cutoff)[0]
|
|
28
28
|
# print(f"horizontal_idx: {horizontal_idx}")
|
|
29
|
-
height = len(horizontal_idx)
|
|
29
|
+
# height = len(horizontal_idx)
|
|
30
30
|
# show_wait_destroy("horizontal", horizontal) # this has the horizontal lines
|
|
31
31
|
|
|
32
32
|
rows = vertical.shape[0]
|
|
@@ -39,7 +39,7 @@ def extract_lines(bw):
|
|
|
39
39
|
vertical_cutoff = np.percentile(vertical_means, 50)
|
|
40
40
|
vertical_idx = np.where(vertical_means > vertical_cutoff)[0]
|
|
41
41
|
# print(f"vertical_idx: {vertical_idx}")
|
|
42
|
-
width = len(vertical_idx)
|
|
42
|
+
# width = len(vertical_idx)
|
|
43
43
|
# print(f"height: {height}, width: {width}")
|
|
44
44
|
# print(f"vertical_means: {vertical_means}")
|
|
45
45
|
# show_wait_destroy("vertical", vertical) # this has the vertical lines
|
|
@@ -126,7 +126,6 @@ def main(image):
|
|
|
126
126
|
print(f"vertical_idx: {vertical_idx}")
|
|
127
127
|
arr = np.zeros((height - 1, width - 1), dtype=object)
|
|
128
128
|
output = {'top': arr.copy(), 'left': arr.copy(), 'right': arr.copy(), 'bottom': arr.copy()}
|
|
129
|
-
target = 200_000
|
|
130
129
|
hists = {'top': {}, 'left': {}, 'right': {}, 'bottom': {}}
|
|
131
130
|
for j in range(height - 1):
|
|
132
131
|
for i in range(width - 1):
|
|
@@ -244,4 +243,6 @@ if __name__ == '__main__':
|
|
|
244
243
|
# main(Path(__file__).parent / 'input_output' / 'norinori_501d93110d6b4b818c268378973afbf268f96cfa8d7b4.png')
|
|
245
244
|
# main(Path(__file__).parent / 'input_output' / 'norinori_OTo0LDc0Miw5MTU.png')
|
|
246
245
|
# main(Path(__file__).parent / 'input_output' / 'heyawake_MDoxNiwxNDQ=.png')
|
|
247
|
-
main(Path(__file__).parent / 'input_output' / 'heyawake_MTQ6ODQ4LDEzOQ==.png')
|
|
246
|
+
# main(Path(__file__).parent / 'input_output' / 'heyawake_MTQ6ODQ4LDEzOQ==.png')
|
|
247
|
+
main(Path(__file__).parent / 'input_output' / 'sudoku_jigsaw.png')
|
|
248
|
+
|
|
@@ -77,7 +77,7 @@ class Board:
|
|
|
77
77
|
# print(f'{pos}:{direction} must == {neighbor}:{opposite_direction}')
|
|
78
78
|
|
|
79
79
|
# all blocks connected exactly N times (N usually 1 but can be 2 or 3)
|
|
80
|
-
for
|
|
80
|
+
for connections in self.block_neighbors.values():
|
|
81
81
|
is_connected_list = []
|
|
82
82
|
for pos_a, direction_a, pos_b, direction_b in connections:
|
|
83
83
|
v = self.model.NewBoolVar(f'{pos_a}:{direction_a}->{pos_b}:{direction_b}')
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from typing import Union, Optional
|
|
2
|
+
from collections import defaultdict
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
5
|
from ortools.sat.python import cp_model
|
|
@@ -35,32 +36,77 @@ def get_block_pos(i: int, Bv: int, Bh: int) -> list[Pos]:
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
class Board:
|
|
38
|
-
def __init__(self,
|
|
39
|
+
def __init__(self,
|
|
40
|
+
board: np.array,
|
|
41
|
+
constrain_blocks: bool = True,
|
|
42
|
+
block_size: Optional[tuple[int, int]] = None,
|
|
43
|
+
sandwich: Optional[dict[str, list[int]]] = None,
|
|
44
|
+
unique_diagonal: bool = False,
|
|
45
|
+
jigsaw: Optional[np.array] = None,
|
|
46
|
+
killer: Optional[tuple[np.array, dict[str, int]]] = None,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
board: 2d array of characters
|
|
50
|
+
constrain_blocks: whether to constrain the blocks. If True, each block must contain all numbers from 1 to 9 exactly once.
|
|
51
|
+
block_size: tuple of block size (vertical, horizontal). If not provided, the block size is the square root of the board size.
|
|
52
|
+
sandwich: dictionary of sandwich clues (side, bottom). If provided, the sum of the values between 1 and 9 for each row and column is equal to the clue.
|
|
53
|
+
unique_diagonal: whether to constrain the 2 diagonals to be unique. If True, each diagonal must contain all numbers from 1 to 9 exactly once.
|
|
54
|
+
killer: tuple of (killer board, killer clues). If provided, the killer board must be a 2d array of ids of the killer blocks. The killer clues must be a dictionary of killer block ids to clues.
|
|
55
|
+
Each numbers in a killer block must be unique and sum to the clue.
|
|
56
|
+
"""
|
|
39
57
|
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
40
58
|
assert board.shape[0] == board.shape[1], 'board must be square'
|
|
41
59
|
assert all(isinstance(i.item(), str) and len(i.item()) == 1 and (i.item().isalnum() or i.item() == ' ') for i in np.nditer(board)), 'board must contain only alphanumeric characters or space'
|
|
42
60
|
self.board = board
|
|
43
61
|
self.V, self.H = board.shape
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
self.L = max(self.V, self.H)
|
|
63
|
+
self.constrain_blocks = constrain_blocks
|
|
64
|
+
self.unique_diagonal = unique_diagonal
|
|
65
|
+
self.sandwich = None
|
|
66
|
+
self.jigsaw_id_to_pos = None
|
|
67
|
+
self.killer = None
|
|
68
|
+
|
|
69
|
+
if self.constrain_blocks:
|
|
70
|
+
if block_size is None:
|
|
71
|
+
B = np.sqrt(self.V) # block size
|
|
72
|
+
assert B.is_integer(), 'board size must be a perfect square or provide block_size'
|
|
73
|
+
Bv, Bh = int(B), int(B)
|
|
74
|
+
else:
|
|
75
|
+
Bv, Bh = block_size
|
|
76
|
+
assert Bv * Bh == self.V, 'block size must be a factor of board size'
|
|
77
|
+
# can be different in 4x3 for example
|
|
78
|
+
self.Bv = Bv
|
|
79
|
+
self.Bh = Bh
|
|
80
|
+
self.B = Bv * Bh # block count
|
|
48
81
|
else:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
82
|
+
assert block_size is None, 'cannot set block size if blocks are not constrained'
|
|
83
|
+
|
|
84
|
+
if jigsaw is not None:
|
|
85
|
+
if self.constrain_blocks is not None:
|
|
86
|
+
print('Warning: jigsaw and blocks are both constrained, are you sure you want to do this?')
|
|
87
|
+
assert jigsaw.ndim == 2, f'jigsaw must be 2d, got {jigsaw.ndim}'
|
|
88
|
+
assert jigsaw.shape[0] == self.V and jigsaw.shape[1] == self.H, 'jigsaw must be the same size as the board'
|
|
89
|
+
assert all(isinstance(i.item(), str) and i.item().isdecimal() for i in np.nditer(jigsaw)), 'jigsaw must contain only digits or space'
|
|
90
|
+
self.jigsaw_id_to_pos: dict[int, list[Pos]] = defaultdict(list)
|
|
91
|
+
for pos in get_all_pos(self.V, self.H):
|
|
92
|
+
v = get_char(jigsaw, pos)
|
|
93
|
+
if v.isdecimal():
|
|
94
|
+
self.jigsaw_id_to_pos[int(v)].append(pos)
|
|
95
|
+
assert all(len(pos_list) <= self.L for pos_list in self.jigsaw_id_to_pos.values()), 'jigsaw areas cannot be larger than the number of digits'
|
|
96
|
+
|
|
55
97
|
if sandwich is not None:
|
|
56
98
|
assert set(sandwich.keys()) == set(['side', 'bottom']), 'sandwich must contain only side and bottom'
|
|
57
99
|
assert len(sandwich['side']) == self.H, 'side must be equal to board width'
|
|
58
100
|
assert len(sandwich['bottom']) == self.V, 'bottom must be equal to board height'
|
|
59
101
|
self.sandwich = sandwich
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
102
|
+
|
|
103
|
+
if killer is not None:
|
|
104
|
+
assert killer[0].ndim == 2, f'killer board must be 2d, got {killer[0].ndim}'
|
|
105
|
+
assert killer[0].shape[0] == self.V and killer[0].shape[1] == self.H, 'killer board must be the same size as the board'
|
|
106
|
+
assert all(isinstance(i.item(), str) and i.item().isdecimal() for i in np.nditer(killer[0])), 'killer board must contain only digits or space'
|
|
107
|
+
assert set(killer[1].keys()).issubset(set(killer[0].flatten())), f'killer clues must contain all killer block ids, {set(killer[0].flatten()) - set(killer[1].keys())}'
|
|
108
|
+
self.killer = killer
|
|
109
|
+
|
|
64
110
|
self.model = cp_model.CpModel()
|
|
65
111
|
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
66
112
|
|
|
@@ -69,7 +115,7 @@ class Board:
|
|
|
69
115
|
|
|
70
116
|
def create_vars(self):
|
|
71
117
|
for pos in get_all_pos(self.V, self.H):
|
|
72
|
-
self.model_vars[pos] = self.model.NewIntVar(1, self.
|
|
118
|
+
self.model_vars[pos] = self.model.NewIntVar(1, self.L, f'{pos}')
|
|
73
119
|
|
|
74
120
|
def add_all_constraints(self):
|
|
75
121
|
# some squares are already filled
|
|
@@ -86,16 +132,21 @@ class Board:
|
|
|
86
132
|
for col in range(self.H):
|
|
87
133
|
col_vars = [self.model_vars[pos] for pos in get_col_pos(col, V=self.V)]
|
|
88
134
|
self.model.AddAllDifferent(col_vars)
|
|
89
|
-
# each block
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
135
|
+
if self.constrain_blocks: # each block must contain all numbers from 1 to 9 exactly once
|
|
136
|
+
for block_i in range(self.B):
|
|
137
|
+
block_vars = [self.model_vars[p] for p in get_block_pos(block_i, Bv=self.Bv, Bh=self.Bh)]
|
|
138
|
+
self.model.AddAllDifferent(block_vars)
|
|
93
139
|
if self.sandwich is not None:
|
|
94
140
|
self.add_sandwich_constraints()
|
|
95
141
|
if self.unique_diagonal:
|
|
96
142
|
self.add_unique_diagonal_constraints()
|
|
143
|
+
if self.jigsaw_id_to_pos is not None:
|
|
144
|
+
self.add_jigsaw_constraints()
|
|
145
|
+
if self.killer is not None:
|
|
146
|
+
self.add_killer_constraints()
|
|
97
147
|
|
|
98
148
|
def add_sandwich_constraints(self):
|
|
149
|
+
"""Sandwich constraints, enforce that the sum of the values between 1 and 9 for each row and column is equal to the clue."""
|
|
99
150
|
for c, clue in enumerate(self.sandwich['bottom']):
|
|
100
151
|
if clue is None or int(clue) < 0:
|
|
101
152
|
continue
|
|
@@ -113,6 +164,26 @@ class Board:
|
|
|
113
164
|
anti_diagonal_vars = [self.model_vars[get_pos(x=i, y=self.V-i-1)] for i in range(min(self.V, self.H))]
|
|
114
165
|
self.model.AddAllDifferent(anti_diagonal_vars)
|
|
115
166
|
|
|
167
|
+
def add_jigsaw_constraints(self):
|
|
168
|
+
"""All digits in one jigsaw area must be unique."""
|
|
169
|
+
for pos_list in self.jigsaw_id_to_pos.values():
|
|
170
|
+
self.model.AddAllDifferent([self.model_vars[p] for p in pos_list])
|
|
171
|
+
|
|
172
|
+
def add_killer_constraints(self):
|
|
173
|
+
"""Killer constraints, enforce that the sum of the values in each killer block is equal to the clue and all numbers in a block are unique."""
|
|
174
|
+
killer_board, killer_clues = self.killer
|
|
175
|
+
# change clue keys to ints
|
|
176
|
+
killer_clues = {int(k): v for k, v in killer_clues.items()}
|
|
177
|
+
killer_id_to_pos = defaultdict(list)
|
|
178
|
+
for pos in get_all_pos(self.V, self.H):
|
|
179
|
+
v = get_char(killer_board, pos)
|
|
180
|
+
if v.isdecimal():
|
|
181
|
+
killer_id_to_pos[int(v)].append(pos)
|
|
182
|
+
for killer_id, pos_list in killer_id_to_pos.items():
|
|
183
|
+
self.model.AddAllDifferent([self.model_vars[p] for p in pos_list])
|
|
184
|
+
clue = killer_clues[killer_id]
|
|
185
|
+
self.model.Add(sum([self.model_vars[p] for p in pos_list]) == clue)
|
|
186
|
+
|
|
116
187
|
def solve_and_print(self, verbose: bool = True):
|
|
117
188
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
118
189
|
assignment: dict[Pos, int] = {}
|
|
@@ -45,14 +45,14 @@ class Board:
|
|
|
45
45
|
if get_char(self.board, neighbour) != ' ':
|
|
46
46
|
continue
|
|
47
47
|
self.model.Add(self.is_tent[neighbour] == 0).OnlyEnforceIf(self.is_tent[pos])
|
|
48
|
-
# - the number of tents in each row and column matches the numbers around the edge of the grid
|
|
48
|
+
# - the number of tents in each row and column matches the numbers around the edge of the grid
|
|
49
49
|
for row in range(self.N):
|
|
50
50
|
row_vars = [self.is_tent[pos] for pos in get_row_pos(row, self.N)]
|
|
51
51
|
self.model.Add(lxp.sum(row_vars) == self.sides['side'][row])
|
|
52
52
|
for col in range(self.N):
|
|
53
53
|
col_vars = [self.is_tent[pos] for pos in get_col_pos(col, self.N)]
|
|
54
54
|
self.model.Add(lxp.sum(col_vars) == self.sides['top'][col])
|
|
55
|
-
# - it is possible to match tents to trees so that each tree is orthogonally adjacent to its own tent (but may also be adjacent to other tents).
|
|
55
|
+
# - it is possible to match tents to trees so that each tree is orthogonally adjacent to its own tent (but may also be adjacent to other tents).
|
|
56
56
|
# for each tree, one of the following must be true:
|
|
57
57
|
# a tent on its left has direction RIGHT
|
|
58
58
|
# a tent on its right has direction LEFT
|
|
@@ -120,7 +120,7 @@ class Board:
|
|
|
120
120
|
# create a single bool which decides if I can see it or not
|
|
121
121
|
res = self.model.NewBoolVar(name)
|
|
122
122
|
self.model.AddBoolAnd(lits).OnlyEnforceIf(res)
|
|
123
|
-
self.model.AddBoolOr([res] + [
|
|
123
|
+
self.model.AddBoolOr([res] + [lit.Not() for lit in lits])
|
|
124
124
|
return res
|
|
125
125
|
|
|
126
126
|
def solve_and_print(self, verbose: bool = True):
|
|
@@ -131,7 +131,7 @@ class Board:
|
|
|
131
131
|
pos = get_pos(x=i, y=self.N-1)
|
|
132
132
|
beam_result = beam(self.board, pos, Direction.UP)
|
|
133
133
|
self.model.add(self.get_var(beam_result) == ground)
|
|
134
|
-
|
|
134
|
+
|
|
135
135
|
if self.monster_count is not None:
|
|
136
136
|
for monster, count in self.monster_count.items():
|
|
137
137
|
if count == -1:
|
|
@@ -49,7 +49,7 @@ class Board:
|
|
|
49
49
|
continue
|
|
50
50
|
v = 1 if c == 'B' else 0
|
|
51
51
|
self.model.Add(self.model_vars[pos] == v)
|
|
52
|
-
# no three consecutive squares, horizontally or vertically, are the same colour
|
|
52
|
+
# no three consecutive squares, horizontally or vertically, are the same colour
|
|
53
53
|
for pos in get_all_pos(self.V, self.H):
|
|
54
54
|
horiz, vert = get_3_consecutive_horiz_and_vert(pos, self.V, self.H)
|
|
55
55
|
if len(horiz) == 3:
|
|
@@ -48,7 +48,7 @@ class Board:
|
|
|
48
48
|
continue
|
|
49
49
|
self.model.AddBoolOr([self.B[tl], self.B[tr], self.B[bl], self.B[br]])
|
|
50
50
|
self.model.AddBoolOr([self.B[tl].Not(), self.B[tr].Not(), self.B[bl].Not(), self.B[br].Not()])
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
def disallow_checkers(self):
|
|
53
53
|
# from https://ralphwaldo.github.io/yinyang_summary.html
|
|
54
54
|
for pos in get_all_pos(self.V, self.H): # disallow (WB/BW) and (BW/WB)
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
puzzle_solver/__init__.py,sha256=q8lMGHFeg0ZbUxDApJHDcGcQtpNKPHCh3muRfPwpPN4,3693
|
|
2
|
-
puzzle_solver/core/utils.py,sha256=XBW5j-IwtJMPMP-ycmY6SqRCM1NOVl5O6UeoGqNj618,8153
|
|
3
|
-
puzzle_solver/core/utils_ortools.py,sha256=_i8cixHOB5XGqqcr-493bOiZgYJidnvxQMEfj--Trns,10278
|
|
4
|
-
puzzle_solver/core/utils_visualizer.py,sha256=c4cq_BpwDc1oao6KkghisiawwaIVQpTOIEZtVWIkA3w,20249
|
|
5
|
-
puzzle_solver/puzzles/aquarium/aquarium.py,sha256=BUfkAS2d9eG3TdMoe1cOGGeNYgKUebRvn-z9nsC9gvE,5708
|
|
6
|
-
puzzle_solver/puzzles/battleships/battleships.py,sha256=RuYCrs4j0vUjlU139NRYYP-uNPAgO0V7hAzbsHrRwD8,7446
|
|
7
|
-
puzzle_solver/puzzles/binairo/binairo.py,sha256=4xgYd1ewYIQCqEzsHdgp6hWzyW_TF_2rt6PO8QLFKWU,6838
|
|
8
|
-
puzzle_solver/puzzles/binairo/binairo_plus.py,sha256=TvLG3olwANtft3LuCF-y4OofpU9PNa4IXDqgZqsD-g0,267
|
|
9
|
-
puzzle_solver/puzzles/black_box/black_box.py,sha256=ZnHDVt6PFS_r1kMNSsbz9hav1hxIrNDUvPyERGPjLjM,15635
|
|
10
|
-
puzzle_solver/puzzles/bridges/bridges.py,sha256=15A9uV4xjoqPRo_9CTnoKeGRxS3z2aMF619T1n0dTOQ,5402
|
|
11
|
-
puzzle_solver/puzzles/chess_range/chess_melee.py,sha256=D-_Oi8OyxsVe1j3dIKYwRlxgeb3NWLmDWGcv-oclY0c,195
|
|
12
|
-
puzzle_solver/puzzles/chess_range/chess_range.py,sha256=uMQGTIwzGskHIhI-tPYjT9a3wHUBIkZ18eXjV9IpUE4,21071
|
|
13
|
-
puzzle_solver/puzzles/chess_range/chess_solo.py,sha256=U3v766UsZHx_dC3gxqU90VbjAXn-OlYhtrnnvJYFvrQ,401
|
|
14
|
-
puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
|
|
15
|
-
puzzle_solver/puzzles/dominosa/dominosa.py,sha256=Nmb7pn8U27QJwGy9F3wo8ylqo2_U51OAo3GN2soaNpc,7195
|
|
16
|
-
puzzle_solver/puzzles/filling/filling.py,sha256=vrOIil285_r3IQ0F4c9mUBWMRVlPH4vowog_z1tCGdI,5567
|
|
17
|
-
puzzle_solver/puzzles/flip/flip.py,sha256=ZngJLUhRNc7qqo2wtNLdMPx4u9w9JTUge27PmdXyDCw,3985
|
|
18
|
-
puzzle_solver/puzzles/galaxies/galaxies.py,sha256=p10lpmW0FjtneFCMEjG1FSiEpQuvD8zZG9FG8zYGoes,5582
|
|
19
|
-
puzzle_solver/puzzles/galaxies/parse_map/parse_map.py,sha256=v5TCrdREeOB69s9_QFgPHKA7flG69Im1HVzIdxH0qQc,9355
|
|
20
|
-
puzzle_solver/puzzles/guess/guess.py,sha256=sH-NlYhxM3DNbhk4eGde09kgM0KaDvSbLrpHQiwcFGo,10791
|
|
21
|
-
puzzle_solver/puzzles/heyawake/heyawake.py,sha256=qMnc_CuHn8K5Rw40tefjueI1pycpHQ7eN1R9Xg5WEuw,5601
|
|
22
|
-
puzzle_solver/puzzles/inertia/inertia.py,sha256=gJBahkh69CrSWNscalKEoP1j4X-Q3XpbIBMiG9PUpU0,5657
|
|
23
|
-
puzzle_solver/puzzles/inertia/tsp.py,sha256=gobiISHtARA4Elq0jr90p6Yhq11ULjGoqsS-rLFhYcc,15389
|
|
24
|
-
puzzle_solver/puzzles/inertia/parse_map/parse_map.py,sha256=A9JQTNqamUdzlwqks0XQp3Hge3mzyTIVK6YtDJvqpL4,8422
|
|
25
|
-
puzzle_solver/puzzles/kakurasu/kakurasu.py,sha256=VNGMJnBHDi6WkghLObRLhUvkmrPaGphTTUDMC0TkQvQ,2064
|
|
26
|
-
puzzle_solver/puzzles/kakuro/kakuro.py,sha256=Jf0Iilv32EPcaWikX92_vgBOVRp5MAE27aFRmnLotGQ,4374
|
|
27
|
-
puzzle_solver/puzzles/keen/keen.py,sha256=tDb6C5S3Q0JAKPsdw-84WQ6PxRADELZHr_BK8FDH-NA,5039
|
|
28
|
-
puzzle_solver/puzzles/light_up/light_up.py,sha256=iSA1rjZMFsnI0V0Nxivxox4qZkB7PvUrROSHXcoUXds,4541
|
|
29
|
-
puzzle_solver/puzzles/lits/lits.py,sha256=3fPIkhAIUz8JokcfaE_ZM3b0AFEnf5xPzGJ2qnm8SWY,7099
|
|
30
|
-
puzzle_solver/puzzles/magnets/magnets.py,sha256=-Wl49JD_PKeq735zQVMQ3XSQX6gdHiY-7PKw-Sh16jw,6474
|
|
31
|
-
puzzle_solver/puzzles/map/map.py,sha256=sxc57tapB8Tsgam-yoDitln1o-EB_SbIYvO6WEYy3us,2582
|
|
32
|
-
puzzle_solver/puzzles/minesweeper/minesweeper.py,sha256=LiQVOGkWCsc1WtX8CdPgL_WwAcaeUFuoi5_eqH8U2Og,5876
|
|
33
|
-
puzzle_solver/puzzles/mosaic/mosaic.py,sha256=QX_nVpVKQg8OfaUcqFk9tKqsDyVqvZc6-XWvfI3YcSw,2175
|
|
34
|
-
puzzle_solver/puzzles/nonograms/nonograms.py,sha256=1jmDTOCnmivmBlwtMDyyk3TVqH5IjapzLn7zLQ4qubk,6056
|
|
35
|
-
puzzle_solver/puzzles/norinori/norinori.py,sha256=uC8vXAw35xsTmpmTeKqYW7tbcssms9LCcXFBONtV2Ng,4743
|
|
36
|
-
puzzle_solver/puzzles/nurikabe/nurikabe.py,sha256=VMJjB9KAKmfBkG1mDT3Jf2I1PZJb--Qx0BicN8xL4eg,6519
|
|
37
|
-
puzzle_solver/puzzles/palisade/palisade.py,sha256=T-LXlaLU5OwUQ24QWJWhBUFUktg0qDODTilNmBaXs4I,5014
|
|
38
|
-
puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
|
|
39
|
-
puzzle_solver/puzzles/range/range.py,sha256=rruvD5ZSaOgvQuX6uGV_Dkr82nSiWZ5kDz03_j7Tt24,4425
|
|
40
|
-
puzzle_solver/puzzles/rectangles/rectangles.py,sha256=zaPg3qI9TNxr2iXmNi2kOL8R2RsS9DyQPUTY3ukgYIA,7033
|
|
41
|
-
puzzle_solver/puzzles/shakashaka/shakashaka.py,sha256=PRpg_qI7XA3ysAo_g1TRJsT3VwB5Vial2UcFyBOMwKQ,9571
|
|
42
|
-
puzzle_solver/puzzles/shingoki/shingoki.py,sha256=uwX1ZIGGDlshMtsZedlgGYE8hDB1ou3h6aBnZEr_l8I,7425
|
|
43
|
-
puzzle_solver/puzzles/signpost/signpost.py,sha256=-0_S6ycwzwlUf9-ZhP127Rgo5gMBOHiTM6t08dLLDac,3869
|
|
44
|
-
puzzle_solver/puzzles/singles/singles.py,sha256=KKn_Yl-eW874Bl1UmmcqoQ5vhNiO1JbM7fxKczOV5M4,2847
|
|
45
|
-
puzzle_solver/puzzles/slant/slant.py,sha256=xF-N4PuXYfx638NP1f1mi6YncIZB4mLtXtdS79XyPbg,6122
|
|
46
|
-
puzzle_solver/puzzles/slant/parse_map/parse_map.py,sha256=dxnALSDXe9wU0uSD0QEXnzoh1q801mj1ePTNLtG0n60,4796
|
|
47
|
-
puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=e1A_f_3J-QXN9fmt_Nf3FsYnp-TmE9TRKN06Wn4NnAU,7056
|
|
48
|
-
puzzle_solver/puzzles/star_battle/star_battle.py,sha256=IX6w4H3sifN01kPPtrAVRCK0Nl_xlXXSHvJKw8K1EuE,3718
|
|
49
|
-
puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
|
|
50
|
-
puzzle_solver/puzzles/stitches/stitches.py,sha256=iK8t02q43gH3FPbuIDn4dK0sbaOgZOnw8yHNRNvNuIU,6534
|
|
51
|
-
puzzle_solver/puzzles/stitches/parse_map/parse_map.py,sha256=f49ZGVBPXjAGgqZnqPab6PcO_DsFDFZnG3uA8b-1d7k,10441
|
|
52
|
-
puzzle_solver/puzzles/sudoku/sudoku.py,sha256=SE4TM_gic6Jj0fkDR_NzUJdX2XKyQ8eeOnVAQ011Xbo,8870
|
|
53
|
-
puzzle_solver/puzzles/tapa/tapa.py,sha256=TsOQhnEvlC1JxaWiEjQg2KxRXJR49GrN71DsMvPpia8,5337
|
|
54
|
-
puzzle_solver/puzzles/tents/tents.py,sha256=iyVK2WXfIT5j_9qqlQg0WmwvixwXlZSsHGK3XA-KpII,6283
|
|
55
|
-
puzzle_solver/puzzles/thermometers/thermometers.py,sha256=nsvJZkm7G8FALT27bpaB0lv5E_AWawqmvapQI8QcYXw,4015
|
|
56
|
-
puzzle_solver/puzzles/towers/towers.py,sha256=QvL0Pp-Z2ewCeq9ZkNrh8MShKOh-Y52sFBSudve68wk,6496
|
|
57
|
-
puzzle_solver/puzzles/tracks/tracks.py,sha256=98xds9SKNqtOLFTRUX_KSMC7XYmZo567LOFeqotVQaM,7237
|
|
58
|
-
puzzle_solver/puzzles/undead/undead.py,sha256=IrCUfzQFBem658P5KKqldG7vd2TugTHehcwseCarerM,6604
|
|
59
|
-
puzzle_solver/puzzles/unequal/unequal.py,sha256=ExY2XDCrqROCDpRLfHo8uVr1zuli1QvbCdNCiDhlCac,6978
|
|
60
|
-
puzzle_solver/puzzles/unruly/unruly.py,sha256=sDF0oKT50G-NshyW2DYrvAgD9q9Ku9ANUyNhGSAu7cQ,3827
|
|
61
|
-
puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=WrRdNhmKhIARdGOt_36gpRxRzrfLGv3wl7igBpPFM64,5259
|
|
62
|
-
puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
|
|
63
|
-
puzzle_solver/utils/visualizer.py,sha256=tsX1yEKwmwXBYuBJpx_oZGe2UUt1g5yV73G3UbtmvtE,6817
|
|
64
|
-
multi_puzzle_solver-0.9.31.dist-info/METADATA,sha256=yVpID_W67orWIzxxJ49qwggTacSPLjOrS0KjfUZm31U,336388
|
|
65
|
-
multi_puzzle_solver-0.9.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
66
|
-
multi_puzzle_solver-0.9.31.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
|
|
67
|
-
multi_puzzle_solver-0.9.31.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|