multi-puzzle-solver 1.0.9__py3-none-any.whl → 1.0.10__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,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-puzzle-solver
3
- Version: 1.0.9
4
- Summary: Efficient solvers for countless (50+) types of puzzles (like Sudoku, Minesweeper, etc.) with a simple python API.
3
+ Version: 1.0.10
4
+ Summary: Efficient solvers for countless (60+) types of puzzles (like Sudoku, Minesweeper, etc.) with a simple python API.
5
5
  Author: Ar-Kareem
6
6
  Project-URL: Homepage, https://github.com/Ar-Kareem/puzzle_solver
7
7
  Project-URL: Repository, https://github.com/Ar-Kareem/puzzle_solver
@@ -26,7 +26,7 @@ Requires-Dist: pytest-xdist; extra == "dev"
26
26
 
27
27
  # Python Puzzle Solver
28
28
 
29
- Solve countless (50+) classical logic puzzles automatically in Python.
29
+ Solve countless (60+) classical logic puzzles automatically in Python.
30
30
 
31
31
  ## Quick Start
32
32
 
@@ -73,7 +73,7 @@ Time taken: 0.04 seconds
73
73
 
74
74
  ## Introduction
75
75
 
76
- The aim of this repo is to provide very efficient solvers (i.e. not brute force solvers) for countless (50+) popular pencil logic puzzles like Nonograms, Sudoku, Minesweeper, and many more lesser known ones.
76
+ The aim of this repo is to provide very efficient solvers (i.e. not brute force solvers) for countless (60+) popular pencil logic puzzles like Nonograms, Sudoku, Minesweeper, and many more lesser known ones.
77
77
 
78
78
  If you happen to have a puzzle similar to the ones listed below and want to solve it (or see how many potential solutions a partially covered board has), then this repo is perfect for you.
79
79
 
@@ -417,6 +417,18 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
417
417
  <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/puzzles/mathema_grids_solved.png" alt="Mathema Grids" width="140">
418
418
  </a>
419
419
  </td>
420
+ <td align="center">
421
+ <a href="#split-ends-puzzle-type-60"><b>N-Queens</b><br><br>
422
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/puzzles/7_queens_solved.png" alt="N-Queens" width="140">
423
+ </a>
424
+ </td>
425
+ </tr>
426
+ <tr>
427
+ <td align="center">
428
+ <a href="#split-ends-puzzle-type-61"><b>Split Ends</b><br><br>
429
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/puzzles/split_ends_solved.png" alt="Split Ends" width="140">
430
+ </a>
431
+ </td>
420
432
  </tr>
421
433
  </table>
422
434
 
