multi-puzzle-solver 0.9.25__py3-none-any.whl → 0.9.27__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.25.dist-info → multi_puzzle_solver-0.9.27.dist-info}/METADATA +199 -3
- {multi_puzzle_solver-0.9.25.dist-info → multi_puzzle_solver-0.9.27.dist-info}/RECORD +12 -10
- puzzle_solver/__init__.py +3 -2
- puzzle_solver/core/utils.py +3 -154
- puzzle_solver/core/utils_visualizer.py +310 -0
- puzzle_solver/puzzles/flip/flip.py +45 -16
- puzzle_solver/puzzles/nurikabe/nurikabe.py +126 -0
- puzzle_solver/puzzles/palisade/palisade.py +3 -2
- puzzle_solver/puzzles/rectangles/rectangles.py +2 -2
- puzzle_solver/puzzles/slitherlink/slitherlink.py +2 -1
- {multi_puzzle_solver-0.9.25.dist-info → multi_puzzle_solver-0.9.27.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.9.25.dist-info → multi_puzzle_solver-0.9.27.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.27
|
|
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
|
|
@@ -331,6 +331,16 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
331
331
|
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/palisade_solved.png" alt="Palisade" width="140">
|
|
332
332
|
</a>
|
|
333
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>
|
|
339
|
+
<td align="center">
|
|
340
|
+
<a href="#nurikabe-puzzle-type-45"><b>Nurikabe</b><br><br>
|
|
341
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/nurikabe_unsolved.png" alt="Nurikabe" width="140">
|
|
342
|
+
</a>
|
|
343
|
+
</td>
|
|
334
344
|
</tr>
|
|
335
345
|
</table>
|
|
336
346
|
|
|
@@ -389,6 +399,8 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
389
399
|
- [Binairo (Puzzle Type #41)](#binairo-puzzle-type-41)
|
|
390
400
|
- [Rectangles (Puzzle Type #42)](#rectangles-puzzle-type-42)
|
|
391
401
|
- [Palisade (Puzzle Type #43)](#palisade-puzzle-type-43)
|
|
402
|
+
- [Flip (Puzzle Type #44)](#flip-puzzle-type-44)
|
|
403
|
+
- [Nurikabe (Puzzle Type #45)](#nurikabe-puzzle-type-45)
|
|
392
404
|
- [Why SAT / CP-SAT?](#why-sat--cp-sat)
|
|
393
405
|
- [Testing](#testing)
|
|
394
406
|
- [Contributing](#contributing)
|
|
@@ -3730,14 +3742,13 @@ Applying the solution to the puzzle visually:
|
|
|
3730
3742
|
|
|
3731
3743
|
---
|
|
3732
3744
|
|
|
3733
|
-
|
|
3734
3745
|
## Palisade (Puzzle Type #43)
|
|
3735
3746
|
|
|
3736
3747
|
* [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/palisade.html)
|
|
3737
3748
|
|
|
3738
3749
|
* [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/palisade.html#palisade)
|
|
3739
3750
|
|
|
3740
|
-
* [**Solver Code**][
|
|
3751
|
+
* [**Solver Code**][43]
|
|
3741
3752
|
|
|
3742
3753
|
<details>
|
|
3743
3754
|
<summary><strong>Rules</strong></summary>
|
|
@@ -3819,6 +3830,189 @@ Applying the solution to the puzzle visually:
|
|
|
3819
3830
|
|
|
3820
3831
|
---
|
|
3821
3832
|
|
|
3833
|
+
## Flip (Puzzle Type #44)
|
|
3834
|
+
|
|
3835
|
+
* [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/flip.html)
|
|
3836
|
+
|
|
3837
|
+
* [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/flip.html#flip)
|
|
3838
|
+
|
|
3839
|
+
* [**Solver Code**][44]
|
|
3840
|
+
|
|
3841
|
+
<details>
|
|
3842
|
+
<summary><strong>Rules</strong></summary>
|
|
3843
|
+
|
|
3844
|
+
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.
|
|
3845
|
+
|
|
3846
|
+
</details>
|
|
3847
|
+
|
|
3848
|
+
**Unsolved puzzle**
|
|
3849
|
+
|
|
3850
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flip_unsolved.png" alt="Flip unsolved" width="500">
|
|
3851
|
+
|
|
3852
|
+
Code to utilize this package and solve the puzzle:
|
|
3853
|
+
|
|
3854
|
+
(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)
|
|
3855
|
+
|
|
3856
|
+
```python
|
|
3857
|
+
import numpy as np
|
|
3858
|
+
from puzzle_solver import flip_solver as solver
|
|
3859
|
+
board = np.array([
|
|
3860
|
+
['B', 'W', 'W', 'W', 'W', 'W', 'W'],
|
|
3861
|
+
['B', 'B', 'W', 'W', 'W', 'B', 'B'],
|
|
3862
|
+
['W', 'B', 'W', 'W', 'B', 'B', 'W'],
|
|
3863
|
+
['B', 'B', 'B', 'W', 'W', 'B', 'W'],
|
|
3864
|
+
['W', 'W', 'B', 'B', 'W', 'B', 'W'],
|
|
3865
|
+
['B', 'W', 'B', 'B', 'W', 'W', 'W'],
|
|
3866
|
+
['B', 'W', 'B', 'W', 'W', 'B', 'B'],
|
|
3867
|
+
])
|
|
3868
|
+
binst = solver.Board(board=board)
|
|
3869
|
+
solutions = binst.solve_and_print()
|
|
3870
|
+
```
|
|
3871
|
+
|
|
3872
|
+
**Script Output**
|
|
3873
|
+
|
|
3874
|
+
The output tells you which squares to tap to solve the puzzle.
|
|
3875
|
+
|
|
3876
|
+
```python
|
|
3877
|
+
Solution found
|
|
3878
|
+
[['T' ' ' 'T' 'T' 'T' ' ' ' ']
|
|
3879
|
+
[' ' ' ' ' ' 'T' ' ' 'T' ' ']
|
|
3880
|
+
[' ' 'T' ' ' ' ' 'T' ' ' ' ']
|
|
3881
|
+
['T' ' ' 'T' ' ' ' ' 'T' ' ']
|
|
3882
|
+
[' ' ' ' ' ' 'T' ' ' ' ' 'T']
|
|
3883
|
+
['T' ' ' 'T' ' ' 'T' 'T' 'T']
|
|
3884
|
+
[' ' ' ' ' ' ' ' ' ' 'T' 'T']]
|
|
3885
|
+
Solutions found: 1
|
|
3886
|
+
status: OPTIMAL
|
|
3887
|
+
```
|
|
3888
|
+
|
|
3889
|
+
**Solved puzzle**
|
|
3890
|
+
|
|
3891
|
+
This picture won't mean much as the game is about the sequence of moves not the final frame as shown here.
|
|
3892
|
+
|
|
3893
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flip_solved.png" alt="Flip solved" width="500">
|
|
3894
|
+
|
|
3895
|
+
---
|
|
3896
|
+
## Nurikabe (Puzzle Type #45)
|
|
3897
|
+
|
|
3898
|
+
* [**Play online**](https://www.puzzle-nurikabe.com/)
|
|
3899
|
+
|
|
3900
|
+
* [**Instructions**](https://www.logicgamesonline.com/nurikabe/)
|
|
3901
|
+
|
|
3902
|
+
* [**Solver Code**][45]
|
|
3903
|
+
|
|
3904
|
+
<details>
|
|
3905
|
+
<summary><strong>Rules</strong></summary>
|
|
3906
|
+
|
|
3907
|
+
Nurikabe is a binary determination puzzle. You must decide for each cell if it is white or black according to the following rules:
|
|
3908
|
+
|
|
3909
|
+
- All of the black cells must be connected.
|
|
3910
|
+
- Each numbered cell must be part of a white island of connected white cells.
|
|
3911
|
+
- Each island must have the same number of white cells as the number it contains (including the numbered cell).
|
|
3912
|
+
- Two islands may not be connected.
|
|
3913
|
+
- There cannot be any 2x2 blocks of black cells.
|
|
3914
|
+
|
|
3915
|
+
Read more about the history and methods behind nurikabe in the [Wikipedia nurikabe article](https://en.wikipedia.org/wiki/Nurikabe).
|
|
3916
|
+
|
|
3917
|
+
</details>
|
|
3918
|
+
|
|
3919
|
+
**Unsolved puzzle**
|
|
3920
|
+
|
|
3921
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/nurikabe_unsolved.png" alt="Nurikabe unsolved" width="500">
|
|
3922
|
+
|
|
3923
|
+
Code to utilize this package and solve the puzzle:
|
|
3924
|
+
|
|
3925
|
+
(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)
|
|
3926
|
+
|
|
3927
|
+
```python
|
|
3928
|
+
import numpy as np
|
|
3929
|
+
from puzzle_solver import nurikabe_solver as solver
|
|
3930
|
+
board = np.array([
|
|
3931
|
+
['2', ' ', '3', ' ', '3', ' ', ' ', ' ', '3', ' ', ' ', '3', ' ', ' ', ' ', '2', ' ', '2', ' ', ' '],
|
|
3932
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
3933
|
+
[' ', ' ', ' ', ' ', '2', ' ', ' ', '1', ' ', ' ', '1', ' ', '3', ' ', ' ', ' ', '3', ' ', ' ', ' '],
|
|
3934
|
+
['2', ' ', ' ', '1', ' ', ' ', '3', ' ', ' ', '2', ' ', '2', ' ', ' ', ' ', '1', ' ', ' ', ' ', ' '],
|
|
3935
|
+
[' ', ' ', '2', ' ', '2', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '3', ' ', ' ', ' '],
|
|
3936
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '2', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
3937
|
+
['1', ' ', ' ', ' ', ' ', '1', ' ', '2', ' ', ' ', '3', ' ', ' ', ' ', ' ', ' ', ' ', '1', ' ', '2'],
|
|
3938
|
+
[' ', '2', ' ', '2', ' ', ' ', '1', ' ', ' ', ' ', ' ', ' ', ' ', '6', ' ', ' ', '2', ' ', ' ', ' '],
|
|
3939
|
+
[' ', ' ', ' ', ' ', ' ', '2', ' ', '7', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '3', ' '],
|
|
3940
|
+
[' ', ' ', '3', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '2', ' ', '2', ' ', '2', ' ', ' ', ' ', ' ', ' '],
|
|
3941
|
+
['4', ' ', ' ', ' ', ' ', ' ', '7', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
3942
|
+
[' ', ' ', ' ', ' ', '7', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '3', ' ', ' ', ' ', ' '],
|
|
3943
|
+
[' ', ' ', '2', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '1', ' ', '1', ' ', ' ', ' ', ' ', ' ', '3'],
|
|
3944
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '2', ' ', '4', ' ', ' ', '7', ' ', ' ', ' ', ' '],
|
|
3945
|
+
[' ', '1', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
3946
|
+
[' ', ' ', '2', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '1', ' ', ' '],
|
|
3947
|
+
['2', ' ', ' ', ' ', ' ', ' ', ' ', '1', ' ', '3', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '2', ' '],
|
|
3948
|
+
[' ', ' ', ' ', '4', ' ', ' ', ' ', ' ', '1', ' ', ' ', ' ', '2', ' ', '1', ' ', '3', ' ', ' ', ' '],
|
|
3949
|
+
[' ', '1', ' ', ' ', ' ', ' ', ' ', '3', ' ', ' ', '1', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '2'],
|
|
3950
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '1', ' ', ' ', ' ', ' ', ' '],
|
|
3951
|
+
])
|
|
3952
|
+
binst = solver.Board(board=board)
|
|
3953
|
+
solutions = binst.solve_and_print()
|
|
3954
|
+
```
|
|
3955
|
+
|
|
3956
|
+
**Script Output**
|
|
3957
|
+
|
|
3958
|
+
The output tells you which squares to tap to solve the puzzle.
|
|
3959
|
+
|
|
3960
|
+
```python
|
|
3961
|
+
Solution found
|
|
3962
|
+
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1
|
|
3963
|
+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
|
|
3964
|
+
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
|
|
3965
|
+
0│ 2 │▒▒▒│ 3 │▒▒▒│ 3 │ │ │▒▒▒│ 3 │ │▒▒▒│ 3 │ │ │▒▒▒│ 2 │▒▒▒│ 2 │ │▒▒▒│
|
|
3966
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3967
|
+
1│ │▒▒▒│ │▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│ │▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│ │▒▒▒│▒▒▒│▒▒▒│▒▒▒│
|
|
3968
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3969
|
+
2│▒▒▒│▒▒▒│ │▒▒▒│ 2 │ │▒▒▒│ 1 │▒▒▒│▒▒▒│ 1 │▒▒▒│ 3 │ │▒▒▒│▒▒▒│ 3 │ │ │▒▒▒│
|
|
3970
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3971
|
+
3│ 2 │▒▒▒│▒▒▒│ 1 │▒▒▒│▒▒▒│ 3 │▒▒▒│▒▒▒│ 2 │▒▒▒│ 2 │▒▒▒│ │▒▒▒│ 1 │▒▒▒│▒▒▒│▒▒▒│▒▒▒│
|
|
3972
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3973
|
+
4│ │▒▒▒│ 2 │▒▒▒│ 2 │▒▒▒│ │ │▒▒▒│ │▒▒▒│ │▒▒▒│▒▒▒│▒▒▒│▒▒▒│ 3 │ │ │▒▒▒│
|
|
3974
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3975
|
+
5│▒▒▒│▒▒▒│ │▒▒▒│ │▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│ 2 │▒▒▒│ │▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│
|
|
3976
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3977
|
+
6│ 1 │▒▒▒│▒▒▒│▒▒▒│▒▒▒│ 1 │▒▒▒│ 2 │ │▒▒▒│ 3 │▒▒▒│ │▒▒▒│ │ │▒▒▒│ 1 │▒▒▒│ 2 │
|
|
3978
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3979
|
+
7│▒▒▒│ 2 │▒▒▒│ 2 │▒▒▒│▒▒▒│ 1 │▒▒▒│▒▒▒│▒▒▒│ │ │▒▒▒│ 6 │ │▒▒▒│ 2 │▒▒▒│▒▒▒│ │
|
|
3980
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3981
|
+
8│▒▒▒│ │▒▒▒│ │▒▒▒│ 2 │▒▒▒│ 7 │ │▒▒▒│▒▒▒│▒▒▒│▒▒▒│ │▒▒▒│▒▒▒│ │▒▒▒│ 3 │▒▒▒│
|
|
3982
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3983
|
+
9│▒▒▒│▒▒▒│ 3 │▒▒▒│▒▒▒│ │▒▒▒│▒▒▒│ │▒▒▒│ 2 │▒▒▒│ 2 │▒▒▒│ 2 │▒▒▒│▒▒▒│▒▒▒│ │▒▒▒│
|
|
3984
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3985
|
+
10│ 4 │▒▒▒│ │ │▒▒▒│▒▒▒│ 7 │▒▒▒│ │▒▒▒│ │▒▒▒│ │▒▒▒│ │▒▒▒│ │▒▒▒│ │▒▒▒│
|
|
3986
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3987
|
+
11│ │▒▒▒│▒▒▒│▒▒▒│ 7 │▒▒▒│ │▒▒▒│ │ │▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│ 3 │ │▒▒▒│▒▒▒│▒▒▒│
|
|
3988
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3989
|
+
12│ │▒▒▒│ 2 │▒▒▒│ │▒▒▒│ │▒▒▒│▒▒▒│ │▒▒▒│ 1 │▒▒▒│ 1 │▒▒▒│▒▒▒│▒▒▒│ │▒▒▒│ 3 │
|
|
3990
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3991
|
+
13│ │▒▒▒│ │▒▒▒│ │▒▒▒│ │ │▒▒▒│▒▒▒│ 2 │▒▒▒│ 4 │▒▒▒│▒▒▒│ 7 │ │ │▒▒▒│ │
|
|
3992
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3993
|
+
14│▒▒▒│ 1 │▒▒▒│▒▒▒│ │▒▒▒│▒▒▒│ │ │▒▒▒│ │▒▒▒│ │ │▒▒▒│ │▒▒▒│▒▒▒│▒▒▒│ │
|
|
3994
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3995
|
+
15│▒▒▒│▒▒▒│ 2 │▒▒▒│ │ │▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│ │▒▒▒│ │▒▒▒│ 1 │▒▒▒│▒▒▒│
|
|
3996
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3997
|
+
16│ 2 │▒▒▒│ │▒▒▒│▒▒▒│ │▒▒▒│ 1 │▒▒▒│ 3 │ │ │▒▒▒│▒▒▒│▒▒▒│ │▒▒▒│▒▒▒│ 2 │ │
|
|
3998
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
3999
|
+
17│ │▒▒▒│▒▒▒│ 4 │▒▒▒│▒▒▒│▒▒▒│▒▒▒│ 1 │▒▒▒│▒▒▒│▒▒▒│ 2 │▒▒▒│ 1 │▒▒▒│ 3 │▒▒▒│▒▒▒│▒▒▒│
|
|
4000
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
4001
|
+
18│▒▒▒│ 1 │▒▒▒│ │ │ │▒▒▒│ 3 │▒▒▒│▒▒▒│ 1 │▒▒▒│ │▒▒▒│▒▒▒│▒▒▒│ │ │▒▒▒│ 2 │
|
|
4002
|
+
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
|
4003
|
+
19│▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│ │ │▒▒▒│▒▒▒│▒▒▒│▒▒▒│▒▒▒│ 1 │▒▒▒│▒▒▒│▒▒▒│▒▒▒│ │
|
|
4004
|
+
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
|
|
4005
|
+
Solutions found: 1
|
|
4006
|
+
status: OPTIMAL
|
|
4007
|
+
Time taken: 1.62 seconds
|
|
4008
|
+
```
|
|
4009
|
+
|
|
4010
|
+
**Solved puzzle**
|
|
4011
|
+
|
|
4012
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/nurikabe_solved.png" alt="Nurikabe solved" width="500">
|
|
4013
|
+
|
|
4014
|
+
---
|
|
4015
|
+
|
|
3822
4016
|
---
|
|
3823
4017
|
|
|
3824
4018
|
## Why SAT / CP-SAT?
|
|
@@ -3913,3 +4107,5 @@ Issues and PRs welcome!
|
|
|
3913
4107
|
[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"
|
|
3914
4108
|
[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
4109
|
[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"
|
|
4110
|
+
[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"
|
|
4111
|
+
[45]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/nurikabe "puzzle_solver/src/puzzle_solver/puzzles/nurikabe at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
puzzle_solver/__init__.py,sha256=
|
|
2
|
-
puzzle_solver/core/utils.py,sha256=
|
|
1
|
+
puzzle_solver/__init__.py,sha256=FzOcK5T5beTloncBQ-qo9hpqtxu4sbmR7m3fPUnnqec,3272
|
|
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=D1eNTZ3eJ76pBoznKRMwvlxXxQsz89K2fBfXP_dASwo,12868
|
|
4
5
|
puzzle_solver/puzzles/aquarium/aquarium.py,sha256=BUfkAS2d9eG3TdMoe1cOGGeNYgKUebRvn-z9nsC9gvE,5708
|
|
5
6
|
puzzle_solver/puzzles/battleships/battleships.py,sha256=RuYCrs4j0vUjlU139NRYYP-uNPAgO0V7hAzbsHrRwD8,7446
|
|
6
7
|
puzzle_solver/puzzles/binairo/binairo.py,sha256=sRtflnlGrN8xQ64beRZBGr74R8KptzxYDdFgXuW27pM,4595
|
|
@@ -12,7 +13,7 @@ puzzle_solver/puzzles/chess_range/chess_solo.py,sha256=U3v766UsZHx_dC3gxqU90VbjA
|
|
|
12
13
|
puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
|
|
13
14
|
puzzle_solver/puzzles/dominosa/dominosa.py,sha256=Nmb7pn8U27QJwGy9F3wo8ylqo2_U51OAo3GN2soaNpc,7195
|
|
14
15
|
puzzle_solver/puzzles/filling/filling.py,sha256=vrOIil285_r3IQ0F4c9mUBWMRVlPH4vowog_z1tCGdI,5567
|
|
15
|
-
puzzle_solver/puzzles/flip/flip.py,sha256=
|
|
16
|
+
puzzle_solver/puzzles/flip/flip.py,sha256=ZngJLUhRNc7qqo2wtNLdMPx4u9w9JTUge27PmdXyDCw,3985
|
|
16
17
|
puzzle_solver/puzzles/galaxies/galaxies.py,sha256=p10lpmW0FjtneFCMEjG1FSiEpQuvD8zZG9FG8zYGoes,5582
|
|
17
18
|
puzzle_solver/puzzles/galaxies/parse_map/parse_map.py,sha256=v5TCrdREeOB69s9_QFgPHKA7flG69Im1HVzIdxH0qQc,9355
|
|
18
19
|
puzzle_solver/puzzles/guess/guess.py,sha256=sH-NlYhxM3DNbhk4eGde09kgM0KaDvSbLrpHQiwcFGo,10791
|
|
@@ -29,15 +30,16 @@ puzzle_solver/puzzles/minesweeper/minesweeper.py,sha256=LiQVOGkWCsc1WtX8CdPgL_Ww
|
|
|
29
30
|
puzzle_solver/puzzles/mosaic/mosaic.py,sha256=QX_nVpVKQg8OfaUcqFk9tKqsDyVqvZc6-XWvfI3YcSw,2175
|
|
30
31
|
puzzle_solver/puzzles/nonograms/nonograms.py,sha256=1jmDTOCnmivmBlwtMDyyk3TVqH5IjapzLn7zLQ4qubk,6056
|
|
31
32
|
puzzle_solver/puzzles/norinori/norinori.py,sha256=uC8vXAw35xsTmpmTeKqYW7tbcssms9LCcXFBONtV2Ng,4743
|
|
32
|
-
puzzle_solver/puzzles/
|
|
33
|
+
puzzle_solver/puzzles/nurikabe/nurikabe.py,sha256=VMJjB9KAKmfBkG1mDT3Jf2I1PZJb--Qx0BicN8xL4eg,6519
|
|
34
|
+
puzzle_solver/puzzles/palisade/palisade.py,sha256=T-LXlaLU5OwUQ24QWJWhBUFUktg0qDODTilNmBaXs4I,5014
|
|
33
35
|
puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
|
|
34
36
|
puzzle_solver/puzzles/range/range.py,sha256=rruvD5ZSaOgvQuX6uGV_Dkr82nSiWZ5kDz03_j7Tt24,4425
|
|
35
|
-
puzzle_solver/puzzles/rectangles/rectangles.py,sha256=
|
|
37
|
+
puzzle_solver/puzzles/rectangles/rectangles.py,sha256=zaPg3qI9TNxr2iXmNi2kOL8R2RsS9DyQPUTY3ukgYIA,7033
|
|
36
38
|
puzzle_solver/puzzles/signpost/signpost.py,sha256=-0_S6ycwzwlUf9-ZhP127Rgo5gMBOHiTM6t08dLLDac,3869
|
|
37
39
|
puzzle_solver/puzzles/singles/singles.py,sha256=3wACiUa1Vmh2ce6szQ2hPjyBuH7aHiQ888p4R2jFkW4,3342
|
|
38
40
|
puzzle_solver/puzzles/slant/slant.py,sha256=xF-N4PuXYfx638NP1f1mi6YncIZB4mLtXtdS79XyPbg,6122
|
|
39
41
|
puzzle_solver/puzzles/slant/parse_map/parse_map.py,sha256=dxnALSDXe9wU0uSD0QEXnzoh1q801mj1ePTNLtG0n60,4796
|
|
40
|
-
puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=
|
|
42
|
+
puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=e1A_f_3J-QXN9fmt_Nf3FsYnp-TmE9TRKN06Wn4NnAU,7056
|
|
41
43
|
puzzle_solver/puzzles/star_battle/star_battle.py,sha256=IX6w4H3sifN01kPPtrAVRCK0Nl_xlXXSHvJKw8K1EuE,3718
|
|
42
44
|
puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
|
|
43
45
|
puzzle_solver/puzzles/stitches/stitches.py,sha256=iK8t02q43gH3FPbuIDn4dK0sbaOgZOnw8yHNRNvNuIU,6534
|
|
@@ -53,7 +55,7 @@ puzzle_solver/puzzles/unruly/unruly.py,sha256=sDF0oKT50G-NshyW2DYrvAgD9q9Ku9ANUy
|
|
|
53
55
|
puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=WrRdNhmKhIARdGOt_36gpRxRzrfLGv3wl7igBpPFM64,5259
|
|
54
56
|
puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
|
|
55
57
|
puzzle_solver/utils/visualizer.py,sha256=tsX1yEKwmwXBYuBJpx_oZGe2UUt1g5yV73G3UbtmvtE,6817
|
|
56
|
-
multi_puzzle_solver-0.9.
|
|
57
|
-
multi_puzzle_solver-0.9.
|
|
58
|
-
multi_puzzle_solver-0.9.
|
|
59
|
-
multi_puzzle_solver-0.9.
|
|
58
|
+
multi_puzzle_solver-0.9.27.dist-info/METADATA,sha256=ZZDovGlLzsfNSjwsqHiYi91vCPJCA2-Na3jiaQ8JiRU,224813
|
|
59
|
+
multi_puzzle_solver-0.9.27.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
60
|
+
multi_puzzle_solver-0.9.27.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
|
|
61
|
+
multi_puzzle_solver-0.9.27.dist-info/RECORD,,
|
puzzle_solver/__init__.py
CHANGED
|
@@ -8,7 +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
|
-
|
|
11
|
+
from puzzle_solver.puzzles.flip import flip as flip_solver
|
|
12
12
|
from puzzle_solver.puzzles.galaxies import galaxies as galaxies_solver
|
|
13
13
|
from puzzle_solver.puzzles.guess import guess as guess_solver
|
|
14
14
|
from puzzle_solver.puzzles.inertia import inertia as inertia_solver
|
|
@@ -21,6 +21,7 @@ from puzzle_solver.puzzles.minesweeper import minesweeper as minesweeper_solver
|
|
|
21
21
|
from puzzle_solver.puzzles.mosaic import mosaic as mosaic_solver
|
|
22
22
|
from puzzle_solver.puzzles.nonograms import nonograms as nonograms_solver
|
|
23
23
|
from puzzle_solver.puzzles.norinori import norinori as norinori_solver
|
|
24
|
+
from puzzle_solver.puzzles.nurikabe import nurikabe as nurikabe_solver
|
|
24
25
|
from puzzle_solver.puzzles.palisade import palisade as palisade_solver
|
|
25
26
|
from puzzle_solver.puzzles.lits import lits as lits_solver
|
|
26
27
|
from puzzle_solver.puzzles.pearl import pearl as pearl_solver
|
|
@@ -45,4 +46,4 @@ from puzzle_solver.puzzles.yin_yang import yin_yang as yin_yang_solver
|
|
|
45
46
|
|
|
46
47
|
from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
|
|
47
48
|
|
|
48
|
-
__version__ = '0.9.
|
|
49
|
+
__version__ = '0.9.27'
|
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):
|
|
@@ -228,156 +230,3 @@ def polyominoes_with_shape_id(N):
|
|
|
228
230
|
result = {(frozenset(Pos(x, y) for x, y in s), _id) for s, _id in result}
|
|
229
231
|
return result
|
|
230
232
|
|
|
231
|
-
|
|
232
|
-
def render_grid(cell_flags: np.ndarray,
|
|
233
|
-
center_char: Union[np.ndarray, str, None] = None,
|
|
234
|
-
show_axes: bool = True,
|
|
235
|
-
scale_x: int = 2) -> str:
|
|
236
|
-
"""
|
|
237
|
-
most of this function was AI generated then modified by me, I don't currently care about the details of rendering to the terminal this looked good enough during my testing.
|
|
238
|
-
cell_flags: np.ndarray of shape (N, N) with characters 'U', 'D', 'L', 'R' to represent the edges of the cells.
|
|
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
|
-
scale_x: horizontal stretch factor (>=1). Try 2 or 3 for squarer cells.
|
|
241
|
-
"""
|
|
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
|
-
|
|
256
|
-
# Bitmask for corner connections
|
|
257
|
-
U, Rb, D, Lb = 1, 2, 4, 8
|
|
258
|
-
JUNCTION = {
|
|
259
|
-
0: ' ',
|
|
260
|
-
U: '│', D: '│', U|D: '│',
|
|
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: '┼',
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
assert scale_x >= 1
|
|
268
|
-
assert H.shape == (R+1, C) and V.shape == (R, C+1)
|
|
269
|
-
|
|
270
|
-
rows = 2*R + 1
|
|
271
|
-
cols = 2*C*scale_x + 1
|
|
272
|
-
canvas = [[' ']*cols for _ in range(rows)]
|
|
273
|
-
|
|
274
|
-
def x_corner(c): # x of corner column c (0..C)
|
|
275
|
-
return (2*c) * scale_x
|
|
276
|
-
def x_between(c,k): # kth in-between col (1..2*scale_x-1) between corners c and c+1
|
|
277
|
-
return (2*c) * scale_x + k
|
|
278
|
-
|
|
279
|
-
# horizontal edges: fill the stretched band between corners with '─'
|
|
280
|
-
for r in range(R+1):
|
|
281
|
-
rr = 2*r
|
|
282
|
-
for c in range(C):
|
|
283
|
-
if H[r, c]:
|
|
284
|
-
for k in range(1, scale_x*2): # 1..(2*scale_x-1)
|
|
285
|
-
canvas[rr][x_between(c, k)] = '─'
|
|
286
|
-
|
|
287
|
-
# vertical edges: at the corner columns
|
|
288
|
-
for r in range(R):
|
|
289
|
-
rr = 2*r + 1
|
|
290
|
-
for c in range(C+1):
|
|
291
|
-
if V[r, c]:
|
|
292
|
-
canvas[rr][x_corner(c)] = '│'
|
|
293
|
-
|
|
294
|
-
# junctions at every corner grid point
|
|
295
|
-
for r in range(R+1):
|
|
296
|
-
rr = 2*r
|
|
297
|
-
for c in range(C+1):
|
|
298
|
-
m = 0
|
|
299
|
-
if r > 0 and V[r-1, c]: m |= U
|
|
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
|
-
canvas[rr][x_corner(c)] = JUNCTION[m]
|
|
304
|
-
|
|
305
|
-
# centers (safe for multi-character strings)
|
|
306
|
-
def put_center_text(rr: int, c: int, text: str):
|
|
307
|
-
left = x_corner(c) + 1
|
|
308
|
-
right = x_corner(c+1) - 1
|
|
309
|
-
if right < left:
|
|
310
|
-
return
|
|
311
|
-
span_width = right - left + 1
|
|
312
|
-
s = str(text)
|
|
313
|
-
if len(s) > span_width:
|
|
314
|
-
s = s[:span_width] # truncate to protect borders
|
|
315
|
-
start = left + (span_width - len(s)) // 2
|
|
316
|
-
for i, ch in enumerate(s):
|
|
317
|
-
canvas[rr][start + i] = ch
|
|
318
|
-
|
|
319
|
-
if center_char is not None:
|
|
320
|
-
for r in range(R):
|
|
321
|
-
rr = 2*r + 1
|
|
322
|
-
for c in range(C):
|
|
323
|
-
val = center_char if isinstance(center_char, str) else center_char[r, c]
|
|
324
|
-
put_center_text(rr, c, '' if val is None else str(val))
|
|
325
|
-
|
|
326
|
-
# rows -> strings
|
|
327
|
-
art_rows = [''.join(row) for row in canvas]
|
|
328
|
-
if not show_axes:
|
|
329
|
-
return '\n'.join(art_rows)
|
|
330
|
-
|
|
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
|
|
333
|
-
gutter = ' ' * gut
|
|
334
|
-
top_tens = list(gutter + ' ' * cols)
|
|
335
|
-
top_ones = list(gutter + ' ' * cols)
|
|
336
|
-
|
|
337
|
-
for c in range(C):
|
|
338
|
-
xc_center = x_corner(c) + scale_x
|
|
339
|
-
if C >= 10:
|
|
340
|
-
top_tens[gut + xc_center] = str((c // 10) % 10)
|
|
341
|
-
top_ones[gut + xc_center] = str(c % 10)
|
|
342
|
-
|
|
343
|
-
if gut >= 2:
|
|
344
|
-
top_tens[gut-2:gut] = list(' ')
|
|
345
|
-
top_ones[gut-2:gut] = list(' ')
|
|
346
|
-
|
|
347
|
-
labeled = []
|
|
348
|
-
for r, line in enumerate(art_rows):
|
|
349
|
-
if r % 2 == 1: # cell-center row
|
|
350
|
-
label = str(r//2).rjust(gut)
|
|
351
|
-
else:
|
|
352
|
-
label = ' ' * gut
|
|
353
|
-
labeled.append(label + line)
|
|
354
|
-
|
|
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
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Union, Callable, Optional
|
|
3
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_next_pos, in_bounds, set_char, get_char, Direction
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def render_grid(cell_flags: np.ndarray,
|
|
7
|
+
center_char: Union[np.ndarray, str, None] = None,
|
|
8
|
+
show_axes: bool = True,
|
|
9
|
+
scale_x: int = 2) -> str:
|
|
10
|
+
"""
|
|
11
|
+
most of this function was AI generated then modified by me, I don't currently care about the details of rendering to the terminal this looked good enough during my testing.
|
|
12
|
+
cell_flags: np.ndarray of shape (N, N) with characters 'U', 'D', 'L', 'R' to represent the edges of the cells.
|
|
13
|
+
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.
|
|
14
|
+
scale_x: horizontal stretch factor (>=1). Try 2 or 3 for squarer cells.
|
|
15
|
+
"""
|
|
16
|
+
assert cell_flags is not None and cell_flags.ndim == 2
|
|
17
|
+
R, C = cell_flags.shape
|
|
18
|
+
|
|
19
|
+
# Edge presence arrays (note the rectangular shapes)
|
|
20
|
+
H = np.zeros((R+1, C), dtype=bool) # horizontal edges between rows
|
|
21
|
+
V = np.zeros((R, C+1), dtype=bool) # vertical edges between cols
|
|
22
|
+
for r in range(R):
|
|
23
|
+
for c in range(C):
|
|
24
|
+
s = cell_flags[r, c]
|
|
25
|
+
if 'U' in s: H[r, c] = True
|
|
26
|
+
if 'D' in s: H[r+1, c] = True
|
|
27
|
+
if 'L' in s: V[r, c] = True
|
|
28
|
+
if 'R' in s: V[r, c+1] = True
|
|
29
|
+
|
|
30
|
+
# Bitmask for corner connections
|
|
31
|
+
U, Rb, D, Lb = 1, 2, 4, 8
|
|
32
|
+
JUNCTION = {
|
|
33
|
+
0: ' ',
|
|
34
|
+
U: '│', D: '│', U|D: '│',
|
|
35
|
+
Lb: '─', Rb: '─', Lb|Rb: '─',
|
|
36
|
+
U|Rb: '└', Rb|D: '┌', D|Lb: '┐', Lb|U: '┘',
|
|
37
|
+
U|D|Lb: '┤', U|D|Rb: '├', Lb|Rb|U: '┴', Lb|Rb|D: '┬',
|
|
38
|
+
U|Rb|D|Lb: '┼',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
assert scale_x >= 1
|
|
42
|
+
assert H.shape == (R+1, C) and V.shape == (R, C+1)
|
|
43
|
+
|
|
44
|
+
rows = 2*R + 1
|
|
45
|
+
cols = 2*C*scale_x + 1
|
|
46
|
+
canvas = [[' ']*cols for _ in range(rows)]
|
|
47
|
+
|
|
48
|
+
def x_corner(c): # x of corner column c (0..C)
|
|
49
|
+
return (2*c) * scale_x
|
|
50
|
+
def x_between(c,k): # kth in-between col (1..2*scale_x-1) between corners c and c+1
|
|
51
|
+
return (2*c) * scale_x + k
|
|
52
|
+
|
|
53
|
+
# horizontal edges: fill the stretched band between corners with '─'
|
|
54
|
+
for r in range(R+1):
|
|
55
|
+
rr = 2*r
|
|
56
|
+
for c in range(C):
|
|
57
|
+
if H[r, c]:
|
|
58
|
+
for k in range(1, scale_x*2): # 1..(2*scale_x-1)
|
|
59
|
+
canvas[rr][x_between(c, k)] = '─'
|
|
60
|
+
|
|
61
|
+
# vertical edges: at the corner columns
|
|
62
|
+
for r in range(R):
|
|
63
|
+
rr = 2*r + 1
|
|
64
|
+
for c in range(C+1):
|
|
65
|
+
if V[r, c]:
|
|
66
|
+
canvas[rr][x_corner(c)] = '│'
|
|
67
|
+
|
|
68
|
+
# junctions at every corner grid point
|
|
69
|
+
for r in range(R+1):
|
|
70
|
+
rr = 2*r
|
|
71
|
+
for c in range(C+1):
|
|
72
|
+
m = 0
|
|
73
|
+
if r > 0 and V[r-1, c]: m |= U
|
|
74
|
+
if c < C and H[r, c]: m |= Rb
|
|
75
|
+
if r < R and V[r, c]: m |= D
|
|
76
|
+
if c > 0 and H[r, c-1]: m |= Lb
|
|
77
|
+
canvas[rr][x_corner(c)] = JUNCTION[m]
|
|
78
|
+
|
|
79
|
+
# centers (safe for multi-character strings)
|
|
80
|
+
def put_center_text(rr: int, c: int, text: str):
|
|
81
|
+
left = x_corner(c) + 1
|
|
82
|
+
right = x_corner(c+1) - 1
|
|
83
|
+
if right < left:
|
|
84
|
+
return
|
|
85
|
+
span_width = right - left + 1
|
|
86
|
+
s = str(text)
|
|
87
|
+
if len(s) > span_width:
|
|
88
|
+
s = s[:span_width] # truncate to protect borders
|
|
89
|
+
start = left + (span_width - len(s)) // 2
|
|
90
|
+
for i, ch in enumerate(s):
|
|
91
|
+
canvas[rr][start + i] = ch
|
|
92
|
+
|
|
93
|
+
if center_char is not None:
|
|
94
|
+
for r in range(R):
|
|
95
|
+
rr = 2*r + 1
|
|
96
|
+
for c in range(C):
|
|
97
|
+
val = center_char if isinstance(center_char, str) else center_char[r, c]
|
|
98
|
+
put_center_text(rr, c, '' if val is None else str(val))
|
|
99
|
+
|
|
100
|
+
# rows -> strings
|
|
101
|
+
art_rows = [''.join(row) for row in canvas]
|
|
102
|
+
if not show_axes:
|
|
103
|
+
return '\n'.join(art_rows)
|
|
104
|
+
|
|
105
|
+
# Axes labels: row indices on the left, column indices on top (handle C, not R)
|
|
106
|
+
gut = max(2, len(str(R-1))) # gutter width based on row index width
|
|
107
|
+
gutter = ' ' * gut
|
|
108
|
+
top_tens = list(gutter + ' ' * cols)
|
|
109
|
+
top_ones = list(gutter + ' ' * cols)
|
|
110
|
+
|
|
111
|
+
for c in range(C):
|
|
112
|
+
xc_center = x_corner(c) + scale_x
|
|
113
|
+
if C >= 10:
|
|
114
|
+
top_tens[gut + xc_center] = str((c // 10) % 10)
|
|
115
|
+
top_ones[gut + xc_center] = str(c % 10)
|
|
116
|
+
|
|
117
|
+
if gut >= 2:
|
|
118
|
+
top_tens[gut-2:gut] = list(' ')
|
|
119
|
+
top_ones[gut-2:gut] = list(' ')
|
|
120
|
+
|
|
121
|
+
labeled = []
|
|
122
|
+
for r, line in enumerate(art_rows):
|
|
123
|
+
if r % 2 == 1: # cell-center row
|
|
124
|
+
label = str(r//2).rjust(gut)
|
|
125
|
+
else:
|
|
126
|
+
label = ' ' * gut
|
|
127
|
+
labeled.append(label + line)
|
|
128
|
+
|
|
129
|
+
return ''.join(top_tens) + '\n' + ''.join(top_ones) + '\n' + '\n'.join(labeled)
|
|
130
|
+
|
|
131
|
+
def id_board_to_wall_board(id_board: np.array, border_is_wall = True) -> np.array:
|
|
132
|
+
"""In many instances, we have a 2d array where cell values are arbitrary ids
|
|
133
|
+
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.
|
|
134
|
+
Args:
|
|
135
|
+
id_board: np.array of shape (N, N) with arbitrary ids.
|
|
136
|
+
border_is_wall: if True, the edges of the board are considered to be walls.
|
|
137
|
+
Returns:
|
|
138
|
+
np.array of shape (N, N) with walls "U", "D", "L", "R".
|
|
139
|
+
"""
|
|
140
|
+
res = np.full((id_board.shape[0], id_board.shape[1]), '', dtype=object)
|
|
141
|
+
V, H = id_board.shape
|
|
142
|
+
def append_char(pos: Pos, s: str):
|
|
143
|
+
set_char(res, pos, get_char(res, pos) + s)
|
|
144
|
+
def handle_pos_direction(pos: Pos, direction: Direction, s: str):
|
|
145
|
+
pos2 = get_next_pos(pos, direction)
|
|
146
|
+
if in_bounds(pos2, V, H):
|
|
147
|
+
if get_char(id_board, pos2) != get_char(id_board, pos):
|
|
148
|
+
append_char(pos, s)
|
|
149
|
+
else:
|
|
150
|
+
if border_is_wall:
|
|
151
|
+
append_char(pos, s)
|
|
152
|
+
for pos in get_all_pos(V, H):
|
|
153
|
+
handle_pos_direction(pos, Direction.LEFT, 'L')
|
|
154
|
+
handle_pos_direction(pos, Direction.RIGHT, 'R')
|
|
155
|
+
handle_pos_direction(pos, Direction.UP, 'U')
|
|
156
|
+
handle_pos_direction(pos, Direction.DOWN, 'D')
|
|
157
|
+
return res
|
|
158
|
+
|
|
159
|
+
def render_shaded_grid(V: int,
|
|
160
|
+
H: int,
|
|
161
|
+
is_shaded: Callable[[int, int], bool],
|
|
162
|
+
*,
|
|
163
|
+
scale_x: int = 2,
|
|
164
|
+
scale_y: int = 1,
|
|
165
|
+
fill_char: str = '▒',
|
|
166
|
+
empty_char: str = ' ',
|
|
167
|
+
empty_text: Optional[Union[str, Callable[[int, int], Optional[str]]]] = None,
|
|
168
|
+
show_axes: bool = True) -> str:
|
|
169
|
+
"""
|
|
170
|
+
Most of this function was AI generated then modified by me, I don't currently care about the details of rendering to the terminal this looked good enough during my testing.
|
|
171
|
+
Visualize a V x H grid where each cell is shaded if is_shaded(r, c) is True.
|
|
172
|
+
The grid lines are always present.
|
|
173
|
+
|
|
174
|
+
scale_x: horizontal stretch (>=1). Interior width per cell = 2*scale_x - 1.
|
|
175
|
+
scale_y: vertical stretch (>=1). Interior height per cell = scale_y.
|
|
176
|
+
fill_char: character to fill shaded cell interiors (single char).
|
|
177
|
+
empty_char: background character for unshaded interiors (single char).
|
|
178
|
+
empty_text: Optional text for unshaded cells. If a string, used for all unshaded
|
|
179
|
+
cells. If a callable (r, c) -> str|None, used per cell. Text is
|
|
180
|
+
centered within the interior row and truncated to fit.
|
|
181
|
+
"""
|
|
182
|
+
assert V >= 1 and H >= 1
|
|
183
|
+
assert scale_x >= 1 and scale_y >= 1
|
|
184
|
+
assert len(fill_char) == 1 and len(empty_char) == 1
|
|
185
|
+
|
|
186
|
+
# ── Layout helpers ─────────────────────────────────────────────────────
|
|
187
|
+
def x_corner(c: int) -> int: # column of vertical border at grid column c (0..H)
|
|
188
|
+
return (2 * c) * scale_x
|
|
189
|
+
def y_border(r: int) -> int: # row of horizontal border at grid row r (0..V)
|
|
190
|
+
return (scale_y + 1) * r
|
|
191
|
+
|
|
192
|
+
rows = y_border(V) + 1
|
|
193
|
+
cols = x_corner(H) + 1
|
|
194
|
+
canvas = [[empty_char] * cols for _ in range(rows)]
|
|
195
|
+
|
|
196
|
+
# ── Shading first (borders will overwrite as needed) ───────────────────
|
|
197
|
+
shaded_map = [[False]*H for _ in range(V)]
|
|
198
|
+
for r in range(V):
|
|
199
|
+
top = y_border(r) + 1
|
|
200
|
+
bottom = y_border(r + 1) - 1 # inclusive
|
|
201
|
+
if top > bottom:
|
|
202
|
+
continue
|
|
203
|
+
for c in range(H):
|
|
204
|
+
left = x_corner(c) + 1
|
|
205
|
+
right = x_corner(c + 1) - 1 # inclusive
|
|
206
|
+
if left > right:
|
|
207
|
+
continue
|
|
208
|
+
shaded = bool(is_shaded(r, c))
|
|
209
|
+
shaded_map[r][c] = shaded
|
|
210
|
+
ch = fill_char if shaded else empty_char
|
|
211
|
+
for yy in range(top, bottom + 1):
|
|
212
|
+
for xx in range(left, right + 1):
|
|
213
|
+
canvas[yy][xx] = ch
|
|
214
|
+
|
|
215
|
+
# ── Grid lines ─────────────────────────────────────────────────────────
|
|
216
|
+
U, Rb, D, Lb = 1, 2, 4, 8
|
|
217
|
+
JUNCTION = {
|
|
218
|
+
0: ' ',
|
|
219
|
+
U: '│', D: '│', U | D: '│',
|
|
220
|
+
Lb: '─', Rb: '─', Lb | Rb: '─',
|
|
221
|
+
U | Rb: '└', Rb | D: '┌', D | Lb: '┐', Lb | U: '┘',
|
|
222
|
+
U | D | Lb: '┤', U | D | Rb: '├', Lb | Rb | U: '┴', Lb | Rb | D: '┬',
|
|
223
|
+
U | Rb | D | Lb: '┼',
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# Horizontal borders (every y_border row)
|
|
227
|
+
for r in range(V + 1):
|
|
228
|
+
yy = y_border(r)
|
|
229
|
+
for c in range(H):
|
|
230
|
+
base = x_corner(c)
|
|
231
|
+
for k in range(1, 2 * scale_x): # 1..(2*scale_x-1)
|
|
232
|
+
canvas[yy][base + k] = '─'
|
|
233
|
+
|
|
234
|
+
# Vertical borders (every x_corner col)
|
|
235
|
+
for c in range(H + 1):
|
|
236
|
+
xx = x_corner(c)
|
|
237
|
+
for r in range(V):
|
|
238
|
+
for ky in range(1, scale_y + 1):
|
|
239
|
+
canvas[y_border(r) + ky][xx] = '│'
|
|
240
|
+
|
|
241
|
+
# Junctions at intersections
|
|
242
|
+
for r in range(V + 1):
|
|
243
|
+
yy = y_border(r)
|
|
244
|
+
for c in range(H + 1):
|
|
245
|
+
xx = x_corner(c)
|
|
246
|
+
m = 0
|
|
247
|
+
if r > 0: m |= U
|
|
248
|
+
if r < V: m |= D
|
|
249
|
+
if c > 0: m |= Lb
|
|
250
|
+
if c < H: m |= Rb
|
|
251
|
+
canvas[yy][xx] = JUNCTION[m]
|
|
252
|
+
|
|
253
|
+
# ── Optional per-cell text for UNshaded cells ──────────────────────────
|
|
254
|
+
def put_center_text(r_cell: int, c_cell: int, s: str):
|
|
255
|
+
# interior box
|
|
256
|
+
left = x_corner(c_cell) + 1
|
|
257
|
+
right = x_corner(c_cell + 1) - 1
|
|
258
|
+
top = y_border(r_cell) + 1
|
|
259
|
+
bottom= y_border(r_cell + 1) - 1
|
|
260
|
+
if left > right or top > bottom:
|
|
261
|
+
return
|
|
262
|
+
span_w = right - left + 1
|
|
263
|
+
# choose middle interior row for text
|
|
264
|
+
yy = top + (bottom - top) // 2
|
|
265
|
+
s = '' if s is None else str(s)
|
|
266
|
+
if len(s) > span_w:
|
|
267
|
+
s = s[:span_w]
|
|
268
|
+
start = left + (span_w - len(s)) // 2
|
|
269
|
+
for i, ch in enumerate(s):
|
|
270
|
+
canvas[yy][start + i] = ch
|
|
271
|
+
|
|
272
|
+
if empty_text is not None:
|
|
273
|
+
for r in range(V):
|
|
274
|
+
for c in range(H):
|
|
275
|
+
if not shaded_map[r][c]:
|
|
276
|
+
s = empty_text(r, c) if callable(empty_text) else empty_text
|
|
277
|
+
if s:
|
|
278
|
+
put_center_text(r, c, s)
|
|
279
|
+
|
|
280
|
+
# ── Stringify ──────────────────────────────────────────────────────────
|
|
281
|
+
art_rows = [''.join(row) for row in canvas]
|
|
282
|
+
if not show_axes:
|
|
283
|
+
return '\n'.join(art_rows)
|
|
284
|
+
|
|
285
|
+
# Axes labels: columns on top; rows on left
|
|
286
|
+
gut = max(2, len(str(V - 1)))
|
|
287
|
+
gutter = ' ' * gut
|
|
288
|
+
top_tens = list(gutter + ' ' * cols)
|
|
289
|
+
top_ones = list(gutter + ' ' * cols)
|
|
290
|
+
for c in range(H):
|
|
291
|
+
xc_center = x_corner(c) + scale_x
|
|
292
|
+
if H >= 10:
|
|
293
|
+
top_tens[gut + xc_center] = str((c // 10) % 10)
|
|
294
|
+
top_ones[gut + xc_center] = str(c % 10)
|
|
295
|
+
if gut >= 2:
|
|
296
|
+
top_tens[gut - 2:gut] = list(' ')
|
|
297
|
+
top_ones[gut - 2:gut] = list(' ')
|
|
298
|
+
|
|
299
|
+
labeled = []
|
|
300
|
+
for y, line in enumerate(art_rows):
|
|
301
|
+
mod = y % (scale_y + 1)
|
|
302
|
+
if 1 <= mod <= scale_y:
|
|
303
|
+
r = y // (scale_y + 1)
|
|
304
|
+
mid = (scale_y + 1) // 2
|
|
305
|
+
label = (str(r).rjust(gut) if mod == mid else ' ' * gut)
|
|
306
|
+
else:
|
|
307
|
+
label = ' ' * gut
|
|
308
|
+
labeled.append(label + line)
|
|
309
|
+
|
|
310
|
+
return ''.join(top_tens) + '\n' + ''.join(top_ones) + '\n' + '\n'.join(labeled)
|
|
@@ -1,18 +1,41 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
from ortools.sat.python import cp_model
|
|
3
|
-
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
4
5
|
|
|
5
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, set_char, get_char,
|
|
6
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_neighbors4, set_char, get_char, Direction, get_next_pos
|
|
6
7
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class Board:
|
|
10
|
-
def __init__(self, board: np.array):
|
|
11
|
+
def __init__(self, board: np.array, random_mapping: Optional[dict[Pos, Any]] = None):
|
|
11
12
|
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
12
|
-
assert
|
|
13
|
-
assert all((c.item() == ' ') or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or digits'
|
|
13
|
+
assert all((c.item() in ['B', 'W']) for c in np.nditer(board)), 'board must contain only B or W'
|
|
14
14
|
self.board = board
|
|
15
|
-
self.
|
|
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
|
+
|
|
16
39
|
self.model = cp_model.CpModel()
|
|
17
40
|
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
18
41
|
|
|
@@ -20,16 +43,22 @@ class Board:
|
|
|
20
43
|
self.add_all_constraints()
|
|
21
44
|
|
|
22
45
|
def create_vars(self):
|
|
23
|
-
for pos in get_all_pos(self.
|
|
24
|
-
self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
|
|
46
|
+
for pos in get_all_pos(self.V, self.H):
|
|
47
|
+
self.model_vars[pos] = self.model.NewBoolVar(f'tap:{pos}')
|
|
25
48
|
|
|
26
49
|
def add_all_constraints(self):
|
|
27
|
-
for pos in get_all_pos(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
|
|
28
52
|
c = get_char(self.board, pos)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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)
|
|
33
62
|
|
|
34
63
|
def solve_and_print(self, verbose: bool = True):
|
|
35
64
|
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
@@ -39,10 +68,10 @@ class Board:
|
|
|
39
68
|
return SingleSolution(assignment=assignment)
|
|
40
69
|
def callback(single_res: SingleSolution):
|
|
41
70
|
print("Solution found")
|
|
42
|
-
res = np.full((self.
|
|
43
|
-
for pos in get_all_pos(self.
|
|
71
|
+
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
72
|
+
for pos in get_all_pos(self.V, self.H):
|
|
44
73
|
c = get_char(self.board, pos)
|
|
45
|
-
c = '
|
|
74
|
+
c = 'T' if single_res.assignment[pos] == 1 else ' '
|
|
46
75
|
set_char(res, pos, c)
|
|
47
76
|
print(res)
|
|
48
77
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
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, get_pos, in_bounds, set_char, get_char, polyominoes, Shape, Direction, get_next_pos
|
|
7
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
8
|
+
from puzzle_solver.core.utils_visualizer import render_shaded_grid
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ShapeOnBoard:
|
|
13
|
+
is_active: cp_model.IntVar
|
|
14
|
+
N: int
|
|
15
|
+
body: set[Pos]
|
|
16
|
+
force_water: set[Pos]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Board:
|
|
20
|
+
def __init__(self, board: np.array):
|
|
21
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
22
|
+
assert all((c.item() == ' ') or str(c.item()).isdecimal() or c.item() == '#' for c in np.nditer(board)), 'board must contain only space, #, or digits'
|
|
23
|
+
self.board = board
|
|
24
|
+
self.V, self.H = board.shape
|
|
25
|
+
self.illegal_positions: set[Pos] = {pos for pos in get_all_pos(self.V, self.H) if get_char(self.board, pos) == '#'}
|
|
26
|
+
|
|
27
|
+
unique_numbers: set[int] = {int(c) for c in np.nditer(board) if str(c).isdecimal()}
|
|
28
|
+
self.polyominoes: dict[int, set[Shape]] = {n: polyominoes(n) for n in unique_numbers}
|
|
29
|
+
self.hints = {pos: int(get_char(self.board, pos)) for pos in get_all_pos(self.V, self.H) if str(get_char(self.board, pos)).isdecimal()}
|
|
30
|
+
self.all_hint_pos: set[Pos] = set(self.hints.keys())
|
|
31
|
+
|
|
32
|
+
self.model = cp_model.CpModel()
|
|
33
|
+
self.W: dict[Pos, cp_model.IntVar] = {}
|
|
34
|
+
self.B: dict[Pos, cp_model.IntVar] = {}
|
|
35
|
+
self.shapes_on_board: list[ShapeOnBoard] = []
|
|
36
|
+
|
|
37
|
+
self.create_vars()
|
|
38
|
+
self.add_all_constraints()
|
|
39
|
+
|
|
40
|
+
def create_vars(self):
|
|
41
|
+
for pos in self.get_all_legal_pos():
|
|
42
|
+
self.W[pos] = self.model.NewBoolVar(f'W:{pos}')
|
|
43
|
+
self.B[pos] = self.model.NewBoolVar(f'B:{pos}')
|
|
44
|
+
self.model.AddExactlyOne([self.W[pos], self.B[pos]])
|
|
45
|
+
|
|
46
|
+
def get_all_legal_pos(self) -> set[Pos]:
|
|
47
|
+
return {pos for pos in get_all_pos(self.V, self.H) if pos not in self.illegal_positions}
|
|
48
|
+
|
|
49
|
+
def in_bounds_and_legal(self, pos: Pos) -> bool:
|
|
50
|
+
return in_bounds(pos, self.V, self.H) and pos not in self.illegal_positions
|
|
51
|
+
|
|
52
|
+
def add_all_constraints(self):
|
|
53
|
+
for pos in self.W.keys():
|
|
54
|
+
self.model.AddExactlyOne([self.W[pos], self.B[pos]])
|
|
55
|
+
|
|
56
|
+
# init shapes on board for each hint
|
|
57
|
+
for hint_pos, hint_value in self.hints.items():
|
|
58
|
+
hint_shapes = []
|
|
59
|
+
for shape in self.polyominoes[hint_value]:
|
|
60
|
+
hint_single_shape = self.init_shape_on_board(shape, hint_pos, hint_value) # a "single shape" is translated many times
|
|
61
|
+
hint_shapes.extend(hint_single_shape)
|
|
62
|
+
assert len(hint_shapes) > 0, f'no shapes found for hint {hint_pos} with value {hint_value}'
|
|
63
|
+
self.model.AddExactlyOne([s.is_active for s in hint_shapes])
|
|
64
|
+
self.shapes_on_board.extend(hint_shapes)
|
|
65
|
+
|
|
66
|
+
# if no shape is active on the spot then it must be black
|
|
67
|
+
for pos in self.get_all_legal_pos():
|
|
68
|
+
shapes_here = [s for s in self.shapes_on_board if pos in s.body]
|
|
69
|
+
self.model.AddExactlyOne([s.is_active for s in shapes_here] + [self.B[pos]])
|
|
70
|
+
|
|
71
|
+
# if a shape is active, then all its body must be white and force water must be black
|
|
72
|
+
for shape_on_board in self.shapes_on_board:
|
|
73
|
+
for pos in shape_on_board.body:
|
|
74
|
+
self.model.Add(self.W[pos] == 1).OnlyEnforceIf(shape_on_board.is_active)
|
|
75
|
+
for pos in shape_on_board.force_water:
|
|
76
|
+
self.model.Add(self.B[pos] == 1).OnlyEnforceIf(shape_on_board.is_active)
|
|
77
|
+
|
|
78
|
+
# disallow 2x2 blacks
|
|
79
|
+
for pos in get_all_pos(self.V, self.H):
|
|
80
|
+
tl = pos
|
|
81
|
+
tr = get_next_pos(pos, Direction.RIGHT)
|
|
82
|
+
bl = get_next_pos(pos, Direction.DOWN)
|
|
83
|
+
br = get_next_pos(bl, Direction.RIGHT)
|
|
84
|
+
if any(not in_bounds(p, self.V, self.H) for p in [tl, tr, bl, br]):
|
|
85
|
+
continue
|
|
86
|
+
self.model.AddBoolOr([self.B[tl].Not(), self.B[tr].Not(), self.B[bl].Not(), self.B[br].Not()])
|
|
87
|
+
|
|
88
|
+
# all black is single connected component
|
|
89
|
+
force_connected_component(self.model, self.B)
|
|
90
|
+
|
|
91
|
+
def init_shape_on_board(self, shape: Shape, hint_pos: Pos, hint_value: int):
|
|
92
|
+
other_hint_pos: set[Pos] = self.all_hint_pos - {hint_pos}
|
|
93
|
+
max_x = max(p.x for p in shape)
|
|
94
|
+
max_y = max(p.y for p in shape)
|
|
95
|
+
hint_shapes = []
|
|
96
|
+
for dx in range(0, max_x + 1):
|
|
97
|
+
for dy in range(0, max_y + 1):
|
|
98
|
+
body = {get_pos(x=p.x + hint_pos.x - dx, y=p.y + hint_pos.y - dy) for p in shape} # translate shape by fixed hint position then dynamic moving dx and dy
|
|
99
|
+
if hint_pos not in body: # the hint must still be in the body after translation
|
|
100
|
+
continue
|
|
101
|
+
if any(not self.in_bounds_and_legal(p) for p in body): # illegal shape
|
|
102
|
+
continue
|
|
103
|
+
water = set(p for pos in body for p in get_neighbors4(pos, self.V, self.H))
|
|
104
|
+
water -= body
|
|
105
|
+
water -= self.illegal_positions
|
|
106
|
+
if any(p in other_hint_pos for p in body) or any(w in other_hint_pos for w in water): # shape touches another hint or forces water on another hint, illegal
|
|
107
|
+
continue
|
|
108
|
+
shape_on_board = ShapeOnBoard(
|
|
109
|
+
is_active=self.model.NewBoolVar(f'{hint_pos}:{dx}:{dy}:is_active'),
|
|
110
|
+
N=hint_value,
|
|
111
|
+
body=body,
|
|
112
|
+
force_water=water,
|
|
113
|
+
)
|
|
114
|
+
hint_shapes.append(shape_on_board)
|
|
115
|
+
return hint_shapes
|
|
116
|
+
|
|
117
|
+
def solve_and_print(self, verbose: bool = True):
|
|
118
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
119
|
+
assignment: dict[Pos, int] = {}
|
|
120
|
+
for pos, var in board.B.items():
|
|
121
|
+
assignment[pos] = solver.Value(var)
|
|
122
|
+
return SingleSolution(assignment=assignment)
|
|
123
|
+
def callback(single_res: SingleSolution):
|
|
124
|
+
print("Solution found")
|
|
125
|
+
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: str(self.board[r, c])))
|
|
126
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -4,8 +4,9 @@ from collections import defaultdict
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from ortools.sat.python import cp_model
|
|
6
6
|
|
|
7
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, get_pos,
|
|
7
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_pos, set_char, in_bounds, get_next_pos, Direction, polyominoes
|
|
8
8
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import id_board_to_wall_board, render_grid
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
|
|
@@ -44,7 +45,7 @@ def get_valid_translations(shape: Shape, board: np.array) -> set[Pos]:
|
|
|
44
45
|
if c != ' ' and c != str(shape_borders[i]): # there is a clue and it doesn't match my translated shape, skip
|
|
45
46
|
break
|
|
46
47
|
else:
|
|
47
|
-
yield
|
|
48
|
+
yield frozenset(get_pos(x=p[0], y=p[1]) for p in body)
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
|
|
@@ -3,10 +3,10 @@ from dataclasses import dataclass
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
5
|
from ortools.sat.python import cp_model
|
|
6
|
-
from ortools.sat.python.cp_model import LinearExpr as lxp
|
|
7
6
|
|
|
8
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, in_bounds, set_char, get_char,
|
|
7
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, in_bounds, set_char, get_char, Direction, get_next_pos
|
|
9
8
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
9
|
+
from puzzle_solver.core.utils_visualizer import render_grid
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def factor_pairs(N: int, upper_limit_i: int, upper_limit_j: int):
|
|
@@ -2,8 +2,9 @@ import numpy as np
|
|
|
2
2
|
from collections import defaultdict
|
|
3
3
|
from ortools.sat.python import cp_model
|
|
4
4
|
|
|
5
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char,
|
|
5
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, Direction, get_row_pos, get_col_pos, get_next_pos, in_bounds, get_opposite_direction
|
|
6
6
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
7
|
+
from puzzle_solver.core.utils_visualizer import render_grid
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
CellBorder = tuple[Pos, Direction]
|
|
File without changes
|
|
File without changes
|