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,44 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ortools.sat.python import cp_model
|
|
3
|
+
|
|
4
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_pos, get_row_pos, get_col_pos
|
|
5
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
6
|
+
from puzzle_solver.core.utils_visualizer import combined_function
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Board:
|
|
10
|
+
def __init__(self, board: np.array):
|
|
11
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
12
|
+
assert all((c.item() == ' ') or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or digits'
|
|
13
|
+
self.board = board
|
|
14
|
+
self.V, self.H = board.shape
|
|
15
|
+
self.N = max(self.V, self.H)
|
|
16
|
+
|
|
17
|
+
self.model = cp_model.CpModel()
|
|
18
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
19
|
+
self.create_vars()
|
|
20
|
+
self.add_all_constraints()
|
|
21
|
+
|
|
22
|
+
def create_vars(self):
|
|
23
|
+
for pos in get_all_pos(self.V, self.H):
|
|
24
|
+
if get_char(self.board, pos).strip():
|
|
25
|
+
self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
|
|
26
|
+
|
|
27
|
+
def add_all_constraints(self):
|
|
28
|
+
for v in range(1, self.N + 1): # each digit is circled once
|
|
29
|
+
self.model.AddExactlyOne([self.model_vars[pos] for pos in get_all_pos(self.V, self.H) if get_char(self.board, pos) == str(v)])
|
|
30
|
+
for row in range(self.V): # each row contains 1 circle
|
|
31
|
+
self.model.AddExactlyOne([self.model_vars[pos] for pos in get_row_pos(row, self.H) if pos in self.model_vars])
|
|
32
|
+
for col in range(self.H): # each column contains 1 circle
|
|
33
|
+
self.model.AddExactlyOne([self.model_vars[pos] for pos in get_col_pos(col, self.V) if pos in self.model_vars])
|
|
34
|
+
|
|
35
|
+
def solve_and_print(self, verbose: bool = True):
|
|
36
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
37
|
+
return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
|
|
38
|
+
def callback(single_res: SingleSolution):
|
|
39
|
+
print("Solution found")
|
|
40
|
+
print(combined_function(self.V, self.H,
|
|
41
|
+
cell_flags=lambda r, c: 'ULRD' if single_res.assignment.get(get_pos(x=c, y=r), 0) == 1 else '',
|
|
42
|
+
center_char=lambda r, c: self.board[r, c].strip() if self.board[r, c].strip() else '.',
|
|
43
|
+
))
|
|
44
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
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_pos, get_row_pos, get_col_pos
|
|
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
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ShapeOnBoard:
|
|
14
|
+
body: frozenset[Pos]
|
|
15
|
+
is_active: cp_model.IntVar
|
|
16
|
+
disallow: frozenset[Pos]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Board:
|
|
20
|
+
def __init__(self, side: np.array, top: np.array):
|
|
21
|
+
self.V = side.shape[0]
|
|
22
|
+
self.H = top.shape[0]
|
|
23
|
+
self.side = side
|
|
24
|
+
self.top = top
|
|
25
|
+
|
|
26
|
+
self.model = cp_model.CpModel()
|
|
27
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
28
|
+
self.shapes_on_board: set[ShapeOnBoard] = set()
|
|
29
|
+
self.create_vars()
|
|
30
|
+
self.add_all_constraints()
|
|
31
|
+
|
|
32
|
+
def create_vars(self):
|
|
33
|
+
for x in range(0, self.H):
|
|
34
|
+
for end_x in range(x, self.H + 1):
|
|
35
|
+
if (end_x - x) < 2:
|
|
36
|
+
continue
|
|
37
|
+
max_allowed_height = np.min(self.top[x:end_x])
|
|
38
|
+
for y in range(0, self.V):
|
|
39
|
+
for end_y in range(y, self.V + 1):
|
|
40
|
+
if (end_y - y) < 2 or (end_y - y) > max_allowed_height:
|
|
41
|
+
continue
|
|
42
|
+
max_allowed_width = np.min(self.side[y:end_y])
|
|
43
|
+
if (end_x - x) > max_allowed_width:
|
|
44
|
+
continue
|
|
45
|
+
body = frozenset(get_pos(x=i, y=j) for i in range(x, end_x) for j in range(y, end_y))
|
|
46
|
+
disallow = frozenset(get_pos(x=i, y=j) for i in range(x-1, end_x+1) for j in range(y-1, end_y+1)) - body
|
|
47
|
+
self.shapes_on_board.add(ShapeOnBoard(
|
|
48
|
+
body=body,
|
|
49
|
+
is_active=self.model.NewBoolVar(f'{x}-{y}-{end_x}-{end_y}-is_active'),
|
|
50
|
+
disallow=disallow
|
|
51
|
+
))
|
|
52
|
+
for pos in get_all_pos(self.V, self.H):
|
|
53
|
+
self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
|
|
54
|
+
|
|
55
|
+
def add_all_constraints(self):
|
|
56
|
+
# if a piece is active then all its body is active and the disallow is inactive
|
|
57
|
+
for shape in self.shapes_on_board:
|
|
58
|
+
for pos in shape.body:
|
|
59
|
+
self.model.Add(self.model_vars[pos] == 1).OnlyEnforceIf(shape.is_active)
|
|
60
|
+
for pos in shape.disallow:
|
|
61
|
+
if pos not in self.model_vars:
|
|
62
|
+
continue
|
|
63
|
+
self.model.Add(self.model_vars[pos] == 0).OnlyEnforceIf(shape.is_active)
|
|
64
|
+
# if a spot is active then exactly one piece (with a body there) is active
|
|
65
|
+
for pos in get_all_pos(self.V, self.H):
|
|
66
|
+
pieces_on_pos = [shape for shape in self.shapes_on_board if pos in shape.body]
|
|
67
|
+
# if pos is on then exactly one shape is active. if pos is off then 0 shapes are active.
|
|
68
|
+
self.model.Add(lxp.Sum([shape.is_active for shape in pieces_on_pos]) == self.model_vars[pos])
|
|
69
|
+
for row in range(self.V): # force side counts
|
|
70
|
+
self.model.Add(lxp.Sum([self.model_vars[pos] for pos in get_row_pos(row, self.H)]) == self.side[row])
|
|
71
|
+
for col in range(self.H): # force top counts
|
|
72
|
+
self.model.Add(lxp.Sum([self.model_vars[pos] for pos in get_col_pos(col, self.V)]) == self.top[col])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def solve_and_print(self, verbose: bool = True):
|
|
76
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
77
|
+
return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
|
|
78
|
+
def callback(single_res: SingleSolution):
|
|
79
|
+
print("Solution found")
|
|
80
|
+
print(combined_function(self.V, self.H, is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1))
|
|
81
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ortools.sat.python import cp_model
|
|
3
|
+
|
|
4
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_pos, get_neighbors4
|
|
5
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
6
|
+
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Board:
|
|
10
|
+
def __init__(self, board: np.array):
|
|
11
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
12
|
+
self.board = board
|
|
13
|
+
self.V, self.H = board.shape
|
|
14
|
+
self.unique_colors = set([str(c.item()).strip() for c in np.nditer(board) if str(c.item()).strip() not in ['', '#']])
|
|
15
|
+
assert all(np.count_nonzero(board == color) == 2 for color in self.unique_colors), f'each color must appear == 2 times, got {self.unique_colors}'
|
|
16
|
+
self.model = cp_model.CpModel()
|
|
17
|
+
self.model_vars: dict[tuple[Pos, str], cp_model.IntVar] = {}
|
|
18
|
+
self.create_vars()
|
|
19
|
+
self.add_all_constraints()
|
|
20
|
+
|
|
21
|
+
def create_vars(self):
|
|
22
|
+
for pos in get_all_pos(self.V, self.H):
|
|
23
|
+
for color in self.unique_colors:
|
|
24
|
+
self.model_vars[(pos, color)] = self.model.NewBoolVar(f'{pos}:{color}')
|
|
25
|
+
|
|
26
|
+
def add_all_constraints(self):
|
|
27
|
+
for pos in get_all_pos(self.V, self.H):
|
|
28
|
+
c = get_char(self.board, pos).strip()
|
|
29
|
+
if c == '#': # a wall, thus no color
|
|
30
|
+
self.model.Add(sum([self.model_vars[(pos, color)] for color in self.unique_colors]) == 0)
|
|
31
|
+
continue
|
|
32
|
+
self.model.AddExactlyOne([self.model_vars[(pos, color)] for color in self.unique_colors])
|
|
33
|
+
if c != '': # an endpoint, thus must be the color
|
|
34
|
+
self.model.Add(self.model_vars[(pos, c)] == 1)
|
|
35
|
+
self.model.Add(sum([self.model_vars[(n, c)] for n in get_neighbors4(pos, self.V, self.H)]) == 1) # endpoints must have exactly 1 neighbor
|
|
36
|
+
else: # not an endpoint, thus must have exactly 2 neighbors
|
|
37
|
+
for color in self.unique_colors:
|
|
38
|
+
self.model.Add(sum([self.model_vars[(n, color)] for n in get_neighbors4(pos, self.V, self.H)]) == 2).OnlyEnforceIf(self.model_vars[(pos, color)])
|
|
39
|
+
for color in self.unique_colors:
|
|
40
|
+
force_connected_component(self.model, {pos: self.model_vars[(pos, color)] for pos in get_all_pos(self.V, self.H)})
|
|
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: color for (pos, color), var in board.model_vars.items() if solver.Value(var) == 1})
|
|
45
|
+
def callback(single_res: SingleSolution):
|
|
46
|
+
print("Solution found")
|
|
47
|
+
print(combined_function(self.V, self.H,
|
|
48
|
+
cell_flags=id_board_to_wall_fn(np.array([[single_res.assignment.get(get_pos(x=c, y=r), '') for c in range(self.H)] for r in range(self.V)])),
|
|
49
|
+
center_char=lambda r, c: single_res.assignment.get(get_pos(x=c, y=r), self.board[r, c])))
|
|
50
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ortools.sat.python import cp_model
|
|
3
|
+
|
|
4
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_pos, Direction, get_char, get_ray
|
|
5
|
+
from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, SingleSolution, force_connected_component
|
|
6
|
+
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Board:
|
|
10
|
+
def __init__(self, board: np.ndarray):
|
|
11
|
+
assert board.ndim == 2 and board.shape[0] > 0 and board.shape[1] > 0, f'board must be 2d, got {board.ndim}'
|
|
12
|
+
assert all(str(i.item()).strip() in ['', 'W', 'P'] or str(i.item()).strip().isdecimal() for i in np.nditer(board)), f'board must be empty or a W or a P or a number, got {list(np.nditer(board))}'
|
|
13
|
+
self.V, self.H = board.shape
|
|
14
|
+
self.board = board
|
|
15
|
+
|
|
16
|
+
self.model = cp_model.CpModel()
|
|
17
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
18
|
+
self.outside_fence: dict[Pos, cp_model.IntVar] = {}
|
|
19
|
+
self.create_vars()
|
|
20
|
+
self.add_all_constraints()
|
|
21
|
+
|
|
22
|
+
def create_vars(self):
|
|
23
|
+
for pos in get_all_pos(self.V, self.H):
|
|
24
|
+
self.model_vars[pos] = self.model.NewBoolVar(f"{pos}")
|
|
25
|
+
self.outside_fence[pos] = self.model_vars[pos].Not()
|
|
26
|
+
|
|
27
|
+
def add_all_constraints(self):
|
|
28
|
+
for pos in get_all_pos(self.V, self.H):
|
|
29
|
+
c = str(get_char(self.board, pos)).strip()
|
|
30
|
+
if c == '':
|
|
31
|
+
continue
|
|
32
|
+
elif c in ['W', 'P']: # cow or cactus
|
|
33
|
+
self.model.Add(self.model_vars[pos] == (c == 'W'))
|
|
34
|
+
else:
|
|
35
|
+
self.range_clue(pos, int(c))
|
|
36
|
+
force_connected_component(self.model, self.model_vars)
|
|
37
|
+
def is_outside_neighbor(p1: Pos, p2: Pos) -> bool:
|
|
38
|
+
if abs(p1.x - p2.x) + abs(p1.y - p2.y) == 1: # manhattan distance is 1
|
|
39
|
+
return True
|
|
40
|
+
# both are on the border
|
|
41
|
+
p1_on_border = p1.x == 0 or p1.x == self.H- 1 or p1.y == 0 or p1.y == self.V - 1
|
|
42
|
+
p2_on_border = p2.x == 0 or p2.x == self.H- 1 or p2.y == 0 or p2.y == self.V - 1
|
|
43
|
+
return p1_on_border and p2_on_border
|
|
44
|
+
force_connected_component(self.model, self.outside_fence, is_neighbor=is_outside_neighbor)
|
|
45
|
+
|
|
46
|
+
def range_clue(self, pos: Pos, c: int):
|
|
47
|
+
self.model.Add(self.model_vars[pos] == 1) # Force it white
|
|
48
|
+
vis_vars: list[cp_model.IntVar] = []
|
|
49
|
+
for direction in Direction: # Build visibility chains in four direction
|
|
50
|
+
ray = get_ray(pos, direction, self.V, self.H) # cells outward
|
|
51
|
+
for idx in range(len(ray)):
|
|
52
|
+
v = self.model.NewBoolVar(f"vis[{pos}]->({direction.name})[{idx}]")
|
|
53
|
+
and_constraint(self.model, target=v, cs=[self.model_vars[p] for p in ray[:idx+1]])
|
|
54
|
+
vis_vars.append(v)
|
|
55
|
+
self.model.Add(1 + sum(vis_vars) == int(c)) # Sum of visible whites = 1 (itself) + sum(chains) == k
|
|
56
|
+
|
|
57
|
+
def solve_and_print(self, verbose: bool = True):
|
|
58
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
59
|
+
return SingleSolution(assignment={pos: solver.Value(board.model_vars[pos]) for pos in get_all_pos(board.V, board.H)})
|
|
60
|
+
def callback(single_res: SingleSolution):
|
|
61
|
+
print("Solution:")
|
|
62
|
+
print(combined_function(self.V, self.H,
|
|
63
|
+
cell_flags=id_board_to_wall_fn(np.array([[single_res.assignment[get_pos(x=c, y=r)] for c in range(self.H)] for r in range(self.V)])),
|
|
64
|
+
center_char=lambda r, c: self.board[r, c].strip(),
|
|
65
|
+
))
|
|
66
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from ortools.sat.python import cp_model
|
|
7
|
+
|
|
8
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, Direction, get_next_pos, in_bounds, get_pos
|
|
9
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
10
|
+
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ShapeOnBoard:
|
|
15
|
+
uid: str
|
|
16
|
+
is_active: cp_model.IntVar
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Board:
|
|
20
|
+
def __init__(self, board: np.array, target_pairs: Optional[list[tuple[int, int]]] = None):
|
|
21
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
22
|
+
assert all(str(i.item()).isdecimal() for i in np.nditer(board)), 'board must contain only digits'
|
|
23
|
+
self.board = board
|
|
24
|
+
self.V, self.H = board.shape
|
|
25
|
+
self.target_pairs = target_pairs
|
|
26
|
+
if target_pairs is None:
|
|
27
|
+
nums = [int(i.item()) for i in np.nditer(board)]
|
|
28
|
+
assert min(nums) == 0, 'expected board to start from 0'
|
|
29
|
+
self.target_pairs = [(i, j) for i in range(max(nums) + 1) for j in range(i, max(nums) + 1)]
|
|
30
|
+
|
|
31
|
+
self.model = cp_model.CpModel()
|
|
32
|
+
self.pair_to_shapes: dict[tuple[int, int], set[ShapeOnBoard]] = defaultdict(set)
|
|
33
|
+
self.pos_to_shapes: dict[Pos, set[ShapeOnBoard]] = defaultdict(set)
|
|
34
|
+
self.create_vars()
|
|
35
|
+
self.add_all_constraints()
|
|
36
|
+
|
|
37
|
+
def create_vars(self):
|
|
38
|
+
for pos in get_all_pos(self.V, self.H):
|
|
39
|
+
for direction in [Direction.RIGHT, Direction.DOWN]:
|
|
40
|
+
next_pos = get_next_pos(pos, direction)
|
|
41
|
+
if not in_bounds(next_pos, self.V, self.H):
|
|
42
|
+
continue
|
|
43
|
+
c1 = int(get_char(self.board, pos))
|
|
44
|
+
c2 = int(get_char(self.board, next_pos))
|
|
45
|
+
pair = tuple(sorted((c1, c2)))
|
|
46
|
+
uid = f'{pos.x}-{pos.y}-{direction.name[0]}'
|
|
47
|
+
s = ShapeOnBoard(uid=uid, is_active=self.model.NewBoolVar(uid))
|
|
48
|
+
self.pair_to_shapes[pair].add(s)
|
|
49
|
+
self.pos_to_shapes[pos].add(s)
|
|
50
|
+
self.pos_to_shapes[next_pos].add(s)
|
|
51
|
+
|
|
52
|
+
def add_all_constraints(self):
|
|
53
|
+
for pair in self.target_pairs: # exactly one shape active for each pair
|
|
54
|
+
self.model.AddExactlyOne(s.is_active for s in self.pair_to_shapes[pair])
|
|
55
|
+
for pos in get_all_pos(self.V, self.H): # at most one shape active at each position
|
|
56
|
+
self.model.AddAtMostOne(s.is_active for s in self.pos_to_shapes[pos])
|
|
57
|
+
|
|
58
|
+
def solve_and_print(self, verbose: bool = True):
|
|
59
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
60
|
+
return SingleSolution(assignment={pos: s.uid for pos in get_all_pos(self.V, self.H) for s in self.pos_to_shapes[pos] if solver.Value(s.is_active) == 1})
|
|
61
|
+
def callback(single_res: SingleSolution):
|
|
62
|
+
print("Solution found")
|
|
63
|
+
print(combined_function(self.V, self.H,
|
|
64
|
+
cell_flags=id_board_to_wall_fn(np.array([[single_res.assignment[get_pos(x=c, y=r)] for c in range(self.H)] for r in range(self.V)])),
|
|
65
|
+
center_char=lambda r, c: str(self.board[r, c])
|
|
66
|
+
))
|
|
67
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
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, polyominoes, in_bounds, get_next_pos, Direction
|
|
7
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
8
|
+
from puzzle_solver.core.utils_visualizer import combined_function, id_board_to_wall_fn
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ShapeOnBoard:
|
|
13
|
+
is_active: cp_model.IntVar
|
|
14
|
+
N: int
|
|
15
|
+
body: set[Pos]
|
|
16
|
+
disallow_same_shape: set[Pos]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Board:
|
|
20
|
+
def __init__(self, board: np.ndarray, digits = (1, 2, 3, 4, 5, 6, 7, 8, 9)):
|
|
21
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
22
|
+
self.board = board
|
|
23
|
+
self.V, self.H = board.shape
|
|
24
|
+
assert all((c == ' ') or (str(c).isdecimal() and 0 <= int(c) <= 9) for c in np.nditer(board)), "board must contain space or digits 0..9"
|
|
25
|
+
self.digits = digits
|
|
26
|
+
self.polyominoes = {d: polyominoes(d) for d in self.digits}
|
|
27
|
+
|
|
28
|
+
self.model = cp_model.CpModel()
|
|
29
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
30
|
+
self.digit_to_shapes = {d: [] for d in self.digits}
|
|
31
|
+
self.body_loc_to_shape = {(d,p): [] for d in self.digits for p in get_all_pos(self.V, self.H)}
|
|
32
|
+
self.forced_pos: dict[Pos, int] = {}
|
|
33
|
+
|
|
34
|
+
self.create_vars()
|
|
35
|
+
self.force_hints()
|
|
36
|
+
self.init_polyominoes_on_board()
|
|
37
|
+
self.add_all_constraints()
|
|
38
|
+
|
|
39
|
+
def create_vars(self):
|
|
40
|
+
for pos in get_all_pos(self.V, self.H):
|
|
41
|
+
for d in self.digits:
|
|
42
|
+
self.model_vars[(d,pos)] = self.model.NewBoolVar(f'{d}:{pos}')
|
|
43
|
+
|
|
44
|
+
def force_hints(self):
|
|
45
|
+
for pos in get_all_pos(self.V, self.H):
|
|
46
|
+
c = get_char(self.board, pos)
|
|
47
|
+
if c.isdecimal():
|
|
48
|
+
self.model.Add(self.model_vars[(int(c),pos)] == 1)
|
|
49
|
+
self.forced_pos[pos] = int(c)
|
|
50
|
+
|
|
51
|
+
def init_polyominoes_on_board(self):
|
|
52
|
+
for d in self.digits: # all digits
|
|
53
|
+
digit_count = 0
|
|
54
|
+
for pos in get_all_pos(self.V, self.H): # translate by shape
|
|
55
|
+
for shape in self.polyominoes[d]: # all shapes of d digits
|
|
56
|
+
body = {pos + p for p in shape}
|
|
57
|
+
if any(not in_bounds(p, self.V, self.H) for p in body):
|
|
58
|
+
continue
|
|
59
|
+
if any(p in self.forced_pos and self.forced_pos[p] != d for p in body): # part of this shape's body is already forced to a different digit, skip
|
|
60
|
+
continue
|
|
61
|
+
disallow_same_shape = set(get_next_pos(p, direction) for p in body for direction in Direction)
|
|
62
|
+
disallow_same_shape = {p for p in disallow_same_shape if p not in body and in_bounds(p, self.V, self.H)}
|
|
63
|
+
shape_on_board = ShapeOnBoard(
|
|
64
|
+
is_active=self.model.NewBoolVar(f'd{d}:{digit_count}:{pos}:is_active'),
|
|
65
|
+
N=d,
|
|
66
|
+
body=body,
|
|
67
|
+
disallow_same_shape=disallow_same_shape,
|
|
68
|
+
)
|
|
69
|
+
self.digit_to_shapes[d].append(shape_on_board)
|
|
70
|
+
for p in body:
|
|
71
|
+
self.body_loc_to_shape[(d,p)].append(shape_on_board)
|
|
72
|
+
digit_count += 1
|
|
73
|
+
|
|
74
|
+
def add_all_constraints(self):
|
|
75
|
+
for pos in get_all_pos(self.V, self.H):
|
|
76
|
+
self.model.AddExactlyOne(self.model_vars[(d,pos)] for d in self.digits) # exactly one digit is active at every position
|
|
77
|
+
self.model.AddExactlyOne(s.is_active for d in self.digits for s in self.body_loc_to_shape[(d,pos)]) # exactly one shape is active at that position
|
|
78
|
+
for s_list in self.body_loc_to_shape.values(): # if a shape is active then all its body is active
|
|
79
|
+
for s in s_list:
|
|
80
|
+
for p in s.body:
|
|
81
|
+
self.model.Add(self.model_vars[(s.N,p)] == 1).OnlyEnforceIf(s.is_active)
|
|
82
|
+
for d, s_list in self.digit_to_shapes.items(): # same shape cannot touch each other
|
|
83
|
+
for s in s_list:
|
|
84
|
+
for disallow_pos in s.disallow_same_shape:
|
|
85
|
+
self.model.Add(self.model_vars[(d,disallow_pos)] == 0).OnlyEnforceIf(s.is_active)
|
|
86
|
+
|
|
87
|
+
def solve_and_print(self, verbose: bool = True):
|
|
88
|
+
def board_to_solution(board: "Board", solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
89
|
+
return SingleSolution(assignment={pos: d for pos in get_all_pos(self.V, self.H) for d in self.digits if solver.Value(self.model_vars[(d,pos)]) == 1})
|
|
90
|
+
def callback(single_res: SingleSolution):
|
|
91
|
+
print("Solution found")
|
|
92
|
+
res_arr = np.array([[single_res.assignment[get_pos(x=c, y=r)] for c in range(self.H)] for r in range(self.V)])
|
|
93
|
+
print(combined_function(self.V, self.H, cell_flags=id_board_to_wall_fn(res_arr), center_char=lambda r, c: res_arr[r, c]))
|
|
94
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from typing import Any, 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_neighbors4, get_char, Direction, get_next_pos, get_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 _to_pos(pos: Pos, s: str) -> Pos:
|
|
12
|
+
d = {'L': Direction.LEFT, 'R': Direction.RIGHT, 'U': Direction.UP, 'D': Direction.DOWN}[s[0]]
|
|
13
|
+
r = get_next_pos(pos, d)
|
|
14
|
+
if len(s) == 1:
|
|
15
|
+
return r
|
|
16
|
+
else:
|
|
17
|
+
return _to_pos(r, s[1:])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Board:
|
|
21
|
+
def __init__(self, board: np.array, random_mapping: Optional[dict[Pos, Any]] = None):
|
|
22
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
23
|
+
assert all((c.item() in ['B', 'W']) for c in np.nditer(board)), 'board must contain only B or W'
|
|
24
|
+
self.board = board
|
|
25
|
+
self.V, self.H = board.shape
|
|
26
|
+
if random_mapping is None:
|
|
27
|
+
self.tap_mapping: dict[Pos, set[Pos]] = {pos: list(get_neighbors4(pos, self.V, self.H, include_self=True)) for pos in get_all_pos(self.V, self.H)}
|
|
28
|
+
else:
|
|
29
|
+
mapping_value = list(random_mapping.values())[0]
|
|
30
|
+
if isinstance(mapping_value, (set, list, tuple)) and isinstance(list(mapping_value)[0], Pos):
|
|
31
|
+
self.tap_mapping: dict[Pos, set[Pos]] = {pos: set(random_mapping[pos]) for pos in get_all_pos(self.V, self.H)}
|
|
32
|
+
elif isinstance(mapping_value, (set, list, tuple)) and isinstance(list(mapping_value)[0], str): # strings like "L", "UR", etc.
|
|
33
|
+
self.tap_mapping: dict[Pos, set[Pos]] = {pos: set(_to_pos(pos, s) for s in random_mapping[pos]) for pos in get_all_pos(self.V, self.H)}
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError(f'invalid random_mapping: {random_mapping}')
|
|
36
|
+
for k, v in self.tap_mapping.items():
|
|
37
|
+
if k not in v:
|
|
38
|
+
v.add(k)
|
|
39
|
+
|
|
40
|
+
self.model = cp_model.CpModel()
|
|
41
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
42
|
+
self.create_vars()
|
|
43
|
+
self.add_all_constraints()
|
|
44
|
+
|
|
45
|
+
def create_vars(self):
|
|
46
|
+
for pos in get_all_pos(self.V, self.H):
|
|
47
|
+
self.model_vars[pos] = self.model.NewBoolVar(f'tap:{pos}')
|
|
48
|
+
|
|
49
|
+
def add_all_constraints(self):
|
|
50
|
+
for pos in get_all_pos(self.V, self.H):
|
|
51
|
+
# the state of a position is its starting state + if it is tapped + if any pos pointing to it is tapped
|
|
52
|
+
pos_that_will_turn_me = [k for k,v in self.tap_mapping.items() if pos in v]
|
|
53
|
+
literals = [self.model_vars[p] for p in pos_that_will_turn_me]
|
|
54
|
+
if get_char(self.board, pos) == 'W': # if started as white then needs an even number of taps while xor checks for odd number
|
|
55
|
+
literals.append(self.model.NewConstant(True))
|
|
56
|
+
self.model.AddBoolXOr(literals)
|
|
57
|
+
|
|
58
|
+
def solve_and_print(self, verbose: bool = True):
|
|
59
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
60
|
+
return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
|
|
61
|
+
def callback(single_res: SingleSolution):
|
|
62
|
+
print("Solution found")
|
|
63
|
+
print(combined_function(self.V, self.H, is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1))
|
|
64
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from ortools.sat.python import cp_model
|
|
8
|
+
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
9
|
+
|
|
10
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_neighbors4, get_char
|
|
11
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Board:
|
|
15
|
+
def __init__(self, nodes: dict[int, int], edges: dict[int, set[int]], horizon: int, start_node_id: int):
|
|
16
|
+
self.T = horizon
|
|
17
|
+
self.nodes = nodes
|
|
18
|
+
self.edges = edges
|
|
19
|
+
self.start_node_id = start_node_id
|
|
20
|
+
self.K = len(set(nodes.values()))
|
|
21
|
+
|
|
22
|
+
self.model = cp_model.CpModel()
|
|
23
|
+
self.decision: dict[tuple[int, int], cp_model.IntVar] = {} # (t, k)
|
|
24
|
+
self.connected: dict[tuple[int, int], cp_model.IntVar] = {} # (t, cluster_id)
|
|
25
|
+
|
|
26
|
+
self.create_vars()
|
|
27
|
+
self.add_all_constraints()
|
|
28
|
+
|
|
29
|
+
def create_vars(self):
|
|
30
|
+
for t in range(self.T - 1): # (N-1) actions (we dont need to decide at time N)
|
|
31
|
+
for k in range(self.K):
|
|
32
|
+
self.decision[t, k] = self.model.NewBoolVar(f'decision:{t}:{k}')
|
|
33
|
+
for t in range(self.T):
|
|
34
|
+
for cluster_id in self.nodes:
|
|
35
|
+
self.connected[t, cluster_id] = self.model.NewBoolVar(f'connected:{t}:{cluster_id}')
|
|
36
|
+
|
|
37
|
+
def add_all_constraints(self):
|
|
38
|
+
# init time t=0, all clusters are not connected except start_node
|
|
39
|
+
for cluster_id in self.nodes:
|
|
40
|
+
if cluster_id == self.start_node_id:
|
|
41
|
+
self.model.Add(self.connected[0, cluster_id] == 1)
|
|
42
|
+
else:
|
|
43
|
+
self.model.Add(self.connected[0, cluster_id] == 0)
|
|
44
|
+
# each timestep I will pick either one or zero colors
|
|
45
|
+
for t in range(self.T - 1):
|
|
46
|
+
# print('fixing decision at time t=', t, 'to single action with colors', self.K)
|
|
47
|
+
self.model.Add(lxp.sum([self.decision[t, k] for k in range(self.K)]) <= 1)
|
|
48
|
+
# at the end of the game, all clusters must be connected
|
|
49
|
+
for cluster_id in self.nodes:
|
|
50
|
+
self.model.Add(self.connected[self.T-1, cluster_id] == 1)
|
|
51
|
+
|
|
52
|
+
for t in range(1, self.T):
|
|
53
|
+
for cluster_id in self.nodes:
|
|
54
|
+
# connected[t, i] must be 0 if all connencted clusters at t-1 are 0 (thus connected[t, i] <= sum(connected[t-1, j] for j in touching)
|
|
55
|
+
sum_neighbors = lxp.sum([self.connected[t-1, j] for j in self.edges[cluster_id]]) + self.connected[t-1, cluster_id]
|
|
56
|
+
self.model.Add(self.connected[t, cluster_id] <= sum_neighbors)
|
|
57
|
+
# connected[t, i] must be 0 if color chosen at time t does not match color of cluster i and not connected at t-1
|
|
58
|
+
cluster_color = self.nodes[cluster_id]
|
|
59
|
+
self.model.Add(self.connected[t, cluster_id] == 0).OnlyEnforceIf([self.decision[t-1, cluster_color].Not(), self.connected[t-1, cluster_id].Not()])
|
|
60
|
+
self.model.Add(self.connected[t, cluster_id] == 1).OnlyEnforceIf([self.connected[t-1, cluster_id]])
|
|
61
|
+
|
|
62
|
+
pairs = [(self.decision[t, k], t+1) for t in range(self.T - 1) for k in range(self.K)]
|
|
63
|
+
self.model.Minimize(lxp.weighted_sum([p[0] for p in pairs], [p[1] for p in pairs]))
|
|
64
|
+
|
|
65
|
+
def solve(self) -> list[SingleSolution]:
|
|
66
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
67
|
+
assignment: list[str] = [None for _ in range(self.T - 1)]
|
|
68
|
+
for t in range(self.T - 1):
|
|
69
|
+
for k in range(self.K):
|
|
70
|
+
if solver.Value(self.decision[t, k]) == 1:
|
|
71
|
+
assignment[t] = k
|
|
72
|
+
break
|
|
73
|
+
return SingleSolution(assignment=assignment)
|
|
74
|
+
return generic_solve_all(self, board_to_solution, verbose=False, max_solutions=1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def solve_minimum_steps(board: np.array, start_pos: Optional[Pos] = None, verbose: bool = True) -> int:
|
|
78
|
+
tic = time.time()
|
|
79
|
+
all_colors: set[str] = {c.item().strip() for c in np.nditer(board) if c.item().strip()}
|
|
80
|
+
color_to_int: dict[str, int] = {c: i for i, c in enumerate(sorted(all_colors))} # colors string to color id
|
|
81
|
+
int_to_color: dict[int, str] = {i: c for c, i in color_to_int.items()}
|
|
82
|
+
|
|
83
|
+
graph: dict[Pos, int] = _board_to_graph(board) # position to cluster id
|
|
84
|
+
nodes: dict[int, int] = {cluster_id: color_to_int[get_char(board, pos)] for pos, cluster_id in graph.items()}
|
|
85
|
+
edges = _graph_to_edges(board, graph) # cluster id to touching cluster ids
|
|
86
|
+
if start_pos is None:
|
|
87
|
+
start_pos = Pos(0,0)
|
|
88
|
+
|
|
89
|
+
def solution_int_to_str(solution: SingleSolution):
|
|
90
|
+
return [int_to_color.get(color_id, '?') for color_id in solution.assignment]
|
|
91
|
+
|
|
92
|
+
def print_solution(solution: SingleSolution):
|
|
93
|
+
solution = solution_int_to_str(solution)
|
|
94
|
+
print("Solution:", solution)
|
|
95
|
+
solution = _binary_search_solution(nodes, edges, graph[start_pos], callback=print_solution if verbose else None, verbose=verbose)
|
|
96
|
+
if verbose:
|
|
97
|
+
if solution is None:
|
|
98
|
+
print("No solution found")
|
|
99
|
+
else:
|
|
100
|
+
solution = solution_int_to_str(solution)
|
|
101
|
+
print(f"Best Horizon is: T={len(solution)}")
|
|
102
|
+
print("Best solution is:", solution)
|
|
103
|
+
toc = time.time()
|
|
104
|
+
print(f"Time taken: {toc - tic:.2f} seconds")
|
|
105
|
+
return solution
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _board_to_graph(board: np.array) -> dict[int, set[int]]:
|
|
109
|
+
def dfs_flood(board: np.array, pos: Pos, cluster_id: int, graph: dict[Pos, int]):
|
|
110
|
+
if pos in graph:
|
|
111
|
+
return
|
|
112
|
+
graph[pos] = cluster_id
|
|
113
|
+
for neighbor in get_neighbors4(pos, board.shape[0], board.shape[1]):
|
|
114
|
+
if get_char(board, neighbor) == get_char(board, pos):
|
|
115
|
+
dfs_flood(board, neighbor, cluster_id, graph)
|
|
116
|
+
graph: dict[Pos, int] = {}
|
|
117
|
+
cluster_id = 0
|
|
118
|
+
V, H = board.shape
|
|
119
|
+
for pos in get_all_pos(V, H):
|
|
120
|
+
if pos in graph:
|
|
121
|
+
continue
|
|
122
|
+
dfs_flood(board, pos, cluster_id, graph)
|
|
123
|
+
cluster_id += 1
|
|
124
|
+
return graph
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _graph_to_edges(board: np.array, graph: dict[Pos, int]) -> dict[int, set[int]]:
|
|
128
|
+
cluster_edges: dict[int, set[int]] = defaultdict(set)
|
|
129
|
+
V, H = board.shape
|
|
130
|
+
for pos in get_all_pos(V, H):
|
|
131
|
+
for neighbor in get_neighbors4(pos, V, H):
|
|
132
|
+
n1, n2 = graph[pos], graph[neighbor]
|
|
133
|
+
if n1 != n2:
|
|
134
|
+
cluster_edges[n1].add(n2)
|
|
135
|
+
cluster_edges[n2].add(n1)
|
|
136
|
+
return cluster_edges
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _binary_search_solution(nodes, edges, start_node_id, callback, verbose: bool = True):
|
|
140
|
+
if len(nodes) <= 1:
|
|
141
|
+
return SingleSolution(assignment=[])
|
|
142
|
+
min_T = 2
|
|
143
|
+
max_T = len(nodes)
|
|
144
|
+
hist = {} # record historical T and best solution
|
|
145
|
+
while min_T <= max_T:
|
|
146
|
+
if max_T - min_T <= 20: # small gap, just take the middle
|
|
147
|
+
T = min_T + (max_T - min_T) // 2
|
|
148
|
+
else: # large gap, just +5 the min to not go too far
|
|
149
|
+
T = min_T + 15
|
|
150
|
+
# main check for binary search
|
|
151
|
+
if T in hist: # already done and found solution
|
|
152
|
+
solutions = hist[T]
|
|
153
|
+
else:
|
|
154
|
+
if verbose:
|
|
155
|
+
print(f"Trying with exactly {T-1} moves...", end='')
|
|
156
|
+
sys.stdout.flush()
|
|
157
|
+
binst = Board(nodes=nodes, edges=edges, horizon=T, start_node_id=start_node_id)
|
|
158
|
+
solutions = binst.solve()
|
|
159
|
+
if verbose:
|
|
160
|
+
print(' Possible!' if len(solutions) > 0 else ' Not possible!')
|
|
161
|
+
if len(solutions) > 0:
|
|
162
|
+
callback(solutions[0])
|
|
163
|
+
if min_T == max_T:
|
|
164
|
+
hist[T] = solutions
|
|
165
|
+
break
|
|
166
|
+
if len(solutions) > 0:
|
|
167
|
+
hist[T] = solutions
|
|
168
|
+
max_T = T
|
|
169
|
+
else:
|
|
170
|
+
min_T = T + 1
|
|
171
|
+
best_solution = min(hist.items(), key=lambda x: x[0])[1][0]
|
|
172
|
+
return best_solution
|
|
173
|
+
|
|
174
|
+
|