multi-puzzle-solver 0.9.13__py3-none-any.whl → 0.9.14__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.13
3
+ Version: 0.9.14
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
@@ -264,6 +264,11 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
264
264
  <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/lits_solved.png" alt="Lits" width="140">
265
265
  </a>
266
266
  </td>
267
+ <td align="center">
268
+ <a href="#black-box-puzzle-type-34"><b>Black Box</b><br><br>
269
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/black_box_solved.png" alt="Black Box" width="140">
270
+ </a>
271
+ </td>
267
272
  </tr>
268
273
  </table>
269
274
 
@@ -312,6 +317,7 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
312
317
  - [Star Battle (Puzzle Type #31)](#star-battle-puzzle-type-31)
313
318
  - [Star Battle Shapeless (Puzzle Type #32)](#star-battle-shapeless-puzzle-type-32)
314
319
  - [Lits (Puzzle Type #33)](#lits-puzzle-type-33)
320
+ - [Black Box (Puzzle Type #34)](#black-box-puzzle-type-34)
315
321
  - [Why SAT / CP-SAT?](#why-sat--cp-sat)
316
322
  - [Testing](#testing)
317
323
  - [Contributing](#contributing)
@@ -804,7 +810,7 @@ You have a grid of squares, some of which contain digits, and the rest of which
804
810
  For example, it follows that no square can contain a zero, and that two adjacent squares can not both contain a one. No region has an area greater than 9 (because then its area would not be a single digit).
805
811
  </details>
806
812
 
807
- (Note: The solver for this puzzle is the only extremely slow solver in this repo and will take a minute to solve a simple 6x7 puzzle)
813
+ Note: It may take a few seconds for the model to be built.
808
814
 
809
815
  **Unsolved puzzle**
810
816
 
@@ -837,7 +843,7 @@ Solution found
837
843
  [1 6 6 1 4 4 4]]
838
844
  Solutions found: 1
839
845
  status: OPTIMAL
840
- Time taken: 46.27 seconds
846
+ Time taken: 0.10 seconds
841
847
  ```
842
848
 
843
849
  **Solved puzzle**
@@ -1212,8 +1218,6 @@ You have a grid of squares; some squares contain numbers. Your job is to colour
1212
1218
  For instance, a square containing the number one must have four black squares as its neighbours by the last criterion; but then it's impossible for it to be connected to any outside white square, which violates the second to last criterion. So no square will contain the number one.
1213
1219
  </details>
1214
1220
 
1215
- (Note: The solver for this puzzle is slightly slower and could take several seconds to solve a 16x11 puzzle)
1216
-
1217
1221
  **Unsolved puzzle**
1218
1222
 
1219
1223
  <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/range_unsolved.png" alt="Range unsolved" width="500">
@@ -1254,7 +1258,7 @@ B . . . B . B . . . . . B . . .
1254
1258
  B . . . . . . B . . . . B . . .
1255
1259
  Solutions found: 1
1256
1260
  status: OPTIMAL
1257
- Time taken: 3.32 seconds
1261
+ Time taken: 0.04 seconds
1258
1262
  ```
1259
1263
 
1260
1264
  **Solved puzzle**
@@ -1410,8 +1414,6 @@ Complete the track from A to B so that the rows and columns contain the same num
1410
1414
 
1411
1415
  </details>
1412
1416
 
1413
- (Note: The solver for this puzzle is slightly slower and could take several seconds to solve a large 15x15 puzzle)
1414
-
1415
1417
  **Unsolved puzzle**
1416
1418
 
1417
1419
  <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/tracks_unsolved.png" alt="Tracks unsolved" width="500">
@@ -1461,7 +1463,7 @@ solutions = binst.solve_and_print()
1461
1463
  [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '┏━' '━┛']]
1462
1464
  Solutions found: 1
1463
1465
  status: OPTIMAL
1464
- Time taken: 9.42 seconds
1466
+ Time taken: 1.04 seconds
1465
1467
  ```
1466
1468
 
1467
1469
  **Solved puzzle**
@@ -2782,6 +2784,114 @@ Time taken: 0.38 seconds
2782
2784
 
2783
2785
  ---
2784
2786
 
2787
+ ## Black Box (Puzzle Type #34)
2788
+
2789
+ * [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/blackbox.html)
2790
+
2791
+ * [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/blackbox.html#blackbox)
2792
+
2793
+ * [**Solver Code**][34]
2794
+
2795
+ <details>
2796
+ <summary><strong>Rules</strong></summary>
2797
+
2798
+ A number of balls are hidden in a rectangular arena. You have to deduce the positions of the balls by firing lasers positioned at the edges of the arena and observing how their beams are deflected.
2799
+
2800
+ Beams will travel straight from their origin until they hit the opposite side of the arena (at which point they emerge), unless affected by balls in one of the following ways:
2801
+
2802
+ A beam that hits a ball head-on is absorbed and will never re-emerge. This includes beams that meet a ball on the first rank of the arena.
2803
+ A beam with a ball in its front-left square and no ball ahead of it gets deflected 90 degrees to the right.
2804
+ A beam with a ball in its front-right square and no ball ahead of it gets similarly deflected to the left.
2805
+ A beam that would re-emerge from its entry location is considered to be ‘reflected’.
2806
+ A beam which would get deflected before entering the arena by a ball to the front-left or front-right of its entry point is also considered to be ‘reflected’.
2807
+
2808
+ Beams that are reflected appear as a ‘R’; beams that hit balls head-on appear as ‘H’. Otherwise, a number appears at the firing point and the location where the beam emerges (this number is unique to that shot).
2809
+
2810
+ You can place guesses as to the location of the balls, based on the entry and exit patterns of the beams; once you have placed enough balls a button appears enabling you to have your guesses checked.
2811
+
2812
+ Here is a diagram showing how the positions of balls can create each of the beam behaviours shown above:
2813
+
2814
+ 1 R H R - - - -
2815
+ | . . O . O . . . |
2816
+ 2 . . . . . . . . 3
2817
+ | . . . . . . . . |
2818
+ | . . . . . . . . |
2819
+ 3 . . . . . . . . |
2820
+ | . . . . . . O . |
2821
+ H . . . . . . . . |
2822
+ | . . . . . O . . |
2823
+ 1 2 - R R - - -
2824
+
2825
+ As shown, it is possible for a beam to receive multiple reflections before re-emerging (see turn 3). Similarly, a beam may be reflected (possibly more than once) before receiving a hit (the ‘H’ on the left side of the example).
2826
+
2827
+ Note that any layout with more than 4 balls may have a non-unique solution. The following diagram illustrates this; if you know the board contains 5 balls, it is impossible to determine where the fifth ball is (possible positions marked with an x):
2828
+
2829
+ - - - - - - - -
2830
+ | . . . . . . . . |
2831
+ | . . . . . . . . |
2832
+ | . . O . . O . . |
2833
+ | . . . x x . . . |
2834
+ | . . . x x . . . |
2835
+ | . . O . . O . . |
2836
+ | . . . . . . . . |
2837
+ | . . . . . . . . |
2838
+ - - - - - - - -
2839
+
2840
+ For this reason, when you have your guesses checked, the game will check that your solution produces the same results as the computer's, rather than that your solution is identical to the computer's. So in the above example, you could put the fifth ball at any of the locations marked with an x, and you would still win.
2841
+ </details>
2842
+
2843
+ Note: This puzzle is one of the very rare puzzles where CP-SAT is not a good fit because for every placement of the balls the state of the beams is dynamically changes and thus required a lot of variables to construct and constraint. This is why the resulting model is large and slow.
2844
+
2845
+ **Unsolved puzzle**
2846
+
2847
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/black_box_unsolved.png" alt="Black Box unsolved" width="500">
2848
+
2849
+ Code to utilize this package and solve the puzzle:
2850
+ ```python
2851
+ from puzzle_solver import black_box_solver as solver
2852
+ top = ['1', 'H', 'R', 'R', 'H', 'R', '2', '3']
2853
+ left = ['H', '1', 'H', '7', '5', '6', 'H', 'H']
2854
+ right = ['2', 'H', '4', 'H', '5', '6', 'H', 'H']
2855
+ bottom = ['7', 'R', 'H', 'R', 'H', 'R', '4', '3']
2856
+
2857
+ # create board and solve; ball count if between 3 and 6
2858
+ binst = solver.Board(top=top, left=left, bottom=bottom, right=right, ball_count=(3, 6))
2859
+ solutions = binst.solve_and_print()
2860
+ ```
2861
+ **Script Output**
2862
+
2863
+ As the instructions say, the solution to this puzzle is not garunteed to be unique.
2864
+
2865
+ ```python
2866
+ Solution found
2867
+ [[' ' ' ' ' ' ' ' 'O' ' ' ' ' ' ']
2868
+ [' ' ' ' ' ' ' ' ' ' 'O' ' ' ' ']
2869
+ [' ' 'O' ' ' ' ' ' ' ' ' ' ' ' ']
2870
+ [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']
2871
+ [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']
2872
+ [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']
2873
+ [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']
2874
+ [' ' ' ' 'O' ' ' 'O' ' ' ' ' ' ']]
2875
+ Solution found
2876
+ [[' ' ' ' ' ' ' ' 'O' ' ' ' ' ' ']
2877
+ [' ' ' ' ' ' ' ' ' ' 'O' ' ' ' ']
2878
+ [' ' 'O' ' ' 'O' ' ' ' ' ' ' ' ']
2879
+ [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']
2880
+ [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']
2881
+ [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']
2882
+ [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']
2883
+ [' ' ' ' 'O' ' ' 'O' ' ' ' ' ' ']]
2884
+ Solutions found: 2
2885
+ status: OPTIMAL
2886
+ Time taken: 30.33 seconds
2887
+ ```
2888
+
2889
+ **Solved puzzle**
2890
+
2891
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/black_box_solved.png" alt="Black Box solved" width="500">
2892
+
2893
+ ---
2894
+
2785
2895
  ---
2786
2896
 
2787
2897
  ## Why SAT / CP-SAT?
@@ -2866,3 +2976,4 @@ Issues and PRs welcome!
2866
2976
  [31]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/star_battle "puzzle_solver/src/puzzle_solver/puzzles/star_battle at master · Ar-Kareem/puzzle_solver · GitHub"
2867
2977
  [32]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/star_battle_shapeless "puzzle_solver/src/puzzle_solver/puzzles/star_battle_shapeless at master · Ar-Kareem/puzzle_solver · GitHub"
2868
2978
  [33]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/lits "puzzle_solver/src/puzzle_solver/puzzles/lits at master · Ar-Kareem/puzzle_solver · GitHub"
2979
+ [34]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/black_box "puzzle_solver/src/puzzle_solver/puzzles/black_box at master · Ar-Kareem/puzzle_solver · GitHub"
@@ -1,33 +1,34 @@
1
- puzzle_solver/__init__.py,sha256=3aQDyvMPWTlYKDo3j-v4x_lUcNbp7KyallMChQEeJY0,2429
2
- puzzle_solver/core/utils.py,sha256=D7enPxJjnsTbGDqqtOtGaRaetwGs0nqrNtTnrqhMB-g,3408
3
- puzzle_solver/core/utils_ortools.py,sha256=eoT9hSJe-c67A_hsu1jnMpyRgMrTtUs5n2j_m5Hk8Do,7362
1
+ puzzle_solver/__init__.py,sha256=-7Mdi0aljanRFso8IlRMDx_TxX1x8joDABRaBpC69fc,2503
2
+ puzzle_solver/core/utils.py,sha256=_LA81kHrsgvqPvq7RISBeaurXmYMKAU9N6qmV8n0G7s,8063
3
+ puzzle_solver/core/utils_ortools.py,sha256=USUl23e6AyE_FGTIN1oYkmMJOuwmKXNges9K2G4ccNM,10360
4
4
  puzzle_solver/puzzles/aquarium/aquarium.py,sha256=BUfkAS2d9eG3TdMoe1cOGGeNYgKUebRvn-z9nsC9gvE,5708
5
- puzzle_solver/puzzles/battleships/battleships.py,sha256=6cYFHnOH7RYLfH5DFVz6SRPHY5GauB9YN4hPU7YNUHU,7444
5
+ puzzle_solver/puzzles/battleships/battleships.py,sha256=RuYCrs4j0vUjlU139NRYYP-uNPAgO0V7hAzbsHrRwD8,7446
6
+ puzzle_solver/puzzles/black_box/black_box.py,sha256=ZnHDVt6PFS_r1kMNSsbz9hav1hxIrNDUvPyERGPjLjM,15635
6
7
  puzzle_solver/puzzles/bridges/bridges.py,sha256=15A9uV4xjoqPRo_9CTnoKeGRxS3z2aMF619T1n0dTOQ,5402
7
8
  puzzle_solver/puzzles/chess_range/chess_melee.py,sha256=D-_Oi8OyxsVe1j3dIKYwRlxgeb3NWLmDWGcv-oclY0c,195
8
9
  puzzle_solver/puzzles/chess_range/chess_range.py,sha256=uMQGTIwzGskHIhI-tPYjT9a3wHUBIkZ18eXjV9IpUE4,21071
9
10
  puzzle_solver/puzzles/chess_range/chess_solo.py,sha256=U3v766UsZHx_dC3gxqU90VbjAXn-OlYhtrnnvJYFvrQ,401
10
11
  puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
11
12
  puzzle_solver/puzzles/dominosa/dominosa.py,sha256=Nmb7pn8U27QJwGy9F3wo8ylqo2_U51OAo3GN2soaNpc,7195
12
- puzzle_solver/puzzles/filling/filling.py,sha256=UAkNYjlfxOrYGrRVmuElhWPeW10xD6kiWuB8oELzy3w,9141
13
+ puzzle_solver/puzzles/filling/filling.py,sha256=vrOIil285_r3IQ0F4c9mUBWMRVlPH4vowog_z1tCGdI,5567
13
14
  puzzle_solver/puzzles/guess/guess.py,sha256=sH-NlYhxM3DNbhk4eGde09kgM0KaDvSbLrpHQiwcFGo,10791
14
15
  puzzle_solver/puzzles/inertia/inertia.py,sha256=gJBahkh69CrSWNscalKEoP1j4X-Q3XpbIBMiG9PUpU0,5657
15
- puzzle_solver/puzzles/inertia/tsp.py,sha256=6ZDxdg2f7GDsZeQpnSR2ay0-T4ZhTESJtkKMcm49OS0,15204
16
+ puzzle_solver/puzzles/inertia/tsp.py,sha256=gobiISHtARA4Elq0jr90p6Yhq11ULjGoqsS-rLFhYcc,15389
16
17
  puzzle_solver/puzzles/inertia/parse_map/parse_map.py,sha256=A9JQTNqamUdzlwqks0XQp3Hge3mzyTIVK6YtDJvqpL4,8422
17
18
  puzzle_solver/puzzles/kakurasu/kakurasu.py,sha256=VNGMJnBHDi6WkghLObRLhUvkmrPaGphTTUDMC0TkQvQ,2064
18
19
  puzzle_solver/puzzles/keen/keen.py,sha256=tDb6C5S3Q0JAKPsdw-84WQ6PxRADELZHr_BK8FDH-NA,5039
19
20
  puzzle_solver/puzzles/light_up/light_up.py,sha256=iSA1rjZMFsnI0V0Nxivxox4qZkB7PvUrROSHXcoUXds,4541
20
- puzzle_solver/puzzles/lits/lits.py,sha256=gYAcsuWucSer2JWs5eKOroiVmjfi-VzccmZvBIUIFks,12014
21
+ puzzle_solver/puzzles/lits/lits.py,sha256=6Yp9EqhQpuWz_rc_rXtu9dG_pUrmBAhWj1Q9IyAfxPk,7891
21
22
  puzzle_solver/puzzles/magnets/magnets.py,sha256=-Wl49JD_PKeq735zQVMQ3XSQX6gdHiY-7PKw-Sh16jw,6474
22
23
  puzzle_solver/puzzles/map/map.py,sha256=sxc57tapB8Tsgam-yoDitln1o-EB_SbIYvO6WEYy3us,2582
23
24
  puzzle_solver/puzzles/minesweeper/minesweeper.py,sha256=LiQVOGkWCsc1WtX8CdPgL_WwAcaeUFuoi5_eqH8U2Og,5876
24
25
  puzzle_solver/puzzles/mosaic/mosaic.py,sha256=QX_nVpVKQg8OfaUcqFk9tKqsDyVqvZc6-XWvfI3YcSw,2175
25
26
  puzzle_solver/puzzles/nonograms/nonograms.py,sha256=1jmDTOCnmivmBlwtMDyyk3TVqH5IjapzLn7zLQ4qubk,6056
26
27
  puzzle_solver/puzzles/norinori/norinori.py,sha256=Z2c0iEn7a6S6gaaJlvNMNNbAQwpztNLB0LTH_XVgu74,12269
27
- puzzle_solver/puzzles/pearl/pearl.py,sha256=AP0whWwwZ-1zKingW14OwseYylNAr6NkXSrvdnPU6Rw,8566
28
- puzzle_solver/puzzles/range/range.py,sha256=g6ZuHuulYLpNFsqbnPoIB5KoGPllYppU10-Zzqfj5f8,6993
28
+ puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
29
+ puzzle_solver/puzzles/range/range.py,sha256=rruvD5ZSaOgvQuX6uGV_Dkr82nSiWZ5kDz03_j7Tt24,4425
29
30
  puzzle_solver/puzzles/signpost/signpost.py,sha256=-0_S6ycwzwlUf9-ZhP127Rgo5gMBOHiTM6t08dLLDac,3869
30
- puzzle_solver/puzzles/singles/singles.py,sha256=kwMENfqQ-OP3YIz5baY6LRcvYCsNfhImEXN00lwazKM,5658
31
+ puzzle_solver/puzzles/singles/singles.py,sha256=3wACiUa1Vmh2ce6szQ2hPjyBuH7aHiQ888p4R2jFkW4,3342
31
32
  puzzle_solver/puzzles/star_battle/star_battle.py,sha256=IX6w4H3sifN01kPPtrAVRCK0Nl_xlXXSHvJKw8K1EuE,3718
32
33
  puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
33
34
  puzzle_solver/puzzles/stitches/stitches.py,sha256=iK8t02q43gH3FPbuIDn4dK0sbaOgZOnw8yHNRNvNuIU,6534
@@ -36,11 +37,11 @@ puzzle_solver/puzzles/sudoku/sudoku.py,sha256=M_pry7XyKKzlfCF5rFi02lyOrj5GWZzXnD
36
37
  puzzle_solver/puzzles/tents/tents.py,sha256=iyVK2WXfIT5j_9qqlQg0WmwvixwXlZSsHGK3XA-KpII,6283
37
38
  puzzle_solver/puzzles/thermometers/thermometers.py,sha256=nsvJZkm7G8FALT27bpaB0lv5E_AWawqmvapQI8QcYXw,4015
38
39
  puzzle_solver/puzzles/towers/towers.py,sha256=QvL0Pp-Z2ewCeq9ZkNrh8MShKOh-Y52sFBSudve68wk,6496
39
- puzzle_solver/puzzles/tracks/tracks.py,sha256=0K1YZMHiRIMmFwoD_JxB2c_xB6GYV8spgNUCL-JwDJM,9073
40
+ puzzle_solver/puzzles/tracks/tracks.py,sha256=98xds9SKNqtOLFTRUX_KSMC7XYmZo567LOFeqotVQaM,7237
40
41
  puzzle_solver/puzzles/undead/undead.py,sha256=IrCUfzQFBem658P5KKqldG7vd2TugTHehcwseCarerM,6604
41
42
  puzzle_solver/puzzles/unruly/unruly.py,sha256=sDF0oKT50G-NshyW2DYrvAgD9q9Ku9ANUyNhGSAu7cQ,3827
42
43
  puzzle_solver/utils/visualizer.py,sha256=tsX1yEKwmwXBYuBJpx_oZGe2UUt1g5yV73G3UbtmvtE,6817
43
- multi_puzzle_solver-0.9.13.dist-info/METADATA,sha256=x82w11xGJo2Rb4cGGQkmlbQNZ1i7MPGuhxCjOyaj61Y,143181
44
- multi_puzzle_solver-0.9.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
45
- multi_puzzle_solver-0.9.13.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
46
- multi_puzzle_solver-0.9.13.dist-info/RECORD,,
44
+ multi_puzzle_solver-0.9.14.dist-info/METADATA,sha256=HYspx5V6VWQi6fmAy1I6WaNrDlGumsChUCY2mjZQArI,148507
45
+ multi_puzzle_solver-0.9.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
+ multi_puzzle_solver-0.9.14.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
47
+ multi_puzzle_solver-0.9.14.dist-info/RECORD,,
puzzle_solver/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from puzzle_solver.puzzles.aquarium import aquarium as aquarium_solver
2
2
  from puzzle_solver.puzzles.battleships import battleships as battleships_solver
3
+ from puzzle_solver.puzzles.black_box import black_box as black_box_solver
3
4
  from puzzle_solver.puzzles.bridges import bridges as bridges_solver
4
5
  from puzzle_solver.puzzles.chess_range import chess_range as chess_range_solver
5
6
  from puzzle_solver.puzzles.chess_range import chess_solo as chess_solo_solver
@@ -34,4 +35,4 @@ from puzzle_solver.puzzles.unruly import unruly as unruly_solver
34
35
 
35
36
  from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
36
37
 
37
- __version__ = '0.9.13'
38
+ __version__ = '0.9.14'
@@ -1,127 +1,228 @@
1
- from dataclasses import dataclass
2
- from typing import Tuple, Iterable, Union
3
- from enum import Enum
4
-
5
- import numpy as np
6
-
7
-
8
- class Direction(Enum):
9
- UP = 1
10
- DOWN = 2
11
- LEFT = 3
12
- RIGHT = 4
13
-
14
- class Direction8(Enum):
15
- UP = 1
16
- DOWN = 2
17
- LEFT = 3
18
- RIGHT = 4
19
- UP_LEFT = 5
20
- UP_RIGHT = 6
21
- DOWN_LEFT = 7
22
- DOWN_RIGHT = 8
23
-
24
- @dataclass(frozen=True, order=True)
25
- class Pos:
26
- x: int
27
- y: int
28
-
29
-
30
- def get_pos(x: int, y: int) -> Pos:
31
- return Pos(x=x, y=y)
32
-
33
-
34
- def get_next_pos(cur_pos: Pos, direction: Union[Direction, Direction8]) -> Pos:
35
- delta_x, delta_y = get_deltas(direction)
36
- return get_pos(cur_pos.x+delta_x, cur_pos.y+delta_y)
37
-
38
-
39
- def get_neighbors4(pos: Pos, V: int, H: int) -> Iterable[Pos]:
40
- for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)):
41
- p2 = get_pos(x=pos.x+dx, y=pos.y+dy)
42
- if in_bounds(p2, V, H):
43
- yield p2
44
-
45
-
46
- def get_neighbors8(pos: Pos, V: int, H: int = None, include_self: bool = False) -> Iterable[Pos]:
47
- if H is None:
48
- H = V
49
- for dx in [-1, 0, 1]:
50
- for dy in [-1, 0, 1]:
51
- if not include_self and (dx, dy) == (0, 0):
52
- continue
53
- d_pos = get_pos(x=pos.x+dx, y=pos.y+dy)
54
- if in_bounds(d_pos, V, H):
55
- yield d_pos
56
-
57
-
58
- def get_row_pos(row_idx: int, H: int) -> Iterable[Pos]:
59
- for x in range(H):
60
- yield get_pos(x=x, y=row_idx)
61
-
62
-
63
- def get_col_pos(col_idx: int, V: int) -> Iterable[Pos]:
64
- for y in range(V):
65
- yield get_pos(x=col_idx, y=y)
66
-
67
-
68
- def get_all_pos(V, H=None):
69
- if H is None:
70
- H = V
71
- for y in range(V):
72
- for x in range(H):
73
- yield get_pos(x=x, y=y)
74
-
75
-
76
- def get_all_pos_to_idx_dict(V, H=None) -> dict[Pos, int]:
77
- if H is None:
78
- H = V
79
- return {get_pos(x=x, y=y): y*H+x for y in range(V) for x in range(H)}
80
-
81
-
82
- def get_char(board: np.array, pos: Pos) -> str:
83
- return board[pos.y][pos.x]
84
-
85
-
86
- def set_char(board: np.array, pos: Pos, char: str):
87
- board[pos.y][pos.x] = char
88
-
89
-
90
- def in_bounds(pos: Pos, V: int, H: int = None) -> bool:
91
- if H is None:
92
- H = V
93
- return 0 <= pos.y < V and 0 <= pos.x < H
94
-
95
-
96
- def get_opposite_direction(direction: Direction) -> Direction:
97
- if direction == Direction.RIGHT:
98
- return Direction.LEFT
99
- elif direction == Direction.LEFT:
100
- return Direction.RIGHT
101
- elif direction == Direction.DOWN:
102
- return Direction.UP
103
- elif direction == Direction.UP:
104
- return Direction.DOWN
105
- else:
106
- raise ValueError(f'invalid direction: {direction}')
107
-
108
-
109
- def get_deltas(direction: Union[Direction, Direction8]) -> Tuple[int, int]:
110
- if direction == Direction.RIGHT or direction == Direction8.RIGHT:
111
- return +1, 0
112
- elif direction == Direction.LEFT or direction == Direction8.LEFT:
113
- return -1, 0
114
- elif direction == Direction.DOWN or direction == Direction8.DOWN:
115
- return 0, +1
116
- elif direction == Direction.UP or direction == Direction8.UP:
117
- return 0, -1
118
- elif direction == Direction8.UP_LEFT:
119
- return -1, -1
120
- elif direction == Direction8.UP_RIGHT:
121
- return +1, -1
122
- elif direction == Direction8.DOWN_LEFT:
123
- return -1, +1
124
- elif direction == Direction8.DOWN_RIGHT:
125
- return +1, +1
126
- else:
127
- raise ValueError(f'invalid direction: {direction}')
1
+ from dataclasses import dataclass
2
+ from typing import Tuple, Iterable, Union
3
+ from enum import Enum
4
+
5
+ import numpy as np
6
+
7
+
8
+ class Direction(Enum):
9
+ UP = 1
10
+ DOWN = 2
11
+ LEFT = 3
12
+ RIGHT = 4
13
+
14
+ class Direction8(Enum):
15
+ UP = 1
16
+ DOWN = 2
17
+ LEFT = 3
18
+ RIGHT = 4
19
+ UP_LEFT = 5
20
+ UP_RIGHT = 6
21
+ DOWN_LEFT = 7
22
+ DOWN_RIGHT = 8
23
+
24
+ @dataclass(frozen=True, order=True)
25
+ class Pos:
26
+ x: int
27
+ y: int
28
+
29
+ def __add__(self, other: 'Pos') -> 'Pos':
30
+ return get_pos(self.x + other.x, self.y + other.y)
31
+
32
+
33
+ Shape = frozenset[Pos] # a shape on the 2d board is just a set of positions
34
+
35
+
36
+ def get_pos(x: int, y: int) -> Pos:
37
+ return Pos(x=x, y=y)
38
+
39
+
40
+ def get_next_pos(cur_pos: Pos, direction: Union[Direction, Direction8]) -> Pos:
41
+ delta_x, delta_y = get_deltas(direction)
42
+ return get_pos(cur_pos.x+delta_x, cur_pos.y+delta_y)
43
+
44
+
45
+ def get_neighbors4(pos: Pos, V: int, H: int) -> Iterable[Pos]:
46
+ for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)):
47
+ p2 = get_pos(x=pos.x+dx, y=pos.y+dy)
48
+ if in_bounds(p2, V, H):
49
+ yield p2
50
+
51
+
52
+ def get_neighbors8(pos: Pos, V: int, H: int = None, include_self: bool = False) -> Iterable[Pos]:
53
+ if H is None:
54
+ H = V
55
+ for dx in [-1, 0, 1]:
56
+ for dy in [-1, 0, 1]:
57
+ if not include_self and (dx, dy) == (0, 0):
58
+ continue
59
+ d_pos = get_pos(x=pos.x+dx, y=pos.y+dy)
60
+ if in_bounds(d_pos, V, H):
61
+ yield d_pos
62
+
63
+
64
+ def get_row_pos(row_idx: int, H: int) -> Iterable[Pos]:
65
+ for x in range(H):
66
+ yield get_pos(x=x, y=row_idx)
67
+
68
+
69
+ def get_col_pos(col_idx: int, V: int) -> Iterable[Pos]:
70
+ for y in range(V):
71
+ yield get_pos(x=col_idx, y=y)
72
+
73
+
74
+ def get_all_pos(V, H=None):
75
+ if H is None:
76
+ H = V
77
+ for y in range(V):
78
+ for x in range(H):
79
+ yield get_pos(x=x, y=y)
80
+
81
+
82
+ def get_all_pos_to_idx_dict(V, H=None) -> dict[Pos, int]:
83
+ if H is None:
84
+ H = V
85
+ return {get_pos(x=x, y=y): y*H+x for y in range(V) for x in range(H)}
86
+
87
+
88
+ def get_char(board: np.array, pos: Pos) -> str:
89
+ return board[pos.y][pos.x]
90
+
91
+
92
+ def set_char(board: np.array, pos: Pos, char: str):
93
+ board[pos.y][pos.x] = char
94
+
95
+
96
+ def in_bounds(pos: Pos, V: int, H: int = None) -> bool:
97
+ if H is None:
98
+ H = V
99
+ return 0 <= pos.y < V and 0 <= pos.x < H
100
+
101
+
102
+ def get_opposite_direction(direction: Direction) -> Direction:
103
+ if direction == Direction.RIGHT:
104
+ return Direction.LEFT
105
+ elif direction == Direction.LEFT:
106
+ return Direction.RIGHT
107
+ elif direction == Direction.DOWN:
108
+ return Direction.UP
109
+ elif direction == Direction.UP:
110
+ return Direction.DOWN
111
+ else:
112
+ raise ValueError(f'invalid direction: {direction}')
113
+
114
+
115
+ def get_deltas(direction: Union[Direction, Direction8]) -> Tuple[int, int]:
116
+ if direction == Direction.RIGHT or direction == Direction8.RIGHT:
117
+ return +1, 0
118
+ elif direction == Direction.LEFT or direction == Direction8.LEFT:
119
+ return -1, 0
120
+ elif direction == Direction.DOWN or direction == Direction8.DOWN:
121
+ return 0, +1
122
+ elif direction == Direction.UP or direction == Direction8.UP:
123
+ return 0, -1
124
+ elif direction == Direction8.UP_LEFT:
125
+ return -1, -1
126
+ elif direction == Direction8.UP_RIGHT:
127
+ return +1, -1
128
+ elif direction == Direction8.DOWN_LEFT:
129
+ return -1, +1
130
+ elif direction == Direction8.DOWN_RIGHT:
131
+ return +1, +1
132
+ else:
133
+ raise ValueError(f'invalid direction: {direction}')
134
+
135
+
136
+ def polyominoes(N):
137
+ """Generate all polyominoes of size N. Every rotation and reflection is considered different and included in the result.
138
+ Translation is not considered different and is removed from the result (otherwise the result would be infinite).
139
+
140
+ Below is the number of unique polyominoes of size N (not including rotations and reflections) and the lenth of the returned result (which includes all rotations and reflections)
141
+ N name #shapes #results
142
+ 1 monomino 1 1
143
+ 2 domino 1 2
144
+ 3 tromino 2 6
145
+ 4 tetromino 5 19
146
+ 5 pentomino 12 63
147
+ 6 hexomino 35 216
148
+ 7 heptomino 108 760
149
+ 8 octomino 369 2,725
150
+ 9 nonomino 1,285 9,910
151
+ 10 decomino 4,655 36,446
152
+ 11 undecomino 17,073 135,268
153
+ 12 dodecomino 63,600 505,861
154
+ Source: https://en.wikipedia.org/wiki/Polyomino
155
+
156
+ Args:
157
+ N (int): The size of the polyominoes to generate.
158
+
159
+ Returns:
160
+ set[(frozenset[Pos], int)]: A set of all polyominoes of size N (rotated and reflected up to D4 symmetry).
161
+ """
162
+ assert N >= 1, 'N cannot be less than 1'
163
+ # need a frozenset because regular sets are not hashable
164
+ FastShape = frozenset[Tuple[int, int]]
165
+ shapes: set[FastShape] = {frozenset({(0, 0)})}
166
+ for i in range(1, N):
167
+ next_shapes: set[FastShape] = set()
168
+ directions = ((1,0),(-1,0),(0,1)) if i > 1 else (((1,0),(0,1))) # cannot take left on first step, if confused read: https://louridas.github.io/rwa/assignments/polyominoes/
169
+ for s in shapes:
170
+ # frontier of a single shape: all 4-neighbors of existing cells not already in the shape
171
+ frontier = set()
172
+ for x, y in s:
173
+ # only need to consider 3 directions and neighbors condition is (n.y > 0 or (n.y == 0 and n.x >= 0)) it's obvious if you plot it
174
+ # if confused read: https://louridas.github.io/rwa/assignments/polyominoes/
175
+ for dx, dy in directions:
176
+ n = (x + dx, y + dy)
177
+ if n not in s and (n[1] > 0 or (n[1] == 0 and n[0] >= 0)):
178
+ frontier.add(n)
179
+ for cell in frontier:
180
+ t = s | {cell}
181
+ # normalize by translation only: shift so min x,y is (0,0). This removes translational symmetries.
182
+ minx = min(x for x, y in t)
183
+ miny = min(y for x, y in t)
184
+ t0 = frozenset((x - minx, y - miny) for x, y in t)
185
+ next_shapes.add(t0)
186
+ shapes = next_shapes
187
+ # shapes is now complete, now classify up to D4 symmetry (rotations/reflections), translations ignored
188
+ shapes = {frozenset(Pos(x, y) for x, y in s) for s in shapes} # regular class, not the dirty-fast one
189
+ return shapes
190
+
191
+ def polyominoes_with_shape_id(N):
192
+ """Refer to polyominoes() for more details. This function returns a set of all polyominoes of size N (rotated and reflected up to D4 symmetry) along with a unique ID for each polyomino that is unique up to D4 symmetry.
193
+ Args:
194
+ N (int): The size of the polyominoes to generate.
195
+
196
+ Returns:
197
+ set[(frozenset[Pos], int)]: A set of all polyominoes of size N (rotated and reflected up to D4 symmetry) along with a unique ID for each polyomino that is unique up to D4 symmetry.
198
+ """
199
+ FastPos = Tuple[int, int]
200
+ FastShape = frozenset[Tuple[int, int]]
201
+ shapes = polyominoes(N)
202
+ shapes = {frozenset((p.x, p.y) for p in s) for s in shapes}
203
+ mats = (
204
+ ( 1, 0, 0, 1), # regular
205
+ (-1, 0, 0, 1), # reflect about x
206
+ ( 1, 0, 0,-1), # reflect about y
207
+ (-1, 0, 0,-1), # reflect about x and y
208
+ # trnaspose then all 4 above
209
+ ( 0, 1, 1, 0), ( 0, 1, -1, 0), ( 0,-1, 1, 0), ( 0,-1, -1, 0),
210
+ )
211
+ # compute canonical representative for each shape (lexicographically smallest normalized transform)
212
+ shape_to_canon: dict[FastShape, tuple[FastPos, ...]] = {}
213
+ for s in shapes:
214
+ reps: list[tuple[FastPos, ...]] = []
215
+ for a, b, c, d in mats:
216
+ pts = {(a*x + b*y, c*x + d*y) for x, y in s}
217
+ minx = min(x for x, y in pts)
218
+ miny = min(y for x, y in pts)
219
+ rep = tuple(sorted((x - minx, y - miny) for x, y in pts))
220
+ reps.append(rep)
221
+ canon = min(reps)
222
+ shape_to_canon[s] = canon
223
+
224
+ canon_set = set(shape_to_canon.values())
225
+ canon_to_id = {canon: i for i, canon in enumerate(sorted(canon_set))}
226
+ result = {(s, canon_to_id[shape_to_canon[s]]) for s in shapes}
227
+ result = {(frozenset(Pos(x, y) for x, y in s), _id) for s, _id in result}
228
+ return result