@@ -491,6 +503,8 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
491
503
  - [Nonograms Colored (Puzzle Type #57)](#nonograms-colored-puzzle-type-57)
492
504
  - [ABC View (Puzzle Type #58)](#abc-view-puzzle-type-58)
493
505
  - [Mathema Grids (Puzzle Type #59)](#mathema-grids-puzzle-type-59)
506
+ - [Split Ends (Puzzle Type #61)](#split-ends-puzzle-type-61)
507
+ - [N-Queens (Puzzle Type #60)](#n-queens-puzzle-type-60)
494
508
  - [Why SAT / CP-SAT?](#why-sat--cp-sat)
495
509
  - [Testing](#testing)
496
510
  - [Contributing](#contributing)
@@ -4399,19 +4413,30 @@ solutions = binst.solve_and_print()
4399
4413
 
4400
4414
  **Script Output**
4401
4415
 
4402
- The output tells you which squares to tap to solve the puzzle.
4416
+ The output tells you which squares to tap to solve the puzzle, the shaded squares are the ones that need to be tapped.
4403
4417
 
4404
4418
  ```python
4405
4419
  Solution found
4406
- [['T' ' ' 'T' 'T' 'T' ' ' ' ']
4407
- [' ' ' ' ' ' 'T' ' ' 'T' ' ']
4408
- [' ' 'T' ' ' ' ' 'T' ' ' ' ']
4409
- ['T' ' ' 'T' ' ' ' ' 'T' ' ']
4410
- [' ' ' ' ' ' 'T' ' ' ' ' 'T']
4411
- ['T' ' ' 'T' ' ' 'T' 'T' 'T']
4412
- [' ' ' ' ' ' ' ' ' ' 'T' 'T']]
4420
+
4421
+ 0 1 2 3 4 5 6
4422
+ ┌───┬───┬───┬───┬───┬───┬───┐
4423
+ 0│▒▒▒│ │▒▒▒│▒▒▒│▒▒▒│ │ │
4424
+ ├───┼───┼───┼───┼───┼───┼───┤
4425
+ 1│ │ │ │▒▒▒│ │▒▒▒│ │
4426
+ ├───┼───┼───┼───┼───┼───┼───┤
4427
+ 2│ │▒▒▒│ │ │▒▒▒│ │ │
4428
+ ├───┼───┼───┼───┼───┼───┼───┤
4429
+ 3│▒▒▒│ │▒▒▒│ │ │▒▒▒│ │
4430
+ ├───┼───┼───┼───┼───┼───┼───┤
4431
+ 4│ │ │ │▒▒▒│ │ │▒▒▒│
4432
+ ├───┼───┼───┼───┼───┼───┼───┤
4433
+ 5│▒▒▒│ │▒▒▒│ │▒▒▒│▒▒▒│▒▒▒│
4434
+ ├───┼───┼───┼───┼───┼───┼───┤
4435
+ 6│ │ │ │ │ │▒▒▒│▒▒▒│
4436
+ └───┴───┴───┴───┴───┴───┴───┘
4413
4437
  Solutions found: 1
4414
4438
  status: OPTIMAL
4439
+ Time taken: 0.01 seconds
4415
4440
  ```
4416
4441
 
4417
4442
  **Solved puzzle**
@@ -5946,6 +5971,142 @@ Time taken: 0.00 seconds
5946
5971
 
5947
5972
  ---
5948
5973
 
5974
+ ## Split Ends (Puzzle Type #61)
5975
+
5976
+ * [**Play online**](https://krazydad.com/play/splitends/)
5977
+
5978
+ * [**Solver Code**](https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/split_ends)
5979
+
5980
+ <details>
5981
+ <summary><strong>Rules</strong></summary>
5982
+
5983
+ Each row and column contains four unique Y shapes (four different orientations) and two Os. Ys should not form straight lines by touching other Ys.
5984
+
5985
+ </details>
5986
+
5987
+ **Unsolved puzzle**
5988
+
5989
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/puzzles/split_ends_unsolved.png" alt="Split Ends unsolved" width="500">
5990
+
5991
+ Code to utilize this package and solve the puzzle:
5992
+
5993
+ ```python
5994
+ import numpy as np
5995
+ from puzzle_solver import split_ends_solver as solver
5996
+ board = np.array([
5997
+ ['O', ' ', 'O', 'L', ' ', 'U'],
5998
+ [' ', ' ', ' ', ' ', ' ', ' '],
5999
+ [' ', 'R', ' ', ' ', 'O', ' '],
6000
+ [' ', 'O', ' ', ' ', 'L', ' '],
6001
+ [' ', ' ', ' ', ' ', ' ', ' '],
6002
+ ['U', ' ', 'L', 'D', ' ', 'R'],
6003
+ ])
6004
+ binst = solver.Board(board=board)
6005
+ solutions = binst.solve_and_print()
6006
+ ```
6007
+
6008
+ **Script Output**
6009
+
6010
+ ```python
6011
+ Solution found
6012
+
6013
+ 0 1 2 3 4 5
6014
+ ┌───┬───┬───┬───┬───┬───┐
6015
+ 0│ O │ D │ O │ L │ R │ U │
6016
+ ├───┼───┼───┼───┼───┼───┤
6017
+ 1│ O │ L │ D │ R │ U │ O │
6018
+ ├───┼───┼───┼───┼───┼───┤
6019
+ 2│ D │ R │ O │ U │ O │ L │
6020
+ ├───┼───┼───┼───┼───┼───┤
6021
+ 3│ R │ O │ U │ O │ L │ D │
6022
+ ├───┼───┼───┼───┼───┼───┤
6023
+ 4│ L │ U │ R │ O │ D │ O │
6024
+ ├───┼───┼───┼───┼───┼───┤
6025
+ 5│ U │ O │ L │ D │ O │ R │
6026
+ └───┴───┴───┴───┴───┴───┘
6027
+ Solutions found: 1
6028
+ status: OPTIMAL
6029
+ Time taken: 0.01 seconds
6030
+ ```
6031
+
6032
+ **Solved puzzle**
6033
+
6034
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/puzzles/split_ends_solved.png" alt="Split Ends solved" width="500">
6035
+
6036
+ ---
6037
+
6038
+ ## N-Queens (Puzzle Type #60)
6039
+
6040
+ Can also solve puzzles such as 7-Queens.
6041
+
6042
+ * [**Play online**](https://krazydad.com/play/queens/)
6043
+
6044
+ * [**Solver Code**](https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/n_queens)
6045
+
6046
+ <details>
6047
+ <summary><strong>Rules</strong></summary>
6048
+
6049
+ 7-Queens Variant: Within each of the seven realms lives a lone queen. To maintain the peace, queens must not threaten each other: no row, column, diagonal, nor region may have more than one queen!
6050
+
6051
+ </details>
6052
+
6053
+ **Unsolved puzzle**
6054
+
6055
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/puzzles/7_queens_unsolved.png" alt="7 Queens unsolved" width="500">
6056
+
6057
+ Code to utilize this package and solve the puzzle:
6058
+
6059
+ ```python
6060
+ import numpy as np
6061
+ from puzzle_solver import n_queens_solver as solver
6062
+ board = np.array([
6063
+ ['00', '00', '00', '00', '01', '01', '02', '02'],
6064
+ ['00', '00', '03', '03', '01', '01', '02', '04'],
6065
+ ['00', '00', '03', '03', '01', '01', '01', '04'],
6066
+ ['03', '03', '03', '03', '01', '01', '01', '05'],
6067
+ ['03', '03', '03', '03', '01', '01', '01', '05'],
6068
+ ['03', '03', '06', '06', '06', '05', '05', '05'],
6069
+ ['06', '06', '06', '06', '06', '06', '05', '05'],
6070
+ ['06', '06', '06', '06', '06', '06', '05', '05']
6071
+ ])
6072
+ binst = solver.Board(board=board)
6073
+ solutions = binst.solve_and_print()
6074
+ ```
6075
+
6076
+ **Script Output**
6077
+
6078
+ ```python
6079
+ Solution found
6080
+
6081
+ 0 1 2 3 4 5 6 7
6082
+ ┌───┬───┬───┬───┬───┬───┬───┬───┐
6083
+ 0│ │ │ │ │ │ │▒▒▒│ │
6084
+ ├───┼───┼───┼───┼───┼───┼───┼───┤
6085
+ 1│▒▒▒│ │ │ │ │ │ │ │
6086
+ ├───┼───┼───┼───┼───┼───┼───┼───┤
6087
+ 2│ │ │ │ │ │ │ │▒▒▒│
6088
+ ├───┼───┼───┼───┼───┼───┼───┼───┤
6089
+ 3│ │ │ │ │▒▒▒│ │ │ │
6090
+ ├───┼───┼───┼───┼───┼───┼───┼───┤
6091
+ 4│ │▒▒▒│ │ │ │ │ │ │
6092
+ ├───┼───┼───┼───┼───┼───┼───┼───┤
6093
+ 5│ │ │ │ │ │▒▒▒│ │ │
6094
+ ├───┼───┼───┼───┼───┼───┼───┼───┤
6095
+ 6│ │ │▒▒▒│ │ │ │ │ │
6096
+ ├───┼───┼───┼───┼───┼───┼───┼───┤
6097
+ 7│ │ │ │ │ │ │ │ │
6098
+ └───┴───┴───┴───┴───┴───┴───┴───┘
6099
+ Solutions found: 1
6100
+ status: OPTIMAL
6101
+ Time taken: 0.00 seconds
6102
+ ```
6103
+
6104
+ **Solved puzzle**
6105
+
6106
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/puzzles/7_queens_solved.png" alt="7 Queens solved" width="500">
6107
+
6108
+ ---
6109
+
5949
6110
  ---
5950
6111
 
5951
6112
  ## Why SAT / CP-SAT?
@@ -1,4 +1,4 @@
1
- puzzle_solver/__init__.py,sha256=mCXXxGttWhDOTXSDFI2CrplOLmSFVF9r_XygisFqiAY,5502
1
+ puzzle_solver/__init__.py,sha256=TUXr71rkfWFDtF44XR1ophXB_U8dKFqrWbyLglbWI6c,5695
2
2
  puzzle_solver/core/utils.py,sha256=FE0106dfQRsgCn2FRBvRq5zILLK7-Z3cPHkAlBWUX0w,8785
3
3
  puzzle_solver/core/utils_ortools.py,sha256=ACV3HgKWpEUTt1lpqsPryK1DeZpu7kdWQKEWTLJ2tfs,10384
4
4
  puzzle_solver/core/utils_visualizer.py,sha256=3EJ7V8rHyasj1peAzplDJfKkPy6Yj9j7BXqMBWQ3eNg,22834
@@ -16,7 +16,7 @@ puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B
16
16
  puzzle_solver/puzzles/connect_the_dots/connect_the_dots.py,sha256=0axF_jtB34mUM6s3j4grjkIWTCjvaw4NZ4oVogZsNvs,2677
17
17
  puzzle_solver/puzzles/dominosa/dominosa.py,sha256=Nmb7pn8U27QJwGy9F3wo8ylqo2_U51OAo3GN2soaNpc,7195
18
18
  puzzle_solver/puzzles/filling/filling.py,sha256=ILgGJqVI7yd6HPvWeKEsr620qorxLtAQYKTq65PqarY,4952
19
- puzzle_solver/puzzles/flip/flip.py,sha256=ZngJLUhRNc7qqo2wtNLdMPx4u9w9JTUge27PmdXyDCw,3985
19
+ puzzle_solver/puzzles/flip/flip.py,sha256=cei6irTfntqctaLS3-rdYu8M2tw7ibzlHHvTIHW18yo,3518
20
20
  puzzle_solver/puzzles/flood_it/flood_it.py,sha256=jnCtH1sZIt6K4hbQDSsiM1Cd8FjQNP7cfw2ObUW5fEQ,7948
21
21
  puzzle_solver/puzzles/flood_it/parse_map/parse_map.py,sha256=m7gcpvN3THZdYLowdR_Jwx3HyttaV4K2DqrX_U7uFqU,8209
22
22
  puzzle_solver/puzzles/galaxies/galaxies.py,sha256=IiKPU3fz5Aokhj5OjeT5jd_vdNuWnLzjZylGOTepsNU,5600
@@ -36,6 +36,7 @@ puzzle_solver/puzzles/map/map.py,sha256=sxc57tapB8Tsgam-yoDitln1o-EB_SbIYvO6WEYy
36
36
  puzzle_solver/puzzles/mathema_grids/mathema_grids.py,sha256=wXj3pfXUMh3deFA6XXndZXod6ZNyCVt9vX1akt9zz20,6380
37
37
  puzzle_solver/puzzles/minesweeper/minesweeper.py,sha256=gSdFsuZ-KrwVxgI1HF2q_pYleZ6vBm9jjRTFlboVnLY,5871
38
38
  puzzle_solver/puzzles/mosaic/mosaic.py,sha256=T89tkyTbob3LT20vwY3hkkEtNi8bxp2_CLaVi1gzhBc,1974
39
+ puzzle_solver/puzzles/n_queens/n_queens.py,sha256=QTZwAwtqkijYKRQ4A6r1KUUOKkxPBcnzdSfMe-x8lc4,4425
39
40
  puzzle_solver/puzzles/nonograms/nonograms.py,sha256=Q-VHI0IPR2igccnE617HPThj5tnBgt27MiLWIZPtYcI,5587
40
41
  puzzle_solver/puzzles/nonograms/nonograms_colored.py,sha256=XpxzpJw0GA-tE7PiltlA-dfypaTvqNIDLnKl1LxIjD4,10500
41
42
  puzzle_solver/puzzles/norinori/norinori.py,sha256=ZEDWrD7zvEuqXOdXGOrELh1n_mWzhzZa3chs6Zqd3Pc,4570
@@ -52,10 +53,11 @@ puzzle_solver/puzzles/singles/singles.py,sha256=AugO2Gnd_OEyrxXUnqg3oPypdmeiFais
52
53
  puzzle_solver/puzzles/slant/slant.py,sha256=l4q9g0BfqQsA6zsySiemJC5iFsqsO6LoqTUqcTEqYEk,5897
53
54
  puzzle_solver/puzzles/slant/parse_map/parse_map.py,sha256=8thQxWbq0qjehKb2VzgUP22PGj-9n9djwbt3LGMVLJw,4811
54
55
  puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=AQsOMHB_vP4bDHbhq5hTRRblv01DecdEzy6F4k23OA0,7118
56
+ puzzle_solver/puzzles/split_ends/split_ends.py,sha256=7z0u1CMsdLwBslVwm-cvq_8xJ31tBIvTSvENlMYgVUA,5764
55
57
  puzzle_solver/puzzles/star_battle/star_battle.py,sha256=GqrattaxVM7eHtPQYUWHAlLG8DQkDm7p1B3R8VeU9g8,4018
56
58
  puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
57
59
  puzzle_solver/puzzles/stitches/stitches.py,sha256=wj9z9979FaVtp2hcVu3YDTGlxtwljoQNBgMMM5U3icc,6109
58
- puzzle_solver/puzzles/stitches/parse_map/parse_map.py,sha256=b21SQvlnDM6wOl_1iUhZ7X6akpBZoOnj3kEzImBCh8Q,10497
60
+ puzzle_solver/puzzles/stitches/parse_map/parse_map.py,sha256=TPpOR0QZHTtjqJ7MhjHJWlxCVVXLANyipIVWn2ZWzgA,11445
59
61
  puzzle_solver/puzzles/sudoku/sudoku.py,sha256=lj3DD98sdA9jNre8Nwe8s_OKpYe7K4iPCmXtJMKfIDs,13344
60
62
  puzzle_solver/puzzles/tapa/tapa.py,sha256=554Xun39M3oJ5kOUwrhLUtbUXbsAYj4DH-GBhtJbjoY,5439
61
63
  puzzle_solver/puzzles/tents/tents.py,sha256=81hAtmNNYIgzh0tEa0BuExmdzQfGVrlwjn_Dw8hEp3c,4943
@@ -69,7 +71,7 @@ puzzle_solver/puzzles/unruly/unruly.py,sha256=_C6FhYm9rqwhlQa6TMTxYr3rWcP_QS-E93
69
71
  puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=D0JacUdK5yPrfScmGqX-p8144VbwxfDgIaqF8hwLXlM,5039
70
72
  puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
71
73
  puzzle_solver/utils/visualizer.py,sha256=T2g5We9J3tkhyXWoN2OrIDIJDjt6w5sDd2ksOub0ZI8,6819
72
- multi_puzzle_solver-1.0.9.dist-info/METADATA,sha256=DsMyTWC0gI51MbgX6UecmA6jdP_pbGE-RLrHCK9Cyhk,456299
73
- multi_puzzle_solver-1.0.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
74
- multi_puzzle_solver-1.0.9.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
75
- multi_puzzle_solver-1.0.9.dist-info/RECORD,,
74
+ multi_puzzle_solver-1.0.10.dist-info/METADATA,sha256=54KCpQTehbNVilhW5T1BcrbCCN6WPL0kntbPpTe6l7c,463353
75
+ multi_puzzle_solver-1.0.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
76
+ multi_puzzle_solver-1.0.10.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
77
+ multi_puzzle_solver-1.0.10.dist-info/RECORD,,
puzzle_solver/__init__.py CHANGED
@@ -26,6 +26,7 @@ from puzzle_solver.puzzles.map import map as map_solver
26
26
  from puzzle_solver.puzzles.mathema_grids import mathema_grids as mathema_grids_solver
27
27
  from puzzle_solver.puzzles.minesweeper import minesweeper as minesweeper_solver
28
28
  from puzzle_solver.puzzles.mosaic import mosaic as mosaic_solver
29
+ from puzzle_solver.puzzles.n_queens import n_queens as n_queens_solver
29
30
  from puzzle_solver.puzzles.nonograms import nonograms as nonograms_solver
30
31
  from puzzle_solver.puzzles.nonograms import nonograms_colored as nonograms_colored_solver
31
32
  from puzzle_solver.puzzles.norinori import norinori as norinori_solver
@@ -42,6 +43,7 @@ from puzzle_solver.puzzles.signpost import signpost as signpost_solver
42
43
  from puzzle_solver.puzzles.singles import singles as singles_solver
43
44
  from puzzle_solver.puzzles.slant import slant as slant_solver
44
45
  from puzzle_solver.puzzles.slitherlink import slitherlink as slitherlink_solver
46
+ from puzzle_solver.puzzles.split_ends import split_ends as split_ends_solver
45
47
  from puzzle_solver.puzzles.star_battle import star_battle as star_battle_solver
46
48
  from puzzle_solver.puzzles.star_battle import star_battle_shapeless as star_battle_shapeless_solver
47
49
  from puzzle_solver.puzzles.stitches import stitches as stitches_solver
@@ -88,6 +90,7 @@ __all__ = [
88
90
  mathema_grids_solver,
89
91
  minesweeper_solver,
90
92
  mosaic_solver,
93
+ n_queens_solver,
91
94
  nonograms_solver,
92
95
  norinori_solver,
93
96
  nonograms_colored_solver,
@@ -104,6 +107,7 @@ __all__ = [
104
107
  singles_solver,
105
108
  slant_solver,
106
109
  slitherlink_solver,
110
+ split_ends_solver,
107
111
  star_battle_solver,
108
112
  star_battle_shapeless_solver,
109
113
  stitches_solver,
@@ -121,4 +125,4 @@ __all__ = [
121
125
  inertia_image_parser,
122
126
  ]
123
127
 
124
- __version__ = '1.0.9'
128
+ __version__ = '1.0.10'
@@ -3,8 +3,18 @@ from typing import Any, Optional
3
3
  import numpy as np
4
4
  from ortools.sat.python import cp_model
5
5
 
6
- from puzzle_solver.core.utils import Pos, get_all_pos, get_neighbors4, set_char, get_char, Direction, get_next_pos
6
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_neighbors4, get_char, Direction, get_next_pos, get_pos
7
7
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
8
+ from puzzle_solver.core.utils_visualizer import combined_function
9
+
10
+
11
+ def _to_pos(pos: Pos, s: str) -> Pos:
12
+ d = {'L': Direction.LEFT, 'R': Direction.RIGHT, 'U': Direction.UP, 'D': Direction.DOWN}[s[0]]
13
+ r = get_next_pos(pos, d)
14
+ if len(s) == 1:
15
+ return r
16
+ else:
17
+ return _to_pos(r, s[1:])
8
18
 
9
19
 
10
20
  class Board:
@@ -13,7 +23,6 @@ class Board:
13
23
  assert all((c.item() in ['B', 'W']) for c in np.nditer(board)), 'board must contain only B or W'
14
24
  self.board = board
15
25
  self.V, self.H = board.shape
16
-
17
26
  if random_mapping is None:
18
27
  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
28
  else:
@@ -21,13 +30,6 @@ class Board:
21
30
  if isinstance(mapping_value, (set, list, tuple)) and isinstance(list(mapping_value)[0], Pos):
22
31
  self.tap_mapping: dict[Pos, set[Pos]] = {pos: set(random_mapping[pos]) for pos in get_all_pos(self.V, self.H)}
23
32
  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
33
  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
34
  else:
33
35
  raise ValueError(f'invalid random_mapping: {random_mapping}')
@@ -35,10 +37,8 @@ class Board:
35
37
  if k not in v:
36
38
  v.add(k)
37
39
 
38
-
39
40
  self.model = cp_model.CpModel()
40
41
  self.model_vars: dict[Pos, cp_model.IntVar] = {}
41
-
42
42
  self.create_vars()
43
43
  self.add_all_constraints()
44
44
 
@@ -49,29 +49,16 @@ class Board:
49
49
  def add_all_constraints(self):
50
50
  for pos in get_all_pos(self.V, self.H):
51
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
52
  pos_that_will_turn_me = [k for k,v in self.tap_mapping.items() if pos in v]
54
53
  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
54
+ if get_char(self.board, pos) == 'W': # if started as white then needs an even number of taps while xor checks for odd number
56
55
  literals.append(self.model.NewConstant(True))
57
- elif c == 'B':
58
- pass
59
- else:
60
- raise ValueError(f'invalid character: {c}')
61
56
  self.model.AddBoolXOr(literals)
62
57
 
63
58
  def solve_and_print(self, verbose: bool = True):
64
59
  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)
60
+ return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
69
61
  def callback(single_res: SingleSolution):
70
62
  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)
63
+ print(combined_function(self.V, self.H, is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1))
77
64
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -0,0 +1,81 @@
1
+ from collections import defaultdict
2
+ from typing import Optional
3
+
4
+ import numpy as np
5
+ from ortools.sat.python import cp_model
6
+ from ortools.sat.python.cp_model import LinearExpr as lxp
7
+
8
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_pos, get_row_pos, get_col_pos, Direction8, in_bounds, get_next_pos
9
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
10
+ from puzzle_solver.core.utils_visualizer import combined_function
11
+
12
+
13
+ def get_ray(pos: Pos, direction: Direction8, V: int, H: int) -> list[Pos]:
14
+ out = []
15
+ while True:
16
+ out.append(pos)
17
+ pos = get_next_pos(pos, direction)
18
+ if not in_bounds(pos, V, H):
19
+ break
20
+ return out
21
+
22
+
23
+ class Board:
24
+ def __init__(self, board: np.array, id_to_count: Optional[dict[int, int]] = None):
25
+ """
26
+ board is a 2d array of location ids
27
+ id_to_count is a dict of int to int, where the key is the id of the location and the value is the count of the queens on that location
28
+ """
29
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
30
+ assert all(str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only digits'
31
+ assert id_to_count is None or (isinstance(id_to_count, dict) and all(isinstance(k, int) and isinstance(v, int) for k, v in id_to_count.items())), 'id_to_count must be a dict of int to int'
32
+ self.board = board
33
+ self.V, self.H = board.shape
34
+ self.location_ids = set([int(c.item()) for c in np.nditer(board)])
35
+ self.location_ids_to_pos: dict[int, set[Pos]] = defaultdict(set)
36
+ for pos in get_all_pos(self.V, self.H):
37
+ self.location_ids_to_pos[int(get_char(self.board, pos))].add(pos)
38
+ self.id_to_count = id_to_count
39
+ if self.id_to_count is None:
40
+ self.id_to_count = {id_: 1 for id_ in self.location_ids}
41
+
42
+ self.model = cp_model.CpModel()
43
+ self.model_vars: dict[Pos, cp_model.IntVar] = {}
44
+ self.create_vars()
45
+ self.add_all_constraints()
46
+
47
+ def create_vars(self):
48
+ for pos in get_all_pos(self.V, self.H):
49
+ self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
50
+
51
+ def add_all_constraints(self):
52
+ # every row has at most one queen
53
+ for row in range(self.V):
54
+ self.model.Add(lxp.Sum([self.model_vars[pos] for pos in get_row_pos(row, self.H)]) <= 1)
55
+ # every column has at most one queen
56
+ for col in range(self.H):
57
+ self.model.Add(lxp.Sum([self.model_vars[pos] for pos in get_col_pos(col, self.V)]) <= 1)
58
+ # every diagonal has at most one queen
59
+ for pos in get_col_pos(0, self.V): # down-right diagonal on left border
60
+ ray = get_ray(pos, Direction8.DOWN_RIGHT, self.V, self.H)
61
+ self.model.Add(lxp.Sum([self.model_vars[pos] for pos in ray]) <= 1)
62
+ for pos in get_row_pos(0, self.H): # down-right diagonal on top border
63
+ ray = get_ray(pos, Direction8.DOWN_RIGHT, self.V, self.H)
64
+ self.model.Add(lxp.Sum([self.model_vars[pos] for pos in ray]) <= 1)
65
+ for pos in get_row_pos(0, self.H): # down-left diagonal on top
66
+ ray = get_ray(pos, Direction8.DOWN_LEFT, self.V, self.H)
67
+ self.model.Add(lxp.Sum([self.model_vars[pos] for pos in ray]) <= 1)
68
+ for pos in get_col_pos(self.H - 1, self.V): # down-left diagonal on right border
69
+ ray = get_ray(pos, Direction8.DOWN_LEFT, self.V, self.H)
70
+ self.model.Add(lxp.Sum([self.model_vars[pos] for pos in ray]) <= 1)
71
+ # every id has at most count queens
72
+ for id_ in self.location_ids:
73
+ self.model.Add(lxp.Sum([self.model_vars[pos] for pos in self.location_ids_to_pos[id_]]) == self.id_to_count[id_])
74
+
75
+ def solve_and_print(self, verbose: bool = True):
76
+ def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
77
+ return SingleSolution(assignment={pos: solver.Value(var) for pos, var in board.model_vars.items()})
78
+ def callback(single_res: SingleSolution):
79
+ print("Solution found")
80
+ print(combined_function(self.V, self.H, is_shaded=lambda r, c: single_res.assignment[get_pos(x=c, y=r)] == 1))
81
+ return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -0,0 +1,94 @@
1
+ import numpy as np
2
+ from ortools.sat.python import cp_model
3
+ from ortools.sat.python.cp_model import LinearExpr as lxp
4
+
5
+ from puzzle_solver.core.utils import Direction8, Pos, get_all_pos, get_char, get_neighbors8, get_pos, get_row_pos, get_col_pos, Direction, get_opposite_direction, get_next_pos, in_bounds
6
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
7
+ from puzzle_solver.core.utils_visualizer import combined_function
8
+
9
+
10
+ class Board:
11
+ def __init__(self, board: np.array):
12
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
13
+ assert all(c.item() in ['L', 'R', 'U', 'D', 'O', ' '] for c in np.nditer(board)), 'board must contain only L, R, U, D, O, or space'
14
+ self.STATES = list(Direction) + ['O']
15
+ self.board = board
16
+ self.V, self.H = board.shape
17
+ assert self.V == 6 and self.H == 6, f'board must be 6x6, got {self.V}x{self.H}'
18
+ self.model = cp_model.CpModel()
19
+ self.model_vars: dict[tuple[Pos, str], cp_model.IntVar] = {}
20
+ self.create_vars()
21
+ self.add_all_constraints()
22
+
23
+ def create_vars(self):
24
+ for pos in get_all_pos(self.V, self.H):
25
+ for direction in self.STATES:
26
+ self.model_vars[(pos, direction)] = self.model.NewBoolVar(f'{pos}:{direction}')
27
+ self.model.AddExactlyOne([self.model_vars[(pos, direction)] for direction in self.STATES])
28
+
29
+ def add_all_constraints(self):
30
+ for pos in get_all_pos(self.V, self.H): # force clues
31
+ c = get_char(self.board, pos)
32
+ c = {'L': Direction.LEFT, 'R': Direction.RIGHT, 'U': Direction.UP, 'D': Direction.DOWN, 'O': 'O'}.get(c, None)
33
+ if c is not None:
34
+ self.model.Add(self.model_vars[(pos, c)] == 1)
35
+ for row in range(self.V): # each row, 1 of each direction and 2 O's
36
+ for direction in Direction:
37
+ self.model.AddExactlyOne([self.model_vars[(pos, direction)] for pos in get_row_pos(row, self.H)])
38
+ for col in range(self.H): # each column, 1 of each direction and 2 O's
39
+ for direction in Direction:
40
+ self.model.AddExactlyOne([self.model_vars[(pos, direction)] for pos in get_col_pos(col, self.V)])
41
+ for pos in get_all_pos(self.V, self.H):
42
+ for direction in Direction:
43
+ self.apply_orientation_rule(pos, direction)
44
+
45
+ def apply_orientation_rule(self, pos: Pos, direction: Direction):
46
+ # if cell is direction (for example L), then the cell to its left must not be R, and the cell to its up-right and down-right must also not be R
47
+ # and the cell to its up-right cant be U and the cell to its down-right cant be D. You have to see the triangles visually for it to make sense.
48
+ assert direction in Direction, f'direction must be in Direction, got {direction}'
49
+ if direction == Direction.LEFT:
50
+ disallow_pairs = [
51
+ (get_next_pos(pos, Direction8.LEFT), Direction.RIGHT),
52
+ (get_next_pos(pos, Direction8.UP_RIGHT), Direction.RIGHT),
53
+ (get_next_pos(pos, Direction8.DOWN_RIGHT), Direction.RIGHT),
54
+ (get_next_pos(pos, Direction8.UP_RIGHT), Direction.UP),
55
+ (get_next_pos(pos, Direction8.DOWN_RIGHT), Direction.DOWN),
56
+ ]
57
+ elif direction == Direction.RIGHT:
58
+ disallow_pairs = [
59
+ (get_next_pos(pos, Direction8.RIGHT), Direction.LEFT),
60
+ (get_next_pos(pos, Direction8.UP_LEFT), Direction.LEFT),
61
+ (get_next_pos(pos, Direction8.DOWN_LEFT), Direction.LEFT),
62
+ (get_next_pos(pos, Direction8.UP_LEFT), Direction.UP),
63
+ (get_next_pos(pos, Direction8.DOWN_LEFT), Direction.DOWN),
64
+ ]
65
+ elif direction == Direction.UP:
66
+ disallow_pairs = [
67
+ (get_next_pos(pos, Direction8.UP), Direction.DOWN),
68
+ (get_next_pos(pos, Direction8.DOWN_LEFT), Direction.DOWN),
69
+ (get_next_pos(pos, Direction8.DOWN_RIGHT), Direction.DOWN),
70
+ (get_next_pos(pos, Direction8.DOWN_LEFT), Direction.LEFT),
71
+ (get_next_pos(pos, Direction8.DOWN_RIGHT), Direction.RIGHT),
72
+ ]
73
+ elif direction == Direction.DOWN:
74
+ disallow_pairs = [
75
+ (get_next_pos(pos, Direction8.DOWN), Direction.UP),
76
+ (get_next_pos(pos, Direction8.UP_LEFT), Direction.UP),
77
+ (get_next_pos(pos, Direction8.UP_RIGHT), Direction.UP),
78
+ (get_next_pos(pos, Direction8.UP_LEFT), Direction.LEFT),
79
+ (get_next_pos(pos, Direction8.UP_RIGHT), Direction.RIGHT),
80
+ ]
81
+ else:
82
+ raise ValueError(f'invalid direction: {direction}')
83
+ disallow_pairs = [d_pair for d_pair in disallow_pairs if in_bounds(d_pair[0], self.V, self.H)]
84
+ for d_pos, d_direction in disallow_pairs:
85
+ self.model.Add(self.model_vars[(d_pos, d_direction)] == 0).OnlyEnforceIf(self.model_vars[(pos, direction)])
86
+
87
+
88
+ def solve_and_print(self, verbose: bool = True):
89
+ def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
90
+ return SingleSolution(assignment={pos: 'O' if direction == 'O' else direction.name[0] for (pos, direction), var in board.model_vars.items() if solver.Value(var) == 1})
91
+ def callback(single_res: SingleSolution):
92
+ print("Solution found")
93
+ print(combined_function(self.V, self.H, center_char=lambda r, c: single_res.assignment.get(get_pos(x=c, y=r), ' ')))
94
+ return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -119,15 +119,19 @@ def main(image):
119
119
  horizontal_idx, vertical_idx = extract_lines(bw)
120
120
  horizontal_idx = mean_consecutives(horizontal_idx)
121
121
  vertical_idx = mean_consecutives(vertical_idx)
122
+ mean_vertical_dist = np.mean(np.diff(vertical_idx))
123
+ mean_horizontal_dist = np.mean(np.diff(horizontal_idx))
122
124
  height = len(horizontal_idx)
123
125
  width = len(vertical_idx)
124
126
  print(f"height: {height}, width: {width}")
125
127
  print(f"horizontal_idx: {horizontal_idx}")
126
128
  print(f"vertical_idx: {vertical_idx}")
127
- arr = np.zeros((height - 1, width - 1), dtype=object)
128
- output = {'top': arr.copy(), 'left': arr.copy(), 'right': arr.copy(), 'bottom': arr.copy()}
129
129
  hists = {'top': {}, 'left': {}, 'right': {}, 'bottom': {}}
130
+ j_idx = 0
131
+ i_len = 0
132
+ j_len = 0
130
133
  for j in range(height - 1):
134
+ i_idx = 0
131
135
  for i in range(width - 1):
132
136
  hidx1, hidx2 = horizontal_idx[j], horizontal_idx[j+1]
133
137
  vidx1, vidx2 = vertical_idx[i], vertical_idx[i+1]
@@ -135,7 +139,11 @@ def main(image):
135
139
  hidx2 = min(src.shape[0], hidx2 + 4)
136
140
  vidx1 = max(0, vidx1 - 2)
137
141
  vidx2 = min(src.shape[1], vidx2 + 4)
142
+ if (hidx2 - hidx1) < mean_horizontal_dist * 0.5 or (vidx2 - vidx1) < mean_vertical_dist * 0.5:
143
+ continue
144
+ print(f"j_idx: {j_idx}, i_idx: {i_idx}")
138
145
  cell = src[hidx1:hidx2, vidx1:vidx2]
146
+ # print(f"cell_shape: {cell.shape}, mean_horizontal_dist: {mean_horizontal_dist}, mean_vertical_dist: {mean_vertical_dist}")
139
147
  mid_x = cell.shape[1] // 2
140
148
  mid_y = cell.shape[0] // 2
141
149
  # if j > height - 4 and i > width - 6:
@@ -143,13 +151,18 @@ def main(image):
143
151
  # show_wait_destroy(f"cell_{i}_{j}", cell)
144
152
  cell = cv.bitwise_not(cell) # invert colors
145
153
  top = cell[0:10, mid_y-5:mid_y+5]
146
- hists['top'][j, i] = np.sum(top)
154
+ hists['top'][j_idx, i_idx] = np.sum(top)
147
155
  left = cell[mid_x-5:mid_x+5, 0:10]
148
- hists['left'][j, i] = np.sum(left)
156
+ hists['left'][j_idx, i_idx] = np.sum(left)
149
157
  right = cell[mid_x-5:mid_x+5, -10:]
150
- hists['right'][j, i] = np.sum(right)
158
+ hists['right'][j_idx, i_idx] = np.sum(right)
151
159
  bottom = cell[-10:, mid_y-5:mid_y+5]
152
- hists['bottom'][j, i] = np.sum(bottom)
160
+ hists['bottom'][j_idx, i_idx] = np.sum(bottom)
161
+ i_idx += 1
162
+ i_len = max(i_len, i_idx)
163
+ if i_idx > 0:
164
+ j_idx += 1
165
+ j_len = max(j_len, j_idx)
153
166
 
154
167
  fig, axs = plt.subplots(2, 2)
155
168
  axs[0, 0].hist(list(hists['top'].values()), bins=100)
@@ -178,9 +191,11 @@ def main(image):
178
191
  axs[1, 1].axvline(target_bottom, color='red')
179
192
  # plt.show()
180
193
  # 1/0
181
- print(f"target_top: {target_top}, target_left: {target_left}, target_right: {target_right}, target_bottom: {target_bottom}")
182
- for j in range(height - 1):
183
- for i in range(width - 1):
194
+ arr = np.zeros((j_len, i_len), dtype=object)
195
+ output = {'top': arr.copy(), 'left': arr.copy(), 'right': arr.copy(), 'bottom': arr.copy()}
196
+ print(f"target_top: {target_top}, target_left: {target_left}, target_right: {target_right}, target_bottom: {target_bottom}, j_len: {j_len}, i_len: {i_len}")
197
+ for j in range(j_len):
198
+ for i in range(i_len):
184
199
  if hists['top'][j, i] > target_top:
185
200
  output['top'][j, i] = 1
186
201
  if hists['left'][j, i] > target_left:
@@ -244,5 +259,8 @@ if __name__ == '__main__':
244
259
  # main(Path(__file__).parent / 'input_output' / 'norinori_OTo0LDc0Miw5MTU.png')
245
260
  # main(Path(__file__).parent / 'input_output' / 'heyawake_MDoxNiwxNDQ=.png')
246
261
  # main(Path(__file__).parent / 'input_output' / 'heyawake_MTQ6ODQ4LDEzOQ==.png')
247
- main(Path(__file__).parent / 'input_output' / 'sudoku_jigsaw.png')
262
+ # main(Path(__file__).parent / 'input_output' / 'sudoku_jigsaw.png')
263
+ # main(Path(__file__).parent / 'input_output' / 'Screenshot 2025-11-01 025846.png')
264
+ # main(Path(__file__).parent / 'input_output' / 'Screenshot 2025-11-01 035658.png')
265
+ main(Path(__file__).parent / 'input_output' / 'Screenshot 2025-11-01 044110.png')
248
266