multi-puzzle-solver 1.0.2__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.

@@ -1,4 +1,4 @@
1
- puzzle_solver/__init__.py,sha256=Ll-qN1ElCTTILeun1u4t5dU0CdI3DkCX0ZNf0Q2UJtE,4886
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=0aw1TbiyxknY2hUAXaP3nXqT6I6mT9BIiERJSCj57xw,8245
20
- puzzle_solver/puzzles/galaxies/galaxies.py,sha256=36X9jaQfvLIWFkBY1VZH6I59eCDkc77U06NDtKRUECY,5571
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,16 +29,17 @@ 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=3fPIkhAIUz8JokcfaE_ZM3b0AFEnf5xPzGJ2qnm8SWY,7099
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=qR7V7NbZRN_ME90R2jL47AkGik1CY6JlAPhLBMXP2Gw,4714
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
42
+ puzzle_solver/puzzles/pipes/pipes.py,sha256=SPPgmYXeqjdzijLqdIb_TtlGmxzIad6MHQ31pyDcgUc,4448
41
43
  puzzle_solver/puzzles/range/range.py,sha256=q0J3crlGfjYZSA6Dh4iMCwP_gRMWid-_8KPgggOrFKk,4410
42
44
  puzzle_solver/puzzles/rectangles/rectangles.py,sha256=MgOhZJGr9DVHb9bB8EAuwus0_8frBqRWqMwrOvMezHQ,6918
43
45
  puzzle_solver/puzzles/shakashaka/shakashaka.py,sha256=PRpg_qI7XA3ysAo_g1TRJsT3VwB5Vial2UcFyBOMwKQ,9571
@@ -47,7 +49,7 @@ puzzle_solver/puzzles/singles/singles.py,sha256=KKn_Yl-eW874Bl1UmmcqoQ5vhNiO1JbM
47
49
  puzzle_solver/puzzles/slant/slant.py,sha256=xF-N4PuXYfx638NP1f1mi6YncIZB4mLtXtdS79XyPbg,6122
48
50
  puzzle_solver/puzzles/slant/parse_map/parse_map.py,sha256=8thQxWbq0qjehKb2VzgUP22PGj-9n9djwbt3LGMVLJw,4811
49
51
  puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=JpyNQk8K4nUziwWKxSvWEkF1RRBGLnCppCWK1Yf5bt0,7052
50
- puzzle_solver/puzzles/star_battle/star_battle.py,sha256=IX6w4H3sifN01kPPtrAVRCK0Nl_xlXXSHvJKw8K1EuE,3718
52
+ puzzle_solver/puzzles/star_battle/star_battle.py,sha256=hFV5IKPQDWrIWr50YpiOS9VF2kUScDQpvAGvZKBwuyM,3937
51
53
  puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
52
54
  puzzle_solver/puzzles/stitches/stitches.py,sha256=bb5JXyclkbKq350MQ9d8AuGteQwSF8knaJ0DU9M92Uw,6515
53
55
  puzzle_solver/puzzles/stitches/parse_map/parse_map.py,sha256=b21SQvlnDM6wOl_1iUhZ7X6akpBZoOnj3kEzImBCh8Q,10497
@@ -57,13 +59,14 @@ puzzle_solver/puzzles/tents/tents.py,sha256=jccUXWA7KWAtPKpVJJYNI6masTYWQgx0eitc
57
59
  puzzle_solver/puzzles/thermometers/thermometers.py,sha256=bGcVmpPeqL5AJtj8jkK8gYThzv9aGCd_QrWEiYBCA2s,4011
58
60
  puzzle_solver/puzzles/towers/towers.py,sha256=OLyTf9nTFR5L32-S_fhVyBmpz4i5YUNJotwOwbw_Fjg,6500
59
61
  puzzle_solver/puzzles/tracks/tracks.py,sha256=98xds9SKNqtOLFTRUX_KSMC7XYmZo567LOFeqotVQaM,7237
62
+ puzzle_solver/puzzles/twiddle/twiddle.py,sha256=3gPoeD0DoiiZbIhtptdXFldO_t1QShL6IxkDqJMzjkk,5446
60
63
  puzzle_solver/puzzles/undead/undead.py,sha256=IGFQysgoaKZH8rKjqlrkoHsH28ve4_hKor2f0QOsWY0,6596
61
64
  puzzle_solver/puzzles/unequal/unequal.py,sha256=ExY2XDCrqROCDpRLfHo8uVr1zuli1QvbCdNCiDhlCac,6978
62
65
  puzzle_solver/puzzles/unruly/unruly.py,sha256=xwOUpC12uHbmlDj2guN60VaaHpLr1Y-WmMD5TKeHbZE,3826
