multi-puzzle-solver 0.9.27__py3-none-any.whl → 0.9.30__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.27.dist-info → multi_puzzle_solver-0.9.30.dist-info}/METADATA +861 -46
- {multi_puzzle_solver-0.9.27.dist-info → multi_puzzle_solver-0.9.30.dist-info}/RECORD +15 -9
- puzzle_solver/__init__.py +7 -1
- puzzle_solver/core/utils_visualizer.py +214 -1
- puzzle_solver/puzzles/binairo/binairo.py +44 -16
- puzzle_solver/puzzles/binairo/binairo_plus.py +7 -0
- puzzle_solver/puzzles/heyawake/heyawake.py +94 -0
- puzzle_solver/puzzles/kakuro/kakuro.py +77 -0
- puzzle_solver/puzzles/shakashaka/shakashaka.py +201 -0
- puzzle_solver/puzzles/shingoki/shingoki.py +158 -0
- puzzle_solver/puzzles/singles/singles.py +14 -40
- puzzle_solver/puzzles/stitches/parse_map/parse_map.py +3 -1
- puzzle_solver/puzzles/tapa/tapa.py +98 -0
- {multi_puzzle_solver-0.9.27.dist-info → multi_puzzle_solver-0.9.30.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.9.27.dist-info → multi_puzzle_solver-0.9.30.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
puzzle_solver/__init__.py,sha256=
|
|
1
|
+
puzzle_solver/__init__.py,sha256=f79JI0EQhfQi12yO6gvuzzLtxXQgojVrq-v8HZrzjS0,3693
|
|
2
2
|
puzzle_solver/core/utils.py,sha256=XBW5j-IwtJMPMP-ycmY6SqRCM1NOVl5O6UeoGqNj618,8153
|
|
3
3
|
puzzle_solver/core/utils_ortools.py,sha256=_i8cixHOB5XGqqcr-493bOiZgYJidnvxQMEfj--Trns,10278
|
|
4
|
-
puzzle_solver/core/utils_visualizer.py,sha256=
|
|
4
|
+
puzzle_solver/core/utils_visualizer.py,sha256=2jBnS2PeI4keFf-rneScSxX669zXu5F1lkClZ5EkMhE,21152
|
|
5
5
|
puzzle_solver/puzzles/aquarium/aquarium.py,sha256=BUfkAS2d9eG3TdMoe1cOGGeNYgKUebRvn-z9nsC9gvE,5708
|
|
6
6
|
puzzle_solver/puzzles/battleships/battleships.py,sha256=RuYCrs4j0vUjlU139NRYYP-uNPAgO0V7hAzbsHrRwD8,7446
|
|
7
|
-
puzzle_solver/puzzles/binairo/binairo.py,sha256=
|
|
7
|
+
puzzle_solver/puzzles/binairo/binairo.py,sha256=4xgYd1ewYIQCqEzsHdgp6hWzyW_TF_2rt6PO8QLFKWU,6838
|
|
8
|
+
puzzle_solver/puzzles/binairo/binairo_plus.py,sha256=TvLG3olwANtft3LuCF-y4OofpU9PNa4IXDqgZqsD-g0,267
|
|
8
9
|
puzzle_solver/puzzles/black_box/black_box.py,sha256=ZnHDVt6PFS_r1kMNSsbz9hav1hxIrNDUvPyERGPjLjM,15635
|
|
9
10
|
puzzle_solver/puzzles/bridges/bridges.py,sha256=15A9uV4xjoqPRo_9CTnoKeGRxS3z2aMF619T1n0dTOQ,5402
|
|
10
11
|
puzzle_solver/puzzles/chess_range/chess_melee.py,sha256=D-_Oi8OyxsVe1j3dIKYwRlxgeb3NWLmDWGcv-oclY0c,195
|
|
@@ -17,10 +18,12 @@ puzzle_solver/puzzles/flip/flip.py,sha256=ZngJLUhRNc7qqo2wtNLdMPx4u9w9JTUge27Pmd
|
|
|
17
18
|
puzzle_solver/puzzles/galaxies/galaxies.py,sha256=p10lpmW0FjtneFCMEjG1FSiEpQuvD8zZG9FG8zYGoes,5582
|
|
18
19
|
puzzle_solver/puzzles/galaxies/parse_map/parse_map.py,sha256=v5TCrdREeOB69s9_QFgPHKA7flG69Im1HVzIdxH0qQc,9355
|
|
19
20
|
puzzle_solver/puzzles/guess/guess.py,sha256=sH-NlYhxM3DNbhk4eGde09kgM0KaDvSbLrpHQiwcFGo,10791
|
|
21
|
+
puzzle_solver/puzzles/heyawake/heyawake.py,sha256=qMnc_CuHn8K5Rw40tefjueI1pycpHQ7eN1R9Xg5WEuw,5601
|
|
20
22
|
puzzle_solver/puzzles/inertia/inertia.py,sha256=gJBahkh69CrSWNscalKEoP1j4X-Q3XpbIBMiG9PUpU0,5657
|
|
21
23
|
puzzle_solver/puzzles/inertia/tsp.py,sha256=gobiISHtARA4Elq0jr90p6Yhq11ULjGoqsS-rLFhYcc,15389
|
|
22
24
|
puzzle_solver/puzzles/inertia/parse_map/parse_map.py,sha256=A9JQTNqamUdzlwqks0XQp3Hge3mzyTIVK6YtDJvqpL4,8422
|
|
23
25
|
puzzle_solver/puzzles/kakurasu/kakurasu.py,sha256=VNGMJnBHDi6WkghLObRLhUvkmrPaGphTTUDMC0TkQvQ,2064
|
|
26
|
+
puzzle_solver/puzzles/kakuro/kakuro.py,sha256=Jf0Iilv32EPcaWikX92_vgBOVRp5MAE27aFRmnLotGQ,4374
|
|
24
27
|
puzzle_solver/puzzles/keen/keen.py,sha256=tDb6C5S3Q0JAKPsdw-84WQ6PxRADELZHr_BK8FDH-NA,5039
|
|
25
28
|
puzzle_solver/puzzles/light_up/light_up.py,sha256=iSA1rjZMFsnI0V0Nxivxox4qZkB7PvUrROSHXcoUXds,4541
|
|
26
29
|
puzzle_solver/puzzles/lits/lits.py,sha256=3fPIkhAIUz8JokcfaE_ZM3b0AFEnf5xPzGJ2qnm8SWY,7099
|
|
@@ -35,16 +38,19 @@ puzzle_solver/puzzles/palisade/palisade.py,sha256=T-LXlaLU5OwUQ24QWJWhBUFUktg0qD
|
|
|
35
38
|
puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
|
|
36
39
|
puzzle_solver/puzzles/range/range.py,sha256=rruvD5ZSaOgvQuX6uGV_Dkr82nSiWZ5kDz03_j7Tt24,4425
|
|
37
40
|
puzzle_solver/puzzles/rectangles/rectangles.py,sha256=zaPg3qI9TNxr2iXmNi2kOL8R2RsS9DyQPUTY3ukgYIA,7033
|
|
41
|
+
puzzle_solver/puzzles/shakashaka/shakashaka.py,sha256=PRpg_qI7XA3ysAo_g1TRJsT3VwB5Vial2UcFyBOMwKQ,9571
|
|
42
|
+
puzzle_solver/puzzles/shingoki/shingoki.py,sha256=uwX1ZIGGDlshMtsZedlgGYE8hDB1ou3h6aBnZEr_l8I,7425
|
|
38
43
|
puzzle_solver/puzzles/signpost/signpost.py,sha256=-0_S6ycwzwlUf9-ZhP127Rgo5gMBOHiTM6t08dLLDac,3869
|
|
39
|
-
puzzle_solver/puzzles/singles/singles.py,sha256=
|
|
44
|
+
puzzle_solver/puzzles/singles/singles.py,sha256=KKn_Yl-eW874Bl1UmmcqoQ5vhNiO1JbM7fxKczOV5M4,2847
|
|
40
45
|
puzzle_solver/puzzles/slant/slant.py,sha256=xF-N4PuXYfx638NP1f1mi6YncIZB4mLtXtdS79XyPbg,6122
|
|
41
46
|
puzzle_solver/puzzles/slant/parse_map/parse_map.py,sha256=dxnALSDXe9wU0uSD0QEXnzoh1q801mj1ePTNLtG0n60,4796
|
|
42
47
|
puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=e1A_f_3J-QXN9fmt_Nf3FsYnp-TmE9TRKN06Wn4NnAU,7056
|
|
43
48
|
puzzle_solver/puzzles/star_battle/star_battle.py,sha256=IX6w4H3sifN01kPPtrAVRCK0Nl_xlXXSHvJKw8K1EuE,3718
|
|
44
49
|
puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
|
|
45
50
|
puzzle_solver/puzzles/stitches/stitches.py,sha256=iK8t02q43gH3FPbuIDn4dK0sbaOgZOnw8yHNRNvNuIU,6534
|
|
46
|
-
puzzle_solver/puzzles/stitches/parse_map/parse_map.py,sha256=
|
|
51
|
+
puzzle_solver/puzzles/stitches/parse_map/parse_map.py,sha256=f49ZGVBPXjAGgqZnqPab6PcO_DsFDFZnG3uA8b-1d7k,10441
|
|
47
52
|
puzzle_solver/puzzles/sudoku/sudoku.py,sha256=SE4TM_gic6Jj0fkDR_NzUJdX2XKyQ8eeOnVAQ011Xbo,8870
|
|
53
|
+
puzzle_solver/puzzles/tapa/tapa.py,sha256=TsOQhnEvlC1JxaWiEjQg2KxRXJR49GrN71DsMvPpia8,5337
|
|
48
54
|
puzzle_solver/puzzles/tents/tents.py,sha256=iyVK2WXfIT5j_9qqlQg0WmwvixwXlZSsHGK3XA-KpII,6283
|
|
49
55
|
puzzle_solver/puzzles/thermometers/thermometers.py,sha256=nsvJZkm7G8FALT27bpaB0lv5E_AWawqmvapQI8QcYXw,4015
|
|
50
56
|
puzzle_solver/puzzles/towers/towers.py,sha256=QvL0Pp-Z2ewCeq9ZkNrh8MShKOh-Y52sFBSudve68wk,6496
|
|
@@ -55,7 +61,7 @@ puzzle_solver/puzzles/unruly/unruly.py,sha256=sDF0oKT50G-NshyW2DYrvAgD9q9Ku9ANUy
|
|
|
55
61
|
puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=WrRdNhmKhIARdGOt_36gpRxRzrfLGv3wl7igBpPFM64,5259
|
|
56
62
|
puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
|
|
57
63
|
puzzle_solver/utils/visualizer.py,sha256=tsX1yEKwmwXBYuBJpx_oZGe2UUt1g5yV73G3UbtmvtE,6817
|
|
58
|
-
multi_puzzle_solver-0.9.
|
|
59
|
-
multi_puzzle_solver-0.9.
|
|
60
|
-
multi_puzzle_solver-0.9.
|
|
61
|
-
multi_puzzle_solver-0.9.
|
|
64
|
+
multi_puzzle_solver-0.9.30.dist-info/METADATA,sha256=yxPV6ZvkvGPOs1O2HpIob3e94uFQXjpm5JJKdCXyc2s,335384
|
|
65
|
+
multi_puzzle_solver-0.9.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
66
|
+
multi_puzzle_solver-0.9.30.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
|
|
67
|
+
multi_puzzle_solver-0.9.30.dist-info/RECORD,,
|
puzzle_solver/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from puzzle_solver.puzzles.aquarium import aquarium as aquarium_solver
|
|
2
2
|
from puzzle_solver.puzzles.battleships import battleships as battleships_solver
|
|
3
3
|
from puzzle_solver.puzzles.binairo import binairo as binairo_solver
|
|
4
|
+
from puzzle_solver.puzzles.binairo import binairo_plus as binairo_plus_solver
|
|
4
5
|
from puzzle_solver.puzzles.black_box import black_box as black_box_solver
|
|
5
6
|
from puzzle_solver.puzzles.bridges import bridges as bridges_solver
|
|
6
7
|
from puzzle_solver.puzzles.chess_range import chess_range as chess_range_solver
|
|
@@ -11,8 +12,10 @@ from puzzle_solver.puzzles.filling import filling as filling_solver
|
|
|
11
12
|
from puzzle_solver.puzzles.flip import flip as flip_solver
|
|
12
13
|
from puzzle_solver.puzzles.galaxies import galaxies as galaxies_solver
|
|
13
14
|
from puzzle_solver.puzzles.guess import guess as guess_solver
|
|
15
|
+
from puzzle_solver.puzzles.heyawake import heyawake as heyawake_solver
|
|
14
16
|
from puzzle_solver.puzzles.inertia import inertia as inertia_solver
|
|
15
17
|
from puzzle_solver.puzzles.kakurasu import kakurasu as kakurasu_solver
|
|
18
|
+
from puzzle_solver.puzzles.kakuro import kakuro as kakuro_solver
|
|
16
19
|
from puzzle_solver.puzzles.keen import keen as keen_solver
|
|
17
20
|
from puzzle_solver.puzzles.light_up import light_up as light_up_solver
|
|
18
21
|
from puzzle_solver.puzzles.magnets import magnets as magnets_solver
|
|
@@ -27,6 +30,8 @@ from puzzle_solver.puzzles.lits import lits as lits_solver
|
|
|
27
30
|
from puzzle_solver.puzzles.pearl import pearl as pearl_solver
|
|
28
31
|
from puzzle_solver.puzzles.range import range as range_solver
|
|
29
32
|
from puzzle_solver.puzzles.rectangles import rectangles as rectangles_solver
|
|
33
|
+
from puzzle_solver.puzzles.shakashaka import shakashaka as shakashaka_solver
|
|
34
|
+
from puzzle_solver.puzzles.shingoki import shingoki as shingoki_solver
|
|
30
35
|
from puzzle_solver.puzzles.signpost import signpost as signpost_solver
|
|
31
36
|
from puzzle_solver.puzzles.singles import singles as singles_solver
|
|
32
37
|
from puzzle_solver.puzzles.slant import slant as slant_solver
|
|
@@ -35,6 +40,7 @@ from puzzle_solver.puzzles.star_battle import star_battle as star_battle_solver
|
|
|
35
40
|
from puzzle_solver.puzzles.star_battle import star_battle_shapeless as star_battle_shapeless_solver
|
|
36
41
|
from puzzle_solver.puzzles.stitches import stitches as stitches_solver
|
|
37
42
|
from puzzle_solver.puzzles.sudoku import sudoku as sudoku_solver
|
|
43
|
+
from puzzle_solver.puzzles.tapa import tapa as tapa_solver
|
|
38
44
|
from puzzle_solver.puzzles.tents import tents as tents_solver
|
|
39
45
|
from puzzle_solver.puzzles.thermometers import thermometers as thermometers_solver
|
|
40
46
|
from puzzle_solver.puzzles.towers import towers as towers_solver
|
|
@@ -46,4 +52,4 @@ from puzzle_solver.puzzles.yin_yang import yin_yang as yin_yang_solver
|
|
|
46
52
|
|
|
47
53
|
from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
|
|
48
54
|
|
|
49
|
-
__version__ = '0.9.
|
|
55
|
+
__version__ = '0.9.30'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
-
from typing import Union, Callable, Optional
|
|
2
|
+
from typing import Union, Callable, Optional, List, Sequence, Literal
|
|
3
3
|
from puzzle_solver.core.utils import Pos, get_all_pos, get_next_pos, in_bounds, set_char, get_char, Direction
|
|
4
4
|
|
|
5
5
|
|
|
@@ -308,3 +308,216 @@ def render_shaded_grid(V: int,
|
|
|
308
308
|
labeled.append(label + line)
|
|
309
309
|
|
|
310
310
|
return ''.join(top_tens) + '\n' + ''.join(top_ones) + '\n' + '\n'.join(labeled)
|
|
311
|
+
|
|
312
|
+
CellVal = Literal["B", "W", "TL", "TR", "BL", "BR"]
|
|
313
|
+
GridLike = Sequence[Sequence[CellVal]]
|
|
314
|
+
|
|
315
|
+
def render_bw_tiles_split(
|
|
316
|
+
grid: GridLike,
|
|
317
|
+
cell_w: int = 6,
|
|
318
|
+
cell_h: int = 3,
|
|
319
|
+
borders: bool = False,
|
|
320
|
+
mode: Literal["ansi", "text"] = "ansi",
|
|
321
|
+
text_palette: Literal["solid", "hatch"] = "solid",
|
|
322
|
+
cell_text: Optional[Callable[[int, int], str]] = None) -> str:
|
|
323
|
+
"""
|
|
324
|
+
Render a VxH grid with '/' or '\\' splits and optional per-cell centered text.
|
|
325
|
+
|
|
326
|
+
`cell_text(r, c) -> str`: if returns non-empty, its first character is drawn
|
|
327
|
+
near the geometric center of cell (r,c), nudged to the black side for halves.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
V = len(grid)
|
|
331
|
+
if V == 0:
|
|
332
|
+
return ""
|
|
333
|
+
H = len(grid[0])
|
|
334
|
+
if any(len(row) != H for row in grid):
|
|
335
|
+
raise ValueError("All rows must have the same length")
|
|
336
|
+
if cell_w < 1 or cell_h < 1:
|
|
337
|
+
raise ValueError("cell_w and cell_h must be >= 1")
|
|
338
|
+
|
|
339
|
+
allowed = {"B","W","TL","TR","BL","BR"}
|
|
340
|
+
for r in range(V):
|
|
341
|
+
for c in range(H):
|
|
342
|
+
if grid[r][c] not in allowed:
|
|
343
|
+
raise ValueError(f"Invalid cell value at ({r},{c}): {grid[r][c]}")
|
|
344
|
+
|
|
345
|
+
# ── Mode setup ─────────────────────────────────────────────────────────
|
|
346
|
+
use_color = (mode == "ansi")
|
|
347
|
+
|
|
348
|
+
def sgr(bg: int | None = None, fg: int | None = None) -> str:
|
|
349
|
+
if not use_color:
|
|
350
|
+
return ""
|
|
351
|
+
parts = []
|
|
352
|
+
if fg is not None: parts.append(str(fg))
|
|
353
|
+
if bg is not None: parts.append(str(bg))
|
|
354
|
+
return ("\x1b[" + ";".join(parts) + "m") if parts else ""
|
|
355
|
+
|
|
356
|
+
RESET = "\x1b[0m" if use_color else ""
|
|
357
|
+
|
|
358
|
+
BG_BLACK, BG_WHITE = 40, 47
|
|
359
|
+
FG_BLACK, FG_WHITE = 30, 37
|
|
360
|
+
|
|
361
|
+
if text_palette == "solid":
|
|
362
|
+
TXT_BLACK, TXT_WHITE = " ", "█"
|
|
363
|
+
elif text_palette == "hatch":
|
|
364
|
+
TXT_BLACK, TXT_WHITE = "░", "▓"
|
|
365
|
+
else:
|
|
366
|
+
raise ValueError("text_palette must be 'solid' or 'hatch'")
|
|
367
|
+
|
|
368
|
+
def diag_kind_and_slash(val: CellVal):
|
|
369
|
+
if val in ("TR", "BL"):
|
|
370
|
+
return "main", "\\"
|
|
371
|
+
elif val in ("TL", "BR"):
|
|
372
|
+
return "anti", "/"
|
|
373
|
+
return None, "?"
|
|
374
|
+
|
|
375
|
+
def is_black(val: CellVal, fx: float, fy: float) -> bool:
|
|
376
|
+
if val == "B":
|
|
377
|
+
return True
|
|
378
|
+
if val == "W":
|
|
379
|
+
return False
|
|
380
|
+
kind, _ = diag_kind_and_slash(val)
|
|
381
|
+
if kind == "main": # y = x
|
|
382
|
+
return (fy < fx) if val == "TR" else (fy > fx)
|
|
383
|
+
else: # y = 1 - x
|
|
384
|
+
return (fy < 1 - fx) if val == "TL" else (fy > 1 - fx)
|
|
385
|
+
|
|
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
|
+
# Build one tile as a matrix of 1-char tokens (already colorized if ANSI)
|
|
397
|
+
def make_tile(val: CellVal) -> List[List[str]]:
|
|
398
|
+
rows: List[List[str]] = []
|
|
399
|
+
kind, slash_ch = diag_kind_and_slash(val)
|
|
400
|
+
|
|
401
|
+
for y in range(cell_h):
|
|
402
|
+
fy = (y + 0.5) / cell_h
|
|
403
|
+
line: List[str] = []
|
|
404
|
+
for x in range(cell_w):
|
|
405
|
+
fx = (x + 0.5) / cell_w
|
|
406
|
+
|
|
407
|
+
if val == "B":
|
|
408
|
+
line.append(sgr(bg=BG_BLACK) + " " + RESET if use_color else TXT_BLACK)
|
|
409
|
+
continue
|
|
410
|
+
if val == "W":
|
|
411
|
+
line.append(sgr(bg=BG_WHITE) + " " + RESET if use_color else TXT_WHITE)
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
black_side = is_black(val, fx, fy)
|
|
415
|
+
boundary = on_boundary(val, fx, fy)
|
|
416
|
+
|
|
417
|
+
if use_color:
|
|
418
|
+
bg = BG_BLACK if black_side else BG_WHITE
|
|
419
|
+
if boundary:
|
|
420
|
+
fg = FG_WHITE if bg == BG_BLACK else FG_BLACK
|
|
421
|
+
line.append(sgr(bg=bg, fg=fg) + slash_ch + RESET)
|
|
422
|
+
else:
|
|
423
|
+
line.append(sgr(bg=bg) + " " + RESET)
|
|
424
|
+
else:
|
|
425
|
+
if boundary:
|
|
426
|
+
line.append(slash_ch)
|
|
427
|
+
else:
|
|
428
|
+
line.append(TXT_BLACK if black_side else TXT_WHITE)
|
|
429
|
+
rows.append(line)
|
|
430
|
+
return rows
|
|
431
|
+
|
|
432
|
+
# Overlay a single character centered (nudged into black side if needed)
|
|
433
|
+
def overlay_center_char(tile: List[List[str]], val: CellVal, ch: str):
|
|
434
|
+
if not ch:
|
|
435
|
+
return
|
|
436
|
+
ch = ch[0] # keep one character (user said single number)
|
|
437
|
+
cx, cy = cell_w // 2, cell_h // 2
|
|
438
|
+
fx = (cx + 0.5) / cell_w
|
|
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
|
|
455
|
+
|
|
456
|
+
# Compose the glyph for that spot
|
|
457
|
+
if use_color:
|
|
458
|
+
# Force black bg + white fg so it pops
|
|
459
|
+
token = sgr(bg=BG_BLACK, fg=FG_WHITE) + ch + RESET
|
|
460
|
+
else:
|
|
461
|
+
# In text mode, just put the raw character
|
|
462
|
+
token = ch
|
|
463
|
+
tile[cy][cx] = token
|
|
464
|
+
|
|
465
|
+
# Optional borders
|
|
466
|
+
if borders:
|
|
467
|
+
horiz = "─" * cell_w
|
|
468
|
+
top = "┌" + "┬".join(horiz for _ in range(H)) + "┐"
|
|
469
|
+
mid = "├" + "┼".join(horiz for _ in range(H)) + "┤"
|
|
470
|
+
bot = "└" + "┴".join(horiz for _ in range(H)) + "┘"
|
|
471
|
+
|
|
472
|
+
out_lines: List[str] = []
|
|
473
|
+
if borders:
|
|
474
|
+
out_lines.append(top)
|
|
475
|
+
|
|
476
|
+
for r in range(V):
|
|
477
|
+
# Build tiles for this row (so we can overlay per-cell text)
|
|
478
|
+
row_tiles: List[List[List[str]]] = []
|
|
479
|
+
for c in range(H):
|
|
480
|
+
t = make_tile(grid[r][c])
|
|
481
|
+
if cell_text is not None:
|
|
482
|
+
label = cell_text(r, c)
|
|
483
|
+
if label:
|
|
484
|
+
overlay_center_char(t, grid[r][c], label)
|
|
485
|
+
row_tiles.append(t)
|
|
486
|
+
|
|
487
|
+
# Emit tile rows
|
|
488
|
+
for y in range(cell_h):
|
|
489
|
+
if borders:
|
|
490
|
+
parts = ["│"]
|
|
491
|
+
for c in range(H):
|
|
492
|
+
parts.append("".join(row_tiles[c][y]))
|
|
493
|
+
parts.append("│")
|
|
494
|
+
out_lines.append("".join(parts))
|
|
495
|
+
else:
|
|
496
|
+
out_lines.append("".join("".join(row_tiles[c][y]) for c in range(H)))
|
|
497
|
+
|
|
498
|
+
if borders and r < V - 1:
|
|
499
|
+
out_lines.append(mid)
|
|
500
|
+
|
|
501
|
+
if borders:
|
|
502
|
+
out_lines.append(bot)
|
|
503
|
+
|
|
504
|
+
return "\n".join(out_lines) + (RESET if use_color else "")
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# demo = [
|
|
510
|
+
# ["TL","TR","BL","BR","B","W","BL","BR","B","W","TL","TR","BL","BR","B","W","BL","BR","B","W","W","BL","BR","B","W"],
|
|
511
|
+
# ["W","BL","TR","BL","TL","BR","BL","BR","W","W","W","B","TR","BL","TL","BR","BL","BR","B","W","BR","BL","BR","B","W"],
|
|
512
|
+
# ["BR","BL","TR","TL","W","B","BL","BR","B","W","BR","BL","TR","TL","W","B","BL","BR","B","W","B","BL","BR","B","W"],
|
|
513
|
+
# ]
|
|
514
|
+
# print(render_bw_tiles_split(demo, cell_w=8, cell_h=4, borders=True, mode="ansi"))
|
|
515
|
+
# art = render_bw_tiles_split(
|
|
516
|
+
# demo,
|
|
517
|
+
# cell_w=8,
|
|
518
|
+
# cell_h=4,
|
|
519
|
+
# borders=True,
|
|
520
|
+
# mode="text", # ← key change
|
|
521
|
+
# text_palette="solid" # try "solid" for stark black/white
|
|
522
|
+
# )
|
|
523
|
+
# print("```text\n" + art + "\n```")
|
|
@@ -1,17 +1,32 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
from ortools.sat.python import cp_model
|
|
3
5
|
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
4
6
|
|
|
5
|
-
from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_next_pos,
|
|
7
|
+
from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_next_pos, get_pos, in_bounds, get_char, get_row_pos, get_col_pos
|
|
6
8
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import render_shaded_grid
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
class Board:
|
|
10
|
-
def __init__(self, board: np.array):
|
|
13
|
+
def __init__(self, board: np.array, arith_rows: Optional[np.array] = None, arith_cols: Optional[np.array] = None, force_unique: bool = True):
|
|
11
14
|
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
12
15
|
assert all(c.item() in [' ', 'B', 'W'] for c in np.nditer(board)), 'board must contain only space or B'
|
|
13
16
|
self.board = board
|
|
14
17
|
self.V, self.H = board.shape
|
|
18
|
+
if arith_rows is not None:
|
|
19
|
+
assert arith_rows.ndim == 2, f'arith_rows must be 2d, got {arith_rows.ndim}'
|
|
20
|
+
assert arith_rows.shape == (self.V, self.H-1), f'arith_rows must be one column less than board, got {arith_rows.shape} for {board.shape}'
|
|
21
|
+
assert all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_rows)), 'arith_rows must contain only space, x, or ='
|
|
22
|
+
if arith_cols is not None:
|
|
23
|
+
assert arith_cols.ndim == 2, f'arith_cols must be 2d, got {arith_cols.ndim}'
|
|
24
|
+
assert arith_cols.shape == (self.V-1, self.H), f'arith_cols must be one column and row less than board, got {arith_cols.shape} for {board.shape}'
|
|
25
|
+
assert all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_cols)), 'arith_cols must contain only space, x, or ='
|
|
26
|
+
self.arith_rows = arith_rows
|
|
27
|
+
self.arith_cols = arith_cols
|
|
28
|
+
self.force_unique = force_unique
|
|
29
|
+
|
|
15
30
|
self.model = cp_model.CpModel()
|
|
16
31
|
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
17
32
|
|
|
@@ -40,11 +55,32 @@ class Board:
|
|
|
40
55
|
for pos in get_all_pos(self.V, self.H):
|
|
41
56
|
self.disallow_three_in_a_row(pos, Direction.RIGHT)
|
|
42
57
|
self.disallow_three_in_a_row(pos, Direction.DOWN)
|
|
58
|
+
|
|
43
59
|
# 3. Each row and column is unique.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
if self.force_unique:
|
|
61
|
+
# a list per row
|
|
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
|
+
# a list per column
|
|
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
|
+
|
|
66
|
+
# if arithmetic is provided, add constraints for it
|
|
67
|
+
if self.arith_rows is not None:
|
|
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}'
|
|
69
|
+
for pos in get_all_pos(self.V, self.H-1):
|
|
70
|
+
c = get_char(self.arith_rows, pos)
|
|
71
|
+
if c == 'x':
|
|
72
|
+
self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, Direction.RIGHT)])
|
|
73
|
+
elif c == '=':
|
|
74
|
+
self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, Direction.RIGHT)])
|
|
75
|
+
if self.arith_cols is not None:
|
|
76
|
+
assert self.arith_cols.shape == (self.V-1, self.H), f'arith_cols must be one row less than board, got {self.arith_cols.shape} for {self.board.shape}'
|
|
77
|
+
for pos in get_all_pos(self.V-1, self.H):
|
|
78
|
+
c = get_char(self.arith_cols, pos)
|
|
79
|
+
if c == 'x':
|
|
80
|
+
self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, Direction.DOWN)])
|
|
81
|
+
elif c == '=':
|
|
82
|
+
self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, Direction.DOWN)])
|
|
83
|
+
|
|
48
84
|
|
|
49
85
|
def disallow_three_in_a_row(self, p1: Pos, direction: Direction):
|
|
50
86
|
p2 = get_next_pos(p1, direction)
|
|
@@ -62,7 +98,7 @@ class Board:
|
|
|
62
98
|
self.model_vars[p3].Not(),
|
|
63
99
|
])
|
|
64
100
|
|
|
65
|
-
def
|
|
101
|
+
def force_unique_double_list(self, model_vars: list[list[cp_model.IntVar]]):
|
|
66
102
|
if not model_vars or len(model_vars) < 2:
|
|
67
103
|
return
|
|
68
104
|
m = len(model_vars[0])
|
|
@@ -86,13 +122,5 @@ class Board:
|
|
|
86
122
|
return SingleSolution(assignment=assignment)
|
|
87
123
|
def callback(single_res: SingleSolution):
|
|
88
124
|
print("Solution found")
|
|
89
|
-
|
|
90
|
-
for pos in get_all_pos(self.V, self.H):
|
|
91
|
-
c = get_char(self.board, pos)
|
|
92
|
-
c = 'B' if single_res.assignment[pos] == 1 else 'W'
|
|
93
|
-
set_char(res, pos, c)
|
|
94
|
-
print('[')
|
|
95
|
-
for row in res:
|
|
96
|
-
print(" [ '" + "', '".join(row.tolist()) + "' ],")
|
|
97
|
-
print(']')
|
|
125
|
+
print(render_shaded_grid(self.V, self.H, lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1))
|
|
98
126
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ortools.sat.python import cp_model
|
|
3
|
+
|
|
4
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_neighbors4, get_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 render_shaded_grid
|
|
7
|
+
|
|
8
|
+
def return_3_consecutives(int_list: list[int]) -> list[tuple[int, int]]:
|
|
9
|
+
"""Given a list of integers (mostly with duplicates), return every consecutive sequence of 3 integer changes.
|
|
10
|
+
i.e. return a list of (begin_idx, end_idx) tuples where for each r=int_list[begin_idx:end_idx] we have r[0]!=r[1] and r[-2]!=r[-1] and len(r)>=3"""
|
|
11
|
+
out = []
|
|
12
|
+
change_indices = [i for i in range(len(int_list) - 1) if int_list[i] != int_list[i+1]]
|
|
13
|
+
# notice how for every subsequence r, the subsequence begining index is in change_indices and the ending index - 1 is in change_indices
|
|
14
|
+
for i in range(len(change_indices) - 1):
|
|
15
|
+
begin_idx = change_indices[i]
|
|
16
|
+
end_idx = change_indices[i+1] + 1 # we want to include the first number in the third sequence
|
|
17
|
+
if end_idx > len(int_list):
|
|
18
|
+
continue
|
|
19
|
+
out.append((begin_idx, end_idx))
|
|
20
|
+
return out
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Board:
|
|
24
|
+
def __init__(self, board: np.array, region_to_clue: dict[str, int]):
|
|
25
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
26
|
+
assert all(str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or digits'
|
|
27
|
+
self.board = board
|
|
28
|
+
self.V, self.H = board.shape
|
|
29
|
+
self.all_regions: set[int] = {int(c.item()) for c in np.nditer(board)}
|
|
30
|
+
self.region_to_clue = {int(k): v for k, v in region_to_clue.items()}
|
|
31
|
+
assert set(self.region_to_clue.keys()).issubset(self.all_regions), f'extra regions in region_to_clue: {set(self.region_to_clue.keys()) - self.all_regions}'
|
|
32
|
+
self.region_to_pos: dict[int, set[Pos]] = {r: set() for r in self.all_regions}
|
|
33
|
+
for pos in get_all_pos(self.V, self.H):
|
|
34
|
+
rid = int(get_char(self.board, pos))
|
|
35
|
+
self.region_to_pos[rid].add(pos)
|
|
36
|
+
|
|
37
|
+
self.model = cp_model.CpModel()
|
|
38
|
+
self.B: dict[Pos, cp_model.IntVar] = {}
|
|
39
|
+
self.W: dict[Pos, cp_model.IntVar] = {}
|
|
40
|
+
|
|
41
|
+
self.create_vars()
|
|
42
|
+
self.add_all_constraints()
|
|
43
|
+
|
|
44
|
+
def create_vars(self):
|
|
45
|
+
for pos in get_all_pos(self.V, self.H):
|
|
46
|
+
self.B[pos] = self.model.NewBoolVar(f'B:{pos}')
|
|
47
|
+
self.W[pos] = self.model.NewBoolVar(f'W:{pos}')
|
|
48
|
+
self.model.AddExactlyOne([self.B[pos], self.W[pos]])
|
|
49
|
+
|
|
50
|
+
def add_all_constraints(self):
|
|
51
|
+
# Regions with a number should contain black cells matching the number.
|
|
52
|
+
for rid, clue in self.region_to_clue.items():
|
|
53
|
+
self.model.Add(sum([self.B[p] for p in self.region_to_pos[rid]]) == clue)
|
|
54
|
+
# 2 black cells cannot be adjacent horizontally or vertically.
|
|
55
|
+
for pos in get_all_pos(self.V, self.H):
|
|
56
|
+
for neighbor in get_neighbors4(pos, self.V, self.H):
|
|
57
|
+
self.model.AddBoolOr([self.W[pos], self.W[neighbor]])
|
|
58
|
+
# All white cells should be connected in a single group.
|
|
59
|
+
force_connected_component(self.model, self.W)
|
|
60
|
+
# A straight (orthogonal) line of connected white cells cannot span across more than 2 regions.
|
|
61
|
+
self.disallow_white_lines_spanning_3_regions()
|
|
62
|
+
|
|
63
|
+
def disallow_white_lines_spanning_3_regions(self):
|
|
64
|
+
# A straight (orthogonal) line of connected white cells cannot span across more than 2 regions.
|
|
65
|
+
row_to_region: dict[int, list[int]] = {row: [] for row in range(self.V)}
|
|
66
|
+
col_to_region: dict[int, list[int]] = {col: [] for col in range(self.H)}
|
|
67
|
+
for pos in get_all_pos(self.V, self.H): # must traverse from least to most (both row and col)
|
|
68
|
+
rid = int(get_char(self.board, pos))
|
|
69
|
+
row_to_region[pos.y].append(rid)
|
|
70
|
+
col_to_region[pos.x].append(rid)
|
|
71
|
+
for row_num, row in row_to_region.items():
|
|
72
|
+
for begin_idx, end_idx in return_3_consecutives(row):
|
|
73
|
+
pos_list = [get_pos(x=x, y=row_num) for x in range(begin_idx, end_idx+1)]
|
|
74
|
+
self.model.AddBoolOr([self.B[p] for p in pos_list])
|
|
75
|
+
for col_num, col in col_to_region.items():
|
|
76
|
+
for begin_idx, end_idx in return_3_consecutives(col):
|
|
77
|
+
pos_list = [get_pos(x=col_num, y=y) for y in range(begin_idx, end_idx+1)]
|
|
78
|
+
self.model.AddBoolOr([self.B[p] for p in pos_list])
|
|
79
|
+
|
|
80
|
+
def solve_and_print(self, verbose: bool = True):
|
|
81
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
82
|
+
assignment: dict[Pos, int] = {}
|
|
83
|
+
for pos, var in board.B.items():
|
|
84
|
+
assignment[pos] = 1 if solver.Value(var) == 1 else 0
|
|
85
|
+
return SingleSolution(assignment=assignment)
|
|
86
|
+
def callback(single_res: SingleSolution):
|
|
87
|
+
print("Solution found")
|
|
88
|
+
# res = np.full((self.V, self.H), ' ', dtype=object)
|
|
89
|
+
# for pos in get_all_pos(self.V, self.H):
|
|
90
|
+
# c = 'B' if single_res.assignment[pos] == 1 else ' '
|
|
91
|
+
# set_char(res, pos, c)
|
|
92
|
+
# print(res)
|
|
93
|
+
print(render_shaded_grid(self.V, self.H, lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1, empty_text=lambda r, c: self.region_to_clue.get(int(self.board[r, c]), ' ')))
|
|
94
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=1)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from typing import Iterator
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ortools.sat.python import cp_model
|
|
5
|
+
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
6
|
+
|
|
7
|
+
from puzzle_solver.core.utils import Direction, Pos, get_all_pos, get_next_pos, get_pos, in_bounds, set_char, get_char, get_neighbors8
|
|
8
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import render_shaded_grid
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Board:
|
|
13
|
+
def __init__(self, board: np.array, row_sums: list[list[int]], col_sums: list[list[int]], N: int = 9):
|
|
14
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
15
|
+
assert all((c.item() in ['#', ' ', '1', '2', '3', '4', '5', '6', '7', '8', '9']) for c in np.nditer(board)), 'board must contain only #, space, or digits'
|
|
16
|
+
assert len(row_sums) == board.shape[0] and all(isinstance(i, list) and all(isinstance(j, int) or j == '#' for j in i) for i in row_sums), 'row_sums must be a list of lists of integers or #'
|
|
17
|
+
assert len(col_sums) == board.shape[1] and all(isinstance(i, list) and all(isinstance(j, int) or j == '#' for j in i) for i in col_sums), 'col_sums must be a list of lists of integers or #'
|
|
18
|
+
self.board = board
|
|
19
|
+
self.row_sums = row_sums
|
|
20
|
+
self.col_sums = col_sums
|
|
21
|
+
self.V, self.H = board.shape
|
|
22
|
+
self.N = N
|
|
23
|
+
self.model = cp_model.CpModel()
|
|
24
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
25
|
+
|
|
26
|
+
self.create_vars()
|
|
27
|
+
self.add_all_constraints()
|
|
28
|
+
|
|
29
|
+
def create_vars(self):
|
|
30
|
+
for pos in get_all_pos(self.V, self.H):
|
|
31
|
+
if get_char(self.board, pos) == '#':
|
|
32
|
+
continue
|
|
33
|
+
self.model_vars[pos] = self.model.NewIntVar(1, self.N, f'{pos}')
|
|
34
|
+
|
|
35
|
+
def get_consecutives(self, pos: Pos, direction: Direction) -> Iterator[list[Pos]]:
|
|
36
|
+
consecutive = []
|
|
37
|
+
while in_bounds(pos, self.V, self.H):
|
|
38
|
+
if get_char(self.board, pos) == '#':
|
|
39
|
+
if len(consecutive) > 0:
|
|
40
|
+
yield consecutive
|
|
41
|
+
consecutive = []
|
|
42
|
+
else:
|
|
43
|
+
consecutive.append(pos)
|
|
44
|
+
pos = get_next_pos(pos, direction)
|
|
45
|
+
if len(consecutive) > 0:
|
|
46
|
+
yield consecutive
|
|
47
|
+
|
|
48
|
+
def add_all_constraints(self):
|
|
49
|
+
for row in range(self.V):
|
|
50
|
+
row_consecutives = self.get_consecutives(get_pos(x=0, y=row), Direction.RIGHT)
|
|
51
|
+
for i, consecutive in enumerate(row_consecutives):
|
|
52
|
+
# print('row', row, 'i', i, 'consecutive', consecutive)
|
|
53
|
+
self.model.AddAllDifferent([self.model_vars[p] for p in consecutive])
|
|
54
|
+
clue = self.row_sums[row][i]
|
|
55
|
+
if clue != '#':
|
|
56
|
+
self.model.Add(lxp.sum([self.model_vars[p] for p in consecutive]) == clue)
|
|
57
|
+
assert len(self.row_sums[row]) == i + 1, f'row_sums[{row}] has {len(self.row_sums[row])} clues, but {i + 1} consecutive cells'
|
|
58
|
+
for col in range(self.H):
|
|
59
|
+
col_consecutives = self.get_consecutives(get_pos(x=col, y=0), Direction.DOWN)
|
|
60
|
+
for i, consecutive in enumerate(col_consecutives):
|
|
61
|
+
# print('col', col, 'i', i, 'consecutive', consecutive)
|
|
62
|
+
self.model.AddAllDifferent([self.model_vars[p] for p in consecutive])
|
|
63
|
+
clue = self.col_sums[col][i]
|
|
64
|
+
if clue != '#':
|
|
65
|
+
self.model.Add(lxp.sum([self.model_vars[p] for p in consecutive]) == clue)
|
|
66
|
+
assert len(self.col_sums[col]) == i + 1, f'col_sums[{col}] has {len(self.col_sums[col])} clues, but {i + 1} consecutive cells'
|
|
67
|
+
|
|
68
|
+
def solve_and_print(self, verbose: bool = True):
|
|
69
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
70
|
+
assignment: dict[Pos, int] = {}
|
|
71
|
+
for pos, var in board.model_vars.items():
|
|
72
|
+
assignment[pos] = solver.Value(var)
|
|
73
|
+
return SingleSolution(assignment=assignment)
|
|
74
|
+
def callback(single_res: SingleSolution):
|
|
75
|
+
print("Solution found")
|
|
76
|
+
print(render_shaded_grid(self.V, self.H, is_shaded=lambda r, c: self.board[r, c] == '#', empty_text=lambda r, c: str(single_res.assignment[get_pos(x=c, y=r)])))
|
|
77
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|