multi-puzzle-solver 1.1.8__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.
- multi_puzzle_solver-1.1.8.dist-info/METADATA +4326 -0
- multi_puzzle_solver-1.1.8.dist-info/RECORD +106 -0
- multi_puzzle_solver-1.1.8.dist-info/WHEEL +5 -0
- multi_puzzle_solver-1.1.8.dist-info/top_level.txt +1 -0
- puzzle_solver/__init__.py +184 -0
- puzzle_solver/core/utils.py +298 -0
- puzzle_solver/core/utils_ortools.py +333 -0
- puzzle_solver/core/utils_visualizer.py +575 -0
- puzzle_solver/puzzles/abc_view/abc_view.py +75 -0
- puzzle_solver/puzzles/aquarium/aquarium.py +97 -0
- puzzle_solver/puzzles/area_51/area_51.py +159 -0
- puzzle_solver/puzzles/battleships/battleships.py +139 -0
- puzzle_solver/puzzles/binairo/binairo.py +98 -0
- puzzle_solver/puzzles/binairo/binairo_plus.py +7 -0
- puzzle_solver/puzzles/black_box/black_box.py +243 -0
- puzzle_solver/puzzles/branches/branches.py +64 -0
- puzzle_solver/puzzles/bridges/bridges.py +104 -0
- puzzle_solver/puzzles/chess_range/chess_melee.py +6 -0
- puzzle_solver/puzzles/chess_range/chess_range.py +406 -0
- puzzle_solver/puzzles/chess_range/chess_solo.py +9 -0
- puzzle_solver/puzzles/chess_sequence/chess_sequence.py +262 -0
- puzzle_solver/puzzles/circle_9/circle_9.py +44 -0
- puzzle_solver/puzzles/clouds/clouds.py +81 -0
- puzzle_solver/puzzles/connect_the_dots/connect_the_dots.py +50 -0
- puzzle_solver/puzzles/cow_and_cactus/cow_and_cactus.py +66 -0
- puzzle_solver/puzzles/dominosa/dominosa.py +67 -0
- puzzle_solver/puzzles/filling/filling.py +94 -0
- puzzle_solver/puzzles/flip/flip.py +64 -0
- puzzle_solver/puzzles/flood_it/flood_it.py +174 -0
- puzzle_solver/puzzles/flood_it/parse_map/parse_map.py +197 -0
- puzzle_solver/puzzles/galaxies/galaxies.py +110 -0
- puzzle_solver/puzzles/galaxies/parse_map/parse_map.py +216 -0
- puzzle_solver/puzzles/guess/guess.py +232 -0
- puzzle_solver/puzzles/heyawake/heyawake.py +152 -0
- puzzle_solver/puzzles/hidden_stars/hidden_stars.py +52 -0
- puzzle_solver/puzzles/hidoku/hidoku.py +59 -0
- puzzle_solver/puzzles/inertia/inertia.py +121 -0
- puzzle_solver/puzzles/inertia/parse_map/parse_map.py +207 -0
- puzzle_solver/puzzles/inertia/tsp.py +400 -0
- puzzle_solver/puzzles/kakurasu/kakurasu.py +38 -0
- puzzle_solver/puzzles/kakuro/kakuro.py +81 -0
- puzzle_solver/puzzles/kakuro/krypto_kakuro.py +95 -0
- puzzle_solver/puzzles/keen/keen.py +76 -0
- puzzle_solver/puzzles/kropki/kropki.py +94 -0
- puzzle_solver/puzzles/light_up/light_up.py +58 -0
- puzzle_solver/puzzles/linesweeper/linesweeper.py +71 -0
- puzzle_solver/puzzles/link_a_pix/link_a_pix.py +91 -0
- puzzle_solver/puzzles/lits/lits.py +138 -0
- puzzle_solver/puzzles/magnets/magnets.py +96 -0
- puzzle_solver/puzzles/map/map.py +56 -0
- puzzle_solver/puzzles/mathema_grids/mathema_grids.py +119 -0
- puzzle_solver/puzzles/mathrax/mathrax.py +93 -0
- puzzle_solver/puzzles/minesweeper/minesweeper.py +123 -0
- puzzle_solver/puzzles/mosaic/mosaic.py +38 -0
- puzzle_solver/puzzles/n_queens/n_queens.py +71 -0
- puzzle_solver/puzzles/nonograms/nonograms.py +121 -0
- puzzle_solver/puzzles/nonograms/nonograms_colored.py +220 -0
- puzzle_solver/puzzles/norinori/norinori.py +96 -0
- puzzle_solver/puzzles/number_path/number_path.py +76 -0
- puzzle_solver/puzzles/numbermaze/numbermaze.py +97 -0
- puzzle_solver/puzzles/nurikabe/nurikabe.py +130 -0
- puzzle_solver/puzzles/palisade/palisade.py +91 -0
- puzzle_solver/puzzles/pearl/pearl.py +107 -0
- puzzle_solver/puzzles/pipes/pipes.py +82 -0
- puzzle_solver/puzzles/range/range.py +59 -0
- puzzle_solver/puzzles/rectangles/rectangles.py +128 -0
- puzzle_solver/puzzles/ripple_effect/ripple_effect.py +83 -0
- puzzle_solver/puzzles/rooms/rooms.py +75 -0
- puzzle_solver/puzzles/schurs_numbers/schurs_numbers.py +73 -0
- puzzle_solver/puzzles/shakashaka/shakashaka.py +201 -0
- puzzle_solver/puzzles/shingoki/shingoki.py +116 -0
- puzzle_solver/puzzles/signpost/signpost.py +93 -0
- puzzle_solver/puzzles/singles/singles.py +53 -0
- puzzle_solver/puzzles/slant/parse_map/parse_map.py +135 -0
- puzzle_solver/puzzles/slant/slant.py +111 -0
- puzzle_solver/puzzles/slitherlink/slitherlink.py +130 -0
- puzzle_solver/puzzles/snail/snail.py +97 -0
- puzzle_solver/puzzles/split_ends/split_ends.py +93 -0
- puzzle_solver/puzzles/star_battle/star_battle.py +75 -0
- puzzle_solver/puzzles/star_battle/star_battle_shapeless.py +7 -0
- puzzle_solver/puzzles/stitches/parse_map/parse_map.py +267 -0
- puzzle_solver/puzzles/stitches/stitches.py +96 -0
- puzzle_solver/puzzles/sudoku/sudoku.py +267 -0
- puzzle_solver/puzzles/suguru/suguru.py +55 -0
- puzzle_solver/puzzles/suko/suko.py +54 -0
- puzzle_solver/puzzles/tapa/tapa.py +97 -0
- puzzle_solver/puzzles/tatami/tatami.py +64 -0
- puzzle_solver/puzzles/tents/tents.py +80 -0
- puzzle_solver/puzzles/thermometers/thermometers.py +82 -0
- puzzle_solver/puzzles/towers/towers.py +89 -0
- puzzle_solver/puzzles/tracks/tracks.py +88 -0
- puzzle_solver/puzzles/trees_logic/trees_logic.py +48 -0
- puzzle_solver/puzzles/troix/dumplings.py +7 -0
- puzzle_solver/puzzles/troix/troix.py +75 -0
- puzzle_solver/puzzles/twiddle/twiddle.py +112 -0
- puzzle_solver/puzzles/undead/undead.py +130 -0
- puzzle_solver/puzzles/unequal/unequal.py +128 -0
- puzzle_solver/puzzles/unruly/unruly.py +54 -0
- puzzle_solver/puzzles/vectors/vectors.py +94 -0
- puzzle_solver/puzzles/vermicelli/vermicelli.py +74 -0
- puzzle_solver/puzzles/walls/walls.py +52 -0
- puzzle_solver/puzzles/yajilin/yajilin.py +87 -0
- puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py +172 -0
- puzzle_solver/puzzles/yin_yang/yin_yang.py +103 -0
- puzzle_solver/utils/etc/parser/board_color_digit.py +497 -0
- puzzle_solver/utils/visualizer.py +155 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from typing import Iterator
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ortools.sat.python import cp_model
|
|
5
|
+
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
6
|
+
|
|
7
|
+
from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_next_pos, get_pos, in_bounds, get_char
|
|
8
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Board:
|
|
13
|
+
def __init__(self, board: np.array, row_sums: list[list[int]], col_sums: list[list[int]], N: int = 9):
|
|
14
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
15
|
+
assert all((c.item() in ['#', ' ', '1', '2', '3', '4', '5', '6', '7', '8', '9']) for c in np.nditer(board)), 'board must contain only #, space, or digits'
|
|
16
|
+
assert len(row_sums) == board.shape[0] and all(isinstance(i, list) and all(isinstance(j, int) or j == '#' for j in i) for i in row_sums), 'row_sums must be a list of lists of integers or #'
|
|
17
|
+
assert len(col_sums) == board.shape[1] and all(isinstance(i, list) and all(isinstance(j, int) or j == '#' for j in i) for i in col_sums), 'col_sums must be a list of lists of integers or #'
|
|
18
|
+
self.board = board
|
|
19
|
+
self.row_sums = row_sums
|
|
20
|
+
self.col_sums = col_sums
|
|
21
|
+
self.V, self.H = board.shape
|
|
22
|
+
self.N = N
|
|
23
|
+
self.model = cp_model.CpModel()
|
|
24
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
25
|
+
|
|
26
|
+
self.create_vars()
|
|
27
|
+
self.add_all_constraints()
|
|
28
|
+
|
|
29
|
+
def create_vars(self):
|
|
30
|
+
for pos in get_all_pos(self.V, self.H):
|
|
31
|
+
if get_char(self.board, pos) == '#':
|
|
32
|
+
continue
|
|
33
|
+
self.model_vars[pos] = self.model.NewIntVar(1, self.N, f'{pos}')
|
|
34
|
+
|
|
35
|
+
def get_consecutives(self, pos: Pos, direction: Direction) -> Iterator[list[Pos]]:
|
|
36
|
+
consecutive = []
|
|
37
|
+
while in_bounds(pos, self.V, self.H):
|
|
38
|
+
if get_char(self.board, pos) == '#':
|
|
39
|
+
if len(consecutive) > 0:
|
|
40
|
+
yield consecutive
|
|
41
|
+
consecutive = []
|
|
42
|
+
else:
|
|
43
|
+
consecutive.append(pos)
|
|
44
|
+
pos = get_next_pos(pos, direction)
|
|
45
|
+
if len(consecutive) > 0:
|
|
46
|
+
yield consecutive
|
|
47
|
+
|
|
48
|
+
def add_all_constraints(self):
|
|
49
|
+
for row in range(self.V):
|
|
50
|
+
row_consecutives = self.get_consecutives(get_pos(x=0, y=row), Direction.RIGHT)
|
|
51
|
+
for i, consecutive in enumerate(row_consecutives):
|
|
52
|
+
# print('row', row, 'i', i, 'consecutive', consecutive)
|
|
53
|
+
self.model.AddAllDifferent([self.model_vars[p] for p in consecutive])
|
|
54
|
+
clue = self.row_sums[row][i]
|
|
55
|
+
if clue != '#':
|
|
56
|
+
self.model.Add(lxp.sum([self.model_vars[p] for p in consecutive]) == clue)
|
|
57
|
+
assert len(self.row_sums[row]) == i + 1, f'row_sums[{row}] has {len(self.row_sums[row])} clues, but {i + 1} consecutive cells'
|
|
58
|
+
for col in range(self.H):
|
|
59
|
+
col_consecutives = self.get_consecutives(get_pos(x=col, y=0), Direction.DOWN)
|
|
60
|
+
for i, consecutive in enumerate(col_consecutives):
|
|
61
|
+
# print('col', col, 'i', i, 'consecutive', consecutive)
|
|
62
|
+
self.model.AddAllDifferent([self.model_vars[p] for p in consecutive])
|
|
63
|
+
clue = self.col_sums[col][i]
|
|
64
|
+
if clue != '#':
|
|
65
|
+
self.model.Add(lxp.sum([self.model_vars[p] for p in consecutive]) == clue)
|
|
66
|
+
assert len(self.col_sums[col]) == i + 1, f'col_sums[{col}] has {len(self.col_sums[col])} clues, but {i + 1} consecutive cells'
|
|
67
|
+
|
|
68
|
+
def solve_and_print(self, verbose: bool = True):
|
|
69
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
70
|
+
assignment: dict[Pos, int] = {}
|
|
71
|
+
for pos, var in board.model_vars.items():
|
|
72
|
+
assignment[pos] = solver.Value(var)
|
|
73
|
+
return SingleSolution(assignment=assignment)
|
|
74
|
+
def callback(single_res: SingleSolution):
|
|
75
|
+
print("Solution found")
|
|
76
|
+
print(combined_function(self.V, self.H,
|
|
77
|
+
is_shaded=lambda r, c: self.board[r, c] == '#',
|
|
78
|
+
center_char=lambda r, c: str(single_res.assignment[get_pos(x=c, y=r)]),
|
|
79
|
+
text_on_shaded_cells=False
|
|
80
|
+
))
|
|
81
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from typing import Iterator
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ortools.sat.python import cp_model
|
|
5
|
+
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
6
|
+
|
|
7
|
+
from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_next_pos, get_pos, in_bounds, get_char
|
|
8
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Board:
|
|
13
|
+
def __init__(self, board: np.array, row_sums: list[list[str]], col_sums: list[list[str]], characters: list[str], min_value: int = 0, max_value: int = 9):
|
|
14
|
+
legal_chars = characters + ['#'] + [str(i) for i in range(min_value, max_value + 1)]
|
|
15
|
+
assert (len(row_sums), len(col_sums)) == board.shape, f'row_sums and col_sums must be the same shape as board, got {len(row_sums)}x{len(col_sums)} and {board.shape}'
|
|
16
|
+
assert all(all(cc in legal_chars for cc in c.item().strip()) for c in np.nditer(board)), 'board must contain only #, space, or characters'
|
|
17
|
+
assert all(all(all(cc in legal_chars for cc in c) for c in s) for s in row_sums), 'row_sums must be a list of lists of strings containing only # or characters'
|
|
18
|
+
assert all(all(all(cc in legal_chars for cc in c) for c in s) for s in col_sums), 'col_sums must be a list of lists of strings containing only # or characters'
|
|
19
|
+
self.board = board
|
|
20
|
+
self.row_sums, self.col_sums = row_sums, col_sums
|
|
21
|
+
self.characters = characters
|
|
22
|
+
self.min_value, self.max_value = min_value, max_value
|
|
23
|
+
assert (self.max_value - self.min_value + 1) == len(self.characters), f'max_value - min_value + 1 must be equal to the number of characters, got {self.max_value - self.min_value + 1} != {len(self.characters)}'
|
|
24
|
+
self.V, self.H = board.shape
|
|
25
|
+
self.model = cp_model.CpModel()
|
|
26
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
27
|
+
self.dictionary: dict[str, cp_model.IntVar] = {}
|
|
28
|
+
|
|
29
|
+
self.create_vars()
|
|
30
|
+
self.add_all_constraints()
|
|
31
|
+
|
|
32
|
+
def create_vars(self):
|
|
33
|
+
for char in self.characters:
|
|
34
|
+
self.dictionary[char] = self.model.NewIntVar(self.min_value, self.max_value, f'{char}')
|
|
35
|
+
for pos in get_all_pos(self.V, self.H):
|
|
36
|
+
if get_char(self.board, pos) == '#':
|
|
37
|
+
continue
|
|
38
|
+
self.model_vars[pos] = self.model.NewIntVar(self.min_value, self.max_value, f'{pos}')
|
|
39
|
+
|
|
40
|
+
def get_consecutives(self, pos: Pos, direction: Direction) -> Iterator[list[Pos]]:
|
|
41
|
+
consecutive = []
|
|
42
|
+
while in_bounds(pos, self.V, self.H):
|
|
43
|
+
if get_char(self.board, pos) == '#':
|
|
44
|
+
if len(consecutive) > 0:
|
|
45
|
+
yield consecutive
|
|
46
|
+
consecutive = []
|
|
47
|
+
else:
|
|
48
|
+
consecutive.append(pos)
|
|
49
|
+
pos = get_next_pos(pos, direction)
|
|
50
|
+
if len(consecutive) > 0:
|
|
51
|
+
yield consecutive
|
|
52
|
+
|
|
53
|
+
def clue_to_var(self, clue: str) -> cp_model.IntVar:
|
|
54
|
+
res = []
|
|
55
|
+
for i,c in enumerate(clue[::-1]):
|
|
56
|
+
res.append(self.dictionary[c] * 10**i)
|
|
57
|
+
return lxp.sum(res)
|
|
58
|
+
|
|
59
|
+
def add_all_constraints(self):
|
|
60
|
+
self.model.AddAllDifferent(list(self.dictionary.values())) # dictionary must be unique
|
|
61
|
+
for pos in get_all_pos(self.V, self.H):
|
|
62
|
+
c = get_char(self.board, pos).strip()
|
|
63
|
+
if c not in ['', '#']:
|
|
64
|
+
self.model.Add(self.model_vars[pos] == (self.dictionary[c] if c in self.dictionary else int(c)))
|
|
65
|
+
for row in range(self.V): # for row clues
|
|
66
|
+
row_consecutives = self.get_consecutives(get_pos(x=0, y=row), Direction.RIGHT)
|
|
67
|
+
for i, consecutive in enumerate(row_consecutives):
|
|
68
|
+
self.model.AddAllDifferent([self.model_vars[p] for p in consecutive])
|
|
69
|
+
clue = self.row_sums[row][i]
|
|
70
|
+
if clue != '#':
|
|
71
|
+
self.model.Add(lxp.sum([self.model_vars[p] for p in consecutive]) == self.clue_to_var(clue))
|
|
72
|
+
assert len(self.row_sums[row]) == i + 1, f'row_sums[{row}] has {len(self.row_sums[row])} clues, but {i + 1} consecutive cells'
|
|
73
|
+
for col in range(self.H): # for column clues
|
|
74
|
+
col_consecutives = self.get_consecutives(get_pos(x=col, y=0), Direction.DOWN)
|
|
75
|
+
for i, consecutive in enumerate(col_consecutives):
|
|
76
|
+
self.model.AddAllDifferent([self.model_vars[p] for p in consecutive])
|
|
77
|
+
clue = self.col_sums[col][i]
|
|
78
|
+
if clue != '#':
|
|
79
|
+
self.model.Add(lxp.sum([self.model_vars[p] for p in consecutive]) == self.clue_to_var(clue))
|
|
80
|
+
assert len(self.col_sums[col]) == i + 1, f'col_sums[{col}] has {len(self.col_sums[col])} clues, but {i + 1} consecutive cells'
|
|
81
|
+
|
|
82
|
+
def solve_and_print(self, verbose: bool = True):
|
|
83
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
84
|
+
assignment: dict[Pos, int] = {}
|
|
85
|
+
for pos, var in board.model_vars.items():
|
|
86
|
+
assignment[pos] = solver.Value(var)
|
|
87
|
+
return SingleSolution(assignment=assignment)
|
|
88
|
+
def callback(single_res: SingleSolution):
|
|
89
|
+
print("Solution found")
|
|
90
|
+
print(combined_function(self.V, self.H,
|
|
91
|
+
is_shaded=lambda r, c: self.board[r, c] == '#',
|
|
92
|
+
center_char=lambda r, c: str(single_res.assignment[get_pos(x=c, y=r)]),
|
|
93
|
+
text_on_shaded_cells=False
|
|
94
|
+
))
|
|
95
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ortools.sat.python import cp_model
|
|
5
|
+
|
|
6
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_pos, get_row_pos, get_col_pos
|
|
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
|
+
def add_opcode_constraint(model: cp_model.CpModel, vlist: list[cp_model.IntVar], opcode: str, result: int):
|
|
12
|
+
assert opcode in ['+', '-', '*', '/'], "Invalid opcode"
|
|
13
|
+
assert opcode not in ['-', '/'] or len(vlist) == 2, f"Opcode '{opcode}' requires exactly 2 variables"
|
|
14
|
+
if opcode == '+':
|
|
15
|
+
model.Add(sum(vlist) == result)
|
|
16
|
+
elif opcode == '*':
|
|
17
|
+
model.AddMultiplicationEquality(result, vlist)
|
|
18
|
+
elif opcode == '-':
|
|
19
|
+
# either vlist[0] - vlist[1] == result OR vlist[1] - vlist[0] == result
|
|
20
|
+
b = model.NewBoolVar('sub_gate')
|
|
21
|
+
model.Add(vlist[0] - vlist[1] == result).OnlyEnforceIf(b)
|
|
22
|
+
model.Add(vlist[1] - vlist[0] == result).OnlyEnforceIf(b.Not())
|
|
23
|
+
elif opcode == '/':
|
|
24
|
+
# either v0 / v1 == result or v1 / v0 == result
|
|
25
|
+
b = model.NewBoolVar('div_gate')
|
|
26
|
+
# Ensure no division by zero
|
|
27
|
+
model.Add(vlist[0] != 0)
|
|
28
|
+
model.Add(vlist[1] != 0)
|
|
29
|
+
# case 1: v0 / v1 == result → v0 == v1 * result
|
|
30
|
+
model.Add(vlist[0] == vlist[1] * result).OnlyEnforceIf(b)
|
|
31
|
+
# case 2: v1 / v0 == result → v1 == v0 * result
|
|
32
|
+
model.Add(vlist[1] == vlist[0] * result).OnlyEnforceIf(b.Not())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Board:
|
|
36
|
+
def __init__(self, board: np.ndarray, block_results: dict[str, tuple[str, int]], clues: Optional[np.ndarray] = None):
|
|
37
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
38
|
+
assert clues is None or clues.shape == board.shape, f'clues must be 2d, got {clues.shape}'
|
|
39
|
+
assert all((c.item().startswith('d') and c.item()[1:].isdecimal()) for c in np.nditer(board)), "board must contain 'd' prefixed digits"
|
|
40
|
+
block_names = set(c.item() for c in np.nditer(board))
|
|
41
|
+
assert set(block_results.keys()).issubset(block_names), f'block results must contain all block names, {block_names - set(block_results.keys())}'
|
|
42
|
+
self.board = board
|
|
43
|
+
self.clues = clues
|
|
44
|
+
self.V, self.H = board.shape
|
|
45
|
+
self.block_results = {block: (op, result) for block, (op, result) in block_results.items()}
|
|
46
|
+
|
|
47
|
+
self.model = cp_model.CpModel()
|
|
48
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
49
|
+
self.create_vars()
|
|
50
|
+
self.add_all_constraints()
|
|
51
|
+
|
|
52
|
+
def create_vars(self):
|
|
53
|
+
for pos in get_all_pos(self.V, self.H):
|
|
54
|
+
self.model_vars[pos] = self.model.NewIntVar(1, max(self.V, self.H), f'{pos}')
|
|
55
|
+
if self.clues is not None:
|
|
56
|
+
for pos in get_all_pos(self.V, self.H):
|
|
57
|
+
c = get_char(self.clues, pos).strip()
|
|
58
|
+
if int(c) >= 1:
|
|
59
|
+
self.model.Add(self.model_vars[pos] == int(c))
|
|
60
|
+
|
|
61
|
+
def add_all_constraints(self):
|
|
62
|
+
for row in range(self.V): # 1 number per row
|
|
63
|
+
self.model.AddAllDifferent([self.model_vars[pos] for pos in get_row_pos(row, self.H)])
|
|
64
|
+
for col in range(self.H): # 1 number per column
|
|
65
|
+
self.model.AddAllDifferent([self.model_vars[pos] for pos in get_col_pos(col, self.V)])
|
|
66
|
+
for block, (op, result) in self.block_results.items(): # cage op code
|
|
67
|
+
block_vars = [self.model_vars[p] for p in get_all_pos(self.V, self.H) if get_char(self.board, p) == block]
|
|
68
|
+
add_opcode_constraint(self.model, vlist=block_vars, opcode=op, result=result)
|
|
69
|
+
|
|
70
|
+
def solve_and_print(self, verbose: bool = True):
|
|
71
|
+
def board_to_solution(board: "Board", solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
72
|
+
return SingleSolution(assignment={pos: solver.Value(board.model_vars[pos]) for pos in get_all_pos(board.V, board.H)})
|
|
73
|
+
def callback(single_res: SingleSolution):
|
|
74
|
+
print("Solution found")
|
|
75
|
+
print(combined_function(self.V, self.H, center_char=lambda r, c: single_res.assignment[get_pos(x=c, y=r)]))
|
|
76
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=10)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ortools.sat.python import cp_model
|
|
5
|
+
|
|
6
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_pos, Direction, in_bounds, get_next_pos
|
|
7
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, or_constraint
|
|
8
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_eq_var(model: cp_model.CpModel, a: cp_model.IntVar, b: cp_model.IntVar) -> cp_model.IntVar:
|
|
12
|
+
eq_var = model.NewBoolVar(f'{a}:{b}:eq')
|
|
13
|
+
model.Add(a == b).OnlyEnforceIf(eq_var)
|
|
14
|
+
model.Add(a != b).OnlyEnforceIf(eq_var.Not())
|
|
15
|
+
return eq_var
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Board:
|
|
19
|
+
def __init__(self, board: np.array, horiz_board: np.array, vert_board: np.array, digits: Optional[list[int]] = None):
|
|
20
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
21
|
+
assert all((str(c.item()).strip() == '') or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or digits'
|
|
22
|
+
self.board = board
|
|
23
|
+
self.V, self.H = board.shape
|
|
24
|
+
assert horiz_board.shape == (self.V, self.H - 1), f'horiz_board must be {(self.V, self.H - 1)}, got {horiz_board.shape}'
|
|
25
|
+
assert all((str(c.item()).strip() == '') or str(c.item()).strip() in ['B', 'W'] for c in np.nditer(horiz_board)), 'horiz_board must contain only space or digits'
|
|
26
|
+
assert vert_board.shape == (self.V - 1, self.H), f'vert_board must be {(self.V - 1, self.H)}, got {vert_board.shape}'
|
|
27
|
+
assert all((str(c.item()).strip() == '') or str(c.item()).strip() in ['B', 'W'] for c in np.nditer(vert_board)), 'vert_board must contain only space or digits'
|
|
28
|
+
self.horiz_board = horiz_board
|
|
29
|
+
self.vert_board = vert_board
|
|
30
|
+
if digits is None:
|
|
31
|
+
digits = list(range(1, max(self.V, self.H) + 1))
|
|
32
|
+
assert len(digits) >= max(self.V, self.H), 'digits must be at least as long as the board'
|
|
33
|
+
self.digits = digits
|
|
34
|
+
|
|
35
|
+
self.model = cp_model.CpModel()
|
|
36
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
37
|
+
self.digits_2_1_ratio: dict[tuple[Pos, Pos], cp_model.IntVar] = {}
|
|
38
|
+
self.digits_consecutive: dict[tuple[Pos, Pos], cp_model.IntVar] = {}
|
|
39
|
+
self.create_vars()
|
|
40
|
+
self.add_all_constraints()
|
|
41
|
+
|
|
42
|
+
def create_vars(self):
|
|
43
|
+
allowed_values = cp_model.Domain.FromValues(self.digits)
|
|
44
|
+
for pos in get_all_pos(self.V, self.H): # force clues
|
|
45
|
+
self.model_vars[pos] = self.model.NewIntVarFromDomain(allowed_values, f'{pos}')
|
|
46
|
+
for direction in [Direction.RIGHT, Direction.DOWN]:
|
|
47
|
+
neighbor = get_next_pos(pos, direction)
|
|
48
|
+
if not in_bounds(neighbor, self.V, self.H):
|
|
49
|
+
continue
|
|
50
|
+
self.digits_2_1_ratio[(pos, neighbor)] = self.model.NewBoolVar(f'{pos}:{neighbor}:2_1')
|
|
51
|
+
self.digits_consecutive[(pos, neighbor)] = self.model.NewBoolVar(f'{pos}:{neighbor}:consecutive')
|
|
52
|
+
|
|
53
|
+
def add_all_constraints(self):
|
|
54
|
+
for pos in get_all_pos(self.V, self.H): # force clues
|
|
55
|
+
c = get_char(self.board, pos)
|
|
56
|
+
if not str(c).isdecimal():
|
|
57
|
+
continue
|
|
58
|
+
self.model.Add(self.model_vars[pos] == int(c))
|
|
59
|
+
# all columns and rows are unique
|
|
60
|
+
for row in range(self.V):
|
|
61
|
+
self.model.AddAllDifferent([self.model_vars[get_pos(x=c, y=row)] for c in range(self.H)])
|
|
62
|
+
for col in range(self.H):
|
|
63
|
+
self.model.AddAllDifferent([self.model_vars[get_pos(x=col, y=r)] for r in range(self.V)])
|
|
64
|
+
for p in get_all_pos(self.V, self.H): # force horiz and vert relationships between digits
|
|
65
|
+
for direction in [Direction.RIGHT, Direction.DOWN]:
|
|
66
|
+
neighbor = get_next_pos(p, direction)
|
|
67
|
+
if not in_bounds(neighbor, self.V, self.H):
|
|
68
|
+
continue
|
|
69
|
+
self.setup_aux(p, direction)
|
|
70
|
+
c = get_char(self.horiz_board if direction == Direction.RIGHT else self.vert_board, p)
|
|
71
|
+
if c == 'B': # 2:1 ratio
|
|
72
|
+
self.model.Add(self.digits_2_1_ratio[(p, neighbor)] == 1)
|
|
73
|
+
elif c == 'W': # consecutive
|
|
74
|
+
self.model.Add(self.digits_consecutive[(p, neighbor)] == 1)
|
|
75
|
+
else: # neither
|
|
76
|
+
self.model.Add(self.digits_2_1_ratio[(p, neighbor)] == 0)
|
|
77
|
+
self.model.Add(self.digits_consecutive[(p, neighbor)] == 0)
|
|
78
|
+
|
|
79
|
+
def setup_aux(self, pos: Pos, direction: Direction):
|
|
80
|
+
neighbor = get_next_pos(pos, direction)
|
|
81
|
+
a_plus_one_b = get_eq_var(self.model, self.model_vars[pos] + 1, self.model_vars[neighbor])
|
|
82
|
+
b_plus_one_a = get_eq_var(self.model, self.model_vars[neighbor] + 1, self.model_vars[pos])
|
|
83
|
+
or_constraint(self.model, self.digits_consecutive[(pos, neighbor)], [a_plus_one_b, b_plus_one_a]) # consecutive aux
|
|
84
|
+
a_twice_b = get_eq_var(self.model, self.model_vars[pos], 2 * self.model_vars[neighbor])
|
|
85
|
+
b_twice_a = get_eq_var(self.model, self.model_vars[neighbor], 2 * self.model_vars[pos])
|
|
86
|
+
or_constraint(self.model, self.digits_2_1_ratio[(pos, neighbor)], [a_twice_b, b_twice_a]) # 2:1 ratio aux
|
|
87
|
+
|
|
88
|
+
def solve_and_print(self, verbose: bool = True):
|
|
89
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
90
|
+
return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
|
|
91
|
+
def callback(single_res: SingleSolution):
|
|
92
|
+
print("Solution found")
|
|
93
|
+
print(combined_function(self.V, self.H, center_char=lambda r, c: str(single_res.assignment[get_pos(x=c, y=r)])))
|
|
94
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=1)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ortools.sat.python import cp_model
|
|
3
|
+
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
4
|
+
|
|
5
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, in_bounds, get_next_pos, get_neighbors4, Direction, get_pos
|
|
6
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
7
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def laser_out(board: np.array, init_pos: Pos) -> list[Pos]:
|
|
11
|
+
'laser out in all 4 directions until we hit a wall or out of bounds'
|
|
12
|
+
V, H = board.shape
|
|
13
|
+
result = []
|
|
14
|
+
for direction in Direction:
|
|
15
|
+
cur_pos = init_pos
|
|
16
|
+
while True:
|
|
17
|
+
cur_pos = get_next_pos(cur_pos, direction)
|
|
18
|
+
if not in_bounds(cur_pos, V, H) or get_char(board, cur_pos).strip() != '':
|
|
19
|
+
break
|
|
20
|
+
result.append(cur_pos)
|
|
21
|
+
return result
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Board:
|
|
25
|
+
def __init__(self, board: np.array):
|
|
26
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
27
|
+
assert all((c.item().strip() in ['', 'W']) or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or W or numbers'
|
|
28
|
+
self.board = board
|
|
29
|
+
self.V, self.H = board.shape
|
|
30
|
+
|
|
31
|
+
self.model = cp_model.CpModel()
|
|
32
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
33
|
+
self.create_vars()
|
|
34
|
+
self.add_all_constraints()
|
|
35
|
+
|
|
36
|
+
def create_vars(self):
|
|
37
|
+
for pos in get_all_pos(self.V, self.H):
|
|
38
|
+
self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
|
|
39
|
+
|
|
40
|
+
def add_all_constraints(self):
|
|
41
|
+
for pos in get_all_pos(self.V, self.H): # force N lights touching the number N
|
|
42
|
+
c = get_char(self.board, pos).strip()
|
|
43
|
+
if c not in ['', 'W']:
|
|
44
|
+
self.model.Add(self.model_vars[pos] == 0)
|
|
45
|
+
self.model.Add(lxp.Sum([self.model_vars[p] for p in get_neighbors4(pos, self.V, self.H)]) == int(c))
|
|
46
|
+
else: # not numbered, must be lit
|
|
47
|
+
orthoginals = laser_out(self.board, pos)
|
|
48
|
+
self.model.AddAtLeastOne([self.model_vars[p] for p in orthoginals] + [self.model_vars[pos]])
|
|
49
|
+
for ortho in orthoginals:
|
|
50
|
+
self.model.Add(self.model_vars[ortho] == 0).OnlyEnforceIf(self.model_vars[pos])
|
|
51
|
+
|
|
52
|
+
def solve_and_print(self, verbose: bool = True):
|
|
53
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
54
|
+
return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
|
|
55
|
+
def callback(single_res: SingleSolution):
|
|
56
|
+
print("Solution found")
|
|
57
|
+
print(combined_function(self.V, self.H, is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1, center_char=lambda r, c: str(self.board[r, c]).strip()))
|
|
58
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ortools.sat.python import cp_model
|
|
5
|
+
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
6
|
+
|
|
7
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_neighbors8, get_pos, Direction, get_next_pos, get_opposite_direction, in_bounds
|
|
8
|
+
from puzzle_solver.core.utils_ortools import force_connected_component, generic_solve_all, SingleSolution
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Board:
|
|
13
|
+
def __init__(self, board: np.array):
|
|
14
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
15
|
+
assert all((c.item() == ' ') or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or digits'
|
|
16
|
+
self.board = board
|
|
17
|
+
self.V, self.H = board.shape
|
|
18
|
+
|
|
19
|
+
self.model = cp_model.CpModel()
|
|
20
|
+
self.cell_active: dict[Pos, cp_model.IntVar] = {}
|
|
21
|
+
self.cell_direction: dict[tuple[Pos, Direction], cp_model.IntVar] = {}
|
|
22
|
+
self.create_vars()
|
|
23
|
+
self.add_all_constraints()
|
|
24
|
+
|
|
25
|
+
def create_vars(self):
|
|
26
|
+
for pos in get_all_pos(self.V, self.H):
|
|
27
|
+
self.cell_active[pos] = self.model.NewBoolVar(f'{pos}')
|
|
28
|
+
for direction in Direction:
|
|
29
|
+
next_pos = get_next_pos(pos, direction)
|
|
30
|
+
opposite_direction = get_opposite_direction(direction)
|
|
31
|
+
if (next_pos, opposite_direction) in self.cell_direction:
|
|
32
|
+
self.cell_direction[(pos, direction)] = self.cell_direction[(next_pos, opposite_direction)]
|
|
33
|
+
elif not in_bounds(next_pos, self.V, self.H):
|
|
34
|
+
self.cell_direction[(pos, direction)] = self.model.NewConstant(0)
|
|
35
|
+
else:
|
|
36
|
+
self.cell_direction[(pos, direction)] = self.model.NewBoolVar(f'{pos}:{direction}')
|
|
37
|
+
|
|
38
|
+
def add_all_constraints(self):
|
|
39
|
+
for pos in get_all_pos(self.V, self.H): # force state
|
|
40
|
+
sum_directions = lxp.Sum([self.cell_direction[(pos, direction)] for direction in Direction])
|
|
41
|
+
self.model.Add(sum_directions == 2).OnlyEnforceIf(self.cell_active[pos])
|
|
42
|
+
self.model.Add(sum_directions == 0).OnlyEnforceIf(self.cell_active[pos].Not())
|
|
43
|
+
c = get_char(self.board, pos).strip() # force clues
|
|
44
|
+
if c:
|
|
45
|
+
self.model.Add(self.cell_active[pos] == 0)
|
|
46
|
+
self.model.Add(lxp.Sum([self.cell_active[n] for n in get_neighbors8(pos, self.V, self.H, include_self=False)]) == int(c))
|
|
47
|
+
def is_neighbor(pd1: tuple[Pos, Direction], pd2: tuple[Pos, Direction]) -> bool:
|
|
48
|
+
p1, d1 = pd1
|
|
49
|
+
p2, d2 = pd2
|
|
50
|
+
if p1 == p2 and d1 != d2: # same position, different direction, is neighbor
|
|
51
|
+
return True
|
|
52
|
+
if get_next_pos(p1, d1) == p2 and d2 == get_opposite_direction(d1):
|
|
53
|
+
return True
|
|
54
|
+
return False
|
|
55
|
+
force_connected_component(self.model, self.cell_direction, is_neighbor=is_neighbor)
|
|
56
|
+
|
|
57
|
+
def solve_and_print(self, verbose: bool = True):
|
|
58
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
59
|
+
assignment: dict[Pos, str] = defaultdict(str)
|
|
60
|
+
for pos in get_all_pos(self.V, self.H):
|
|
61
|
+
for direction in Direction:
|
|
62
|
+
if (pos, direction) in board.cell_direction and solver.Value(board.cell_direction[(pos, direction)]) == 1:
|
|
63
|
+
assignment[pos] += direction.name[0]
|
|
64
|
+
return SingleSolution(assignment=assignment)
|
|
65
|
+
def callback(single_res: SingleSolution):
|
|
66
|
+
print("Solution found")
|
|
67
|
+
print(combined_function(self.V, self.H, show_grid=False,
|
|
68
|
+
special_content=lambda r, c: single_res.assignment[get_pos(x=c, y=r)],
|
|
69
|
+
center_char=lambda r, c: str(self.board[r, c]).strip())
|
|
70
|
+
)
|
|
71
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +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_char, get_pos, shapes_between, Shape
|
|
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
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ShapeOnBoard:
|
|
14
|
+
uid: int
|
|
15
|
+
is_active: cp_model.IntVar
|
|
16
|
+
size: int
|
|
17
|
+
shape: Shape
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Board:
|
|
21
|
+
def __init__(self, board: np.array):
|
|
22
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
23
|
+
self.board = board
|
|
24
|
+
self.V, self.H = board.shape
|
|
25
|
+
self.pos_to_char: dict[Pos, str] = {p: get_char(board, p).strip() for p in get_all_pos(self.V, self.H) if get_char(board, p).strip() != ''}
|
|
26
|
+
|
|
27
|
+
self.model = cp_model.CpModel()
|
|
28
|
+
self.all_shapes: list[ShapeOnBoard] = []
|
|
29
|
+
self.pos_to_shapes: dict[Pos, set[ShapeOnBoard]] = defaultdict(set)
|
|
30
|
+
self.create_shapes()
|
|
31
|
+
self.add_all_constraints()
|
|
32
|
+
|
|
33
|
+
def create_shapes(self):
|
|
34
|
+
num_to_pos: dict[tuple[str, int], list[Pos]] = defaultdict(list)
|
|
35
|
+
single_to_pos: list[tuple[str, Pos]] = []
|
|
36
|
+
for p, s in self.pos_to_char.items(): # all cells that arent empty
|
|
37
|
+
if s.isdecimal(): # black cell
|
|
38
|
+
color = 'black'
|
|
39
|
+
N = int(s)
|
|
40
|
+
else: # colored cell
|
|
41
|
+
color = s.split('_')[0]
|
|
42
|
+
N = int(s.split('_')[1])
|
|
43
|
+
if N == 1: # cell with a 1
|
|
44
|
+
single_to_pos.append((color, p))
|
|
45
|
+
else: # cell with a number >= 2
|
|
46
|
+
num_to_pos[(color, N)].append(p)
|
|
47
|
+
|
|
48
|
+
for color, p in single_to_pos: # all cells with a 1
|
|
49
|
+
s = ShapeOnBoard(uid=len(self.all_shapes), is_active=self.model.NewBoolVar(f'{color}_{p}'), size=1, shape=frozenset([p]))
|
|
50
|
+
self.all_shapes.append(s)
|
|
51
|
+
self.pos_to_shapes[p].add(s)
|
|
52
|
+
|
|
53
|
+
for (_, N), plist in num_to_pos.items(): # all cells with a number >= 2
|
|
54
|
+
assert len(plist) % 2 == 0, f'{s} has {len(plist)} positions, must be even'
|
|
55
|
+
for i, pi in enumerate(plist):
|
|
56
|
+
for _j, pj in enumerate(plist[i+1:]): # don't double count
|
|
57
|
+
self.populate_pair(pi, pj, N)
|
|
58
|
+
|
|
59
|
+
def populate_pair(self, pos1: Pos, pos2: Pos, N: int):
|
|
60
|
+
for shape in shapes_between(pos1, pos2, N):
|
|
61
|
+
number_cells_hit = {p for p in shape if p in self.pos_to_char}
|
|
62
|
+
assert number_cells_hit.issuperset({pos1, pos2}), f'Not possible! shape {shape} should always hit pos1 and pos2; this error means there\'s a bug in shapes_between'
|
|
63
|
+
if number_cells_hit != {pos1, pos2}: # shape hit some numbered cells other than pos1 and pos2
|
|
64
|
+
continue
|
|
65
|
+
s = ShapeOnBoard(uid=len(self.all_shapes), is_active=self.model.NewBoolVar(f'{shape}'), size=N, shape=shape)
|
|
66
|
+
self.all_shapes.append(s)
|
|
67
|
+
self.pos_to_shapes[pos1].add(s)
|
|
68
|
+
self.pos_to_shapes[pos2].add(s)
|
|
69
|
+
|
|
70
|
+
def add_all_constraints(self):
|
|
71
|
+
for pos in self.pos_to_char.keys(): # every numbered cell must have exactly one shape active touch it
|
|
72
|
+
shapes_on_pos = [s.is_active for s in self.pos_to_shapes[pos]]
|
|
73
|
+
assert len(shapes_on_pos) >= 1, f'pos {pos} has no shapes on it. No solution possible!!!'
|
|
74
|
+
self.model.AddExactlyOne(shapes_on_pos)
|
|
75
|
+
for s1 in self.all_shapes: # active shapes can't collide
|
|
76
|
+
for s2 in self.all_shapes:
|
|
77
|
+
if s1.uid != s2.uid and s1.shape.intersection(s2.shape):
|
|
78
|
+
self.model.Add(s1.is_active + s2.is_active <= 1)
|
|
79
|
+
|
|
80
|
+
def solve_and_print(self, verbose: bool = True):
|
|
81
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
82
|
+
return SingleSolution(assignment={pos: (s.uid, s.size) for s in self.all_shapes for pos in s.shape if solver.Value(s.is_active) == 1})
|
|
83
|
+
def callback(single_res: SingleSolution):
|
|
84
|
+
print("Solution found")
|
|
85
|
+
arr_dict = {k: v[0] for k, v in single_res.assignment.items()}
|
|
86
|
+
arr_size = {k: v[1] for k, v in single_res.assignment.items()}
|
|
87
|
+
print(combined_function(self.V, self.H,
|
|
88
|
+
cell_flags=id_board_to_wall_fn(np.array([[arr_dict.get(get_pos(x=c, y=r), "") for c in range(self.H)] for r in range(self.V)])),
|
|
89
|
+
center_char=lambda r, c: f'{arr_size.get(get_pos(x=c, y=r), "")}'
|
|
90
|
+
))
|
|
91
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=99)
|