63
66
  puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=5WixT_7K1HwfQ_dWbuBlQfpU8p69zB2KvOg32XJ8vno,5255
64
67
  puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
65
68
  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,,
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
@@ -29,6 +30,7 @@ from puzzle_solver.puzzles.nurikabe import nurikabe as nurikabe_solver
29
30
  from puzzle_solver.puzzles.palisade import palisade as palisade_solver
30
31
  from puzzle_solver.puzzles.lits import lits as lits_solver
31
32
  from puzzle_solver.puzzles.pearl import pearl as pearl_solver
33
+ from puzzle_solver.puzzles.pipes import pipes as pipes_solver
32
34
  from puzzle_solver.puzzles.range import range as range_solver
33
35
  from puzzle_solver.puzzles.rectangles import rectangles as rectangles_solver
34
36
  from puzzle_solver.puzzles.shakashaka import shakashaka as shakashaka_solver
@@ -46,6 +48,7 @@ from puzzle_solver.puzzles.tents import tents as tents_solver
46
48
  from puzzle_solver.puzzles.thermometers import thermometers as thermometers_solver
47
49
  from puzzle_solver.puzzles.towers import towers as towers_solver
48
50
  from puzzle_solver.puzzles.tracks import tracks as tracks_solver
51
+ from puzzle_solver.puzzles.twiddle import twiddle as twiddle_solver
49
52
  from puzzle_solver.puzzles.undead import undead as undead_solver
50
53
  from puzzle_solver.puzzles.unequal import unequal as unequal_solver
51
54
  from puzzle_solver.puzzles.unruly import unruly as unruly_solver
