multi-puzzle-solver 0.9.24__py3-none-any.whl → 0.9.26__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.26.dist-info}/METADATA +173 -3
- {multi_puzzle_solver-0.9.24.dist-info → multi_puzzle_solver-0.9.26.dist-info}/RECORD +11 -9
- puzzle_solver/__init__.py +3 -1
- puzzle_solver/core/utils.py +80 -58
- puzzle_solver/core/utils_ortools.py +6 -10
- puzzle_solver/puzzles/flip/flip.py +77 -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.26.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.9.24.dist-info → multi_puzzle_solver-0.9.26.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.26
|
|
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,16 @@ 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>
|
|
334
|
+
<td align="center">
|
|
335
|
+
<a href="#flip-puzzle-type-44"><b>Flip</b><br><br>
|
|
336
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flip_unsolved.png" alt="Flip" width="140">
|
|
337
|
+
</a>
|
|
338
|
+
</td>
|
|
329
339
|
</tr>
|
|
330
340
|
</table>
|
|
331
341
|
|
|
@@ -383,6 +393,8 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
383
393
|
- [Yin-Yang (Puzzle Type #40)](#yin-yang-puzzle-type-40)
|
|
384
394
|
- [Binairo (Puzzle Type #41)](#binairo-puzzle-type-41)
|
|
385
395
|
- [Rectangles (Puzzle Type #42)](#rectangles-puzzle-type-42)
|
|
396
|
+
- [Palisade (Puzzle Type #43)](#palisade-puzzle-type-43)
|
|
397
|
+
- [Flip (Puzzle Type #44)](#flip-puzzle-type-44)
|
|
386
398
|
- [Why SAT / CP-SAT?](#why-sat--cp-sat)
|
|
387
399
|
- [Testing](#testing)
|
|
388
400
|
- [Contributing](#contributing)
|
|
@@ -3317,9 +3329,13 @@ Applying the solution to the puzzle visually:
|
|
|
3317
3329
|
|
|
3318
3330
|
## Slitherlink (Puzzle Type #39)
|
|
3319
3331
|
|
|
3320
|
-
Also known as Fences
|
|
3332
|
+
Also known as Fences, Loop the Loop, and Loopy
|
|
3321
3333
|
|
|
3322
|
-
* [**Play online**](https://www.
|
|
3334
|
+
* [**Play online 1**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/loopy.html)
|
|
3335
|
+
|
|
3336
|
+
* [**Play online 2**](https://www.puzzle-loop.com)
|
|
3337
|
+
|
|
3338
|
+
* [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/loopy.html#loopy)
|
|
3323
3339
|
|
|
3324
3340
|
* [**Solver Code**][39]
|
|
3325
3341
|
|
|
@@ -3720,6 +3736,158 @@ Applying the solution to the puzzle visually:
|
|
|
3720
3736
|
|
|
3721
3737
|
---
|
|
3722
3738
|
|
|
3739
|
+
## Palisade (Puzzle Type #43)
|
|
3740
|
+
|
|
3741
|
+
* [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/palisade.html)
|
|
3742
|
+
|
|
3743
|
+
* [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/palisade.html#palisade)
|
|
3744
|
+
|
|
3745
|
+
* [**Solver Code**][43]
|
|
3746
|
+
|
|
3747
|
+
<details>
|
|
3748
|
+
<summary><strong>Rules</strong></summary>
|
|
3749
|
+
|
|
3750
|
+
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).
|
|
3751
|
+
|
|
3752
|
+
</details>
|
|
3753
|
+
|
|
3754
|
+
**Unsolved puzzle**
|
|
3755
|
+
|
|
3756
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/palisade_unsolved.png" alt="Palisade unsolved" width="500">
|
|
3757
|
+
|
|
3758
|
+
Code to utilize this package and solve the puzzle:
|
|
3759
|
+
|
|
3760
|
+
(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)
|
|
3761
|
+
|
|
3762
|
+
```python
|
|
3763
|
+
import numpy as np
|
|
3764
|
+
from puzzle_solver import palisade_solver as solver
|
|
3765
|
+
board = np.array([
|
|
3766
|
+
['2', ' ', ' ', ' ', ' ', '3', ' ', ' ', '1', '1', '3', ' ', ' ', ' ', ' '],
|
|
3767
|
+
['3', '2', '1', ' ', '2', '3', ' ', ' ', ' ', ' ', ' ', '2', ' ', '0', ' '],
|
|
3768
|
+
[' ', ' ', ' ', '1', '1', ' ', ' ', '1', ' ', ' ', ' ', '1', ' ', ' ', ' '],
|
|
3769
|
+
[' ', '3', '2', ' ', ' ', ' ', ' ', '2', '3', ' ', ' ', ' ', '1', ' ', ' '],
|
|
3770
|
+
[' ', '0', '1', ' ', '2', ' ', ' ', '0', ' ', ' ', ' ', '1', ' ', '3', '2'],
|
|
3771
|
+
['1', '0', ' ', ' ', ' ', '2', '2', ' ', '2', ' ', '3', ' ', '0', '2', ' '],
|
|
3772
|
+
[' ', ' ', ' ', ' ', ' ', '3', ' ', ' ', ' ', '2', ' ', ' ', ' ', ' ', ' '],
|
|
3773
|
+
[' ', '1', ' ', ' ', ' ', '3', '1', ' ', '1', ' ', ' ', ' ', ' ', '1', ' '],
|
|
3774
|
+
[' ', ' ', ' ', '0', ' ', ' ', '0', ' ', ' ', '1', '2', ' ', ' ', ' ', '3'],
|
|
3775
|
+
[' ', ' ', ' ', ' ', ' ', ' ', '1', ' ', ' ', '2', ' ', ' ', '1', '2', '1'],
|
|
3776
|
+
[' ', ' ', ' ', ' ', '1', ' ', '2', '3', '1', ' ', ' ', ' ', '2', ' ', '1'],
|
|
3777
|
+
['2', ' ', '1', ' ', '2', '2', '1', ' ', ' ', '2', ' ', ' ', ' ', ' ', ' '],
|
|
3778
|
+
])
|
|
3779
|
+
binst = solver.Board(board, region_size=10)
|
|
3780
|
+
solutions = binst.solve_and_print()
|
|
3781
|
+
```
|
|
3782
|
+
|
|
3783
|
+
**Script Output**
|
|
3784
|
+
|
|
3785
|
+
```python
|
|
3786
|
+
Solution found
|
|
3787
|
+
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1
|
|
3788
|
+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
|
|
3789
|
+
┌───────────────────┬───────────────────────┬───────────────┐
|
|
3790
|
+
0│ 2 · · · · │ 3 · · 1 1 3 │ · · · · │
|
|
3791
|
+
│ ┌───────────┐ ├───────┬───┐ ┌───┴───┐ │
|
|
3792
|
+
1│ 3 │ 2 1 · │ 2 │ 3 · │ · │ · · │ · 2 │ · 0 · │
|
|
3793
|
+
├───┘ │ └───┐ │ └───┐ └───┐ └───┐ │
|
|
3794
|
+
2│ · · · 1 │ 1 · │ · │ 1 · │ · · │ 1 · │ · · │
|
|
3795
|
+
│ ┌───┐ │ ┌───┘ │ ┌───┴───────┘ └───┐ │
|
|
3796
|
+
3│ · │ 3 │ 2 · │ · │ · · │ 2 │ 3 · · · 1 · │ · │
|
|
3797
|
+
├───┘ └───────┼───┘ ┌───┘ └───┬───────────────┬───┴───┤
|
|
3798
|
+
4│ · 0 1 · │ 2 · │ · 0 · │ · · 1 · │ 3 2 │
|
|
3799
|
+
│ ┌───┘ │ │ ┌───┐ └───┐ │
|
|
3800
|
+
5│ 1 0 · │ · · 2 │ 2 · 2 │ · │ 3 │ · 0 2 │ · │
|
|
3801
|
+
│ ┌───┴───────────┼───┬───────┴───┤ ├───┐ │ │
|
|
3802
|
+
6│ · · │ · · · 3 │ · │ · · 2 │ · │ · │ · · │ · │
|
|
3803
|
+
├───────┘ ┌───────────┤ └───┐ │ │ └───────┘ │
|
|
3804
|
+
7│ · 1 · │ · · 3 │ 1 · │ 1 · │ · │ · · 1 · │
|
|
3805
|
+
│ ┌───┘ ┌───┘ │ │ └───────┐ ┌───┤
|
|
3806
|
+
8│ · · │ · 0 · │ · 0 · │ · 1 │ 2 · · │ · │ 3 │
|
|
3807
|
+
│ ┌───┘ ┌───┤ ├───┐ └───┐ ├───┘ │
|
|
3808
|
+
9│ · │ · · · │ · │ · 1 · │ · │ 2 · │ · 1 │ 2 1 │
|
|
3809
|
+
├───┤ ┌───────┘ ├───────┐ │ └───┐ │ │ │
|
|
3810
|
+
10│ · │ · │ · · 1 │ · 2 │ 3 │ 1 · │ · │ · 2 │ · 1 │
|
|
3811
|
+
│ └───┘ │ └───┘ ├───┴───────┘ │
|
|
3812
|
+
11│ 2 · 1 · 2 │ 2 1 · · 2 │ · · · · · │
|
|
3813
|
+
└───────────────────┴───────────────────┴───────────────────┘
|
|
3814
|
+
Solutions found: 1
|
|
3815
|
+
status: OPTIMAL
|
|
3816
|
+
Time taken: 11.94 seconds
|
|
3817
|
+
```
|
|
3818
|
+
|
|
3819
|
+
**Solved puzzle**
|
|
3820
|
+
|
|
3821
|
+
Applying the solution to the puzzle visually:
|
|
3822
|
+
|
|
3823
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/palisade_solved.png" alt="Palisade solved" width="500">
|
|
3824
|
+
|
|
3825
|
+
---
|
|
3826
|
+
|
|
3827
|
+
## Flip (Puzzle Type #44)
|
|
3828
|
+
|
|
3829
|
+
* [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/flip.html)
|
|
3830
|
+
|
|
3831
|
+
* [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/flip.html#flip)
|
|
3832
|
+
|
|
3833
|
+
* [**Solver Code**][44]
|
|
3834
|
+
|
|
3835
|
+
<details>
|
|
3836
|
+
<summary><strong>Rules</strong></summary>
|
|
3837
|
+
|
|
3838
|
+
You have a grid of squares, some light and some dark. Your aim is to light all the squares up at the same time. You can choose any square and flip its state from light to dark or dark to light, but when you do so, other squares around it change state as well.
|
|
3839
|
+
|
|
3840
|
+
</details>
|
|
3841
|
+
|
|
3842
|
+
**Unsolved puzzle**
|
|
3843
|
+
|
|
3844
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flip_unsolved.png" alt="Flip unsolved" width="500">
|
|
3845
|
+
|
|
3846
|
+
Code to utilize this package and solve the puzzle:
|
|
3847
|
+
|
|
3848
|
+
(Note: the solver also supports random mapping of squares to the neighbors they flip, see the test cases in `tests/test_flip.py` for usage examples)
|
|
3849
|
+
|
|
3850
|
+
```python
|
|
3851
|
+
import numpy as np
|
|
3852
|
+
from puzzle_solver import flip_solver as solver
|
|
3853
|
+
board = np.array([
|
|
3854
|
+
['B', 'W', 'W', 'W', 'W', 'W', 'W'],
|
|
3855
|
+
['B', 'B', 'W', 'W', 'W', 'B', 'B'],
|
|
3856
|
+
['W', 'B', 'W', 'W', 'B', 'B', 'W'],
|
|
3857
|
+
['B', 'B', 'B', 'W', 'W', 'B', 'W'],
|
|
3858
|
+
['W', 'W', 'B', 'B', 'W', 'B', 'W'],
|
|
3859
|
+
['B', 'W', 'B', 'B', 'W', 'W', 'W'],
|
|
3860
|
+
['B', 'W', 'B', 'W', 'W', 'B', 'B'],
|
|
3861
|
+
])
|
|
3862
|
+
binst = solver.Board(board=board)
|
|
3863
|
+
solutions = binst.solve_and_print()
|
|
3864
|
+
```
|
|
3865
|
+
|
|
3866
|
+
**Script Output**
|
|
3867
|
+
|
|
3868
|
+
The output tells you which squares to tap to solve the puzzle.
|
|
3869
|
+
|
|
3870
|
+
```python
|
|
3871
|
+
Solution found
|
|
3872
|
+
[['T' ' ' 'T' 'T' 'T' ' ' ' ']
|
|
3873
|
+
[' ' ' ' ' ' 'T' ' ' 'T' ' ']
|
|
3874
|
+
[' ' 'T' ' ' ' ' 'T' ' ' ' ']
|
|
3875
|
+
['T' ' ' 'T' ' ' ' ' 'T' ' ']
|
|
3876
|
+
[' ' ' ' ' ' 'T' ' ' ' ' 'T']
|
|
3877
|
+
['T' ' ' 'T' ' ' 'T' 'T' 'T']
|
|
3878
|
+
[' ' ' ' ' ' ' ' ' ' 'T' 'T']]
|
|
3879
|
+
Solutions found: 1
|
|
3880
|
+
status: OPTIMAL
|
|
3881
|
+
```
|
|
3882
|
+
|
|
3883
|
+
**Solved puzzle**
|
|
3884
|
+
|
|
3885
|
+
This picture won't mean much as the game is about the sequence of moves not the final frame as shown here.
|
|
3886
|
+
|
|
3887
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flip_solved.png" alt="Flip solved" width="500">
|
|
3888
|
+
|
|
3889
|
+
---
|
|
3890
|
+
|
|
3723
3891
|
---
|
|
3724
3892
|
|
|
3725
3893
|
## Why SAT / CP-SAT?
|
|
@@ -3813,3 +3981,5 @@ Issues and PRs welcome!
|
|
|
3813
3981
|
[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
3982
|
[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
3983
|
[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"
|
|
3984
|
+
[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"
|
|
3985
|
+
[44]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/flip "puzzle_solver/src/puzzle_solver/puzzles/flip 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=ScIPz0Gi0xGaY-v7N0JcfFgKUvfdSF3lUggAh8yagdg,3201
|
|
2
|
+
puzzle_solver/core/utils.py,sha256=LyrdhExCcdp7jWJJv1cu2urgS2gpcI44OsIipPeBAeQ,14113
|
|
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=ZngJLUhRNc7qqo2wtNLdMPx4u9w9JTUge27PmdXyDCw,3985
|
|
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=GTtzuc1OZCm3D5p-Po7LzK1d-whJkNSZ9G9rWr3vFMc,4966
|
|
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.26.dist-info/METADATA,sha256=iVRpKnMdTJNLrbdh9eP19RsPCloN1iEFwtlvcwy_peg,211081
|
|
57
|
+
multi_puzzle_solver-0.9.26.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
58
|
+
multi_puzzle_solver-0.9.26.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
|
|
59
|
+
multi_puzzle_solver-0.9.26.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.26'
|
puzzle_solver/core/utils.py
CHANGED
|
@@ -42,7 +42,9 @@ def get_next_pos(cur_pos: Pos, direction: Union[Direction, Direction8]) -> Pos:
|
|
|
42
42
|
return get_pos(cur_pos.x+delta_x, cur_pos.y+delta_y)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def get_neighbors4(pos: Pos, V: int, H: int) -> Iterable[Pos]:
|
|
45
|
+
def get_neighbors4(pos: Pos, V: int, H: int, include_self: bool = False) -> Iterable[Pos]:
|
|
46
|
+
if include_self:
|
|
47
|
+
yield pos
|
|
46
48
|
for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)):
|
|
47
49
|
p2 = get_pos(x=pos.x+dx, y=pos.y+dy)
|
|
48
50
|
if in_bounds(p2, V, H):
|
|
@@ -133,7 +135,7 @@ def get_deltas(direction: Union[Direction, Direction8]) -> Tuple[int, int]:
|
|
|
133
135
|
raise ValueError(f'invalid direction: {direction}')
|
|
134
136
|
|
|
135
137
|
|
|
136
|
-
def polyominoes(N):
|
|
138
|
+
def polyominoes(N) -> set[Shape]:
|
|
137
139
|
"""Generate all polyominoes of size N. Every rotation and reflection is considered different and included in the result.
|
|
138
140
|
Translation is not considered different and is removed from the result (otherwise the result would be infinite).
|
|
139
141
|
|
|
@@ -165,7 +167,7 @@ def polyominoes(N):
|
|
|
165
167
|
shapes: set[FastShape] = {frozenset({(0, 0)})}
|
|
166
168
|
for i in range(1, N):
|
|
167
169
|
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/
|
|
170
|
+
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
171
|
for s in shapes:
|
|
170
172
|
# frontier of a single shape: all 4-neighbors of existing cells not already in the shape
|
|
171
173
|
frontier = set()
|
|
@@ -239,125 +241,145 @@ def render_grid(cell_flags: np.ndarray,
|
|
|
239
241
|
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
242
|
scale_x: horizontal stretch factor (>=1). Try 2 or 3 for squarer cells.
|
|
241
243
|
"""
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
244
|
+
assert cell_flags is not None and cell_flags.ndim == 2
|
|
245
|
+
R, C = cell_flags.shape
|
|
246
|
+
|
|
247
|
+
# Edge presence arrays (note the rectangular shapes)
|
|
248
|
+
H = np.zeros((R+1, C), dtype=bool) # horizontal edges between rows
|
|
249
|
+
V = np.zeros((R, C+1), dtype=bool) # vertical edges between cols
|
|
250
|
+
for r in range(R):
|
|
251
|
+
for c in range(C):
|
|
252
|
+
s = cell_flags[r, c]
|
|
253
|
+
if 'U' in s: H[r, c] = True
|
|
254
|
+
if 'D' in s: H[r+1, c] = True
|
|
255
|
+
if 'L' in s: V[r, c] = True
|
|
256
|
+
if 'R' in s: V[r, c+1] = True
|
|
257
|
+
|
|
254
258
|
# Bitmask for corner connections
|
|
255
|
-
U,
|
|
259
|
+
U, Rb, D, Lb = 1, 2, 4, 8
|
|
256
260
|
JUNCTION = {
|
|
257
261
|
0: ' ',
|
|
258
262
|
U: '│', D: '│', U|D: '│',
|
|
259
|
-
|
|
260
|
-
U|
|
|
261
|
-
U|D|
|
|
262
|
-
U|
|
|
263
|
+
Lb: '─', Rb: '─', Lb|Rb: '─',
|
|
264
|
+
U|Rb: '└', Rb|D: '┌', D|Lb: '┐', Lb|U: '┘',
|
|
265
|
+
U|D|Lb: '┤', U|D|Rb: '├', Lb|Rb|U: '┴', Lb|Rb|D: '┬',
|
|
266
|
+
U|Rb|D|Lb: '┼',
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
assert scale_x >= 1
|
|
266
|
-
|
|
267
|
-
assert H.shape == (N+1, N) and V.shape == (N, N+1)
|
|
270
|
+
assert H.shape == (R+1, C) and V.shape == (R, C+1)
|
|
268
271
|
|
|
269
|
-
rows = 2*
|
|
270
|
-
cols = 2*
|
|
272
|
+
rows = 2*R + 1
|
|
273
|
+
cols = 2*C*scale_x + 1
|
|
271
274
|
canvas = [[' ']*cols for _ in range(rows)]
|
|
272
275
|
|
|
273
|
-
def x_corner(c): # x of corner column c
|
|
276
|
+
def x_corner(c): # x of corner column c (0..C)
|
|
274
277
|
return (2*c) * scale_x
|
|
275
|
-
def x_between(c,k): # kth in-between
|
|
278
|
+
def x_between(c,k): # kth in-between col (1..2*scale_x-1) between corners c and c+1
|
|
276
279
|
return (2*c) * scale_x + k
|
|
277
280
|
|
|
278
281
|
# horizontal edges: fill the stretched band between corners with '─'
|
|
279
|
-
for r in range(
|
|
282
|
+
for r in range(R+1):
|
|
280
283
|
rr = 2*r
|
|
281
|
-
for c in range(
|
|
284
|
+
for c in range(C):
|
|
282
285
|
if H[r, c]:
|
|
283
|
-
|
|
284
|
-
for k in range(1, scale_x*2): # 1..(2*scale_x-1), no gaps
|
|
286
|
+
for k in range(1, scale_x*2): # 1..(2*scale_x-1)
|
|
285
287
|
canvas[rr][x_between(c, k)] = '─'
|
|
286
288
|
|
|
287
|
-
# vertical edges:
|
|
288
|
-
for r in range(
|
|
289
|
+
# vertical edges: at the corner columns
|
|
290
|
+
for r in range(R):
|
|
289
291
|
rr = 2*r + 1
|
|
290
|
-
for c in range(
|
|
292
|
+
for c in range(C+1):
|
|
291
293
|
if V[r, c]:
|
|
292
294
|
canvas[rr][x_corner(c)] = '│'
|
|
293
295
|
|
|
294
|
-
# junctions at
|
|
295
|
-
for r in range(
|
|
296
|
+
# junctions at every corner grid point
|
|
297
|
+
for r in range(R+1):
|
|
296
298
|
rr = 2*r
|
|
297
|
-
for c in range(
|
|
299
|
+
for c in range(C+1):
|
|
298
300
|
m = 0
|
|
299
301
|
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 |=
|
|
302
|
+
if c < C and H[r, c]: m |= Rb
|
|
303
|
+
if r < R and V[r, c]: m |= D
|
|
304
|
+
if c > 0 and H[r, c-1]: m |= Lb
|
|
303
305
|
canvas[rr][x_corner(c)] = JUNCTION[m]
|
|
304
306
|
|
|
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.
|
|
307
|
+
# centers (safe for multi-character strings)
|
|
309
308
|
def put_center_text(rr: int, c: int, text: str):
|
|
310
|
-
# interior span (exclusive of the corner columns)
|
|
311
309
|
left = x_corner(c) + 1
|
|
312
310
|
right = x_corner(c+1) - 1
|
|
313
311
|
if right < left:
|
|
314
|
-
return
|
|
312
|
+
return
|
|
315
313
|
span_width = right - left + 1
|
|
316
|
-
|
|
317
314
|
s = str(text)
|
|
318
315
|
if len(s) > span_width:
|
|
319
|
-
s = s[:span_width] #
|
|
320
|
-
# center within the span
|
|
316
|
+
s = s[:span_width] # truncate to protect borders
|
|
321
317
|
start = left + (span_width - len(s)) // 2
|
|
322
318
|
for i, ch in enumerate(s):
|
|
323
319
|
canvas[rr][start + i] = ch
|
|
324
320
|
|
|
325
321
|
if center_char is not None:
|
|
326
|
-
for r in range(
|
|
322
|
+
for r in range(R):
|
|
327
323
|
rr = 2*r + 1
|
|
328
|
-
for c in range(
|
|
324
|
+
for c in range(C):
|
|
329
325
|
val = center_char if isinstance(center_char, str) else center_char[r, c]
|
|
330
326
|
put_center_text(rr, c, '' if val is None else str(val))
|
|
331
327
|
|
|
332
|
-
#
|
|
328
|
+
# rows -> strings
|
|
333
329
|
art_rows = [''.join(row) for row in canvas]
|
|
334
|
-
|
|
335
330
|
if not show_axes:
|
|
336
331
|
return '\n'.join(art_rows)
|
|
337
332
|
|
|
338
|
-
#
|
|
339
|
-
gut = max(2, len(str(
|
|
333
|
+
# Axes labels: row indices on the left, column indices on top (handle C, not R)
|
|
334
|
+
gut = max(2, len(str(R-1))) # gutter width based on row index width
|
|
340
335
|
gutter = ' ' * gut
|
|
341
336
|
top_tens = list(gutter + ' ' * cols)
|
|
342
337
|
top_ones = list(gutter + ' ' * cols)
|
|
343
338
|
|
|
344
|
-
for c in range(
|
|
339
|
+
for c in range(C):
|
|
345
340
|
xc_center = x_corner(c) + scale_x
|
|
346
|
-
if
|
|
341
|
+
if C >= 10:
|
|
347
342
|
top_tens[gut + xc_center] = str((c // 10) % 10)
|
|
348
343
|
top_ones[gut + xc_center] = str(c % 10)
|
|
349
344
|
|
|
350
|
-
# tiny corner labels
|
|
351
345
|
if gut >= 2:
|
|
352
346
|
top_tens[gut-2:gut] = list(' ')
|
|
353
347
|
top_ones[gut-2:gut] = list(' ')
|
|
354
348
|
|
|
355
349
|
labeled = []
|
|
356
350
|
for r, line in enumerate(art_rows):
|
|
357
|
-
if r % 2 == 1:
|
|
351
|
+
if r % 2 == 1: # cell-center row
|
|
358
352
|
label = str(r//2).rjust(gut)
|
|
359
353
|
else:
|
|
360
354
|
label = ' ' * gut
|
|
361
355
|
labeled.append(label + line)
|
|
362
356
|
|
|
363
357
|
return ''.join(top_tens) + '\n' + ''.join(top_ones) + '\n' + '\n'.join(labeled)
|
|
358
|
+
|
|
359
|
+
def id_board_to_wall_board(id_board: np.array, border_is_wall = True) -> np.array:
|
|
360
|
+
"""In many instances, we have a 2d array where cell values are arbitrary ids
|
|
361
|
+
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.
|
|
362
|
+
Args:
|
|
363
|
+
id_board: np.array of shape (N, N) with arbitrary ids.
|
|
364
|
+
border_is_wall: if True, the edges of the board are considered to be walls.
|
|
365
|
+
Returns:
|
|
366
|
+
np.array of shape (N, N) with walls "U", "D", "L", "R".
|
|
367
|
+
"""
|
|
368
|
+
res = np.full((id_board.shape[0], id_board.shape[1]), '', dtype=object)
|
|
369
|
+
V, H = id_board.shape
|
|
370
|
+
def append_char(pos: Pos, s: str):
|
|
371
|
+
set_char(res, pos, get_char(res, pos) + s)
|
|
372
|
+
def handle_pos_direction(pos: Pos, direction: Direction, s: str):
|
|
373
|
+
pos2 = get_next_pos(pos, direction)
|
|
374
|
+
if in_bounds(pos2, V, H):
|
|
375
|
+
if get_char(id_board, pos2) != get_char(id_board, pos):
|
|
376
|
+
append_char(pos, s)
|
|
377
|
+
else:
|
|
378
|
+
if border_is_wall:
|
|
379
|
+
append_char(pos, s)
|
|
380
|
+
for pos in get_all_pos(V, H):
|
|
381
|
+
handle_pos_direction(pos, Direction.LEFT, 'L')
|
|
382
|
+
handle_pos_direction(pos, Direction.RIGHT, 'R')
|
|
383
|
+
handle_pos_direction(pos, Direction.UP, 'U')
|
|
384
|
+
handle_pos_direction(pos, Direction.DOWN, 'D')
|
|
385
|
+
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,77 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ortools.sat.python import cp_model
|
|
5
|
+
|
|
6
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_neighbors4, set_char, get_char, Direction, get_next_pos
|
|
7
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Board:
|
|
11
|
+
def __init__(self, board: np.array, random_mapping: Optional[dict[Pos, Any]] = None):
|
|
12
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
13
|
+
assert all((c.item() in ['B', 'W']) for c in np.nditer(board)), 'board must contain only B or W'
|
|
14
|
+
self.board = board
|
|
15
|
+
self.V, self.H = board.shape
|
|
16
|
+
|
|
17
|
+
if random_mapping is None:
|
|
18
|
+
self.tap_mapping: dict[Pos, set[Pos]] = {pos: list(get_neighbors4(pos, self.V, self.H, include_self=True)) for pos in get_all_pos(self.V, self.H)}
|
|
19
|
+
else:
|
|
20
|
+
mapping_value = list(random_mapping.values())[0]
|
|
21
|
+
if isinstance(mapping_value, (set, list, tuple)) and isinstance(list(mapping_value)[0], Pos):
|
|
22
|
+
self.tap_mapping: dict[Pos, set[Pos]] = {pos: set(random_mapping[pos]) for pos in get_all_pos(self.V, self.H)}
|
|
23
|
+
elif isinstance(mapping_value, (set, list, tuple)) and isinstance(list(mapping_value)[0], str): # strings like "L", "UR", etc.
|
|
24
|
+
def _to_pos(pos: Pos, s: str) -> Pos:
|
|
25
|
+
d = {'L': Direction.LEFT, 'R': Direction.RIGHT, 'U': Direction.UP, 'D': Direction.DOWN}[s[0]]
|
|
26
|
+
r = get_next_pos(pos, d)
|
|
27
|
+
if len(s) == 1:
|
|
28
|
+
return r
|
|
29
|
+
else:
|
|
30
|
+
return _to_pos(r, s[1:])
|
|
31
|
+
self.tap_mapping: dict[Pos, set[Pos]] = {pos: set(_to_pos(pos, s) for s in random_mapping[pos]) for pos in get_all_pos(self.V, self.H)}
|
|
32
|
+
else:
|
|
33
|
+
raise ValueError(f'invalid random_mapping: {random_mapping}')
|
|
34
|
+
for k, v in self.tap_mapping.items():
|
|
35
|
+
if k not in v:
|
|
36
|
+
v.add(k)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
self.model = cp_model.CpModel()
|
|
40
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
41
|
+
|
|
42
|
+
self.create_vars()
|
|
43
|
+
self.add_all_constraints()
|
|
44
|
+
|
|
45
|
+
def create_vars(self):
|
|
46
|
+
for pos in get_all_pos(self.V, self.H):
|
|
47
|
+
self.model_vars[pos] = self.model.NewBoolVar(f'tap:{pos}')
|
|
48
|
+
|
|
49
|
+
def add_all_constraints(self):
|
|
50
|
+
for pos in get_all_pos(self.V, self.H):
|
|
51
|
+
# the state of a position is its starting state + if it is tapped + if any pos pointing to it is tapped
|
|
52
|
+
c = get_char(self.board, pos)
|
|
53
|
+
pos_that_will_turn_me = [k for k,v in self.tap_mapping.items() if pos in v]
|
|
54
|
+
literals = [self.model_vars[p] for p in pos_that_will_turn_me]
|
|
55
|
+
if c == 'W': # if started as white then needs an even number of taps while xor checks for odd number
|
|
56
|
+
literals.append(self.model.NewConstant(True))
|
|
57
|
+
elif c == 'B':
|
|
58
|
+
pass
|
|
59
|
+
else:
|
|
60
|
+
raise ValueError(f'invalid character: {c}')
|
|
61
|
+
self.model.AddBoolXOr(literals)
|
|
62
|
+
|
|
63
|
+
def solve_and_print(self, verbose: bool = True):
|
|
64
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
65
|
+
assignment: dict[Pos, int] = {}
|
|
66
|
+
for pos, var in board.model_vars.items():
|
|
67
|
+
assignment[pos] = solver.Value(var)
|
|
68
|
+
return SingleSolution(assignment=assignment)
|
|
69
|
+
def callback(single_res: SingleSolution):
|
|
70
|
+
print("Solution found")
|
|
71
|
+
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
72
|
+
for pos in get_all_pos(self.V, self.H):
|
|
73
|
+
c = get_char(self.board, pos)
|
|
74
|
+
c = 'T' if single_res.assignment[pos] == 1 else ' '
|
|
75
|
+
set_char(res, pos, c)
|
|
76
|
+
print(res)
|
|
77
|
+
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 frozenset(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
|