multi-puzzle-solver 0.9.24__py3-none-any.whl → 0.9.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of multi-puzzle-solver might be problematic. Click here for more details.

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