@@ -63,6 +66,7 @@ __all__ = [
63
66
  chess_range_solver,
64
67
  chess_solo_solver,
65
68
  chess_melee_solver,
69
+ connect_the_dots_solver,
66
70
  dominosa_solver,
67
71
  filling_solver,
68
72
  flood_it_solver,
@@ -85,6 +89,7 @@ __all__ = [
85
89
  palisade_solver,
86
90
  lits_solver,
87
91
  pearl_solver,
92
+ pipes_solver,
88
93
  range_solver,
89
94
  rectangles_solver,
90
95
  shakashaka_solver,
@@ -102,6 +107,7 @@ __all__ = [
102
107
  thermometers_solver,
103
108
  towers_solver,
104
109
  tracks_solver,
110
+ twiddle_solver,
105
111
  undead_solver,
106
112
  unequal_solver,
107
113
  unruly_solver,
@@ -109,4 +115,4 @@ __all__ = [
109
115
  inertia_image_parser,
110
116
  ]
111
117
 
112
- __version__ = '1.0.2'
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)
@@ -75,7 +75,6 @@ def mean_consecutives(arr: np.ndarray) -> np.ndarray:
75
75
  def main(image):
76
76
  global Image
77
77
  global cv
78
- import matplotlib.pyplot as plt
79
78
  from PIL import Image as Image_module
80
79
  import cv2 as cv_module
81
80
  Image = Image_module
@@ -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
- def parse_numpy(galaxies: np.ndarray) -> list[tuple[Pos, ...]]:
12
- result = defaultdict(list)
13
- for pos, arr_id in np.ndenumerate(galaxies):
14
- if not arr_id.strip():
15
- continue
16
- result[arr_id].append(get_pos(x=pos[1], y=pos[0]))
17
- return [positions for _, positions in sorted(result.items(), key=lambda x: x[0])]
18
-
19
-
20
- class Board:
21
- def __init__(self, galaxies: Union[list[tuple[Pos, ...]], np.ndarray], V: int = None, H: int = None):
22
- if isinstance(galaxies, np.ndarray):
23
- V, H = galaxies.shape
24
- galaxies = parse_numpy(galaxies)
25
- else:
26
- assert V is not None and H is not None, 'V and H must be provided if galaxies is not a numpy array'
27
- assert V >= 1 and H >= 1, 'V and H must be at least 1'
28
- assert all(isinstance(galaxy, Iterable) for galaxy in galaxies), 'galaxies must be a list of Iterables'
29
- assert all(len(galaxy) in [1, 2, 4] for galaxy in galaxies), 'each galaxy must be exactly 1, 2, or 4 positions'
30
- self.V = V
31
- self.H = H
32
- self.n_galaxies = len(galaxies)
33
- self.galaxies = galaxies
34
- self.prelocated_positions: set[Pos] = {pos: i for i, galaxy in enumerate(galaxies) for pos in galaxy}
35
-
36
- self.model = cp_model.CpModel()
37
- 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
38
- self.allocated_pairs: set[tuple[Pos, Pos]] = set() # each pair is allocated to exactly one galaxy
39
-
40
- self.create_vars()
41
- self.add_all_constraints()
42
-
43
- def create_vars(self):
44
- for i in range(self.n_galaxies):
45
- galaxy = self.galaxies[i]
46
- if len(galaxy) == 1:
47
- p1, p2 = galaxy[0], galaxy[0]
48
- elif len(galaxy) == 2:
49
- p1, p2 = galaxy[0], galaxy[1]
50
- elif len(galaxy) == 4:
51
- p1, p2 = galaxy[0], galaxy[3] # [1] and [2] will be linked with symmetry
52
- self.expand_galaxy(p1, p2, i)
53
-
54
- def expand_galaxy(self, p1: Pos, p2: Pos, galaxy_idx: int):
55
- if (p1, p2) in self.allocated_pairs or (p2, p1) in self.allocated_pairs:
56
- return
57
- if p1 in self.prelocated_positions and self.prelocated_positions[p1] != galaxy_idx:
58
- return
59
- if p2 in self.prelocated_positions and self.prelocated_positions[p2] != galaxy_idx:
60
- return
61
- if not in_bounds(p1, self.V, self.H) or not in_bounds(p2, self.V, self.H):
62
- return
63
- self.bind_pair(p1, p2, galaxy_idx)
64
- # symmetrically expand the galaxy until illegal position is hit
65
- for direction in [Direction.RIGHT, Direction.UP, Direction.DOWN, Direction.LEFT]:
66
- symmetrical_direction = get_opposite_direction(direction)
67
- new_p1 = get_next_pos(p1, direction)
68
- new_p2 = get_next_pos(p2, symmetrical_direction)
69
- self.expand_galaxy(new_p1, new_p2, galaxy_idx)
70
-
71
- def bind_pair(self, p1: Pos, p2: Pos, galaxy_idx: int):
72
- assert galaxy_idx not in self.pos_to_galaxy[p1], f'p1={p1} already has galaxy idx={galaxy_idx}'
73
- assert galaxy_idx not in self.pos_to_galaxy[p2], f'p2={p2} already has galaxy idx={galaxy_idx}'
74
- self.allocated_pairs.add((p1, p2))
75
- v1 = self.model.NewBoolVar(f'{p1}:{galaxy_idx}')
76
- v2 = self.model.NewBoolVar(f'{p2}:{galaxy_idx}')
77
- self.model.Add(v1 == v2)
78
- self.pos_to_galaxy[p1][galaxy_idx] = v1
79
- self.pos_to_galaxy[p2][galaxy_idx] = v2
80
-
81
- def add_all_constraints(self):
82
- galaxy_vars = {}
83
- for pos in get_all_pos(self.V, self.H):
84
- pos_vars = list(self.pos_to_galaxy[pos].values())
85
- self.model.AddExactlyOne(pos_vars)
86
- for galaxy_idx, v in self.pos_to_galaxy[pos].items():
87
- galaxy_vars.setdefault(galaxy_idx, {})[pos] = v
88
- for pos_vars in galaxy_vars.values():
89
- force_connected_component(self.model, pos_vars)
90
-
91
-
92
- def solve_and_print(self, verbose: bool = True):
93
- def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
94
- assignment: dict[Pos, int] = {}
95
- for pos, galaxy_vars in board.pos_to_galaxy.items():
96
- for galaxy_idx, var in galaxy_vars.items(): # every pos is part of exactly one galaxy
97
- if solver.Value(var) == 1:
98
- assignment[pos] = galaxy_idx
99
- break
100
- return SingleSolution(assignment=assignment)
101
- def callback(single_res: SingleSolution):
102
- print("Solution found")
103
- res = np.full((self.V, self.H), ' ', dtype=object)
104
- for pos in get_all_pos(self.V, self.H):
105
- set_char(res, pos, str(single_res.assignment[pos]).zfill(2))
106
- print('[')
107
- for row in range(self.V):
108
- print(' ', res[row].tolist(), end=',\n')
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=str)
132
+ res = np.full((self.V, self.H), ' ', dtype=object)
132
133
  for pos, val in single_res.assignment.items():
133
- c = 'X' if val == 1 else ' '
134
- set_char(res, pos, c)
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
- c = get_char(self.board, pos)
95
- c = 'X' if pos in single_res.assignment else ' '
96
- set_char(res, pos, c)
97
- print('[')
98
- for row in res:
99
- print(" [ '" + "', '".join(row.tolist()) + "' ],")
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)
@@ -0,0 +1,81 @@
1
+ import numpy as np
2
+ from ortools.sat.python import cp_model
3
+ from ortools.sat.python.cp_model import LinearExpr as lxp
4
+
5
+ from puzzle_solver.core.utils import Pos, get_all_pos, set_char, get_char, Direction, get_next_pos, get_opposite_direction
6
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
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().strip() in ['1', '2L', '2I', '3', '4'] for c in np.nditer(board)), 'board must contain only 1, 2L, 2I, 3, 4. Found:' + str(set(c.item().strip() for c in np.nditer(board)) - set(['1', '2L', '2I', '3', '4']))
13
+ self.board = board
14
+ self.V, self.H = board.shape
15
+ self.model = cp_model.CpModel()
16
+ self.model_vars: dict[Pos, cp_model.IntVar] = {}
17
+
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 direction in Direction:
24
+ mirrored = (get_next_pos(pos, direction), get_opposite_direction(direction))
25
+ if mirrored in self.model_vars:
26
+ self.model_vars[(pos, direction)] = self.model_vars[mirrored]
27
+ else:
28
+ self.model_vars[(pos, direction)] = self.model.NewBoolVar(f'{pos}:{direction}')
29
+
30
+ def add_all_constraints(self):
31
+ for pos in get_all_pos(self.V, self.H):
32
+ self.force_position(pos, get_char(self.board, pos).strip())
33
+ # single connected component
34
+ self.force_connected_component()
35
+
36
+ def force_connected_component(self):
37
+ def is_neighbor(pd1: tuple[Pos, Direction], pd2: tuple[Pos, Direction]) -> bool:
38
+ p1, d1 = pd1
39
+ p2, d2 = pd2
40
+ if p1 == p2 and d1 != d2: # same position, different direction, is neighbor
41
+ return True
42
+ if get_next_pos(p1, d1) == p2 and d2 == get_opposite_direction(d1):
43
+ return True
44
+ return False
45
+ force_connected_component(self.model, self.model_vars, is_neighbor=is_neighbor)
46
+
47
+ def force_position(self, pos: Pos, value: str):
48
+ # cells with 1 or 3 or 4 neighbors each only have 1 unique state under rotational symmetry
49
+ # cells with 2 neighbors can either be a straight line (2I) or curved line (2L)
50
+ if value == '1':
51
+ self.model.Add(lxp.sum([self.model_vars[(pos, direction)] for direction in Direction]) == 1)
52
+ elif value == '2L':
53
+ self.model.Add(self.model_vars[(pos, Direction.LEFT)] != self.model_vars[(pos, Direction.RIGHT)])
54
+ self.model.Add(self.model_vars[(pos, Direction.UP)] != self.model_vars[(pos, Direction.DOWN)])
55
+ elif value == '2I':
56
+ self.model.Add(self.model_vars[(pos, Direction.LEFT)] == self.model_vars[(pos, Direction.RIGHT)])
57
+ self.model.Add(self.model_vars[(pos, Direction.UP)] == self.model_vars[(pos, Direction.DOWN)])
58
+ self.model.Add(self.model_vars[(pos, Direction.UP)] != self.model_vars[(pos, Direction.RIGHT)])
59
+ elif value == '3':
60
+ self.model.Add(lxp.sum([self.model_vars[(pos, direction)] for direction in Direction]) == 3)
61
+ elif value == '4':
62
+ self.model.Add(lxp.sum([self.model_vars[(pos, direction)] for direction in Direction]) == 4)
63
+ else:
64
+ raise ValueError(f'invalid value: {value}')
65
+
66
+ def solve_and_print(self, verbose: bool = True):
67
+ def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
68
+ assignment = {}
69
+ for pos in get_all_pos(self.V, self.H):
70
+ assignment[pos] = ''
71
+ for direction in Direction:
72
+ if solver.Value(board.model_vars[(pos, direction)]) == 1:
73
+ assignment[pos] += direction.name[0]
74
+ return SingleSolution(assignment=assignment)
75
+ def callback(single_res: SingleSolution):
76
+ print("Solution found")
77
+ res = np.full((self.V, self.H), ' ', dtype=object)
78
+ for pos in get_all_pos(self.V, self.H):
79
+ set_char(res, pos, single_res.assignment[pos])
80
+ print(res)
81
+ 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
- c = '*' if single_res.assignment[pos] == 1 else ' '
65
- set_char(res, pos, c)
66
- for row in range(self.V):
67
- print(res[row].tolist(), end='')
68
- if row != self.V - 1:
69
- print(',', end='')
70
- print()
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)