multi-puzzle-solver 1.0.3__py3-none-any.whl → 1.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of multi-puzzle-solver might be problematic. Click here for more details.
- {multi_puzzle_solver-1.0.3.dist-info → multi_puzzle_solver-1.0.4.dist-info}/METADATA +305 -112
- {multi_puzzle_solver-1.0.3.dist-info → multi_puzzle_solver-1.0.4.dist-info}/RECORD +12 -10
- puzzle_solver/__init__.py +5 -1
- puzzle_solver/puzzles/connect_the_dots/connect_the_dots.py +48 -0
- puzzle_solver/puzzles/flood_it/parse_map/parse_map.py +0 -1
- puzzle_solver/puzzles/galaxies/galaxies.py +108 -110
- puzzle_solver/puzzles/lits/lits.py +4 -4
- puzzle_solver/puzzles/norinori/norinori.py +11 -9
- puzzle_solver/puzzles/star_battle/star_battle.py +10 -7
- puzzle_solver/puzzles/twiddle/twiddle.py +112 -0
- {multi_puzzle_solver-1.0.3.dist-info → multi_puzzle_solver-1.0.4.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-1.0.3.dist-info → multi_puzzle_solver-1.0.4.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
puzzle_solver/__init__.py,sha256=
|
|
1
|
+
puzzle_solver/__init__.py,sha256=j1j97BCo0zoRY1QF6G4248BMWQCYB9TV55hbkbxbDWI,5178
|
|
2
2
|
puzzle_solver/core/utils.py,sha256=XBW5j-IwtJMPMP-ycmY6SqRCM1NOVl5O6UeoGqNj618,8153
|
|
3
3
|
puzzle_solver/core/utils_ortools.py,sha256=ACV3HgKWpEUTt1lpqsPryK1DeZpu7kdWQKEWTLJ2tfs,10384
|
|
4
4
|
puzzle_solver/core/utils_visualizer.py,sha256=ymuhF75uwJbNhN8XVDYEPqw6sPKoqRaaxlhGeHtXpLs,20201
|
|
@@ -12,12 +12,13 @@ puzzle_solver/puzzles/chess_range/chess_melee.py,sha256=D-_Oi8OyxsVe1j3dIKYwRlxg
|
|
|
12
12
|
puzzle_solver/puzzles/chess_range/chess_range.py,sha256=_VHlpUPnqeBstvSIt9RtTV-w2etSK7UrEHg6sErNqtU,21068
|
|
13
13
|
puzzle_solver/puzzles/chess_range/chess_solo.py,sha256=ByDfcRsk5FVmFicpU_DpLoLTJ99Kr___vX4y8ln8_EQ,400
|
|
14
14
|
puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
|
|
15
|
+
puzzle_solver/puzzles/connect_the_dots/connect_the_dots.py,sha256=PvxwoIUDHITxo51uD_JGoFcW33vf0YXTsvlm-gUjMNY,2610
|
|
15
16
|
puzzle_solver/puzzles/dominosa/dominosa.py,sha256=Nmb7pn8U27QJwGy9F3wo8ylqo2_U51OAo3GN2soaNpc,7195
|
|
16
17
|
puzzle_solver/puzzles/filling/filling.py,sha256=R8UIbztk3zNCeNbVClBJoKZHKeHwK_pesjGmMaEEQO0,5536
|
|
17
18
|
puzzle_solver/puzzles/flip/flip.py,sha256=ZngJLUhRNc7qqo2wtNLdMPx4u9w9JTUge27PmdXyDCw,3985
|
|
18
19
|
puzzle_solver/puzzles/flood_it/flood_it.py,sha256=jnCtH1sZIt6K4hbQDSsiM1Cd8FjQNP7cfw2ObUW5fEQ,7948
|
|
19
|
-
puzzle_solver/puzzles/flood_it/parse_map/parse_map.py,sha256=
|
|
20
|
-
puzzle_solver/puzzles/galaxies/galaxies.py,sha256=
|
|
20
|
+
puzzle_solver/puzzles/flood_it/parse_map/parse_map.py,sha256=m7gcpvN3THZdYLowdR_Jwx3HyttaV4K2DqrX_U7uFqU,8209
|
|
21
|
+
puzzle_solver/puzzles/galaxies/galaxies.py,sha256=CTSCNqRR35QT7qBiYdXYu9GLnDJmI9Cfd4C5t4_Kqhg,5535
|
|
21
22
|
puzzle_solver/puzzles/galaxies/parse_map/parse_map.py,sha256=XmFqVN_oRfq9AZFWy5ViUJ2Szjgx-srrRkFPJXEEyFo,9358
|
|
22
23
|
puzzle_solver/puzzles/guess/guess.py,sha256=MpyrF6YVu0S1fzX-BllwxGKRGacWJpeLbNn5GetuEyo,10792
|
|
23
24
|
puzzle_solver/puzzles/heyawake/heyawake.py,sha256=L_y44dHArOvO_tDyO35dwkvqdk9eEGItO7n4FDfzNDc,5586
|
|
@@ -28,13 +29,13 @@ puzzle_solver/puzzles/kakurasu/kakurasu.py,sha256=VNGMJnBHDi6WkghLObRLhUvkmrPaGp
|
|
|
28
29
|
puzzle_solver/puzzles/kakuro/kakuro.py,sha256=m22Ju-V2BdQl2Ng_pjVUSrxPCtIfqezdpebutURlhvg,4348
|
|
29
30
|
puzzle_solver/puzzles/keen/keen.py,sha256=adSA_pc1m6F6jV7a-PpQxdci1bv4psCNRNt9hMIQdSY,5034
|
|
30
31
|
puzzle_solver/puzzles/light_up/light_up.py,sha256=iSA1rjZMFsnI0V0Nxivxox4qZkB7PvUrROSHXcoUXds,4541
|
|
31
|
-
puzzle_solver/puzzles/lits/lits.py,sha256
|
|
32
|
+
puzzle_solver/puzzles/lits/lits.py,sha256=-iKgsdfFHA0aOQD8QCjSCXabQaHFaHN5idk6LBvgqRc,7166
|
|
32
33
|
puzzle_solver/puzzles/magnets/magnets.py,sha256=-Wl49JD_PKeq735zQVMQ3XSQX6gdHiY-7PKw-Sh16jw,6474
|
|
33
34
|
puzzle_solver/puzzles/map/map.py,sha256=sxc57tapB8Tsgam-yoDitln1o-EB_SbIYvO6WEYy3us,2582
|
|
34
35
|
puzzle_solver/puzzles/minesweeper/minesweeper.py,sha256=gSdFsuZ-KrwVxgI1HF2q_pYleZ6vBm9jjRTFlboVnLY,5871
|
|
35
36
|
puzzle_solver/puzzles/mosaic/mosaic.py,sha256=QX_nVpVKQg8OfaUcqFk9tKqsDyVqvZc6-XWvfI3YcSw,2175
|
|
36
37
|
puzzle_solver/puzzles/nonograms/nonograms.py,sha256=dTKfMwBL49hW3bNd34ETXW7lBRPuQeSPNSCHqHmfybg,6066
|
|
37
|
-
puzzle_solver/puzzles/norinori/norinori.py,sha256=
|
|
38
|
+
puzzle_solver/puzzles/norinori/norinori.py,sha256=iwJ2UgQJfx6JLYPQqJHdg1GJ8IBAjMDOtNybhO1jmNc,4968
|
|
38
39
|
puzzle_solver/puzzles/nurikabe/nurikabe.py,sha256=3cbW7X4kAMQK8PkH_t65fzT5cI0O6tWWOqpQUVyuGT4,6501
|
|
39
40
|
puzzle_solver/puzzles/palisade/palisade.py,sha256=T-LXlaLU5OwUQ24QWJWhBUFUktg0qDODTilNmBaXs4I,5014
|
|
40
41
|
puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
|
|
@@ -48,7 +49,7 @@ puzzle_solver/puzzles/singles/singles.py,sha256=KKn_Yl-eW874Bl1UmmcqoQ5vhNiO1JbM
|
|
|
48
49
|
puzzle_solver/puzzles/slant/slant.py,sha256=xF-N4PuXYfx638NP1f1mi6YncIZB4mLtXtdS79XyPbg,6122
|
|
49
50
|
puzzle_solver/puzzles/slant/parse_map/parse_map.py,sha256=8thQxWbq0qjehKb2VzgUP22PGj-9n9djwbt3LGMVLJw,4811
|
|
50
51
|
puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=JpyNQk8K4nUziwWKxSvWEkF1RRBGLnCppCWK1Yf5bt0,7052
|
|
51
|
-
puzzle_solver/puzzles/star_battle/star_battle.py,sha256=
|
|
52
|
+
puzzle_solver/puzzles/star_battle/star_battle.py,sha256=hFV5IKPQDWrIWr50YpiOS9VF2kUScDQpvAGvZKBwuyM,3937
|
|
52
53
|
puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
|
|
53
54
|
puzzle_solver/puzzles/stitches/stitches.py,sha256=bb5JXyclkbKq350MQ9d8AuGteQwSF8knaJ0DU9M92Uw,6515
|
|
54
55
|
puzzle_solver/puzzles/stitches/parse_map/parse_map.py,sha256=b21SQvlnDM6wOl_1iUhZ7X6akpBZoOnj3kEzImBCh8Q,10497
|
|
@@ -58,13 +59,14 @@ puzzle_solver/puzzles/tents/tents.py,sha256=jccUXWA7KWAtPKpVJJYNI6masTYWQgx0eitc
|
|
|
58
59
|
puzzle_solver/puzzles/thermometers/thermometers.py,sha256=bGcVmpPeqL5AJtj8jkK8gYThzv9aGCd_QrWEiYBCA2s,4011
|
|
59
60
|
puzzle_solver/puzzles/towers/towers.py,sha256=OLyTf9nTFR5L32-S_fhVyBmpz4i5YUNJotwOwbw_Fjg,6500
|
|
60
61
|
puzzle_solver/puzzles/tracks/tracks.py,sha256=98xds9SKNqtOLFTRUX_KSMC7XYmZo567LOFeqotVQaM,7237
|
|
62
|
+
puzzle_solver/puzzles/twiddle/twiddle.py,sha256=3gPoeD0DoiiZbIhtptdXFldO_t1QShL6IxkDqJMzjkk,5446
|
|
61
63
|
puzzle_solver/puzzles/undead/undead.py,sha256=IGFQysgoaKZH8rKjqlrkoHsH28ve4_hKor2f0QOsWY0,6596
|
|
62
64
|
puzzle_solver/puzzles/unequal/unequal.py,sha256=ExY2XDCrqROCDpRLfHo8uVr1zuli1QvbCdNCiDhlCac,6978
|
|
63
65
|
puzzle_solver/puzzles/unruly/unruly.py,sha256=xwOUpC12uHbmlDj2guN60VaaHpLr1Y-WmMD5TKeHbZE,3826
|
|
64
66
|
puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=5WixT_7K1HwfQ_dWbuBlQfpU8p69zB2KvOg32XJ8vno,5255
|
|
65
67
|
puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
|
|
66
68
|
puzzle_solver/utils/visualizer.py,sha256=T2g5We9J3tkhyXWoN2OrIDIJDjt6w5sDd2ksOub0ZI8,6819
|
|
67
|
-
multi_puzzle_solver-1.0.
|
|
68
|
-
multi_puzzle_solver-1.0.
|
|
69
|
-
multi_puzzle_solver-1.0.
|
|
70
|
-
multi_puzzle_solver-1.0.
|
|
69
|
+
multi_puzzle_solver-1.0.4.dist-info/METADATA,sha256=JSM2UZgGRSx0oipFuvGnNmKLfXWWfZGxgVWHNtWIH3I,372540
|
|
70
|
+
multi_puzzle_solver-1.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
71
|
+
multi_puzzle_solver-1.0.4.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
|
|
72
|
+
multi_puzzle_solver-1.0.4.dist-info/RECORD,,
|
puzzle_solver/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ from puzzle_solver.puzzles.bridges import bridges as bridges_solver
|
|
|
7
7
|
from puzzle_solver.puzzles.chess_range import chess_range as chess_range_solver
|
|
8
8
|
from puzzle_solver.puzzles.chess_range import chess_solo as chess_solo_solver
|
|
9
9
|
from puzzle_solver.puzzles.chess_range import chess_melee as chess_melee_solver
|
|
10
|
+
from puzzle_solver.puzzles.connect_the_dots import connect_the_dots as connect_the_dots_solver
|
|
10
11
|
from puzzle_solver.puzzles.dominosa import dominosa as dominosa_solver
|
|
11
12
|
from puzzle_solver.puzzles.filling import filling as filling_solver
|
|
12
13
|
from puzzle_solver.puzzles.flood_it import flood_it as flood_it_solver
|
|
@@ -47,6 +48,7 @@ from puzzle_solver.puzzles.tents import tents as tents_solver
|
|
|
47
48
|
from puzzle_solver.puzzles.thermometers import thermometers as thermometers_solver
|
|
48
49
|
from puzzle_solver.puzzles.towers import towers as towers_solver
|
|
49
50
|
from puzzle_solver.puzzles.tracks import tracks as tracks_solver
|
|
51
|
+
from puzzle_solver.puzzles.twiddle import twiddle as twiddle_solver
|
|
50
52
|
from puzzle_solver.puzzles.undead import undead as undead_solver
|
|
51
53
|
from puzzle_solver.puzzles.unequal import unequal as unequal_solver
|
|
52
54
|
from puzzle_solver.puzzles.unruly import unruly as unruly_solver
|
|
@@ -64,6 +66,7 @@ __all__ = [
|
|
|
64
66
|
chess_range_solver,
|
|
65
67
|
chess_solo_solver,
|
|
66
68
|
chess_melee_solver,
|
|
69
|
+
connect_the_dots_solver,
|
|
67
70
|
dominosa_solver,
|
|
68
71
|
filling_solver,
|
|
69
72
|
flood_it_solver,
|
|
@@ -104,6 +107,7 @@ __all__ = [
|
|
|
104
107
|
thermometers_solver,
|
|
105
108
|
towers_solver,
|
|
106
109
|
tracks_solver,
|
|
110
|
+
twiddle_solver,
|
|
107
111
|
undead_solver,
|
|
108
112
|
unequal_solver,
|
|
109
113
|
unruly_solver,
|
|
@@ -111,4 +115,4 @@ __all__ = [
|
|
|
111
115
|
inertia_image_parser,
|
|
112
116
|
]
|
|
113
117
|
|
|
114
|
-
__version__ = '1.0.
|
|
118
|
+
__version__ = '1.0.4'
|
|
@@ -0,0 +1,48 @@
|
|
|
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, set_char, get_char
|
|
5
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
6
|
+
from puzzle_solver.core.utils_visualizer import id_board_to_wall_board, render_grid
|
|
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
|
+
self.model.AddExactlyOne([self.model_vars[(pos, color)] for color in self.unique_colors])
|
|
29
|
+
c = get_char(self.board, pos)
|
|
30
|
+
if c.strip() not in ['', '#']:
|
|
31
|
+
self.model.Add(self.model_vars[(pos, c)] == 1)
|
|
32
|
+
for color in self.unique_colors:
|
|
33
|
+
force_connected_component(self.model, {pos: self.model_vars[(pos, color)] for pos in get_all_pos(self.V, self.H)})
|
|
34
|
+
|
|
35
|
+
def solve_and_print(self, verbose: bool = True):
|
|
36
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
37
|
+
assignment: dict[Pos, str] = {}
|
|
38
|
+
for (pos, color), var in board.model_vars.items():
|
|
39
|
+
if solver.Value(var) == 1:
|
|
40
|
+
assignment[pos] = color
|
|
41
|
+
return SingleSolution(assignment=assignment)
|
|
42
|
+
def callback(single_res: SingleSolution):
|
|
43
|
+
print("Solution found")
|
|
44
|
+
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
45
|
+
for pos in get_all_pos(self.V, self.H):
|
|
46
|
+
set_char(res, pos, single_res.assignment[pos])
|
|
47
|
+
print(render_grid(id_board_to_wall_board(res), center_char=lambda r, c: res[r][c]))
|
|
48
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -1,110 +1,108 @@
|
|
|
1
|
-
from collections import defaultdict
|
|
2
|
-
from typing import Iterable, Union
|
|
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, set_char, Direction, get_next_pos, in_bounds, get_opposite_direction, get_pos
|
|
8
|
-
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
assert
|
|
29
|
-
assert all(
|
|
30
|
-
|
|
31
|
-
self.
|
|
32
|
-
self.
|
|
33
|
-
self.
|
|
34
|
-
self.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
self.
|
|
38
|
-
self.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
self.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
assert galaxy_idx not in self.pos_to_galaxy[
|
|
74
|
-
self.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
self.model.
|
|
78
|
-
self.
|
|
79
|
-
self.pos_to_galaxy[
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
self.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
print(']')
|
|
110
|
-
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from typing import Iterable, Union
|
|
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, set_char, Direction, get_next_pos, in_bounds, get_opposite_direction, get_pos
|
|
8
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import id_board_to_wall_board, render_grid
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_numpy(galaxies: np.ndarray) -> list[tuple[Pos, ...]]:
|
|
13
|
+
result = defaultdict(list)
|
|
14
|
+
for pos, arr_id in np.ndenumerate(galaxies):
|
|
15
|
+
if not arr_id.strip():
|
|
16
|
+
continue
|
|
17
|
+
result[arr_id].append(get_pos(x=pos[1], y=pos[0]))
|
|
18
|
+
return [positions for _, positions in sorted(result.items(), key=lambda x: x[0])]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Board:
|
|
22
|
+
def __init__(self, galaxies: Union[list[tuple[Pos, ...]], np.ndarray], V: int = None, H: int = None):
|
|
23
|
+
if isinstance(galaxies, np.ndarray):
|
|
24
|
+
V, H = galaxies.shape
|
|
25
|
+
galaxies = parse_numpy(galaxies)
|
|
26
|
+
else:
|
|
27
|
+
assert V is not None and H is not None, 'V and H must be provided if galaxies is not a numpy array'
|
|
28
|
+
assert V >= 1 and H >= 1, 'V and H must be at least 1'
|
|
29
|
+
assert all(isinstance(galaxy, Iterable) for galaxy in galaxies), 'galaxies must be a list of Iterables'
|
|
30
|
+
assert all(len(galaxy) in [1, 2, 4] for galaxy in galaxies), 'each galaxy must be exactly 1, 2, or 4 positions'
|
|
31
|
+
self.V = V
|
|
32
|
+
self.H = H
|
|
33
|
+
self.n_galaxies = len(galaxies)
|
|
34
|
+
self.galaxies = galaxies
|
|
35
|
+
self.prelocated_positions: set[Pos] = {pos: i for i, galaxy in enumerate(galaxies) for pos in galaxy}
|
|
36
|
+
|
|
37
|
+
self.model = cp_model.CpModel()
|
|
38
|
+
self.pos_to_galaxy: dict[Pos, dict[int, cp_model.IntVar]] = {p: {} for p in get_all_pos(V, H)} # each position can be part of exactly one out of many possible galaxies
|
|
39
|
+
self.allocated_pairs: set[tuple[Pos, Pos]] = set() # each pair is allocated to exactly one galaxy
|
|
40
|
+
|
|
41
|
+
self.create_vars()
|
|
42
|
+
self.add_all_constraints()
|
|
43
|
+
|
|
44
|
+
def create_vars(self):
|
|
45
|
+
for i in range(self.n_galaxies):
|
|
46
|
+
galaxy = self.galaxies[i]
|
|
47
|
+
if len(galaxy) == 1:
|
|
48
|
+
p1, p2 = galaxy[0], galaxy[0]
|
|
49
|
+
elif len(galaxy) == 2:
|
|
50
|
+
p1, p2 = galaxy[0], galaxy[1]
|
|
51
|
+
elif len(galaxy) == 4:
|
|
52
|
+
p1, p2 = galaxy[0], galaxy[3] # [1] and [2] will be linked with symmetry
|
|
53
|
+
self.expand_galaxy(p1, p2, i)
|
|
54
|
+
|
|
55
|
+
def expand_galaxy(self, p1: Pos, p2: Pos, galaxy_idx: int):
|
|
56
|
+
if (p1, p2) in self.allocated_pairs or (p2, p1) in self.allocated_pairs:
|
|
57
|
+
return
|
|
58
|
+
if p1 in self.prelocated_positions and self.prelocated_positions[p1] != galaxy_idx:
|
|
59
|
+
return
|
|
60
|
+
if p2 in self.prelocated_positions and self.prelocated_positions[p2] != galaxy_idx:
|
|
61
|
+
return
|
|
62
|
+
if not in_bounds(p1, self.V, self.H) or not in_bounds(p2, self.V, self.H):
|
|
63
|
+
return
|
|
64
|
+
self.bind_pair(p1, p2, galaxy_idx)
|
|
65
|
+
# symmetrically expand the galaxy until illegal position is hit
|
|
66
|
+
for direction in [Direction.RIGHT, Direction.UP, Direction.DOWN, Direction.LEFT]:
|
|
67
|
+
symmetrical_direction = get_opposite_direction(direction)
|
|
68
|
+
new_p1 = get_next_pos(p1, direction)
|
|
69
|
+
new_p2 = get_next_pos(p2, symmetrical_direction)
|
|
70
|
+
self.expand_galaxy(new_p1, new_p2, galaxy_idx)
|
|
71
|
+
|
|
72
|
+
def bind_pair(self, p1: Pos, p2: Pos, galaxy_idx: int):
|
|
73
|
+
assert galaxy_idx not in self.pos_to_galaxy[p1], f'p1={p1} already has galaxy idx={galaxy_idx}'
|
|
74
|
+
assert galaxy_idx not in self.pos_to_galaxy[p2], f'p2={p2} already has galaxy idx={galaxy_idx}'
|
|
75
|
+
self.allocated_pairs.add((p1, p2))
|
|
76
|
+
v1 = self.model.NewBoolVar(f'{p1}:{galaxy_idx}')
|
|
77
|
+
v2 = self.model.NewBoolVar(f'{p2}:{galaxy_idx}')
|
|
78
|
+
self.model.Add(v1 == v2)
|
|
79
|
+
self.pos_to_galaxy[p1][galaxy_idx] = v1
|
|
80
|
+
self.pos_to_galaxy[p2][galaxy_idx] = v2
|
|
81
|
+
|
|
82
|
+
def add_all_constraints(self):
|
|
83
|
+
galaxy_vars = {}
|
|
84
|
+
for pos in get_all_pos(self.V, self.H):
|
|
85
|
+
pos_vars = list(self.pos_to_galaxy[pos].values())
|
|
86
|
+
self.model.AddExactlyOne(pos_vars)
|
|
87
|
+
for galaxy_idx, v in self.pos_to_galaxy[pos].items():
|
|
88
|
+
galaxy_vars.setdefault(galaxy_idx, {})[pos] = v
|
|
89
|
+
for pos_vars in galaxy_vars.values():
|
|
90
|
+
force_connected_component(self.model, pos_vars)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def solve_and_print(self, verbose: bool = True):
|
|
94
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
95
|
+
assignment: dict[Pos, int] = {}
|
|
96
|
+
for pos, galaxy_vars in board.pos_to_galaxy.items():
|
|
97
|
+
for galaxy_idx, var in galaxy_vars.items(): # every pos is part of exactly one galaxy
|
|
98
|
+
if solver.Value(var) == 1:
|
|
99
|
+
assignment[pos] = galaxy_idx
|
|
100
|
+
break
|
|
101
|
+
return SingleSolution(assignment=assignment)
|
|
102
|
+
def callback(single_res: SingleSolution):
|
|
103
|
+
print("Solution found")
|
|
104
|
+
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
105
|
+
for pos in get_all_pos(self.V, self.H):
|
|
106
|
+
set_char(res, pos, single_res.assignment[pos])
|
|
107
|
+
print(render_grid(id_board_to_wall_board(res), center_char=lambda r, c: '.' if (Pos(x=c, y=r) in self.prelocated_positions) else ' '))
|
|
108
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -6,6 +6,7 @@ import numpy as np
|
|
|
6
6
|
|
|
7
7
|
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_pos, in_bounds, Direction, get_next_pos, polyominoes_with_shape_id
|
|
8
8
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import id_board_to_wall_board, render_grid
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
# a shape on the 2d board is just a set of positions
|
|
@@ -128,9 +129,8 @@ class Board:
|
|
|
128
129
|
return SingleSolution(assignment=assignment)
|
|
129
130
|
def callback(single_res: SingleSolution):
|
|
130
131
|
print("Solution found")
|
|
131
|
-
res = np.full((self.V, self.H), ' ', dtype=
|
|
132
|
+
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
132
133
|
for pos, val in single_res.assignment.items():
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
print('[\n' + '\n'.join([' ' + str(res[row].tolist()) + ',' for row in range(self.V)]) + '\n]')
|
|
134
|
+
set_char(res, pos, '▒▒▒' if val == 1 else ' ')
|
|
135
|
+
print(render_grid(id_board_to_wall_board(self.board), center_char=lambda r, c: res[r][c]))
|
|
136
136
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose_callback else None, verbose=verbose, max_solutions=max_solutions)
|
|
@@ -5,6 +5,7 @@ from ortools.sat.python import cp_model
|
|
|
5
5
|
|
|
6
6
|
from puzzle_solver.core.utils import Pos, Shape, get_all_pos, get_char, set_char, in_bounds, get_next_pos, Direction
|
|
7
7
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
8
|
+
from puzzle_solver.core.utils_visualizer import id_board_to_wall_board, render_grid
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@dataclass
|
|
@@ -89,13 +90,14 @@ class Board:
|
|
|
89
90
|
return SingleSolution(assignment=assignment)
|
|
90
91
|
def callback(single_res: SingleSolution):
|
|
91
92
|
print("Solution found")
|
|
92
|
-
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
93
|
-
for pos in get_all_pos(self.V, self.H):
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
print('[')
|
|
98
|
-
for row in res:
|
|
99
|
-
|
|
100
|
-
print(']')
|
|
93
|
+
# res = np.full((self.V, self.H), ' ', dtype=object)
|
|
94
|
+
# for pos in get_all_pos(self.V, self.H):
|
|
95
|
+
# c = get_char(self.board, pos)
|
|
96
|
+
# c = 'X' if pos in single_res.assignment else ' '
|
|
97
|
+
# set_char(res, pos, c)
|
|
98
|
+
# print('[')
|
|
99
|
+
# for row in res:
|
|
100
|
+
# print(" [ '" + "', '".join(row.tolist()) + "' ],")
|
|
101
|
+
# print(']')
|
|
102
|
+
print(render_grid(id_board_to_wall_board(self.board), center_char=lambda r, c: 'X' if (Pos(x=c, y=r) in single_res.assignment) else ' '))
|
|
101
103
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -3,6 +3,7 @@ from ortools.sat.python import cp_model
|
|
|
3
3
|
|
|
4
4
|
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_neighbors8, get_row_pos, get_col_pos
|
|
5
5
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
6
|
+
from puzzle_solver.core.utils_visualizer import id_board_to_wall_board, render_grid
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class Board:
|
|
@@ -61,11 +62,13 @@ class Board:
|
|
|
61
62
|
print("Solution found")
|
|
62
63
|
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
63
64
|
for pos in get_all_pos(self.V, self.H):
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
if single_res.assignment[pos] == 1:
|
|
66
|
+
set_char(res, pos, 'X')
|
|
67
|
+
else:
|
|
68
|
+
b = get_char(self.board, pos)
|
|
69
|
+
if b == 'B':
|
|
70
|
+
set_char(res, pos, ' ')
|
|
71
|
+
else:
|
|
72
|
+
set_char(res, pos, '.')
|
|
73
|
+
print(render_grid(id_board_to_wall_board(self.board), center_char=lambda r, c: res[r][c]))
|
|
71
74
|
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
|
+
|
|
File without changes
|
|
File without changes
|