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,82 @@
|
|
|
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, set_char, Direction, get_opposite_direction, get_next_pos, in_bounds, get_row_pos, get_col_pos
|
|
7
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
8
|
+
|
|
9
|
+
def get_direction(pos: Pos, board: np.array) -> Optional[Direction]:
|
|
10
|
+
if get_char(board, pos) == 'R':
|
|
11
|
+
return Direction.RIGHT
|
|
12
|
+
elif get_char(board, pos) == 'D':
|
|
13
|
+
return Direction.DOWN
|
|
14
|
+
elif get_char(board, pos) == 'U':
|
|
15
|
+
return Direction.UP
|
|
16
|
+
elif get_char(board, pos) == 'L':
|
|
17
|
+
return Direction.LEFT
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
def move_backward(pos: Pos, board: np.array) -> Pos:
|
|
21
|
+
for direction in Direction:
|
|
22
|
+
opposite_direction = get_opposite_direction(direction)
|
|
23
|
+
neighbor = get_next_pos(pos, direction)
|
|
24
|
+
if in_bounds(neighbor, board.shape[0], board.shape[1]) and get_direction(neighbor, board) == opposite_direction: # the neighbor is pointing to me
|
|
25
|
+
return neighbor
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
class Board:
|
|
29
|
+
def __init__(self, board: np.array, top: np.array, side: np.array):
|
|
30
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
31
|
+
self.V = board.shape[0]
|
|
32
|
+
self.H = board.shape[1]
|
|
33
|
+
assert top.ndim == 1 and top.shape[0] == self.H, 'top must be a 1d array of length board width'
|
|
34
|
+
assert side.ndim == 1 and side.shape[0] == self.V, 'side must be a 1d array of length board height'
|
|
35
|
+
assert all((c in ['R', 'D', 'U', 'X', 'L']) for c in np.nditer(board)), 'board must contain only valid characters: R, D, U, X, L'
|
|
36
|
+
self.board = board
|
|
37
|
+
self.top = top
|
|
38
|
+
self.side = side
|
|
39
|
+
self.tip: set[Pos] = {pos for pos in get_all_pos(self.V, self.H) if get_char(self.board, pos) == 'X'}
|
|
40
|
+
|
|
41
|
+
self.model = cp_model.CpModel()
|
|
42
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
43
|
+
|
|
44
|
+
self.create_vars()
|
|
45
|
+
self.add_all_constraints()
|
|
46
|
+
|
|
47
|
+
def create_vars(self):
|
|
48
|
+
for pos in get_all_pos(self.V, self.H):
|
|
49
|
+
self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
|
|
50
|
+
|
|
51
|
+
def add_all_constraints(self):
|
|
52
|
+
visited: set[Pos] = set()
|
|
53
|
+
for cur_pos in self.tip:
|
|
54
|
+
visited.add(cur_pos)
|
|
55
|
+
while cur_pos is not None:
|
|
56
|
+
backward_pos = move_backward(cur_pos, self.board)
|
|
57
|
+
if backward_pos is None:
|
|
58
|
+
break
|
|
59
|
+
self.model.Add(self.model_vars[backward_pos] == 1).OnlyEnforceIf(self.model_vars[cur_pos])
|
|
60
|
+
cur_pos = backward_pos
|
|
61
|
+
visited.add(cur_pos)
|
|
62
|
+
assert len(visited) == self.V * self.H, f'all positions must be visited, got {len(visited)}. missing {set(get_all_pos(self.V, self.H)) - visited}'
|
|
63
|
+
for row in range(self.V):
|
|
64
|
+
self.model.Add(sum([self.model_vars[pos] for pos in get_row_pos(row, self.H)]) == self.side[row])
|
|
65
|
+
for col in range(self.H):
|
|
66
|
+
self.model.Add(sum([self.model_vars[pos] for pos in get_col_pos(col, self.V)]) == self.top[col])
|
|
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
|
+
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
77
|
+
for pos in get_all_pos(self.V, self.H):
|
|
78
|
+
c = get_char(self.board, pos)
|
|
79
|
+
c = 'X' if single_res.assignment[pos] == 1 else ' '
|
|
80
|
+
set_char(res, pos, c)
|
|
81
|
+
print(res)
|
|
82
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,89 @@
|
|
|
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, set_char, get_pos, get_row_pos, get_col_pos
|
|
6
|
+
from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, SingleSolution
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def bool_from_greater_than(model, a, b, name):
|
|
10
|
+
res = model.NewBoolVar(name)
|
|
11
|
+
model.add(a > b).OnlyEnforceIf(res)
|
|
12
|
+
model.add(a <= b).OnlyEnforceIf(res.Not())
|
|
13
|
+
return res
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Board:
|
|
17
|
+
def __init__(self, board: np.array, top: np.array, bottom: np.array, right: np.array, left: np.array):
|
|
18
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
19
|
+
assert all((c == ' ') or str(c).isdecimal() for c in np.nditer(board)), 'board must contain space or digits'
|
|
20
|
+
self.board = board
|
|
21
|
+
self.V, self.H = board.shape
|
|
22
|
+
assert top.shape == (self.H,) and bottom.shape == (self.H,) and right.shape == (self.V,) and left.shape == (self.V,), 'top, bottom, right, and left must be 1d arrays of length board width and height'
|
|
23
|
+
self.top = top
|
|
24
|
+
self.bottom = bottom
|
|
25
|
+
self.right = right
|
|
26
|
+
self.left = left
|
|
27
|
+
self.model = cp_model.CpModel()
|
|
28
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
29
|
+
|
|
30
|
+
self.create_vars()
|
|
31
|
+
self.add_all_constraints()
|
|
32
|
+
|
|
33
|
+
def create_vars(self):
|
|
34
|
+
for pos in get_all_pos(self.V, self.H):
|
|
35
|
+
self.model_vars[pos] = self.model.NewIntVar(1, max(self.V, self.H), f'{pos}')
|
|
36
|
+
|
|
37
|
+
def add_all_constraints(self):
|
|
38
|
+
for pos in get_all_pos(self.V, self.H): # force board clues
|
|
39
|
+
v = get_char(self.board, pos)
|
|
40
|
+
if str(v).isdecimal():
|
|
41
|
+
self.model.Add(self.model_vars[pos] == int(v))
|
|
42
|
+
for row_i in range(self.V): # all different for rows
|
|
43
|
+
self.model.AddAllDifferent([self.model_vars[pos] for pos in get_row_pos(row_i, self.H)])
|
|
44
|
+
for col_i in range(self.H): # all different for cols
|
|
45
|
+
self.model.AddAllDifferent([self.model_vars[pos] for pos in get_col_pos(col_i, self.V)])
|
|
46
|
+
for x in range(self.H): # top
|
|
47
|
+
self.tower_constraints(real=self.top[x], pos_list=[get_pos(x=x, y=y) for y in range(self.V)], name=f'top:{x}')
|
|
48
|
+
for x in range(self.H): # bottom
|
|
49
|
+
self.tower_constraints(real=self.bottom[x], pos_list=[get_pos(x=x, y=y) for y in range(self.V-1, -1, -1)], name=f'bottom:{x}')
|
|
50
|
+
for y in range(self.V): # left
|
|
51
|
+
self.tower_constraints(real=self.left[y], pos_list=[get_pos(x=x, y=y) for x in range(self.H)], name=f'left:{y}')
|
|
52
|
+
for y in range(self.H): # right
|
|
53
|
+
self.tower_constraints(real=self.right[y], pos_list=[get_pos(x=x, y=y) for x in range(self.V-1, -1, -1)], name=f'right:{y}')
|
|
54
|
+
|
|
55
|
+
def tower_constraints(self, real: int, pos_list: list[Pos], name: str):
|
|
56
|
+
if real == -1:
|
|
57
|
+
return
|
|
58
|
+
can_see_variables = []
|
|
59
|
+
previous_towers: list[cp_model.IntVar] = []
|
|
60
|
+
for pos in pos_list:
|
|
61
|
+
current_tower = self.model_vars[pos]
|
|
62
|
+
can_see_variables.append(self.can_see_tower(previous_towers, current_tower, f'{name}:{pos}'))
|
|
63
|
+
previous_towers.append(current_tower)
|
|
64
|
+
self.model.add(lxp.sum(can_see_variables) == real)
|
|
65
|
+
|
|
66
|
+
def can_see_tower(self, blocks: list[cp_model.IntVar], tower: cp_model.IntVar, name: str) -> cp_model.IntVar:
|
|
67
|
+
"""Returns a boolean variable of whether a position BEFORE the blocks can see the "tower" parameter."""
|
|
68
|
+
if len(blocks) == 0:
|
|
69
|
+
return True
|
|
70
|
+
# I can see "tower" if it's larger that all the blocks before it, lits is a list of [(tower > b0), (tower > b1), ..., (tower > bi)]
|
|
71
|
+
res = self.model.NewBoolVar(name)
|
|
72
|
+
and_constraint(self.model, target=res, cs=[bool_from_greater_than(self.model, tower, block, f'{name}:lits:{block}') for block in blocks])
|
|
73
|
+
return res
|
|
74
|
+
|
|
75
|
+
def solve_and_print(self, verbose: bool = True):
|
|
76
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
77
|
+
assignment: dict[Pos, int] = {}
|
|
78
|
+
for pos, var in board.model_vars.items():
|
|
79
|
+
assignment[pos] = solver.value(var)
|
|
80
|
+
return SingleSolution(assignment=assignment)
|
|
81
|
+
def callback(single_res: SingleSolution):
|
|
82
|
+
print("Solution found")
|
|
83
|
+
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
84
|
+
for pos in get_all_pos(self.V, self.H):
|
|
85
|
+
c = get_char(self.board, pos)
|
|
86
|
+
c = single_res.assignment[pos]
|
|
87
|
+
set_char(res, pos, c)
|
|
88
|
+
print(res)
|
|
89
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ortools.sat.python import cp_model
|
|
4
|
+
|
|
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
|
+
from puzzle_solver.core.utils_ortools import force_connected_component, generic_solve_all, SingleSolution
|
|
7
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Board:
|
|
11
|
+
def __init__(self, board: np.array, side: np.array, top: np.array):
|
|
12
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
13
|
+
assert all((len(c.item()) == 2) and all(ch in [' ', 'U', 'L', 'D', 'R'] for ch in c.item()) for c in np.nditer(board)), 'board must contain only digits or space'
|
|
14
|
+
self.board = board
|
|
15
|
+
self.V, self.H = board.shape
|
|
16
|
+
self.side = side
|
|
17
|
+
self.top = top
|
|
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
|
+
|
|
23
|
+
self.create_vars()
|
|
24
|
+
self.add_all_constraints()
|
|
25
|
+
|
|
26
|
+
def create_vars(self):
|
|
27
|
+
for pos in get_all_pos(self.V, self.H):
|
|
28
|
+
self.cell_active[pos] = self.model.NewBoolVar(f'{pos}')
|
|
29
|
+
for direction in Direction:
|
|
30
|
+
next_pos = get_next_pos(pos, direction)
|
|
31
|
+
opposite_direction = get_opposite_direction(direction)
|
|
32
|
+
if (next_pos, opposite_direction) in self.cell_direction:
|
|
33
|
+
self.cell_direction[(pos, direction)] = self.cell_direction[(next_pos, opposite_direction)]
|
|
34
|
+
else:
|
|
35
|
+
self.cell_direction[(pos, direction)] = self.model.NewBoolVar(f'{pos}:{direction}')
|
|
36
|
+
|
|
37
|
+
def add_all_constraints(self):
|
|
38
|
+
# force the already given hints
|
|
39
|
+
str_to_direction = {'U': Direction.UP, 'L': Direction.LEFT, 'D': Direction.DOWN, 'R': Direction.RIGHT}
|
|
40
|
+
for pos in get_all_pos(self.V, self.H):
|
|
41
|
+
for char in get_char(self.board, pos).strip():
|
|
42
|
+
self.model.Add(self.cell_direction[(pos, str_to_direction[char])] == 1)
|
|
43
|
+
|
|
44
|
+
# force the already given sides
|
|
45
|
+
for i in range(self.V):
|
|
46
|
+
self.model.Add(sum([self.cell_active[pos] for pos in get_row_pos(i, self.H)]) == self.side[i])
|
|
47
|
+
for i in range(self.H):
|
|
48
|
+
self.model.Add(sum([self.cell_active[pos] for pos in get_col_pos(i, self.V)]) == self.top[i])
|
|
49
|
+
|
|
50
|
+
# cell active means exactly 2 directions are active, cell not active means no directions are active
|
|
51
|
+
for pos in get_all_pos(self.V, self.H):
|
|
52
|
+
s = sum([self.cell_direction[(pos, direction)] for direction in Direction])
|
|
53
|
+
self.model.Add(s == 2).OnlyEnforceIf(self.cell_active[pos])
|
|
54
|
+
self.model.Add(s == 0).OnlyEnforceIf(self.cell_active[pos].Not())
|
|
55
|
+
|
|
56
|
+
# force borders to only have 2 point outside the board
|
|
57
|
+
pointing_outside = []
|
|
58
|
+
for pos in get_col_pos(0, self.V): # left border
|
|
59
|
+
pointing_outside.append(self.cell_direction[(pos, Direction.LEFT)])
|
|
60
|
+
for pos in get_col_pos(self.H - 1, self.V): # right border
|
|
61
|
+
pointing_outside.append(self.cell_direction[(pos, Direction.RIGHT)])
|
|
62
|
+
for pos in get_row_pos(0, self.H): # top border
|
|
63
|
+
pointing_outside.append(self.cell_direction[(pos, Direction.UP)])
|
|
64
|
+
for pos in get_row_pos(self.V - 1, self.H): # bottom border
|
|
65
|
+
pointing_outside.append(self.cell_direction[(pos, Direction.DOWN)])
|
|
66
|
+
self.model.Add(sum(pointing_outside) == 2)
|
|
67
|
+
|
|
68
|
+
# force single connected component
|
|
69
|
+
def is_neighbor(pd1: tuple[Pos, Direction], pd2: tuple[Pos, Direction]) -> bool:
|
|
70
|
+
p1, d1 = pd1
|
|
71
|
+
p2, d2 = pd2
|
|
72
|
+
if p1 == p2: # same position, different direction, is neighbor
|
|
73
|
+
return True
|
|
74
|
+
if get_next_pos(p1, d1) == p2 and d2 == get_opposite_direction(d1):
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
force_connected_component(self.model, self.cell_direction, is_neighbor=is_neighbor)
|
|
78
|
+
|
|
79
|
+
def solve_and_print(self, verbose: bool = True):
|
|
80
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
81
|
+
assignment: dict[Pos, str] = defaultdict(str)
|
|
82
|
+
for (pos, direction), var in board.cell_direction.items():
|
|
83
|
+
assignment[pos] += direction.name[0] if solver.BooleanValue(var) else ''
|
|
84
|
+
return SingleSolution(assignment=assignment)
|
|
85
|
+
def callback(single_res: SingleSolution):
|
|
86
|
+
print("Solution found")
|
|
87
|
+
print(combined_function(self.V, self.H, show_grid=False, special_content=lambda r, c: single_res.assignment[get_pos(x=c, y=r)].strip(), center_char=lambda r, c: '.', text_on_shaded_cells=False))
|
|
88
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=20)
|
|
@@ -0,0 +1,48 @@
|
|
|
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, get_neighbors8, get_pos, get_row_pos, get_col_pos
|
|
6
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
7
|
+
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Board:
|
|
11
|
+
def __init__(self, board: np.array):
|
|
12
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
13
|
+
assert all((str(c.item()).isdecimal() for c in np.nditer(board))), 'board must contain only digits'
|
|
14
|
+
self.board = board
|
|
15
|
+
self.V, self.H = board.shape
|
|
16
|
+
self.block_numbers = {int(c.item()) for c in np.nditer(board)}
|
|
17
|
+
self.blocks = {num: [] for num in self.block_numbers}
|
|
18
|
+
for pos in get_all_pos(self.V, self.H):
|
|
19
|
+
self.blocks[int(get_char(board, pos))].append(pos)
|
|
20
|
+
|
|
21
|
+
self.model = cp_model.CpModel()
|
|
22
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
23
|
+
self.create_vars()
|
|
24
|
+
self.add_all_constraints()
|
|
25
|
+
|
|
26
|
+
def create_vars(self):
|
|
27
|
+
for pos in get_all_pos(self.V, self.H):
|
|
28
|
+
self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
|
|
29
|
+
|
|
30
|
+
def add_all_constraints(self):
|
|
31
|
+
for row in range(self.V): # each row must have exactly one tree
|
|
32
|
+
self.model.Add(lxp.sum([self.model_vars[pos] for pos in get_row_pos(row, self.H)]) == 1)
|
|
33
|
+
for col in range(self.H): # each column must have exactly one tree
|
|
34
|
+
self.model.Add(lxp.sum([self.model_vars[pos] for pos in get_col_pos(col, self.V)]) == 1)
|
|
35
|
+
for block_number in self.block_numbers: # each block must have exactly one tree
|
|
36
|
+
self.model.Add(lxp.sum([self.model_vars[pos] for pos in self.blocks[block_number]]) == 1)
|
|
37
|
+
# trees cannot touch even diagonally
|
|
38
|
+
for pos in get_all_pos(self.V, self.H):
|
|
39
|
+
for neighbor in get_neighbors8(pos, self.V, self.H, include_self=False):
|
|
40
|
+
self.model.Add(self.model_vars[neighbor] == 0).OnlyEnforceIf(self.model_vars[pos])
|
|
41
|
+
|
|
42
|
+
def solve_and_print(self, verbose: bool = True):
|
|
43
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
44
|
+
return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
|
|
45
|
+
def callback(single_res: SingleSolution):
|
|
46
|
+
print("Solution found")
|
|
47
|
+
print(combined_function(self.V, self.H, is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1, cell_flags=id_board_to_wall_fn(self.board)))
|
|
48
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import Optional
|
|
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_char, get_next_pos, get_pos, get_row_pos, get_col_pos, in_bounds
|
|
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, characters: set[str] = None, illegal_run: Optional[int] = 3, wall_char: Optional[str] = None):
|
|
14
|
+
if characters is None:
|
|
15
|
+
characters = set(c.item() for c in np.nditer(board) if c.item() not in [' ', wall_char])
|
|
16
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
17
|
+
assert all(c.item() in ([' ', wall_char] + list(characters)) for c in np.nditer(board)), 'board must contain only space or characters'
|
|
18
|
+
self.board = board
|
|
19
|
+
self.V, self.H = board.shape
|
|
20
|
+
assert self.V % len(characters) == 0, f'board height must be divisible by number of characters, got {self.V} % {len(characters)} = {self.V % len(characters)}'
|
|
21
|
+
assert self.H % len(characters) == 0, f'board width must be divisible by number of characters, got {self.H} % {len(characters)} = {self.H % len(characters)}'
|
|
22
|
+
self.num_repeats_v = self.V // len(characters)
|
|
23
|
+
self.num_repeats_h = self.H // len(characters)
|
|
24
|
+
self.characters = characters
|
|
25
|
+
self.illegal_run = illegal_run
|
|
26
|
+
self.wall_char = wall_char
|
|
27
|
+
|
|
28
|
+
self.model = cp_model.CpModel()
|
|
29
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
30
|
+
self.create_vars()
|
|
31
|
+
self.add_all_constraints()
|
|
32
|
+
|
|
33
|
+
def create_vars(self):
|
|
34
|
+
for pos in get_all_pos(self.V, self.H):
|
|
35
|
+
if get_char(self.board, pos) == self.wall_char:
|
|
36
|
+
continue
|
|
37
|
+
for char in self.characters:
|
|
38
|
+
self.model_vars[pos, char] = self.model.NewBoolVar(f'{pos}:{char}')
|
|
39
|
+
|
|
40
|
+
def add_all_constraints(self):
|
|
41
|
+
for pos in get_all_pos(self.V, self.H):
|
|
42
|
+
c = get_char(self.board, pos)
|
|
43
|
+
if c == self.wall_char:
|
|
44
|
+
continue
|
|
45
|
+
self.model.AddExactlyOne([self.model_vars[pos, char] for char in self.characters])
|
|
46
|
+
if c in self.characters:
|
|
47
|
+
self.model.Add(self.model_vars[pos, c] == 1)
|
|
48
|
+
for char in self.characters:
|
|
49
|
+
for row in range(self.V):
|
|
50
|
+
self.model.Add(lxp.Sum([self.model_vars[pos, char] for pos in get_row_pos(row, self.H)]) == self.num_repeats_v)
|
|
51
|
+
for col in range(self.H):
|
|
52
|
+
self.model.Add(lxp.Sum([self.model_vars[pos, char] for pos in get_col_pos(col, self.V)]) == self.num_repeats_h)
|
|
53
|
+
if self.illegal_run is not None:
|
|
54
|
+
for pos in get_all_pos(self.V, self.H):
|
|
55
|
+
self.disallow_run_constraint(pos, Direction.RIGHT)
|
|
56
|
+
self.disallow_run_constraint(pos, Direction.DOWN)
|
|
57
|
+
|
|
58
|
+
def disallow_run_constraint(self, pos: Pos, direction: Direction):
|
|
59
|
+
run = [pos]
|
|
60
|
+
while len(run) < self.illegal_run:
|
|
61
|
+
pos = get_next_pos(pos, direction)
|
|
62
|
+
if not in_bounds(pos, self.V, self.H):
|
|
63
|
+
return
|
|
64
|
+
run.append(pos)
|
|
65
|
+
assert len(run) == self.illegal_run, f'SHOULD NOT HAPPEN: run length != max run, {len(run)} != {self.illegal_run}'
|
|
66
|
+
for char in self.characters:
|
|
67
|
+
self.model.Add(lxp.Sum([self.model_vars[p, char] for p in run]) < self.illegal_run)
|
|
68
|
+
|
|
69
|
+
def solve_and_print(self, verbose: bool = True):
|
|
70
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
71
|
+
return SingleSolution(assignment={pos: char for (pos, char), var in board.model_vars.items() if solver.Value(var) == 1})
|
|
72
|
+
def callback(single_res: SingleSolution):
|
|
73
|
+
print("Solution found")
|
|
74
|
+
print(combined_function(self.V, self.H, center_char=lambda r, c: single_res.assignment.get(get_pos(x=c, y=r), self.board[r, c]), text_on_shaded_cells=False))
|
|
75
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ortools.sat.python import cp_model
|
|
4
|
+
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
5
|
+
|
|
6
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_next_pos, Direction
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Board:
|
|
10
|
+
def __init__(self, board: np.array, time_horizon: int = 10):
|
|
11
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
12
|
+
assert all(str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only digits'
|
|
13
|
+
self.board = board
|
|
14
|
+
self.target_state = np.sort(board, axis=None).reshape(board.shape)
|
|
15
|
+
self.V, self.H = board.shape
|
|
16
|
+
self.min_value = int(np.min(board.flatten()))
|
|
17
|
+
self.max_value = int(np.max(board.flatten()))
|
|
18
|
+
self.time_horizon = time_horizon
|
|
19
|
+
|
|
20
|
+
self.model = cp_model.CpModel()
|
|
21
|
+
self.state: dict[tuple[Pos, int], cp_model.IntVar] = {}
|
|
22
|
+
self.decision: dict[int, dict[Pos, cp_model.IntVar]] = {t: {} for t in range(self.time_horizon - 1)}
|
|
23
|
+
|
|
24
|
+
self.create_vars()
|
|
25
|
+
self.add_all_constraints()
|
|
26
|
+
self.minimize_actions()
|
|
27
|
+
self.constrain_final_state()
|
|
28
|
+
|
|
29
|
+
def create_vars(self):
|
|
30
|
+
for pos in get_all_pos(self.V, self.H):
|
|
31
|
+
for t in range(self.time_horizon):
|
|
32
|
+
self.state[pos, t] = self.model.NewIntVar(self.min_value, self.max_value, f'state:{pos}:{t}')
|
|
33
|
+
for t in range(self.time_horizon - 1):
|
|
34
|
+
self.decision[t]['NOOP'] = self.model.NewBoolVar(f'decision:NOOP:{t}')
|
|
35
|
+
for pos in get_all_pos(self.V, self.H):
|
|
36
|
+
if pos.x == self.H - 1 or pos.y == self.V - 1:
|
|
37
|
+
continue
|
|
38
|
+
self.decision[t][pos] = self.model.NewBoolVar(f'decision:{pos}:{t}')
|
|
39
|
+
|
|
40
|
+
def add_all_constraints(self):
|
|
41
|
+
# one action at most every time
|
|
42
|
+
for decision_at_t in self.decision.values():
|
|
43
|
+
self.model.AddExactlyOne(list(decision_at_t.values()))
|
|
44
|
+
# constrain the state at t=0
|
|
45
|
+
for pos in get_all_pos(self.V, self.H):
|
|
46
|
+
self.model.Add(self.state[pos, 0] == get_char(self.board, pos))
|
|
47
|
+
# constrain the state dynamics at t=1..T
|
|
48
|
+
for action_pos in get_all_pos(self.V, self.H):
|
|
49
|
+
if action_pos.x == self.H - 1 or action_pos.y == self.V - 1:
|
|
50
|
+
continue
|
|
51
|
+
self.constrain_state(action_pos)
|
|
52
|
+
# state does not change if NOOP is chosen
|
|
53
|
+
for t in range(1, self.time_horizon):
|
|
54
|
+
noop_var = self.decision[t - 1]['NOOP']
|
|
55
|
+
for pos in get_all_pos(self.V, self.H):
|
|
56
|
+
self.model.Add(self.state[pos, t] == self.state[pos, t - 1]).OnlyEnforceIf(noop_var)
|
|
57
|
+
|
|
58
|
+
def constrain_state(self, action: Pos):
|
|
59
|
+
tl = action
|
|
60
|
+
tr = get_next_pos(tl, Direction.RIGHT)
|
|
61
|
+
bl = get_next_pos(tl, Direction.DOWN)
|
|
62
|
+
br = get_next_pos(tr, Direction.DOWN)
|
|
63
|
+
two_by_two = (tl, tr, br, bl)
|
|
64
|
+
# lock state outside the two by two
|
|
65
|
+
for pos in get_all_pos(self.V, self.H):
|
|
66
|
+
if pos in two_by_two:
|
|
67
|
+
continue
|
|
68
|
+
for t in range(1, self.time_horizon):
|
|
69
|
+
self.model.Add(self.state[pos, t] == self.state[pos, t - 1]).OnlyEnforceIf(self.decision[t - 1][action])
|
|
70
|
+
# rotate clockwise inside the two by two
|
|
71
|
+
clockwise = two_by_two[-1:] + two_by_two[:-1]
|
|
72
|
+
# print('action', action)
|
|
73
|
+
# print('two_by_two', two_by_two)
|
|
74
|
+
# print('clockwise', clockwise)
|
|
75
|
+
for pre_pos, post_pos in zip(clockwise, two_by_two):
|
|
76
|
+
for t in range(1, self.time_horizon):
|
|
77
|
+
# print(f'IF self.decision[{t - 1}][{action}] THEN self.state[{post_pos}, {t}] == self.state[{pre_pos}, {t - 1}]')
|
|
78
|
+
self.model.Add(self.state[post_pos, t] == self.state[pre_pos, t - 1]).OnlyEnforceIf(self.decision[t - 1][action])
|
|
79
|
+
|
|
80
|
+
def constrain_final_state(self):
|
|
81
|
+
final_time = self.time_horizon - 1
|
|
82
|
+
for pos in get_all_pos(self.V, self.H):
|
|
83
|
+
self.model.Add(self.state[pos, final_time] == get_char(self.target_state, pos))
|
|
84
|
+
|
|
85
|
+
def minimize_actions(self):
|
|
86
|
+
flat_decisions = [(var, t+1) for t, tvs in self.decision.items() for pos, var in tvs.items() if pos != 'NOOP']
|
|
87
|
+
self.model.Minimize(lxp.weighted_sum([p[0] for p in flat_decisions], [p[1] for p in flat_decisions]))
|
|
88
|
+
|
|
89
|
+
def solve_and_print(self, verbose: bool = True):
|
|
90
|
+
solver = cp_model.CpSolver()
|
|
91
|
+
tic = time.time()
|
|
92
|
+
solver.solve(self.model)
|
|
93
|
+
assignment: dict[Pos] = [None for _ in range(self.time_horizon - 1)]
|
|
94
|
+
if solver.StatusName() in ['OPTIMAL', 'FEASIBLE']:
|
|
95
|
+
for t, tvs in self.decision.items():
|
|
96
|
+
for pos, var in tvs.items():
|
|
97
|
+
if solver.Value(var) == 1:
|
|
98
|
+
assignment[t] = (pos.x, pos.y) if pos != 'NOOP' else 'NOOP'
|
|
99
|
+
for t in range(self.time_horizon):
|
|
100
|
+
res_at_t = np.full((self.V, self.H), ' ', dtype=object)
|
|
101
|
+
for pos in get_all_pos(self.V, self.H):
|
|
102
|
+
res_at_t[pos.y][pos.x] = solver.Value(self.state[pos, t])
|
|
103
|
+
print(f't={t}')
|
|
104
|
+
print(res_at_t)
|
|
105
|
+
if verbose:
|
|
106
|
+
print("Solution found:", assignment)
|
|
107
|
+
if verbose:
|
|
108
|
+
print("status:", solver.StatusName())
|
|
109
|
+
toc = time.time()
|
|
110
|
+
print(f"Time taken: {toc - tic:.2f} seconds")
|
|
111
|
+
return assignment
|
|
112
|
+
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from ortools.sat.python import cp_model
|
|
7
|
+
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
8
|
+
|
|
9
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_pos, get_next_pos, in_bounds, get_char, Direction
|
|
10
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
11
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Monster(Enum):
|
|
15
|
+
VAMPIRE = "VAMPIRE"
|
|
16
|
+
ZOMBIE = "ZOMBIE"
|
|
17
|
+
GHOST = "GHOST"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SingleBeamResult:
|
|
22
|
+
position: Pos
|
|
23
|
+
reflect_count: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def can_see(reflect_count: int, monster: Monster) -> bool:
|
|
27
|
+
if monster == Monster.ZOMBIE:
|
|
28
|
+
return True
|
|
29
|
+
elif monster == Monster.VAMPIRE:
|
|
30
|
+
return reflect_count == 0
|
|
31
|
+
elif monster == Monster.GHOST:
|
|
32
|
+
return reflect_count > 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def beam(board, start_pos: Pos, direction: Direction) -> list[SingleBeamResult]:
|
|
36
|
+
V, H = board.shape
|
|
37
|
+
cur_result: list[SingleBeamResult] = []
|
|
38
|
+
reflect_count = 0
|
|
39
|
+
cur_pos = start_pos
|
|
40
|
+
while True:
|
|
41
|
+
if not in_bounds(cur_pos, V, H):
|
|
42
|
+
break
|
|
43
|
+
cur_pos_char = get_char(board, cur_pos)
|
|
44
|
+
if cur_pos_char == '//':
|
|
45
|
+
direction = {
|
|
46
|
+
Direction.RIGHT: Direction.UP,
|
|
47
|
+
Direction.UP: Direction.RIGHT,
|
|
48
|
+
Direction.DOWN: Direction.LEFT,
|
|
49
|
+
Direction.LEFT: Direction.DOWN
|
|
50
|
+
}[direction]
|
|
51
|
+
reflect_count += 1
|
|
52
|
+
elif cur_pos_char == '\\':
|
|
53
|
+
direction = {
|
|
54
|
+
Direction.RIGHT: Direction.DOWN,
|
|
55
|
+
Direction.DOWN: Direction.RIGHT,
|
|
56
|
+
Direction.UP: Direction.LEFT,
|
|
57
|
+
Direction.LEFT: Direction.UP
|
|
58
|
+
}[direction]
|
|
59
|
+
reflect_count += 1
|
|
60
|
+
else: # not a mirror
|
|
61
|
+
cur_result.append(SingleBeamResult(cur_pos, reflect_count))
|
|
62
|
+
cur_pos = get_next_pos(cur_pos, direction)
|
|
63
|
+
return cur_result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Board:
|
|
67
|
+
def __init__(self, board: np.array, sides: dict[str, np.array], monster_count: Optional[dict[Monster, int]] = None):
|
|
68
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
69
|
+
assert set(sides.keys()) == set(['right', 'left', 'top', 'bottom'])
|
|
70
|
+
self.board = board
|
|
71
|
+
self.V, self.H = board.shape
|
|
72
|
+
assert sides['top'].shape == (self.H,) and sides['bottom'].shape == (self.H,) and sides['right'].shape == (self.V,) and sides['left'].shape == (self.V,), 'all sides must be equal to board size'
|
|
73
|
+
self.sides = sides
|
|
74
|
+
self.monster_count = monster_count or {}
|
|
75
|
+
|
|
76
|
+
self.model = cp_model.CpModel()
|
|
77
|
+
self.model_vars: dict[tuple[Pos, str], cp_model.IntVar] = {}
|
|
78
|
+
self.create_vars()
|
|
79
|
+
self.add_all_constraints()
|
|
80
|
+
|
|
81
|
+
def create_vars(self):
|
|
82
|
+
for pos in get_all_pos(self.V, self.H):
|
|
83
|
+
for monster in Monster:
|
|
84
|
+
self.model_vars[(pos, monster)] = self.model.NewBoolVar(f"{pos}_is_{monster}")
|
|
85
|
+
|
|
86
|
+
def add_all_constraints(self):
|
|
87
|
+
for pos in get_all_pos(self.V, self.H):
|
|
88
|
+
if get_char(self.board, pos).strip():
|
|
89
|
+
self.model.Add(lxp.Sum([self.model_vars[(pos, monster)] for monster in Monster]) == 0)
|
|
90
|
+
continue
|
|
91
|
+
self.model.AddExactlyOne([self.model_vars[(pos, monster)] for monster in Monster])
|
|
92
|
+
for i, ground in enumerate(self.sides['top']): # top edge
|
|
93
|
+
if ground == -1:
|
|
94
|
+
continue
|
|
95
|
+
beam_result = beam(self.board, get_pos(x=i, y=0), Direction.DOWN)
|
|
96
|
+
self.model.add(self.get_var(beam_result) == ground)
|
|
97
|
+
for i, ground in enumerate(self.sides['left']): # left edge
|
|
98
|
+
if ground == -1:
|
|
99
|
+
continue
|
|
100
|
+
beam_result = beam(self.board, get_pos(x=0, y=i), Direction.RIGHT)
|
|
101
|
+
self.model.add(self.get_var(beam_result) == ground)
|
|
102
|
+
for i, ground in enumerate(self.sides['right']): # right edge
|
|
103
|
+
if ground == -1:
|
|
104
|
+
continue
|
|
105
|
+
beam_result = beam(self.board, get_pos(x=self.H-1, y=i), Direction.LEFT)
|
|
106
|
+
self.model.add(self.get_var(beam_result) == ground)
|
|
107
|
+
for i, ground in enumerate(self.sides['bottom']): # bottom edge
|
|
108
|
+
if ground == -1:
|
|
109
|
+
continue
|
|
110
|
+
beam_result = beam(self.board, get_pos(x=i, y=self.V-1), Direction.UP)
|
|
111
|
+
self.model.add(self.get_var(beam_result) == ground)
|
|
112
|
+
for monster, count in self.monster_count.items():
|
|
113
|
+
self.model.add(lxp.Sum([self.model_vars.get((pos, monster), 0) for pos in get_all_pos(self.V, self.H)]) == count)
|
|
114
|
+
|
|
115
|
+
def get_var(self, path: list[SingleBeamResult]) -> lxp:
|
|
116
|
+
path_vars = []
|
|
117
|
+
for square in path:
|
|
118
|
+
assert get_char(self.board, square.position).strip() == '', f'square {square.position} is not a star position'
|
|
119
|
+
for monster in Monster:
|
|
120
|
+
if can_see(square.reflect_count, monster):
|
|
121
|
+
path_vars.append(self.model_vars[(square.position, monster)])
|
|
122
|
+
return lxp.Sum(path_vars) if path_vars else 0
|
|
123
|
+
|
|
124
|
+
def solve_and_print(self, verbose: bool = True):
|
|
125
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
126
|
+
return SingleSolution(assignment={pos: monster.name[0] for (pos, monster), var in board.model_vars.items() if solver.BooleanValue(var)})
|
|
127
|
+
def callback(single_res: SingleSolution):
|
|
128
|
+
print("Solution found")
|
|
129
|
+
print(combined_function(self.V, self.H, center_char=lambda r, c: single_res.assignment.get(get_pos(x=c, y=r), self.board[r, c].replace('//', '/')).strip()))
|
|
130
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|