multi-puzzle-solver 0.9.30__py3-none-any.whl → 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of multi-puzzle-solver might be problematic. Click here for more details.
- {multi_puzzle_solver-0.9.30.dist-info → multi_puzzle_solver-1.0.2.dist-info}/METADATA +331 -76
- multi_puzzle_solver-1.0.2.dist-info/RECORD +69 -0
- puzzle_solver/__init__.py +58 -1
- puzzle_solver/core/utils_ortools.py +8 -6
- puzzle_solver/core/utils_visualizer.py +23 -41
- puzzle_solver/puzzles/binairo/binairo.py +4 -4
- puzzle_solver/puzzles/black_box/black_box.py +5 -11
- puzzle_solver/puzzles/bridges/bridges.py +1 -1
- puzzle_solver/puzzles/chess_range/chess_range.py +3 -3
- puzzle_solver/puzzles/chess_range/chess_solo.py +1 -1
- puzzle_solver/puzzles/filling/filling.py +3 -3
- puzzle_solver/puzzles/flood_it/flood_it.py +174 -0
- puzzle_solver/puzzles/flood_it/parse_map/parse_map.py +198 -0
- puzzle_solver/puzzles/galaxies/galaxies.py +1 -1
- puzzle_solver/puzzles/galaxies/parse_map/parse_map.py +3 -3
- puzzle_solver/puzzles/guess/guess.py +1 -1
- puzzle_solver/puzzles/heyawake/heyawake.py +3 -3
- puzzle_solver/puzzles/inertia/inertia.py +1 -1
- puzzle_solver/puzzles/inertia/parse_map/parse_map.py +13 -10
- puzzle_solver/puzzles/inertia/tsp.py +5 -7
- puzzle_solver/puzzles/kakuro/kakuro.py +1 -1
- puzzle_solver/puzzles/keen/keen.py +2 -2
- puzzle_solver/puzzles/minesweeper/minesweeper.py +2 -3
- puzzle_solver/puzzles/nonograms/nonograms.py +3 -3
- puzzle_solver/puzzles/norinori/norinori.py +2 -2
- puzzle_solver/puzzles/nurikabe/nurikabe.py +2 -2
- puzzle_solver/puzzles/range/range.py +1 -1
- puzzle_solver/puzzles/rectangles/rectangles.py +2 -6
- puzzle_solver/puzzles/shingoki/shingoki.py +1 -1
- puzzle_solver/puzzles/signpost/signpost.py +2 -2
- puzzle_solver/puzzles/slant/parse_map/parse_map.py +7 -5
- puzzle_solver/puzzles/slitherlink/slitherlink.py +1 -1
- puzzle_solver/puzzles/stitches/parse_map/parse_map.py +6 -5
- puzzle_solver/puzzles/stitches/stitches.py +1 -1
- puzzle_solver/puzzles/sudoku/sudoku.py +91 -20
- puzzle_solver/puzzles/tents/tents.py +2 -2
- puzzle_solver/puzzles/thermometers/thermometers.py +1 -1
- puzzle_solver/puzzles/towers/towers.py +1 -1
- puzzle_solver/puzzles/undead/undead.py +1 -1
- puzzle_solver/puzzles/unruly/unruly.py +1 -1
- puzzle_solver/puzzles/yin_yang/yin_yang.py +1 -1
- puzzle_solver/utils/visualizer.py +1 -1
- multi_puzzle_solver-0.9.30.dist-info/RECORD +0 -67
- {multi_puzzle_solver-0.9.30.dist-info → multi_puzzle_solver-1.0.2.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.9.30.dist-info → multi_puzzle_solver-1.0.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
puzzle_solver/__init__.py,sha256=Ll-qN1ElCTTILeun1u4t5dU0CdI3DkCX0ZNf0Q2UJtE,4886
|
|
2
|
+
puzzle_solver/core/utils.py,sha256=XBW5j-IwtJMPMP-ycmY6SqRCM1NOVl5O6UeoGqNj618,8153
|
|
3
|
+
puzzle_solver/core/utils_ortools.py,sha256=ACV3HgKWpEUTt1lpqsPryK1DeZpu7kdWQKEWTLJ2tfs,10384
|
|
4
|
+
puzzle_solver/core/utils_visualizer.py,sha256=ymuhF75uwJbNhN8XVDYEPqw6sPKoqRaaxlhGeHtXpLs,20201
|
|
5
|
+
puzzle_solver/puzzles/aquarium/aquarium.py,sha256=BUfkAS2d9eG3TdMoe1cOGGeNYgKUebRvn-z9nsC9gvE,5708
|
|
6
|
+
puzzle_solver/puzzles/battleships/battleships.py,sha256=RuYCrs4j0vUjlU139NRYYP-uNPAgO0V7hAzbsHrRwD8,7446
|
|
7
|
+
puzzle_solver/puzzles/binairo/binairo.py,sha256=NmVPIoyVCoMLaSFhsN0TcJQYvav9hi4hSwoAVirYhDU,6835
|
|
8
|
+
puzzle_solver/puzzles/binairo/binairo_plus.py,sha256=TvLG3olwANtft3LuCF-y4OofpU9PNa4IXDqgZqsD-g0,267
|
|
9
|
+
puzzle_solver/puzzles/black_box/black_box.py,sha256=EiCVkbhUP0x94otvQirv7MrggTu0ok8MIUPbxv6jkIU,15544
|
|
10
|
+
puzzle_solver/puzzles/bridges/bridges.py,sha256=QwOhZyO5urbatkNyPmQxZ_lGM01ZejndMr_eoiBkr7g,5394
|
|
11
|
+
puzzle_solver/puzzles/chess_range/chess_melee.py,sha256=D-_Oi8OyxsVe1j3dIKYwRlxgeb3NWLmDWGcv-oclY0c,195
|
|
12
|
+
puzzle_solver/puzzles/chess_range/chess_range.py,sha256=_VHlpUPnqeBstvSIt9RtTV-w2etSK7UrEHg6sErNqtU,21068
|
|
13
|
+
puzzle_solver/puzzles/chess_range/chess_solo.py,sha256=ByDfcRsk5FVmFicpU_DpLoLTJ99Kr___vX4y8ln8_EQ,400
|
|
14
|
+
puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
|
|
15
|
+
puzzle_solver/puzzles/dominosa/dominosa.py,sha256=Nmb7pn8U27QJwGy9F3wo8ylqo2_U51OAo3GN2soaNpc,7195
|
|
16
|
+
puzzle_solver/puzzles/filling/filling.py,sha256=R8UIbztk3zNCeNbVClBJoKZHKeHwK_pesjGmMaEEQO0,5536
|
|
17
|
+
puzzle_solver/puzzles/flip/flip.py,sha256=ZngJLUhRNc7qqo2wtNLdMPx4u9w9JTUge27PmdXyDCw,3985
|
|
18
|
+
puzzle_solver/puzzles/flood_it/flood_it.py,sha256=jnCtH1sZIt6K4hbQDSsiM1Cd8FjQNP7cfw2ObUW5fEQ,7948
|
|
19
|
+
puzzle_solver/puzzles/flood_it/parse_map/parse_map.py,sha256=0aw1TbiyxknY2hUAXaP3nXqT6I6mT9BIiERJSCj57xw,8245
|
|
20
|
+
puzzle_solver/puzzles/galaxies/galaxies.py,sha256=36X9jaQfvLIWFkBY1VZH6I59eCDkc77U06NDtKRUECY,5571
|
|
21
|
+
puzzle_solver/puzzles/galaxies/parse_map/parse_map.py,sha256=XmFqVN_oRfq9AZFWy5ViUJ2Szjgx-srrRkFPJXEEyFo,9358
|
|
22
|
+
puzzle_solver/puzzles/guess/guess.py,sha256=MpyrF6YVu0S1fzX-BllwxGKRGacWJpeLbNn5GetuEyo,10792
|
|
23
|
+
puzzle_solver/puzzles/heyawake/heyawake.py,sha256=L_y44dHArOvO_tDyO35dwkvqdk9eEGItO7n4FDfzNDc,5586
|
|
24
|
+
puzzle_solver/puzzles/inertia/inertia.py,sha256=-Y5fr7aK20zwmGHsZql7pYCq1kyMZglvkVZ6uIDf1HA,5658
|
|
25
|
+
puzzle_solver/puzzles/inertia/tsp.py,sha256=mAhlSjCWespASeN8uLZ0JkYDw-ZqFEpal6NM-ubpCXw,15313
|
|
26
|
+
puzzle_solver/puzzles/inertia/parse_map/parse_map.py,sha256=x0d64gTBd0HC2lO5uOpX2VKWfwj8rRiz0mQM_lqNmWs,8457
|
|
27
|
+
puzzle_solver/puzzles/kakurasu/kakurasu.py,sha256=VNGMJnBHDi6WkghLObRLhUvkmrPaGphTTUDMC0TkQvQ,2064
|
|
28
|
+
puzzle_solver/puzzles/kakuro/kakuro.py,sha256=m22Ju-V2BdQl2Ng_pjVUSrxPCtIfqezdpebutURlhvg,4348
|
|
29
|
+
puzzle_solver/puzzles/keen/keen.py,sha256=adSA_pc1m6F6jV7a-PpQxdci1bv4psCNRNt9hMIQdSY,5034
|
|
30
|
+
puzzle_solver/puzzles/light_up/light_up.py,sha256=iSA1rjZMFsnI0V0Nxivxox4qZkB7PvUrROSHXcoUXds,4541
|
|
31
|
+
puzzle_solver/puzzles/lits/lits.py,sha256=3fPIkhAIUz8JokcfaE_ZM3b0AFEnf5xPzGJ2qnm8SWY,7099
|
|
32
|
+
puzzle_solver/puzzles/magnets/magnets.py,sha256=-Wl49JD_PKeq735zQVMQ3XSQX6gdHiY-7PKw-Sh16jw,6474
|
|
33
|
+
puzzle_solver/puzzles/map/map.py,sha256=sxc57tapB8Tsgam-yoDitln1o-EB_SbIYvO6WEYy3us,2582
|
|
34
|
+
puzzle_solver/puzzles/minesweeper/minesweeper.py,sha256=gSdFsuZ-KrwVxgI1HF2q_pYleZ6vBm9jjRTFlboVnLY,5871
|
|
35
|
+
puzzle_solver/puzzles/mosaic/mosaic.py,sha256=QX_nVpVKQg8OfaUcqFk9tKqsDyVqvZc6-XWvfI3YcSw,2175
|
|
36
|
+
puzzle_solver/puzzles/nonograms/nonograms.py,sha256=dTKfMwBL49hW3bNd34ETXW7lBRPuQeSPNSCHqHmfybg,6066
|
|
37
|
+
puzzle_solver/puzzles/norinori/norinori.py,sha256=qR7V7NbZRN_ME90R2jL47AkGik1CY6JlAPhLBMXP2Gw,4714
|
|
38
|
+
puzzle_solver/puzzles/nurikabe/nurikabe.py,sha256=3cbW7X4kAMQK8PkH_t65fzT5cI0O6tWWOqpQUVyuGT4,6501
|
|
39
|
+
puzzle_solver/puzzles/palisade/palisade.py,sha256=T-LXlaLU5OwUQ24QWJWhBUFUktg0qDODTilNmBaXs4I,5014
|
|
40
|
+
puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
|
|
41
|
+
puzzle_solver/puzzles/range/range.py,sha256=q0J3crlGfjYZSA6Dh4iMCwP_gRMWid-_8KPgggOrFKk,4410
|
|
42
|
+
puzzle_solver/puzzles/rectangles/rectangles.py,sha256=MgOhZJGr9DVHb9bB8EAuwus0_8frBqRWqMwrOvMezHQ,6918
|
|
43
|
+
puzzle_solver/puzzles/shakashaka/shakashaka.py,sha256=PRpg_qI7XA3ysAo_g1TRJsT3VwB5Vial2UcFyBOMwKQ,9571
|
|
44
|
+
puzzle_solver/puzzles/shingoki/shingoki.py,sha256=heMuL9sm3jBegItjnqX05ttmDNiHSLB77BRljpeLLWk,7417
|
|
45
|
+
puzzle_solver/puzzles/signpost/signpost.py,sha256=38LlMvP5Fx4qrTXmw4aNCt3yUbG3fhdSk6-YXmhAHFg,3861
|
|
46
|
+
puzzle_solver/puzzles/singles/singles.py,sha256=KKn_Yl-eW874Bl1UmmcqoQ5vhNiO1JbM7fxKczOV5M4,2847
|
|
47
|
+
puzzle_solver/puzzles/slant/slant.py,sha256=xF-N4PuXYfx638NP1f1mi6YncIZB4mLtXtdS79XyPbg,6122
|
|
48
|
+
puzzle_solver/puzzles/slant/parse_map/parse_map.py,sha256=8thQxWbq0qjehKb2VzgUP22PGj-9n9djwbt3LGMVLJw,4811
|
|
49
|
+
puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=JpyNQk8K4nUziwWKxSvWEkF1RRBGLnCppCWK1Yf5bt0,7052
|
|
50
|
+
puzzle_solver/puzzles/star_battle/star_battle.py,sha256=IX6w4H3sifN01kPPtrAVRCK0Nl_xlXXSHvJKw8K1EuE,3718
|
|
51
|
+
puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
|
|
52
|
+
puzzle_solver/puzzles/stitches/stitches.py,sha256=bb5JXyclkbKq350MQ9d8AuGteQwSF8knaJ0DU9M92Uw,6515
|
|
53
|
+
puzzle_solver/puzzles/stitches/parse_map/parse_map.py,sha256=b21SQvlnDM6wOl_1iUhZ7X6akpBZoOnj3kEzImBCh8Q,10497
|
|
54
|
+
puzzle_solver/puzzles/sudoku/sudoku.py,sha256=rLq0N34v3Hb10CiptXtKxX-37OlQIyjIle9Es1FAtpM,13378
|
|
55
|
+
puzzle_solver/puzzles/tapa/tapa.py,sha256=TsOQhnEvlC1JxaWiEjQg2KxRXJR49GrN71DsMvPpia8,5337
|
|
56
|
+
puzzle_solver/puzzles/tents/tents.py,sha256=jccUXWA7KWAtPKpVJJYNI6masTYWQgx0eitcQw0-6Fc,6281
|
|
57
|
+
puzzle_solver/puzzles/thermometers/thermometers.py,sha256=bGcVmpPeqL5AJtj8jkK8gYThzv9aGCd_QrWEiYBCA2s,4011
|
|
58
|
+
puzzle_solver/puzzles/towers/towers.py,sha256=OLyTf9nTFR5L32-S_fhVyBmpz4i5YUNJotwOwbw_Fjg,6500
|
|
59
|
+
puzzle_solver/puzzles/tracks/tracks.py,sha256=98xds9SKNqtOLFTRUX_KSMC7XYmZo567LOFeqotVQaM,7237
|
|
60
|
+
puzzle_solver/puzzles/undead/undead.py,sha256=IGFQysgoaKZH8rKjqlrkoHsH28ve4_hKor2f0QOsWY0,6596
|
|
61
|
+
puzzle_solver/puzzles/unequal/unequal.py,sha256=ExY2XDCrqROCDpRLfHo8uVr1zuli1QvbCdNCiDhlCac,6978
|
|
62
|
+
puzzle_solver/puzzles/unruly/unruly.py,sha256=xwOUpC12uHbmlDj2guN60VaaHpLr1Y-WmMD5TKeHbZE,3826
|
|
63
|
+
puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=5WixT_7K1HwfQ_dWbuBlQfpU8p69zB2KvOg32XJ8vno,5255
|
|
64
|
+
puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
|
|
65
|
+
puzzle_solver/utils/visualizer.py,sha256=T2g5We9J3tkhyXWoN2OrIDIJDjt6w5sDd2ksOub0ZI8,6819
|
|
66
|
+
multi_puzzle_solver-1.0.2.dist-info/METADATA,sha256=LCKeSEhi50eG0kd-PUEbBBrpY7ZPuWau5Kz4csMTN84,347154
|
|
67
|
+
multi_puzzle_solver-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
68
|
+
multi_puzzle_solver-1.0.2.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
|
|
69
|
+
multi_puzzle_solver-1.0.2.dist-info/RECORD,,
|
puzzle_solver/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ 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
10
|
from puzzle_solver.puzzles.dominosa import dominosa as dominosa_solver
|
|
11
11
|
from puzzle_solver.puzzles.filling import filling as filling_solver
|
|
12
|
+
from puzzle_solver.puzzles.flood_it import flood_it as flood_it_solver
|
|
12
13
|
from puzzle_solver.puzzles.flip import flip as flip_solver
|
|
13
14
|
from puzzle_solver.puzzles.galaxies import galaxies as galaxies_solver
|
|
14
15
|
from puzzle_solver.puzzles.guess import guess as guess_solver
|
|
@@ -52,4 +53,60 @@ from puzzle_solver.puzzles.yin_yang import yin_yang as yin_yang_solver
|
|
|
52
53
|
|
|
53
54
|
from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
__all__ = [
|
|
57
|
+
aquarium_solver,
|
|
58
|
+
battleships_solver,
|
|
59
|
+
binairo_solver,
|
|
60
|
+
binairo_plus_solver,
|
|
61
|
+
black_box_solver,
|
|
62
|
+
bridges_solver,
|
|
63
|
+
chess_range_solver,
|
|
64
|
+
chess_solo_solver,
|
|
65
|
+
chess_melee_solver,
|
|
66
|
+
dominosa_solver,
|
|
67
|
+
filling_solver,
|
|
68
|
+
flood_it_solver,
|
|
69
|
+
flip_solver,
|
|
70
|
+
galaxies_solver,
|
|
71
|
+
guess_solver,
|
|
72
|
+
heyawake_solver,
|
|
73
|
+
inertia_solver,
|
|
74
|
+
kakurasu_solver,
|
|
75
|
+
kakuro_solver,
|
|
76
|
+
keen_solver,
|
|
77
|
+
light_up_solver,
|
|
78
|
+
magnets_solver,
|
|
79
|
+
map_solver,
|
|
80
|
+
minesweeper_solver,
|
|
81
|
+
mosaic_solver,
|
|
82
|
+
nonograms_solver,
|
|
83
|
+
norinori_solver,
|
|
84
|
+
nurikabe_solver,
|
|
85
|
+
palisade_solver,
|
|
86
|
+
lits_solver,
|
|
87
|
+
pearl_solver,
|
|
88
|
+
range_solver,
|
|
89
|
+
rectangles_solver,
|
|
90
|
+
shakashaka_solver,
|
|
91
|
+
shingoki_solver,
|
|
92
|
+
signpost_solver,
|
|
93
|
+
singles_solver,
|
|
94
|
+
slant_solver,
|
|
95
|
+
slitherlink_solver,
|
|
96
|
+
star_battle_solver,
|
|
97
|
+
star_battle_shapeless_solver,
|
|
98
|
+
stitches_solver,
|
|
99
|
+
sudoku_solver,
|
|
100
|
+
tapa_solver,
|
|
101
|
+
tents_solver,
|
|
102
|
+
thermometers_solver,
|
|
103
|
+
towers_solver,
|
|
104
|
+
tracks_solver,
|
|
105
|
+
undead_solver,
|
|
106
|
+
unequal_solver,
|
|
107
|
+
unruly_solver,
|
|
108
|
+
yin_yang_solver,
|
|
109
|
+
inertia_image_parser,
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
__version__ = '1.0.2'
|
|
@@ -14,6 +14,8 @@ class SingleSolution:
|
|
|
14
14
|
assignment: dict[Pos, Union[str, int]]
|
|
15
15
|
|
|
16
16
|
def get_hashable_solution(self) -> str:
|
|
17
|
+
if isinstance(self.assignment, list):
|
|
18
|
+
return json.dumps(self.assignment)
|
|
17
19
|
result = []
|
|
18
20
|
for pos, v in self.assignment.items():
|
|
19
21
|
result.append((pos.x, pos.y, v))
|
|
@@ -93,14 +95,14 @@ def force_connected_component(model: cp_model.CpModel, vars_to_force: dict[Any,
|
|
|
93
95
|
Total new variables: =4V [for N by M 2D grid total is 4NM]
|
|
94
96
|
"""
|
|
95
97
|
if is_neighbor is None:
|
|
96
|
-
is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1
|
|
98
|
+
is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1 # noqa: E731
|
|
97
99
|
|
|
98
100
|
vs = vars_to_force
|
|
99
101
|
v_count = len(vs)
|
|
100
102
|
if v_count <= 2: # graph must have at least 3 nodes to possibly be disconnected
|
|
101
103
|
return {}
|
|
102
104
|
# =V model variables, one for each variable
|
|
103
|
-
is_root: dict[Pos, cp_model.IntVar] = {} # =V, defines the unique
|
|
105
|
+
is_root: dict[Pos, cp_model.IntVar] = {} # =V, defines the unique
|
|
104
106
|
prefix_zero: dict[Pos, cp_model.IntVar] = {} # =V, used for picking the unique root
|
|
105
107
|
node_height: dict[Pos, cp_model.IntVar] = {} # =V, trickles down from the root
|
|
106
108
|
max_neighbor_height: dict[Pos, cp_model.IntVar] = {} # =V, the height of the tallest neighbor
|
|
@@ -141,7 +143,7 @@ def force_connected_component(model: cp_model.CpModel, vars_to_force: dict[Any,
|
|
|
141
143
|
model.Add(node_height[pi] == max_neighbor_height[pi] - 1).OnlyEnforceIf([vs[pi], is_root[pi].Not()])
|
|
142
144
|
model.Add(node_height[pi] == v_count).OnlyEnforceIf(is_root[pi])
|
|
143
145
|
model.Add(node_height[pi] == 0).OnlyEnforceIf(vs[pi].Not())
|
|
144
|
-
|
|
146
|
+
|
|
145
147
|
# final check: all active nodes have height > 0
|
|
146
148
|
for p in keys_in_order:
|
|
147
149
|
model.Add(node_height[p] > 0).OnlyEnforceIf(vs[p])
|
|
@@ -161,7 +163,7 @@ def force_no_loops(model: cp_model.CpModel, vars_to_force: dict[Any, cp_model.In
|
|
|
161
163
|
Returns a dictionary of new variables that can be used to enforce the no component constraint.
|
|
162
164
|
"""
|
|
163
165
|
if is_neighbor is None:
|
|
164
|
-
is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1
|
|
166
|
+
is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1 # noqa: E731
|
|
165
167
|
|
|
166
168
|
vs = vars_to_force
|
|
167
169
|
v_count = len(vs)
|
|
@@ -220,8 +222,8 @@ def force_no_loops(model: cp_model.CpModel, vars_to_force: dict[Any, cp_model.In
|
|
|
220
222
|
model.Add(tree_edge[(p_parent, p)] == 0).OnlyEnforceIf([is_root[p]])
|
|
221
223
|
# every active node has exactly 1 parent except root has none
|
|
222
224
|
model.AddExactlyOne([tree_edge[(p_parent, p)] for p_parent in parent_of[p]] + [vs[p].Not(), is_root[p]])
|
|
223
|
-
|
|
224
|
-
# now each subgraph has directions where each non-root points to a single parent (and its value is parent+1).
|
|
225
|
+
|
|
226
|
+
# now each subgraph has directions where each non-root points to a single parent (and its value is parent+1).
|
|
225
227
|
# to break cycles, every non-root active node must be > all neighbors that arent children
|
|
226
228
|
|
|
227
229
|
all_new_vars: dict[str, cp_model.IntVar] = {}
|
|
@@ -94,7 +94,7 @@ def render_grid(cell_flags: np.ndarray,
|
|
|
94
94
|
for r in range(R):
|
|
95
95
|
rr = 2*r + 1
|
|
96
96
|
for c in range(C):
|
|
97
|
-
val = center_char if isinstance(center_char, str) else center_char[r, c]
|
|
97
|
+
val = center_char if isinstance(center_char, str) else (center_char(r, c) if callable(center_char) else center_char[r, c])
|
|
98
98
|
put_center_text(rr, c, '' if val is None else str(val))
|
|
99
99
|
|
|
100
100
|
# rows -> strings
|
|
@@ -159,13 +159,7 @@ def id_board_to_wall_board(id_board: np.array, border_is_wall = True) -> np.arra
|
|
|
159
159
|
def render_shaded_grid(V: int,
|
|
160
160
|
H: int,
|
|
161
161
|
is_shaded: Callable[[int, int], bool],
|
|
162
|
-
|
|
163
|
-
scale_x: int = 2,
|
|
164
|
-
scale_y: int = 1,
|
|
165
|
-
fill_char: str = '▒',
|
|
166
|
-
empty_char: str = ' ',
|
|
167
|
-
empty_text: Optional[Union[str, Callable[[int, int], Optional[str]]]] = None,
|
|
168
|
-
show_axes: bool = True) -> str:
|
|
162
|
+
empty_text: Optional[Union[str, Callable[[int, int], Optional[str]]]] = None,) -> str:
|
|
169
163
|
"""
|
|
170
164
|
Most of this function was AI generated then modified by me, I don't currently care about the details of rendering to the terminal this looked good enough during my testing.
|
|
171
165
|
Visualize a V x H grid where each cell is shaded if is_shaded(r, c) is True.
|
|
@@ -179,6 +173,11 @@ def render_shaded_grid(V: int,
|
|
|
179
173
|
cells. If a callable (r, c) -> str|None, used per cell. Text is
|
|
180
174
|
centered within the interior row and truncated to fit.
|
|
181
175
|
"""
|
|
176
|
+
scale_x: int = 2
|
|
177
|
+
scale_y: int = 1
|
|
178
|
+
fill_char: str = '▒'
|
|
179
|
+
empty_char: str = ' '
|
|
180
|
+
show_axes: bool = True
|
|
182
181
|
assert V >= 1 and H >= 1
|
|
183
182
|
assert scale_x >= 1 and scale_y >= 1
|
|
184
183
|
assert len(fill_char) == 1 and len(empty_char) == 1
|
|
@@ -349,8 +348,10 @@ def render_bw_tiles_split(
|
|
|
349
348
|
if not use_color:
|
|
350
349
|
return ""
|
|
351
350
|
parts = []
|
|
352
|
-
if fg is not None:
|
|
353
|
-
|
|
351
|
+
if fg is not None:
|
|
352
|
+
parts.append(str(fg))
|
|
353
|
+
if bg is not None:
|
|
354
|
+
parts.append(str(bg))
|
|
354
355
|
return ("\x1b[" + ";".join(parts) + "m") if parts else ""
|
|
355
356
|
|
|
356
357
|
RESET = "\x1b[0m" if use_color else ""
|
|
@@ -383,26 +384,17 @@ def render_bw_tiles_split(
|
|
|
383
384
|
else: # y = 1 - x
|
|
384
385
|
return (fy < 1 - fx) if val == "TL" else (fy > 1 - fx)
|
|
385
386
|
|
|
386
|
-
def on_boundary(val: CellVal, fx: float, fy: float) -> bool:
|
|
387
|
-
if val in ("B","W"):
|
|
388
|
-
return False
|
|
389
|
-
kind, _ = diag_kind_and_slash(val)
|
|
390
|
-
eps = 0.5 / max(cell_w, cell_h) # thin boundary
|
|
391
|
-
if kind == "main":
|
|
392
|
-
return abs(fy - fx) <= eps
|
|
393
|
-
else:
|
|
394
|
-
return abs(fy - (1 - fx)) <= eps
|
|
395
|
-
|
|
396
387
|
# Build one tile as a matrix of 1-char tokens (already colorized if ANSI)
|
|
397
388
|
def make_tile(val: CellVal) -> List[List[str]]:
|
|
398
389
|
rows: List[List[str]] = []
|
|
399
|
-
|
|
400
|
-
|
|
390
|
+
_, slash_ch = diag_kind_and_slash(val)
|
|
401
391
|
for y in range(cell_h):
|
|
402
392
|
fy = (y + 0.5) / cell_h
|
|
403
393
|
line: List[str] = []
|
|
394
|
+
prev = None
|
|
404
395
|
for x in range(cell_w):
|
|
405
396
|
fx = (x + 0.5) / cell_w
|
|
397
|
+
fx_next = (x + 1.5) / cell_w
|
|
406
398
|
|
|
407
399
|
if val == "B":
|
|
408
400
|
line.append(sgr(bg=BG_BLACK) + " " + RESET if use_color else TXT_BLACK)
|
|
@@ -412,7 +404,12 @@ def render_bw_tiles_split(
|
|
|
412
404
|
continue
|
|
413
405
|
|
|
414
406
|
black_side = is_black(val, fx, fy)
|
|
415
|
-
|
|
407
|
+
next_black_side = is_black(val, fx_next, fy)
|
|
408
|
+
boundary = False # if true places a "/" or "\" at the current position
|
|
409
|
+
if prev is not None and not prev and black_side: # prev white and cur black => boundary now
|
|
410
|
+
boundary = True
|
|
411
|
+
if black_side and not next_black_side: # cur black and next white => boundary now
|
|
412
|
+
boundary = True
|
|
416
413
|
|
|
417
414
|
if use_color:
|
|
418
415
|
bg = BG_BLACK if black_side else BG_WHITE
|
|
@@ -426,6 +423,7 @@ def render_bw_tiles_split(
|
|
|
426
423
|
line.append(slash_ch)
|
|
427
424
|
else:
|
|
428
425
|
line.append(TXT_BLACK if black_side else TXT_WHITE)
|
|
426
|
+
prev = black_side
|
|
429
427
|
rows.append(line)
|
|
430
428
|
return rows
|
|
431
429
|
|
|
@@ -435,23 +433,7 @@ def render_bw_tiles_split(
|
|
|
435
433
|
return
|
|
436
434
|
ch = ch[0] # keep one character (user said single number)
|
|
437
435
|
cx, cy = cell_w // 2, cell_h // 2
|
|
438
|
-
|
|
439
|
-
fy = (cy + 0.5) / cell_h
|
|
440
|
-
|
|
441
|
-
# If center is boundary or not black, nudge horizontally toward black side
|
|
442
|
-
if val in ("TL","TR","BL","BR"):
|
|
443
|
-
kind, _ = diag_kind_and_slash(val)
|
|
444
|
-
# Determine which side is black relative to x at this y
|
|
445
|
-
if kind == "main": # boundary y=x → compare fx vs fy
|
|
446
|
-
want_right = (val == "TR") # black is to the right of boundary
|
|
447
|
-
if on_boundary(val, fx, fy) or (is_black(val, fx, fy) is False):
|
|
448
|
-
if want_right and cx + 1 < cell_w: cx += 1
|
|
449
|
-
elif not want_right and cx - 1 >= 0: cx -= 1
|
|
450
|
-
else: # boundary y=1-x → compare fx vs 1-fy
|
|
451
|
-
want_left = (val == "TL") # black is to the left of boundary
|
|
452
|
-
if on_boundary(val, fx, fy) or (is_black(val, fx, fy) is False):
|
|
453
|
-
if want_left and cx - 1 >= 0: cx -= 1
|
|
454
|
-
elif not want_left and cx + 1 < cell_w: cx += 1
|
|
436
|
+
cx -= 1
|
|
455
437
|
|
|
456
438
|
# Compose the glyph for that spot
|
|
457
439
|
if use_color:
|
|
@@ -520,4 +502,4 @@ def render_bw_tiles_split(
|
|
|
520
502
|
# mode="text", # ← key change
|
|
521
503
|
# text_palette="solid" # try "solid" for stark black/white
|
|
522
504
|
# )
|
|
523
|
-
# print("```text\n" + art + "\n```")
|
|
505
|
+
# print("```text\n" + art + "\n```")
|
|
@@ -56,13 +56,13 @@ class Board:
|
|
|
56
56
|
self.disallow_three_in_a_row(pos, Direction.RIGHT)
|
|
57
57
|
self.disallow_three_in_a_row(pos, Direction.DOWN)
|
|
58
58
|
|
|
59
|
-
# 3. Each row and column is unique.
|
|
59
|
+
# 3. Each row and column is unique.
|
|
60
60
|
if self.force_unique:
|
|
61
61
|
# a list per row
|
|
62
62
|
self.force_unique_double_list([[self.model_vars[pos] for pos in get_row_pos(row, self.H)] for row in range(self.V)])
|
|
63
63
|
# a list per column
|
|
64
64
|
self.force_unique_double_list([[self.model_vars[pos] for pos in get_col_pos(col, self.V)] for col in range(self.H)])
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
# if arithmetic is provided, add constraints for it
|
|
67
67
|
if self.arith_rows is not None:
|
|
68
68
|
assert self.arith_rows.shape == (self.V, self.H-1), f'arith_rows must be one column less than board, got {self.arith_rows.shape} for {self.board.shape}'
|
|
@@ -106,10 +106,10 @@ class Board:
|
|
|
106
106
|
|
|
107
107
|
codes = []
|
|
108
108
|
pow2 = [1 << k for k in range(m)] # weights for bit positions (LSB at index 0)
|
|
109
|
-
for i,
|
|
109
|
+
for i, line in enumerate(model_vars):
|
|
110
110
|
code = self.model.NewIntVar(0, (1 << m) - 1, f"code_{i}")
|
|
111
111
|
# Sum 2^k * r[k] == code
|
|
112
|
-
self.model.Add(code == sum(pow2[k] *
|
|
112
|
+
self.model.Add(code == sum(pow2[k] * line[k] for k in range(m)))
|
|
113
113
|
codes.append(code)
|
|
114
114
|
|
|
115
115
|
self.model.AddAllDifferent(codes)
|
|
@@ -50,7 +50,7 @@ class Board:
|
|
|
50
50
|
self.right_values = right
|
|
51
51
|
self.bottom_values = bottom
|
|
52
52
|
self.left_values = left
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
self.model = cp_model.CpModel()
|
|
55
55
|
self.ball_states: dict[Pos, cp_model.IntVar] = {}
|
|
56
56
|
# (entry_pos, T, cell_pos, direction) -> True if the beam that entered from the board at "entry_pos" is present in "cell_pos" and is going in the direction "direction" at time T
|
|
@@ -86,7 +86,7 @@ class Board:
|
|
|
86
86
|
for cell in self.get_all_pos_extended():
|
|
87
87
|
for direction in Direction:
|
|
88
88
|
self.beam_states[(entry_pos, t, cell, direction)] = self.model.NewBoolVar(f'beam:{entry_pos}:{t}:{cell}:{direction}')
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
for (entry_pos, t, cell, direction) in self.beam_states.keys():
|
|
91
91
|
if t not in self.beam_states_at_t:
|
|
92
92
|
self.beam_states_at_t[t] = {}
|
|
@@ -110,7 +110,7 @@ class Board:
|
|
|
110
110
|
beam_ids.extend((beam_id, Direction.LEFT) for beam_id in self.right_cells)
|
|
111
111
|
beam_ids.extend((beam_id, Direction.UP) for beam_id in self.bottom_cells)
|
|
112
112
|
beam_ids.extend((beam_id, Direction.RIGHT) for beam_id in self.left_cells)
|
|
113
|
-
|
|
113
|
+
|
|
114
114
|
for (beam_id, direction) in beam_ids:
|
|
115
115
|
# beam at t=0 is present at beam_id and facing direction
|
|
116
116
|
self.model.Add(self.beam_states[(beam_id, 0, beam_id, direction)] == 1)
|
|
@@ -189,7 +189,7 @@ class Board:
|
|
|
189
189
|
else:
|
|
190
190
|
ball_right = False
|
|
191
191
|
ball_right_not = True
|
|
192
|
-
|
|
192
|
+
|
|
193
193
|
pos_left = get_next_pos(cur_pos, direction_left)
|
|
194
194
|
pos_right = get_next_pos(cur_pos, direction_right)
|
|
195
195
|
pos_reflected = get_next_pos(cur_pos, reflected)
|
|
@@ -304,10 +304,4 @@ class Board:
|
|
|
304
304
|
ball_state = 'O' if single_res.assignment[pos] else ' '
|
|
305
305
|
res[pos.y][pos.x] = ball_state
|
|
306
306
|
print(res)
|
|
307
|
-
|
|
308
|
-
# print('non unique count:', count)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
307
|
+
generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -73,7 +73,7 @@ class Board:
|
|
|
73
73
|
xhoriz_min = min(horiz_bridge[0].x, horiz_bridge[1].x)
|
|
74
74
|
xhoriz_max = max(horiz_bridge[0].x, horiz_bridge[1].x)
|
|
75
75
|
yhoriz = horiz_bridge[0].y
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
# no equals because thats what the puzzle says
|
|
78
78
|
x_contained = xhoriz_min < xvert < xhoriz_max
|
|
79
79
|
y_contained = yvert_min < yhoriz < yvert_max
|
|
@@ -173,11 +173,11 @@ class Board:
|
|
|
173
173
|
self.H = 8 # board size
|
|
174
174
|
# the puzzle rules mean the only legal positions are the starting positions of the pieces
|
|
175
175
|
self.all_legal_positions: set[Pos] = {pos for _, pos in self.pieces.values()}
|
|
176
|
-
assert len(self.all_legal_positions) == len(self.pieces),
|
|
176
|
+
assert len(self.all_legal_positions) == len(self.pieces), 'positions are not unique'
|
|
177
177
|
|
|
178
178
|
self.model = cp_model.CpModel()
|
|
179
179
|
# Input numbers: N is number of piece, T is number of time steps (=N here), B is board size (=N here because the only legal positions are the starting positions of the pieces):
|
|
180
|
-
# Number of variables
|
|
180
|
+
# Number of variables
|
|
181
181
|
# piece_positions: O(NTB)
|
|
182
182
|
# is_dead: O(NT)
|
|
183
183
|
# mover: O(NT)
|
|
@@ -341,7 +341,7 @@ class Board:
|
|
|
341
341
|
for t in range(self.T - 1):
|
|
342
342
|
self.model.AddExactlyOne([self.victim[(p, t)] for p in range(self.N)])
|
|
343
343
|
|
|
344
|
-
# optional parameter to force
|
|
344
|
+
# optional parameter to force
|
|
345
345
|
if self.max_moves_per_piece is not None:
|
|
346
346
|
for p in range(self.N):
|
|
347
347
|
self.model.Add(sum([self.mover[(p, t)] for t in range(self.T - 1)]) <= self.max_moves_per_piece)
|
|
@@ -4,6 +4,6 @@ from .chess_range import PieceType
|
|
|
4
4
|
class Board(RangeBoard):
|
|
5
5
|
def __init__(self, pieces: list[str]):
|
|
6
6
|
king_pieces = [p for p in range(len(pieces)) if pieces[p][0] == 'K']
|
|
7
|
-
assert len(king_pieces) == 1,
|
|
7
|
+
assert len(king_pieces) == 1, 'exactly one king piece is required'
|
|
8
8
|
super().__init__(pieces, max_moves_per_piece=2, last_piece_alive=PieceType.KING)
|
|
9
9
|
|
|
@@ -3,8 +3,8 @@ from dataclasses import dataclass
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from ortools.sat.python import cp_model
|
|
5
5
|
|
|
6
|
-
from puzzle_solver.core.utils import Pos,
|
|
7
|
-
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
6
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, polyominoes, in_bounds, get_next_pos, Direction
|
|
7
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@dataclass
|
|
@@ -85,7 +85,7 @@ class Board:
|
|
|
85
85
|
# exactly one shape is active at that position
|
|
86
86
|
self.model.AddExactlyOne(s.is_active for d in self.digits for s in self.body_loc_to_shape[(d,pos)])
|
|
87
87
|
# if a shape is active then all its body is active
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
for s_list in self.body_loc_to_shape.values():
|
|
90
90
|
for s in s_list:
|
|
91
91
|
for p in s.body:
|
|
@@ -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
|
+
|