multi-puzzle-solver 0.9.24__py3-none-any.whl → 0.9.25__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.24.dist-info → multi_puzzle_solver-0.9.25.dist-info}/METADATA +103 -3
- {multi_puzzle_solver-0.9.24.dist-info → multi_puzzle_solver-0.9.25.dist-info}/RECORD +11 -9
- puzzle_solver/__init__.py +3 -1
- puzzle_solver/core/utils.py +77 -57
- puzzle_solver/core/utils_ortools.py +6 -10
- puzzle_solver/puzzles/flip/flip.py +48 -0
- puzzle_solver/puzzles/lits/lits.py +2 -28
- puzzle_solver/puzzles/palisade/palisade.py +104 -0
- puzzle_solver/puzzles/slitherlink/slitherlink.py +8 -3
- {multi_puzzle_solver-0.9.24.dist-info → multi_puzzle_solver-0.9.25.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.9.24.dist-info → multi_puzzle_solver-0.9.25.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multi-puzzle-solver
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.25
|
|
4
4
|
Summary: Efficient solvers for numerous popular and esoteric logic puzzles using CP-SAT
|
|
5
5
|
Author: Ar-Kareem
|
|
6
6
|
Project-URL: Homepage, https://github.com/Ar-Kareem/puzzle_solver
|
|
@@ -326,6 +326,11 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
326
326
|
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/rectangles_solved.png" alt="Rectangles" width="140">
|
|
327
327
|
</a>
|
|
328
328
|
</td>
|
|
329
|
+
<td align="center">
|
|
330
|
+
<a href="#palisade-puzzle-type-43"><b>Palisade</b><br><br>
|
|
331
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/palisade_solved.png" alt="Palisade" width="140">
|
|
332
|
+
</a>
|
|
333
|
+
</td>
|
|
329
334
|
</tr>
|
|
330
335
|
</table>
|
|
331
336
|
|
|
@@ -383,6 +388,7 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
383
388
|
- [Yin-Yang (Puzzle Type #40)](#yin-yang-puzzle-type-40)
|
|
384
389
|
- [Binairo (Puzzle Type #41)](#binairo-puzzle-type-41)
|
|
385
390
|
- [Rectangles (Puzzle Type #42)](#rectangles-puzzle-type-42)
|
|
391
|
+
- [Palisade (Puzzle Type #43)](#palisade-puzzle-type-43)
|
|
386
392
|
- [Why SAT / CP-SAT?](#why-sat--cp-sat)
|
|
387
393
|
- [Testing](#testing)
|
|
388
394
|
- [Contributing](#contributing)
|
|
@@ -3317,9 +3323,13 @@ Applying the solution to the puzzle visually:
|
|
|
3317
3323
|
|
|
3318
3324
|
## Slitherlink (Puzzle Type #39)
|
|
3319
3325
|
|
|
3320
|
-
Also known as Fences
|
|
3326
|
+
Also known as Fences, Loop the Loop, and Loopy
|
|
3321
3327
|
|
|
3322
|
-
* [**Play online**](https://www.
|
|
3328
|
+
* [**Play online 1**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/loopy.html)
|
|
3329
|
+
|
|
3330
|
+
* [**Play online 2**](https://www.puzzle-loop.com)
|
|
3331
|
+
|
|
3332
|
+
* [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/loopy.html#loopy)
|
|
3323
3333
|
|
|
3324
3334
|
* [**Solver Code**][39]
|
|
3325
3335
|
|
|
@@ -3720,6 +3730,95 @@ Applying the solution to the puzzle visually:
|
|
|
3720
3730
|
|
|
3721
3731
|
---
|
|
3722
3732
|
|
|
3733
|
+
|
|
3734
|
+
## Palisade (Puzzle Type #43)
|
|
3735
|
+
|
|
3736
|
+
* [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/palisade.html)
|
|
3737
|
+
|
|
3738
|
+
* [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/palisade.html#palisade)
|
|
3739
|
+
|
|
3740
|
+
* [**Solver Code**][42]
|
|
3741
|
+
|
|
3742
|
+
<details>
|
|
3743
|
+
<summary><strong>Rules</strong></summary>
|
|
3744
|
+
|
|
3745
|
+
You're given a grid of N squares and a region size M, some of which contain numbers. Your goal is to subdivide the grid into (N/M) contiguous regions, where every region is of size M, such that each square containing a number is adjacent to exactly that many edges (including those between the inside and the outside of the grid).
|
|
3746
|
+
|
|
3747
|
+
</details>
|
|
3748
|
+
|
|
3749
|
+
**Unsolved puzzle**
|
|
3750
|
+
|
|
3751
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/palisade_unsolved.png" alt="Palisade unsolved" width="500">
|
|
3752
|
+
|
|
3753
|
+
Code to utilize this package and solve the puzzle:
|
|
3754
|
+
|
|
3755
|
+
(Note: it takes a few seconds for the model to be built if the region size is larger than 8 and around 10 seconds for a region size of 10)
|
|
3756
|
+
|
|
3757
|
+
```python
|
|
3758
|
+
import numpy as np
|
|
3759
|
+
from puzzle_solver import palisade_solver as solver
|
|
3760
|
+
board = np.array([
|
|
3761
|
+
['2', ' ', ' ', ' ', ' ', '3', ' ', ' ', '1', '1', '3', ' ', ' ', ' ', ' '],
|
|
3762
|
+
['3', '2', '1', ' ', '2', '3', ' ', ' ', ' ', ' ', ' ', '2', ' ', '0', ' '],
|
|
3763
|
+
[' ', ' ', ' ', '1', '1', ' ', ' ', '1', ' ', ' ', ' ', '1', ' ', ' ', ' '],
|
|
3764
|
+
[' ', '3', '2', ' ', ' ', ' ', ' ', '2', '3', ' ', ' ', ' ', '1', ' ', ' '],
|
|
3765
|
+
[' ', '0', '1', ' ', '2', ' ', ' ', '0', ' ', ' ', ' ', '1', ' ', '3', '2'],
|
|
3766
|
+
['1', '0', ' ', ' ', ' ', '2', '2', ' ', '2', ' ', '3', ' ', '0', '2', ' '],
|
|
3767
|
+
[' ', ' ', ' ', ' ', ' ', '3', ' ', ' ', ' ', '2', ' ', ' ', ' ', ' ', ' '],
|
|
3768
|
+
[' ', '1', ' ', ' ', ' ', '3', '1', ' ', '1', ' ', ' ', ' ', ' ', '1', ' '],
|
|
3769
|
+
[' ', ' ', ' ', '0', ' ', ' ', '0', ' ', ' ', '1', '2', ' ', ' ', ' ', '3'],
|
|
3770
|
+
[' ', ' ', ' ', ' ', ' ', ' ', '1', ' ', ' ', '2', ' ', ' ', '1', '2', '1'],
|
|
3771
|
+
[' ', ' ', ' ', ' ', '1', ' ', '2', '3', '1', ' ', ' ', ' ', '2', ' ', '1'],
|
|
3772
|
+
['2', ' ', '1', ' ', '2', '2', '1', ' ', ' ', '2', ' ', ' ', ' ', ' ', ' '],
|
|
3773
|
+
])
|
|
3774
|
+
binst = solver.Board(board, region_size=10)
|
|
3775
|
+
solutions = binst.solve_and_print()
|
|
3776
|
+
```
|
|
3777
|
+
|
|
3778
|
+
**Script Output**
|
|
3779
|
+
|
|
3780
|
+
```python
|
|
3781
|
+
Solution found
|
|
3782
|
+
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1
|
|
3783
|
+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
|
|
3784
|
+
┌───────────────────┬───────────────────────┬───────────────┐
|
|
3785
|
+
0│ 2 · · · · │ 3 · · 1 1 3 │ · · · · │
|
|
3786
|
+
│ ┌───────────┐ ├───────┬───┐ ┌───┴───┐ │
|
|
3787
|
+
1│ 3 │ 2 1 · │ 2 │ 3 · │ · │ · · │ · 2 │ · 0 · │
|
|
3788
|
+
├───┘ │ └───┐ │ └───┐ └───┐ └───┐ │
|
|
3789
|
+
2│ · · · 1 │ 1 · │ · │ 1 · │ · · │ 1 · │ · · │
|
|
3790
|
+
│ ┌───┐ │ ┌───┘ │ ┌───┴───────┘ └───┐ │
|
|
3791
|
+
3│ · │ 3 │ 2 · │ · │ · · │ 2 │ 3 · · · 1 · │ · │
|
|
3792
|
+
├───┘ └───────┼───┘ ┌───┘ └───┬───────────────┬───┴───┤
|
|
3793
|
+
4│ · 0 1 · │ 2 · │ · 0 · │ · · 1 · │ 3 2 │
|
|
3794
|
+
│ ┌───┘ │ │ ┌───┐ └───┐ │
|
|
3795
|
+
5│ 1 0 · │ · · 2 │ 2 · 2 │ · │ 3 │ · 0 2 │ · │
|
|
3796
|
+
│ ┌───┴───────────┼───┬───────┴───┤ ├───┐ │ │
|
|
3797
|
+
6│ · · │ · · · 3 │ · │ · · 2 │ · │ · │ · · │ · │
|
|
3798
|
+
├───────┘ ┌───────────┤ └───┐ │ │ └───────┘ │
|
|
3799
|
+
7│ · 1 · │ · · 3 │ 1 · │ 1 · │ · │ · · 1 · │
|
|
3800
|
+
│ ┌───┘ ┌───┘ │ │ └───────┐ ┌───┤
|
|
3801
|
+
8│ · · │ · 0 · │ · 0 · │ · 1 │ 2 · · │ · │ 3 │
|
|
3802
|
+
│ ┌───┘ ┌───┤ ├───┐ └───┐ ├───┘ │
|
|
3803
|
+
9│ · │ · · · │ · │ · 1 · │ · │ 2 · │ · 1 │ 2 1 │
|
|
3804
|
+
├───┤ ┌───────┘ ├───────┐ │ └───┐ │ │ │
|
|
3805
|
+
10│ · │ · │ · · 1 │ · 2 │ 3 │ 1 · │ · │ · 2 │ · 1 │
|
|
3806
|
+
│ └───┘ │ └───┘ ├───┴───────┘ │
|
|
3807
|
+
11│ 2 · 1 · 2 │ 2 1 · · 2 │ · · · · · │
|
|
3808
|
+
└───────────────────┴───────────────────┴───────────────────┘
|
|
3809
|
+
Solutions found: 1
|
|
3810
|
+
status: OPTIMAL
|
|
3811
|
+
Time taken: 11.94 seconds
|
|
3812
|
+
```
|
|
3813
|
+
|
|
3814
|
+
**Solved puzzle**
|
|
3815
|
+
|
|
3816
|
+
Applying the solution to the puzzle visually:
|
|
3817
|
+
|
|
3818
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/palisade_solved.png" alt="Palisade solved" width="500">
|
|
3819
|
+
|
|
3820
|
+
---
|
|
3821
|
+
|
|
3723
3822
|
---
|
|
3724
3823
|
|
|
3725
3824
|
## Why SAT / CP-SAT?
|
|
@@ -3813,3 +3912,4 @@ Issues and PRs welcome!
|
|
|
3813
3912
|
[40]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/yin_yang "puzzle_solver/src/puzzle_solver/puzzles/yin_yang at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
3814
3913
|
[41]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/binairo "puzzle_solver/src/puzzle_solver/puzzles/binairo at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
3815
3914
|
[42]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/rectangles "puzzle_solver/src/puzzle_solver/puzzles/rectangles at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
3915
|
+
[43]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/palisade "puzzle_solver/src/puzzle_solver/puzzles/palisade at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
puzzle_solver/__init__.py,sha256=
|
|
2
|
-
puzzle_solver/core/utils.py,sha256=
|
|
3
|
-
puzzle_solver/core/utils_ortools.py,sha256=
|
|
1
|
+
puzzle_solver/__init__.py,sha256=DsQVO-Eo1odFFFvQB1IpbJw-Yr2MTtON6zMnLUi05P8,3203
|
|
2
|
+
puzzle_solver/core/utils.py,sha256=7Wo8_LHLEv8bY5-HsuCuLIjttZMMW09DoL1CcFDiu1Q,14046
|
|
3
|
+
puzzle_solver/core/utils_ortools.py,sha256=_i8cixHOB5XGqqcr-493bOiZgYJidnvxQMEfj--Trns,10278
|
|
4
4
|
puzzle_solver/puzzles/aquarium/aquarium.py,sha256=BUfkAS2d9eG3TdMoe1cOGGeNYgKUebRvn-z9nsC9gvE,5708
|
|
5
5
|
puzzle_solver/puzzles/battleships/battleships.py,sha256=RuYCrs4j0vUjlU139NRYYP-uNPAgO0V7hAzbsHrRwD8,7446
|
|
6
6
|
puzzle_solver/puzzles/binairo/binairo.py,sha256=sRtflnlGrN8xQ64beRZBGr74R8KptzxYDdFgXuW27pM,4595
|
|
@@ -12,6 +12,7 @@ puzzle_solver/puzzles/chess_range/chess_solo.py,sha256=U3v766UsZHx_dC3gxqU90VbjA
|
|
|
12
12
|
puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
|
|
13
13
|
puzzle_solver/puzzles/dominosa/dominosa.py,sha256=Nmb7pn8U27QJwGy9F3wo8ylqo2_U51OAo3GN2soaNpc,7195
|
|
14
14
|
puzzle_solver/puzzles/filling/filling.py,sha256=vrOIil285_r3IQ0F4c9mUBWMRVlPH4vowog_z1tCGdI,5567
|
|
15
|
+
puzzle_solver/puzzles/flip/flip.py,sha256=4rQ-JsC_f33YKDM7aueKVlcVdDwzeBkTJL51K-Vy0gA,2223
|
|
15
16
|
puzzle_solver/puzzles/galaxies/galaxies.py,sha256=p10lpmW0FjtneFCMEjG1FSiEpQuvD8zZG9FG8zYGoes,5582
|
|
16
17
|
puzzle_solver/puzzles/galaxies/parse_map/parse_map.py,sha256=v5TCrdREeOB69s9_QFgPHKA7flG69Im1HVzIdxH0qQc,9355
|
|
17
18
|
puzzle_solver/puzzles/guess/guess.py,sha256=sH-NlYhxM3DNbhk4eGde09kgM0KaDvSbLrpHQiwcFGo,10791
|
|
@@ -21,13 +22,14 @@ puzzle_solver/puzzles/inertia/parse_map/parse_map.py,sha256=A9JQTNqamUdzlwqks0XQ
|
|
|
21
22
|
puzzle_solver/puzzles/kakurasu/kakurasu.py,sha256=VNGMJnBHDi6WkghLObRLhUvkmrPaGphTTUDMC0TkQvQ,2064
|
|
22
23
|
puzzle_solver/puzzles/keen/keen.py,sha256=tDb6C5S3Q0JAKPsdw-84WQ6PxRADELZHr_BK8FDH-NA,5039
|
|
23
24
|
puzzle_solver/puzzles/light_up/light_up.py,sha256=iSA1rjZMFsnI0V0Nxivxox4qZkB7PvUrROSHXcoUXds,4541
|
|
24
|
-
puzzle_solver/puzzles/lits/lits.py,sha256=
|
|
25
|
+
puzzle_solver/puzzles/lits/lits.py,sha256=3fPIkhAIUz8JokcfaE_ZM3b0AFEnf5xPzGJ2qnm8SWY,7099
|
|
25
26
|
puzzle_solver/puzzles/magnets/magnets.py,sha256=-Wl49JD_PKeq735zQVMQ3XSQX6gdHiY-7PKw-Sh16jw,6474
|
|
26
27
|
puzzle_solver/puzzles/map/map.py,sha256=sxc57tapB8Tsgam-yoDitln1o-EB_SbIYvO6WEYy3us,2582
|
|
27
28
|
puzzle_solver/puzzles/minesweeper/minesweeper.py,sha256=LiQVOGkWCsc1WtX8CdPgL_WwAcaeUFuoi5_eqH8U2Og,5876
|
|
28
29
|
puzzle_solver/puzzles/mosaic/mosaic.py,sha256=QX_nVpVKQg8OfaUcqFk9tKqsDyVqvZc6-XWvfI3YcSw,2175
|
|
29
30
|
puzzle_solver/puzzles/nonograms/nonograms.py,sha256=1jmDTOCnmivmBlwtMDyyk3TVqH5IjapzLn7zLQ4qubk,6056
|
|
30
31
|
puzzle_solver/puzzles/norinori/norinori.py,sha256=uC8vXAw35xsTmpmTeKqYW7tbcssms9LCcXFBONtV2Ng,4743
|
|
32
|
+
puzzle_solver/puzzles/palisade/palisade.py,sha256=ZFvBnBVbR0iIcQ5Vm3PtHPjdSDvrO5OUbM91YoTKpHI,4962
|
|
31
33
|
puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
|
|
32
34
|
puzzle_solver/puzzles/range/range.py,sha256=rruvD5ZSaOgvQuX6uGV_Dkr82nSiWZ5kDz03_j7Tt24,4425
|
|
33
35
|
puzzle_solver/puzzles/rectangles/rectangles.py,sha256=V7p6GSCwYrFfILDWiLLUbX08WlnPbQKdhQm8bMa2Mgw,7060
|
|
@@ -35,7 +37,7 @@ puzzle_solver/puzzles/signpost/signpost.py,sha256=-0_S6ycwzwlUf9-ZhP127Rgo5gMBOH
|
|
|
35
37
|
puzzle_solver/puzzles/singles/singles.py,sha256=3wACiUa1Vmh2ce6szQ2hPjyBuH7aHiQ888p4R2jFkW4,3342
|
|
36
38
|
puzzle_solver/puzzles/slant/slant.py,sha256=xF-N4PuXYfx638NP1f1mi6YncIZB4mLtXtdS79XyPbg,6122
|
|
37
39
|
puzzle_solver/puzzles/slant/parse_map/parse_map.py,sha256=dxnALSDXe9wU0uSD0QEXnzoh1q801mj1ePTNLtG0n60,4796
|
|
38
|
-
puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=
|
|
40
|
+
puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=N3jv1Z-yYFlQDinii-DZfuJvLUsn9fT0h5Kyruxjn94,7017
|
|
39
41
|
puzzle_solver/puzzles/star_battle/star_battle.py,sha256=IX6w4H3sifN01kPPtrAVRCK0Nl_xlXXSHvJKw8K1EuE,3718
|
|
40
42
|
puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
|
|
41
43
|
puzzle_solver/puzzles/stitches/stitches.py,sha256=iK8t02q43gH3FPbuIDn4dK0sbaOgZOnw8yHNRNvNuIU,6534
|
|
@@ -51,7 +53,7 @@ puzzle_solver/puzzles/unruly/unruly.py,sha256=sDF0oKT50G-NshyW2DYrvAgD9q9Ku9ANUy
|
|
|
51
53
|
puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=WrRdNhmKhIARdGOt_36gpRxRzrfLGv3wl7igBpPFM64,5259
|
|
52
54
|
puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
|
|
53
55
|
puzzle_solver/utils/visualizer.py,sha256=tsX1yEKwmwXBYuBJpx_oZGe2UUt1g5yV73G3UbtmvtE,6817
|
|
54
|
-
multi_puzzle_solver-0.9.
|
|
55
|
-
multi_puzzle_solver-0.9.
|
|
56
|
-
multi_puzzle_solver-0.9.
|
|
57
|
-
multi_puzzle_solver-0.9.
|
|
56
|
+
multi_puzzle_solver-0.9.25.dist-info/METADATA,sha256=DcVaQpmwyhYN0y0XxOcvomrpoerVjgYdylE8VUFml04,208538
|
|
57
|
+
multi_puzzle_solver-0.9.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
58
|
+
multi_puzzle_solver-0.9.25.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
|
|
59
|
+
multi_puzzle_solver-0.9.25.dist-info/RECORD,,
|
puzzle_solver/__init__.py
CHANGED
|
@@ -8,6 +8,7 @@ from puzzle_solver.puzzles.chess_range import chess_solo as chess_solo_solver
|
|
|
8
8
|
from puzzle_solver.puzzles.chess_range import chess_melee as chess_melee_solver
|
|
9
9
|
from puzzle_solver.puzzles.dominosa import dominosa as dominosa_solver
|
|
10
10
|
from puzzle_solver.puzzles.filling import filling as filling_solver
|
|
11
|
+
# from puzzle_solver.puzzles.flip import flip as flip_solver
|
|
11
12
|
from puzzle_solver.puzzles.galaxies import galaxies as galaxies_solver
|
|
12
13
|
from puzzle_solver.puzzles.guess import guess as guess_solver
|
|
13
14
|
from puzzle_solver.puzzles.inertia import inertia as inertia_solver
|
|
@@ -20,6 +21,7 @@ from puzzle_solver.puzzles.minesweeper import minesweeper as minesweeper_solver
|
|
|
20
21
|
from puzzle_solver.puzzles.mosaic import mosaic as mosaic_solver
|
|
21
22
|
from puzzle_solver.puzzles.nonograms import nonograms as nonograms_solver
|
|
22
23
|
from puzzle_solver.puzzles.norinori import norinori as norinori_solver
|
|
24
|
+
from puzzle_solver.puzzles.palisade import palisade as palisade_solver
|
|
23
25
|
from puzzle_solver.puzzles.lits import lits as lits_solver
|
|
24
26
|
from puzzle_solver.puzzles.pearl import pearl as pearl_solver
|
|
25
27
|
from puzzle_solver.puzzles.range import range as range_solver
|
|
@@ -43,4 +45,4 @@ from puzzle_solver.puzzles.yin_yang import yin_yang as yin_yang_solver
|
|
|
43
45
|
|
|
44
46
|
from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
|
|
45
47
|
|
|
46
|
-
__version__ = '0.9.
|
|
48
|
+
__version__ = '0.9.25'
|
puzzle_solver/core/utils.py
CHANGED
|
@@ -133,7 +133,7 @@ def get_deltas(direction: Union[Direction, Direction8]) -> Tuple[int, int]:
|
|
|
133
133
|
raise ValueError(f'invalid direction: {direction}')
|
|
134
134
|
|
|
135
135
|
|
|
136
|
-
def polyominoes(N):
|
|
136
|
+
def polyominoes(N) -> set[Shape]:
|
|
137
137
|
"""Generate all polyominoes of size N. Every rotation and reflection is considered different and included in the result.
|
|
138
138
|
Translation is not considered different and is removed from the result (otherwise the result would be infinite).
|
|
139
139
|
|
|
@@ -165,7 +165,7 @@ def polyominoes(N):
|
|
|
165
165
|
shapes: set[FastShape] = {frozenset({(0, 0)})}
|
|
166
166
|
for i in range(1, N):
|
|
167
167
|
next_shapes: set[FastShape] = set()
|
|
168
|
-
directions = ((1,0),(-1,0),(0,1)) if i > 1 else (((1,0),(0,1))) # cannot take left on first step, if confused read: https://louridas.github.io/rwa/assignments/polyominoes/
|
|
168
|
+
directions = ((1,0),(-1,0),(0,1),(0,-1)) if i > 1 else (((1,0),(0,1))) # cannot take left on first step, if confused read: https://louridas.github.io/rwa/assignments/polyominoes/
|
|
169
169
|
for s in shapes:
|
|
170
170
|
# frontier of a single shape: all 4-neighbors of existing cells not already in the shape
|
|
171
171
|
frontier = set()
|
|
@@ -239,125 +239,145 @@ def render_grid(cell_flags: np.ndarray,
|
|
|
239
239
|
center_char: np.ndarray of shape (N, N) with the center of the cells, or a string to use for all cells, or None to not show centers.
|
|
240
240
|
scale_x: horizontal stretch factor (>=1). Try 2 or 3 for squarer cells.
|
|
241
241
|
"""
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
242
|
+
assert cell_flags is not None and cell_flags.ndim == 2
|
|
243
|
+
R, C = cell_flags.shape
|
|
244
|
+
|
|
245
|
+
# Edge presence arrays (note the rectangular shapes)
|
|
246
|
+
H = np.zeros((R+1, C), dtype=bool) # horizontal edges between rows
|
|
247
|
+
V = np.zeros((R, C+1), dtype=bool) # vertical edges between cols
|
|
248
|
+
for r in range(R):
|
|
249
|
+
for c in range(C):
|
|
250
|
+
s = cell_flags[r, c]
|
|
251
|
+
if 'U' in s: H[r, c] = True
|
|
252
|
+
if 'D' in s: H[r+1, c] = True
|
|
253
|
+
if 'L' in s: V[r, c] = True
|
|
254
|
+
if 'R' in s: V[r, c+1] = True
|
|
255
|
+
|
|
254
256
|
# Bitmask for corner connections
|
|
255
|
-
U,
|
|
257
|
+
U, Rb, D, Lb = 1, 2, 4, 8
|
|
256
258
|
JUNCTION = {
|
|
257
259
|
0: ' ',
|
|
258
260
|
U: '│', D: '│', U|D: '│',
|
|
259
|
-
|
|
260
|
-
U|
|
|
261
|
-
U|D|
|
|
262
|
-
U|
|
|
261
|
+
Lb: '─', Rb: '─', Lb|Rb: '─',
|
|
262
|
+
U|Rb: '└', Rb|D: '┌', D|Lb: '┐', Lb|U: '┘',
|
|
263
|
+
U|D|Lb: '┤', U|D|Rb: '├', Lb|Rb|U: '┴', Lb|Rb|D: '┬',
|
|
264
|
+
U|Rb|D|Lb: '┼',
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
assert scale_x >= 1
|
|
266
|
-
|
|
267
|
-
assert H.shape == (N+1, N) and V.shape == (N, N+1)
|
|
268
|
+
assert H.shape == (R+1, C) and V.shape == (R, C+1)
|
|
268
269
|
|
|
269
|
-
rows = 2*
|
|
270
|
-
cols = 2*
|
|
270
|
+
rows = 2*R + 1
|
|
271
|
+
cols = 2*C*scale_x + 1
|
|
271
272
|
canvas = [[' ']*cols for _ in range(rows)]
|
|
272
273
|
|
|
273
|
-
def x_corner(c): # x of corner column c
|
|
274
|
+
def x_corner(c): # x of corner column c (0..C)
|
|
274
275
|
return (2*c) * scale_x
|
|
275
|
-
def x_between(c,k): # kth in-between
|
|
276
|
+
def x_between(c,k): # kth in-between col (1..2*scale_x-1) between corners c and c+1
|
|
276
277
|
return (2*c) * scale_x + k
|
|
277
278
|
|
|
278
279
|
# horizontal edges: fill the stretched band between corners with '─'
|
|
279
|
-
for r in range(
|
|
280
|
+
for r in range(R+1):
|
|
280
281
|
rr = 2*r
|
|
281
|
-
for c in range(
|
|
282
|
+
for c in range(C):
|
|
282
283
|
if H[r, c]:
|
|
283
|
-
|
|
284
|
-
for k in range(1, scale_x*2): # 1..(2*scale_x-1), no gaps
|
|
284
|
+
for k in range(1, scale_x*2): # 1..(2*scale_x-1)
|
|
285
285
|
canvas[rr][x_between(c, k)] = '─'
|
|
286
286
|
|
|
287
|
-
# vertical edges:
|
|
288
|
-
for r in range(
|
|
287
|
+
# vertical edges: at the corner columns
|
|
288
|
+
for r in range(R):
|
|
289
289
|
rr = 2*r + 1
|
|
290
|
-
for c in range(
|
|
290
|
+
for c in range(C+1):
|
|
291
291
|
if V[r, c]:
|
|
292
292
|
canvas[rr][x_corner(c)] = '│'
|
|
293
293
|
|
|
294
|
-
# junctions at
|
|
295
|
-
for r in range(
|
|
294
|
+
# junctions at every corner grid point
|
|
295
|
+
for r in range(R+1):
|
|
296
296
|
rr = 2*r
|
|
297
|
-
for c in range(
|
|
297
|
+
for c in range(C+1):
|
|
298
298
|
m = 0
|
|
299
299
|
if r > 0 and V[r-1, c]: m |= U
|
|
300
|
-
if c <
|
|
301
|
-
if r <
|
|
302
|
-
if c > 0 and H[r, c-1]: m |=
|
|
300
|
+
if c < C and H[r, c]: m |= Rb
|
|
301
|
+
if r < R and V[r, c]: m |= D
|
|
302
|
+
if c > 0 and H[r, c-1]: m |= Lb
|
|
303
303
|
canvas[rr][x_corner(c)] = JUNCTION[m]
|
|
304
304
|
|
|
305
|
-
# centers
|
|
306
|
-
# ── Centers (now safe for multi-character strings) ──────────────────────
|
|
307
|
-
# We render center text within the interior span (between corner columns),
|
|
308
|
-
# centered if it fits; otherwise we truncate to the span width.
|
|
305
|
+
# centers (safe for multi-character strings)
|
|
309
306
|
def put_center_text(rr: int, c: int, text: str):
|
|
310
|
-
# interior span (exclusive of the corner columns)
|
|
311
307
|
left = x_corner(c) + 1
|
|
312
308
|
right = x_corner(c+1) - 1
|
|
313
309
|
if right < left:
|
|
314
|
-
return
|
|
310
|
+
return
|
|
315
311
|
span_width = right - left + 1
|
|
316
|
-
|
|
317
312
|
s = str(text)
|
|
318
313
|
if len(s) > span_width:
|
|
319
|
-
s = s[:span_width] #
|
|
320
|
-
# center within the span
|
|
314
|
+
s = s[:span_width] # truncate to protect borders
|
|
321
315
|
start = left + (span_width - len(s)) // 2
|
|
322
316
|
for i, ch in enumerate(s):
|
|
323
317
|
canvas[rr][start + i] = ch
|
|
324
318
|
|
|
325
319
|
if center_char is not None:
|
|
326
|
-
for r in range(
|
|
320
|
+
for r in range(R):
|
|
327
321
|
rr = 2*r + 1
|
|
328
|
-
for c in range(
|
|
322
|
+
for c in range(C):
|
|
329
323
|
val = center_char if isinstance(center_char, str) else center_char[r, c]
|
|
330
324
|
put_center_text(rr, c, '' if val is None else str(val))
|
|
331
325
|
|
|
332
|
-
#
|
|
326
|
+
# rows -> strings
|
|
333
327
|
art_rows = [''.join(row) for row in canvas]
|
|
334
|
-
|
|
335
328
|
if not show_axes:
|
|
336
329
|
return '\n'.join(art_rows)
|
|
337
330
|
|
|
338
|
-
#
|
|
339
|
-
gut = max(2, len(str(
|
|
331
|
+
# Axes labels: row indices on the left, column indices on top (handle C, not R)
|
|
332
|
+
gut = max(2, len(str(R-1))) # gutter width based on row index width
|
|
340
333
|
gutter = ' ' * gut
|
|
341
334
|
top_tens = list(gutter + ' ' * cols)
|
|
342
335
|
top_ones = list(gutter + ' ' * cols)
|
|
343
336
|
|
|
344
|
-
for c in range(
|
|
337
|
+
for c in range(C):
|
|
345
338
|
xc_center = x_corner(c) + scale_x
|
|
346
|
-
if
|
|
339
|
+
if C >= 10:
|
|
347
340
|
top_tens[gut + xc_center] = str((c // 10) % 10)
|
|
348
341
|
top_ones[gut + xc_center] = str(c % 10)
|
|
349
342
|
|
|
350
|
-
# tiny corner labels
|
|
351
343
|
if gut >= 2:
|
|
352
344
|
top_tens[gut-2:gut] = list(' ')
|
|
353
345
|
top_ones[gut-2:gut] = list(' ')
|
|
354
346
|
|
|
355
347
|
labeled = []
|
|
356
348
|
for r, line in enumerate(art_rows):
|
|
357
|
-
if r % 2 == 1:
|
|
349
|
+
if r % 2 == 1: # cell-center row
|
|
358
350
|
label = str(r//2).rjust(gut)
|
|
359
351
|
else:
|
|
360
352
|
label = ' ' * gut
|
|
361
353
|
labeled.append(label + line)
|
|
362
354
|
|
|
363
355
|
return ''.join(top_tens) + '\n' + ''.join(top_ones) + '\n' + '\n'.join(labeled)
|
|
356
|
+
|
|
357
|
+
def id_board_to_wall_board(id_board: np.array, border_is_wall = True) -> np.array:
|
|
358
|
+
"""In many instances, we have a 2d array where cell values are arbitrary ids
|
|
359
|
+
and we want to convert it to a 2d array where cell values are walls "U", "D", "L", "R" to represent the edges that separate me from my neighbors that have different ids.
|
|
360
|
+
Args:
|
|
361
|
+
id_board: np.array of shape (N, N) with arbitrary ids.
|
|
362
|
+
border_is_wall: if True, the edges of the board are considered to be walls.
|
|
363
|
+
Returns:
|
|
364
|
+
np.array of shape (N, N) with walls "U", "D", "L", "R".
|
|
365
|
+
"""
|
|
366
|
+
res = np.full((id_board.shape[0], id_board.shape[1]), '', dtype=object)
|
|
367
|
+
V, H = id_board.shape
|
|
368
|
+
def append_char(pos: Pos, s: str):
|
|
369
|
+
set_char(res, pos, get_char(res, pos) + s)
|
|
370
|
+
def handle_pos_direction(pos: Pos, direction: Direction, s: str):
|
|
371
|
+
pos2 = get_next_pos(pos, direction)
|
|
372
|
+
if in_bounds(pos2, V, H):
|
|
373
|
+
if get_char(id_board, pos2) != get_char(id_board, pos):
|
|
374
|
+
append_char(pos, s)
|
|
375
|
+
else:
|
|
376
|
+
if border_is_wall:
|
|
377
|
+
append_char(pos, s)
|
|
378
|
+
for pos in get_all_pos(V, H):
|
|
379
|
+
handle_pos_direction(pos, Direction.LEFT, 'L')
|
|
380
|
+
handle_pos_direction(pos, Direction.RIGHT, 'R')
|
|
381
|
+
handle_pos_direction(pos, Direction.UP, 'U')
|
|
382
|
+
handle_pos_direction(pos, Direction.DOWN, 'D')
|
|
383
|
+
return res
|
|
@@ -146,16 +146,12 @@ def force_connected_component(model: cp_model.CpModel, vars_to_force: dict[Any,
|
|
|
146
146
|
for p in keys_in_order:
|
|
147
147
|
model.Add(node_height[p] > 0).OnlyEnforceIf(vs[p])
|
|
148
148
|
|
|
149
|
-
all_new_vars
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
all_new_vars[f"{prefix_name}node_height[{k}]"] = v
|
|
156
|
-
for k, v in max_neighbor_height.items():
|
|
157
|
-
all_new_vars[f"{prefix_name}max_neighbor_height[{k}]"] = v
|
|
158
|
-
|
|
149
|
+
all_new_vars = {
|
|
150
|
+
"is_root": is_root,
|
|
151
|
+
"prefix_zero": prefix_zero,
|
|
152
|
+
"node_height": node_height,
|
|
153
|
+
"max_neighbor_height": max_neighbor_height,
|
|
154
|
+
}
|
|
159
155
|
return all_new_vars
|
|
160
156
|
|
|
161
157
|
|
|
@@ -0,0 +1,48 @@
|
|
|
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, get_neighbors8
|
|
6
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
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 board.shape[0] == board.shape[1], 'board must be square'
|
|
13
|
+
assert all((c.item() == ' ') or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or digits'
|
|
14
|
+
self.board = board
|
|
15
|
+
self.N = board.shape[0]
|
|
16
|
+
self.model = cp_model.CpModel()
|
|
17
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
18
|
+
|
|
19
|
+
self.create_vars()
|
|
20
|
+
self.add_all_constraints()
|
|
21
|
+
|
|
22
|
+
def create_vars(self):
|
|
23
|
+
for pos in get_all_pos(self.N):
|
|
24
|
+
self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
|
|
25
|
+
|
|
26
|
+
def add_all_constraints(self):
|
|
27
|
+
for pos in get_all_pos(self.N):
|
|
28
|
+
c = get_char(self.board, pos)
|
|
29
|
+
if not str(c).isdecimal():
|
|
30
|
+
continue
|
|
31
|
+
neighbour_vars = [self.model_vars[p] for p in get_neighbors8(pos, self.N, include_self=True)]
|
|
32
|
+
self.model.Add(lxp.sum(neighbour_vars) == int(c))
|
|
33
|
+
|
|
34
|
+
def solve_and_print(self, verbose: bool = True):
|
|
35
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
36
|
+
assignment: dict[Pos, int] = {}
|
|
37
|
+
for pos, var in board.model_vars.items():
|
|
38
|
+
assignment[pos] = solver.Value(var)
|
|
39
|
+
return SingleSolution(assignment=assignment)
|
|
40
|
+
def callback(single_res: SingleSolution):
|
|
41
|
+
print("Solution found")
|
|
42
|
+
res = np.full((self.N, self.N), ' ', dtype=object)
|
|
43
|
+
for pos in get_all_pos(self.N):
|
|
44
|
+
c = get_char(self.board, pos)
|
|
45
|
+
c = 'B' if single_res.assignment[pos] == 1 else ' '
|
|
46
|
+
set_char(res, pos, c)
|
|
47
|
+
print(res)
|
|
48
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import time
|
|
3
1
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Optional
|
|
2
|
+
from typing import Optional
|
|
5
3
|
|
|
6
4
|
from ortools.sat.python import cp_model
|
|
7
5
|
import numpy as np
|
|
@@ -14,19 +12,6 @@ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution,
|
|
|
14
12
|
Shape = frozenset[Pos]
|
|
15
13
|
|
|
16
14
|
|
|
17
|
-
@dataclass(frozen=True)
|
|
18
|
-
class SingleSolution:
|
|
19
|
-
assignment: dict[Pos, Union[str, int]]
|
|
20
|
-
all_other_variables: dict
|
|
21
|
-
|
|
22
|
-
def get_hashable_solution(self) -> str:
|
|
23
|
-
result = []
|
|
24
|
-
for pos, v in self.assignment.items():
|
|
25
|
-
result.append((pos.x, pos.y, v))
|
|
26
|
-
return json.dumps(result, sort_keys=True)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
15
|
@dataclass
|
|
31
16
|
class ShapeOnBoard:
|
|
32
17
|
is_active: cp_model.IntVar
|
|
@@ -63,7 +48,6 @@ class Board:
|
|
|
63
48
|
def create_vars(self):
|
|
64
49
|
for pos in get_all_pos(self.V, self.H):
|
|
65
50
|
self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
|
|
66
|
-
# print('base vars:', len(self.model_vars))
|
|
67
51
|
|
|
68
52
|
def init_shapes_on_board(self):
|
|
69
53
|
for idx, (shape, shape_id) in enumerate(self.polyominoes):
|
|
@@ -84,7 +68,6 @@ class Board:
|
|
|
84
68
|
body=body,
|
|
85
69
|
disallow_same_shape=disallow_same_shape,
|
|
86
70
|
))
|
|
87
|
-
# print('shapes on board:', len(self.shapes_on_board))
|
|
88
71
|
|
|
89
72
|
def add_all_constraints(self):
|
|
90
73
|
# RULES:
|
|
@@ -99,11 +82,9 @@ class Board:
|
|
|
99
82
|
self.force_one_shape_per_block() # Rule #1
|
|
100
83
|
self.disallow_same_shape_touching() # Rule #2
|
|
101
84
|
self.fc = force_connected_component(self.model, self.model_vars) # Rule #3
|
|
102
|
-
# print('force connected vars:', len(fc))
|
|
103
85
|
shape_2_by_2 = frozenset({Pos(0, 0), Pos(0, 1), Pos(1, 0), Pos(1, 1)})
|
|
104
86
|
self.disallow_shape(shape_2_by_2) # Rule #4
|
|
105
87
|
|
|
106
|
-
|
|
107
88
|
def only_allow_shapes_on_board(self):
|
|
108
89
|
for shape_on_board in self.shapes_on_board:
|
|
109
90
|
# if shape is active then all its body cells must be active
|
|
@@ -118,7 +99,6 @@ class Board:
|
|
|
118
99
|
for block_i in self.block_numbers:
|
|
119
100
|
shapes_on_block = [s for s in self.shapes_on_board if s.body & self.blocks[block_i]]
|
|
120
101
|
assert all(s.body.issubset(self.blocks[block_i]) for s in shapes_on_block), 'expected all shapes on block to be fully contained in the block'
|
|
121
|
-
# print(f'shapes on block {block_i} has {len(shapes_on_block)} shapes')
|
|
122
102
|
self.model.Add(sum(s.is_active for s in shapes_on_block) == 1)
|
|
123
103
|
|
|
124
104
|
def disallow_same_shape_touching(self):
|
|
@@ -138,8 +118,6 @@ class Board:
|
|
|
138
118
|
self.model.Add(sum(self.model_vars[p] for p in cur_body) < len(cur_body))
|
|
139
119
|
|
|
140
120
|
|
|
141
|
-
|
|
142
|
-
|
|
143
121
|
def solve_and_print(self, verbose: bool = True, max_solutions: Optional[int] = None, verbose_callback: Optional[bool] = None):
|
|
144
122
|
if verbose_callback is None:
|
|
145
123
|
verbose_callback = verbose
|
|
@@ -147,10 +125,7 @@ class Board:
|
|
|
147
125
|
assignment: dict[Pos, int] = {}
|
|
148
126
|
for pos, var in board.model_vars.items():
|
|
149
127
|
assignment[pos] = solver.Value(var)
|
|
150
|
-
|
|
151
|
-
'fc': {k: solver.Value(v) for k, v in board.fc.items()}
|
|
152
|
-
}
|
|
153
|
-
return SingleSolution(assignment=assignment, all_other_variables=all_other_variables)
|
|
128
|
+
return SingleSolution(assignment=assignment)
|
|
154
129
|
def callback(single_res: SingleSolution):
|
|
155
130
|
print("Solution found")
|
|
156
131
|
res = np.full((self.V, self.H), ' ', dtype=str)
|
|
@@ -158,5 +133,4 @@ class Board:
|
|
|
158
133
|
c = 'X' if val == 1 else ' '
|
|
159
134
|
set_char(res, pos, c)
|
|
160
135
|
print('[\n' + '\n'.join([' ' + str(res[row].tolist()) + ',' for row in range(self.V)]) + '\n]')
|
|
161
|
-
pass
|
|
162
136
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose_callback else None, verbose=verbose, max_solutions=max_solutions)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from collections import defaultdict
|
|
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, get_pos, id_board_to_wall_board, render_grid, set_char, in_bounds, get_next_pos, Direction, polyominoes
|
|
8
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# a shape on the 2d board is just a set of positions
|
|
13
|
+
Shape = frozenset[Pos]
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ShapeOnBoard:
|
|
17
|
+
is_active: cp_model.IntVar
|
|
18
|
+
shape: Shape
|
|
19
|
+
shape_id: int
|
|
20
|
+
body: set[Pos]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_valid_translations(shape: Shape, board: np.array) -> set[Pos]:
|
|
24
|
+
# give a shape and a board, return all valid translations of the shape that are fully contained in the board AND consistent with the clues on the board
|
|
25
|
+
shape_list = list(shape)
|
|
26
|
+
shape_borders = [] # will contain the number of borders for each pos in the shape; this has to be consistent with the clues on the board
|
|
27
|
+
for pos in shape_list:
|
|
28
|
+
v = 0
|
|
29
|
+
for direction in Direction:
|
|
30
|
+
next_pos = get_next_pos(pos, direction)
|
|
31
|
+
if not in_bounds(next_pos, board.shape[0], board.shape[1]) or next_pos not in shape:
|
|
32
|
+
v += 1
|
|
33
|
+
shape_borders.append(v)
|
|
34
|
+
shape_list = [(p.x, p.y) for p in shape_list]
|
|
35
|
+
# min x/y is always 0
|
|
36
|
+
max_x = max(p[0] for p in shape_list)
|
|
37
|
+
max_y = max(p[1] for p in shape_list)
|
|
38
|
+
|
|
39
|
+
for dy in range(0, board.shape[0] - max_y):
|
|
40
|
+
for dx in range(0, board.shape[1] - max_x):
|
|
41
|
+
body = tuple((p[0] + dx, p[1] + dy) for p in shape_list)
|
|
42
|
+
for i, p in enumerate(body):
|
|
43
|
+
c = board[p[1], p[0]]
|
|
44
|
+
if c != ' ' and c != str(shape_borders[i]): # there is a clue and it doesn't match my translated shape, skip
|
|
45
|
+
break
|
|
46
|
+
else:
|
|
47
|
+
yield tuple(get_pos(x=p[0], y=p[1]) for p in body)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Board:
|
|
52
|
+
def __init__(self, board: np.array, region_size: int):
|
|
53
|
+
assert region_size >= 1 and isinstance(region_size, int), 'region_size must be an integer greater than or equal to 1'
|
|
54
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
55
|
+
assert all((c.item() == ' ') or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or digits'
|
|
56
|
+
self.board = board
|
|
57
|
+
self.V, self.H = board.shape
|
|
58
|
+
self.region_size = region_size
|
|
59
|
+
self.region_count = (self.V * self.H) // self.region_size
|
|
60
|
+
assert self.region_count * self.region_size == self.V * self.H, f'region_size must be a factor of the board size, got {self.region_size} and {self.region_count}'
|
|
61
|
+
|
|
62
|
+
self.polyominoes = polyominoes(self.region_size)
|
|
63
|
+
|
|
64
|
+
self.model = cp_model.CpModel()
|
|
65
|
+
self.shapes_on_board: list[ShapeOnBoard] = [] # will contain every possible shape on the board based on polyomino degrees
|
|
66
|
+
self.pos_to_shapes: dict[Pos, set[ShapeOnBoard]] = defaultdict(set)
|
|
67
|
+
self.create_vars()
|
|
68
|
+
self.add_all_constraints()
|
|
69
|
+
|
|
70
|
+
def create_vars(self):
|
|
71
|
+
for shape in self.polyominoes:
|
|
72
|
+
for body in get_valid_translations(shape, self.board):
|
|
73
|
+
uid = len(self.shapes_on_board)
|
|
74
|
+
shape_on_board = ShapeOnBoard(
|
|
75
|
+
is_active=self.model.NewBoolVar(f'{uid}:is_active'),
|
|
76
|
+
shape=shape,
|
|
77
|
+
shape_id=uid,
|
|
78
|
+
body=body,
|
|
79
|
+
)
|
|
80
|
+
self.shapes_on_board.append(shape_on_board)
|
|
81
|
+
for pos in body:
|
|
82
|
+
self.pos_to_shapes[pos].add(shape_on_board)
|
|
83
|
+
|
|
84
|
+
def add_all_constraints(self):
|
|
85
|
+
for pos in get_all_pos(self.V, self.H): # each position has exactly one shape active
|
|
86
|
+
self.model.AddExactlyOne(shape.is_active for shape in self.pos_to_shapes[pos])
|
|
87
|
+
|
|
88
|
+
def solve_and_print(self, verbose: bool = True):
|
|
89
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
90
|
+
assignment: dict[Pos, int] = {}
|
|
91
|
+
for shape in board.shapes_on_board:
|
|
92
|
+
if solver.Value(shape.is_active) == 1:
|
|
93
|
+
for pos in shape.body:
|
|
94
|
+
assignment[pos] = shape.shape_id
|
|
95
|
+
return SingleSolution(assignment=assignment)
|
|
96
|
+
def callback(single_res: SingleSolution):
|
|
97
|
+
print("Solution found")
|
|
98
|
+
id_board = np.full((self.V, self.H), ' ', dtype=object)
|
|
99
|
+
for pos in get_all_pos(self.V, self.H):
|
|
100
|
+
region_idx = single_res.assignment[pos]
|
|
101
|
+
set_char(id_board, pos, region_idx)
|
|
102
|
+
board = np.where(self.board == ' ', '·', self.board)
|
|
103
|
+
print(render_grid(id_board_to_wall_board(id_board), center_char=board))
|
|
104
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -20,6 +20,10 @@ class Board:
|
|
|
20
20
|
self.cell_borders_to_corners: dict[CellBorder, set[Corner]] = defaultdict(set) # for every cell border, a set of all corners it is connected to
|
|
21
21
|
self.corners_to_cell_borders: dict[Corner, set[CellBorder]] = defaultdict(set) # opposite direction
|
|
22
22
|
|
|
23
|
+
# 2N^2 + 2N edges
|
|
24
|
+
# 4*edges (fully connected component)
|
|
25
|
+
# model variables = edges (on/off) + 4*edges (fully connected component)
|
|
26
|
+
# = 9N^2 + 9N
|
|
23
27
|
self.model = cp_model.CpModel()
|
|
24
28
|
self.model_vars: dict[CellBorder, cp_model.IntVar] = {} # one entry for every unique variable in the model
|
|
25
29
|
self.cell_borders: dict[CellBorder, cp_model.IntVar] = {} # for every position and direction, one entry for that edge (thus the same edge variables are used in opposite directions of neighboring cells)
|
|
@@ -87,10 +91,11 @@ class Board:
|
|
|
87
91
|
if not val.isdecimal():
|
|
88
92
|
continue
|
|
89
93
|
self.model.Add(sum(variables) == int(val))
|
|
94
|
+
|
|
95
|
+
corner_sum_domain = cp_model.Domain.FromValues([0, 2]) # sum of edges touching a corner is 0 or 2
|
|
90
96
|
for corner in self.corner_vars: # a corder always has 0 or 2 active edges
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
self.model.Add(sum(self.corner_vars[corner]) == 2).OnlyEnforceIf(g)
|
|
97
|
+
self.model.AddLinearExpressionInDomain(sum(self.corner_vars[corner]), corner_sum_domain)
|
|
98
|
+
|
|
94
99
|
# single connected component
|
|
95
100
|
def is_neighbor(cb1: CellBorder, cb2: CellBorder) -> bool:
|
|
96
101
|
cb1_corners = self.cell_borders_to_corners[cb1]
|
|
File without changes
|
|
File without changes
|