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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-puzzle-solver
3
- Version: 0.9.25
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**][42]
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=DsQVO-Eo1odFFFvQB1IpbJw-Yr2MTtON6zMnLUi05P8,3203
2
- puzzle_solver/core/utils.py,sha256=7Wo8_LHLEv8bY5-HsuCuLIjttZMMW09DoL1CcFDiu1Q,14046
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=4rQ-JsC_f33YKDM7aueKVlcVdDwzeBkTJL51K-Vy0gA,2223
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/palisade/palisade.py,sha256=ZFvBnBVbR0iIcQ5Vm3PtHPjdSDvrO5OUbM91YoTKpHI,4962
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=V7p6GSCwYrFfILDWiLLUbX08WlnPbQKdhQm8bMa2Mgw,7060
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=N3jv1Z-yYFlQDinii-DZfuJvLUsn9fT0h5Kyruxjn94,7017
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.25.dist-info/METADATA,sha256=DcVaQpmwyhYN0y0XxOcvomrpoerVjgYdylE8VUFml04,208538
57
- multi_puzzle_solver-0.9.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
- multi_puzzle_solver-0.9.25.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
59
- multi_puzzle_solver-0.9.25.dist-info/RECORD,,
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
- # from puzzle_solver.puzzles.flip import flip as flip_solver
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.25'
49
+ __version__ = '0.9.27'
@@ -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, get_neighbors8
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 board.shape[0] == board.shape[1], 'board must be square'
13
- assert all((c.item() == ' ') or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or digits'
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.N = board.shape[0]
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.N):
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.N):
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
- if not str(c).isdecimal():
30
- continue
31
- neighbour_vars = [self.model_vars[p] for p in get_neighbors8(pos, self.N, include_self=True)]
32
- self.model.Add(lxp.sum(neighbour_vars) == int(c))
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.N, self.N), ' ', dtype=object)
43
- for pos in get_all_pos(self.N):
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 = 'B' if single_res.assignment[pos] == 1 else ' '
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, id_board_to_wall_board, render_grid, set_char, in_bounds, get_next_pos, Direction, polyominoes
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 tuple(get_pos(x=p[0], y=p[1]) for p in body)
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, get_neighbors8, Direction, get_next_pos, render_grid
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, get_pos, Direction, get_row_pos, get_col_pos, get_next_pos, in_bounds, get_opposite_direction, render_grid
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]