multi-puzzle-solver 1.0.8__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.8
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)
@@ -610,7 +624,15 @@ Time taken: 0.04 seconds
610
624
 
611
625
  ## Sudoku (Puzzle Type #2)
612
626
 
613
- Also known as Number Place and Solo.
627
+ Also known as Number Place and Solo.
628
+
629
+ The code can:
630
+
631
+ 1. Solve arbitrarily sized valid board sizes, thus can be used to solve:
632
+ - Hex Sudoku (a 16x16 variant)
633
+ - Kidoku (a kid-friendly sudoku variant)
634
+ 2. Solve the ["Sandwich" sudoku variant](https://dkmgames.com/SandwichSudoku/) using the optional parameter `sandwich={'side': [...], 'bottom': [...]}`
635
+ 3. Solve the ["Sudoku-X" variant](https://www.sudopedia.org/wiki/Sudoku-X) using the optional parameter `unique_diagonal=True`
614
636
 
615
637
  * [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/solo.html)
616
638
 
@@ -635,12 +657,6 @@ You are given some of the numbers as clues; your aim is to place the rest of the
635
657
 
636
658
  Code to utilize this package and solve the puzzle:
637
659
 
638
- Note:
639
-
640
- - The solver also supports solving the ["Sandwich" sudoku variant](https://dkmgames.com/SandwichSudoku/) through the optional parameter ``sandwich={'side': [...], 'bottom': [...] }``。
641
-
642
- - The solver also supports solving the ["Sudoku-X" variant](https://www.sudopedia.org/wiki/Sudoku-X) through the optional parameter ``unique_diagonal=True``。
643
-
644
660
  ```python
645
661
  import numpy as np
646
662
  from puzzle_solver import sudoku_solver as solver
@@ -1105,7 +1121,7 @@ Time taken: 0.15 seconds
1105
1121
 
1106
1122
  ## Keen (Puzzle Type #8)
1107
1123
 
1108
- Also known as KenKen or CalcuDoku.
1124
+ Also known as KenKen, CalcuDoku, Inkies, or Inky.
1109
1125
 
1110
1126
  * [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/keen.html)
1111
1127
 
@@ -4043,6 +4059,8 @@ Applying the solution to the puzzle visually:
4043
4059
 
4044
4060
  ## Binairo (Puzzle Type #41)
4045
4061
 
4062
+ Also known as Takuzu, Binero, Tohu-Wa-Vohu (Formless and Empty), Eins und Zwei (One and Two), Binary Puzzles, Binoxxo, Binox, Zernero, Tic-Tac-Logic, and Sudoku Binary.
4063
+
4046
4064
  * [**Play online**](https://www.puzzle-binairo.com)
4047
4065
 
4048
4066
  * [**Solver Code**][41]
@@ -4395,19 +4413,30 @@ solutions = binst.solve_and_print()
4395
4413
 
4396
4414
  **Script Output**
4397
4415
 
4398
- 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.
4399
4417
 
4400
4418
  ```python
4401
4419
  Solution found
4402
- [['T' ' ' 'T' 'T' 'T' ' ' ' ']
4403
- [' ' ' ' ' ' 'T' ' ' 'T' ' ']
4404
- [' ' 'T' ' ' ' ' 'T' ' ' ' ']
4405
- ['T' ' ' 'T' ' ' ' ' 'T' ' ']
4406
- [' ' ' ' ' ' 'T' ' ' ' ' 'T']
4407
- ['T' ' ' 'T' ' ' 'T' 'T' 'T']
4408
- [' ' ' ' ' ' ' ' ' ' '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
+ └───┴───┴───┴───┴───┴───┴───┘
4409
4437
  Solutions found: 1
4410
4438
  status: OPTIMAL
4439
+ Time taken: 0.01 seconds
4411
4440
  ```
4412
4441
 
4413
4442
  **Solved puzzle**
@@ -5942,6 +5971,142 @@ Time taken: 0.00 seconds
5942
5971
 
5943
5972
  ---
5944
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
+
5945
6110
  ---
5946
6111
 
5947
6112
  ## Why SAT / CP-SAT?
@@ -1,11 +1,11 @@
1
- puzzle_solver/__init__.py,sha256=0jnYSJY4kJBhX5PU4UMmMcbTg8PsyFjiX0y23r5sv5k,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
5
5
  puzzle_solver/puzzles/abc_view/abc_view.py,sha256=Qr0rZKmKQ2teStHjQ5VPQ4k-XptsjJAlZ1WXWk5Aax4,4570
6
6
  puzzle_solver/puzzles/aquarium/aquarium.py,sha256=dGqYEWMoh4di5DN4sd-GtYb6QeTpVYFQJHBkrrmrudQ,5649
7
7
  puzzle_solver/puzzles/battleships/battleships.py,sha256=U4xJ_NJC2baHvfaAfJ01YEBjixq9gD0h8GP9L1V-_oM,7223
8
- puzzle_solver/puzzles/binairo/binairo.py,sha256=qKvpixLIBUcugAyJgpBGHV-9q_4nzA1ZOxeDFnltsXA,6843
8
+ puzzle_solver/puzzles/binairo/binairo.py,sha256=EBpXYD9Mxuig4uJl3xkcQ6_tbnoG13mVV7RZpQEXm38,5790
9
9
  puzzle_solver/puzzles/binairo/binairo_plus.py,sha256=TvLG3olwANtft3LuCF-y4OofpU9PNa4IXDqgZqsD-g0,267
10
10
  puzzle_solver/puzzles/black_box/black_box.py,sha256=RXTXQhMAb_Oce9Mk1XpouniYIyy9k3kYGdey-SEeRMU,12559
11
11
  puzzle_solver/puzzles/bridges/bridges.py,sha256=QwOhZyO5urbatkNyPmQxZ_lGM01ZejndMr_eoiBkr7g,5394
@@ -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.8.dist-info/METADATA,sha256=E6zK6c0vVluGrFEvS459AuNa33dET9IWNdmqtIbYJwA,456018
73
- multi_puzzle_solver-1.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
74
- multi_puzzle_solver-1.0.8.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
75
- multi_puzzle_solver-1.0.8.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.8'
128
+ __version__ = '1.0.10'
@@ -10,26 +10,21 @@ from puzzle_solver.core.utils_visualizer import combined_function
10
10
 
11
11
 
12
12
  class Board:
13
- def __init__(self, board: np.array, arith_rows: Optional[np.array] = None, arith_cols: Optional[np.array] = None, force_unique: bool = True):
13
+ def __init__(self, board: np.array, arith_rows: Optional[np.array] = None, arith_cols: Optional[np.array] = None, force_unique: bool = True, disallow_three: bool = True):
14
14
  assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
15
+ assert board.shape[0] % 2 == 0 and board.shape[1] % 2 == 0, f'board must have even number of rows and columns, got {board.shape[0]}x{board.shape[1]}'
15
16
  assert all(c.item() in [' ', 'B', 'W'] for c in np.nditer(board)), 'board must contain only space or B'
17
+ assert arith_rows is None or all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_rows)), 'arith_rows must contain only space, x, or ='
18
+ assert arith_cols is None or all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_cols)), 'arith_cols must contain only space, x, or ='
16
19
  self.board = board
17
20
  self.V, self.H = board.shape
18
- if arith_rows is not None:
19
- assert arith_rows.ndim == 2, f'arith_rows must be 2d, got {arith_rows.ndim}'
20
- assert arith_rows.shape == (self.V, self.H-1), f'arith_rows must be one column less than board, got {arith_rows.shape} for {board.shape}'
21
- assert all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_rows)), 'arith_rows must contain only space, x, or ='
22
- if arith_cols is not None:
23
- assert arith_cols.ndim == 2, f'arith_cols must be 2d, got {arith_cols.ndim}'
24
- assert arith_cols.shape == (self.V-1, self.H), f'arith_cols must be one column and row less than board, got {arith_cols.shape} for {board.shape}'
25
- assert all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_cols)), 'arith_cols must contain only space, x, or ='
26
21
  self.arith_rows = arith_rows
27
22
  self.arith_cols = arith_cols
28
23
  self.force_unique = force_unique
24
+ self.disallow_three = disallow_three
29
25
 
30
26
  self.model = cp_model.CpModel()
31
27
  self.model_vars: dict[Pos, cp_model.IntVar] = {}
32
-
33
28
  self.create_vars()
34
29
  self.add_all_constraints()
35
30
 
@@ -39,11 +34,9 @@ class Board:
39
34
 
40
35
  def add_all_constraints(self):
41
36
  for pos in get_all_pos(self.V, self.H): # force clues
42
- c = get_char(self.board, pos)
43
- if c == 'B':
44
- self.model.Add(self.model_vars[pos] == 1)
45
- elif c == 'W':
46
- self.model.Add(self.model_vars[pos] == 0)
37
+ c = get_char(self.board, pos).strip()
38
+ if c:
39
+ self.model.Add(self.model_vars[pos] == (c == 'B'))
47
40
  # 1. Each row and each column must contain an equal number of white and black circles.
48
41
  for row in range(self.V):
49
42
  row_vars = [self.model_vars[pos] for pos in get_row_pos(row, self.H)]
@@ -51,69 +44,48 @@ class Board:
51
44
  for col in range(self.H):
52
45
  col_vars = [self.model_vars[pos] for pos in get_col_pos(col, self.V)]
53
46
  self.model.Add(lxp.sum(col_vars) == len(col_vars) // 2)
54
- # 2. More than two circles of the same color can't be adjacent.
55
- for pos in get_all_pos(self.V, self.H):
56
- self.disallow_three_in_a_row(pos, Direction.RIGHT)
57
- self.disallow_three_in_a_row(pos, Direction.DOWN)
58
-
47
+ # 2. No three consecutive cells of the same color
48
+ if self.disallow_three:
49
+ for pos in get_all_pos(self.V, self.H):
50
+ self.disallow_three_in_a_row(pos, Direction.RIGHT)
51
+ self.disallow_three_in_a_row(pos, Direction.DOWN)
59
52
  # 3. Each row and column is unique.
60
53
  if self.force_unique:
61
- # a list per row
62
54
  self.force_unique_double_list([[self.model_vars[pos] for pos in get_row_pos(row, self.H)] for row in range(self.V)])
63
- # a list per column
64
55
  self.force_unique_double_list([[self.model_vars[pos] for pos in get_col_pos(col, self.V)] for col in range(self.H)])
65
-
66
56
  # if arithmetic is provided, add constraints for it
67
57
  if self.arith_rows is not None:
68
- assert self.arith_rows.shape == (self.V, self.H-1), f'arith_rows must be one column less than board, got {self.arith_rows.shape} for {self.board.shape}'
69
- for pos in get_all_pos(self.V, self.H-1):
70
- c = get_char(self.arith_rows, pos)
71
- if c == 'x':
72
- self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, Direction.RIGHT)])
73
- elif c == '=':
74
- self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, Direction.RIGHT)])
58
+ self.force_arithmetic(self.arith_rows, Direction.RIGHT, self.V, self.H-1)
75
59
  if self.arith_cols is not None:
76
- assert self.arith_cols.shape == (self.V-1, self.H), f'arith_cols must be one row less than board, got {self.arith_cols.shape} for {self.board.shape}'
77
- for pos in get_all_pos(self.V-1, self.H):
78
- c = get_char(self.arith_cols, pos)
79
- if c == 'x':
80
- self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, Direction.DOWN)])
81
- elif c == '=':
82
- self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, Direction.DOWN)])
83
-
60
+ self.force_arithmetic(self.arith_cols, Direction.DOWN, self.V-1, self.H)
84
61
 
85
62
  def disallow_three_in_a_row(self, p1: Pos, direction: Direction):
86
63
  p2 = get_next_pos(p1, direction)
87
64
  p3 = get_next_pos(p2, direction)
88
- if any(not in_bounds(p, self.V, self.H) for p in [p1, p2, p3]):
89
- return
90
- self.model.AddBoolOr([
91
- self.model_vars[p1],
92
- self.model_vars[p2],
93
- self.model_vars[p3],
94
- ])
95
- self.model.AddBoolOr([
96
- self.model_vars[p1].Not(),
97
- self.model_vars[p2].Not(),
98
- self.model_vars[p3].Not(),
99
- ])
65
+ if all(in_bounds(p, self.V, self.H) for p in [p1, p2, p3]):
66
+ self.model.AddBoolOr([self.model_vars[p1], self.model_vars[p2], self.model_vars[p3]])
67
+ self.model.AddBoolOr([self.model_vars[p1].Not(), self.model_vars[p2].Not(), self.model_vars[p3].Not()])
100
68
 
101
69
  def force_unique_double_list(self, model_vars: list[list[cp_model.IntVar]]):
102
- if not model_vars or len(model_vars) < 2:
103
- return
104
70
  m = len(model_vars[0])
105
- assert m <= 61, f"Too many cells for binary encoding in int64: m={m}, model_vars={model_vars}"
106
-
71
+ assert m <= 61, f'Too many cells for binary encoding in int64: m={m}, model_vars={model_vars}'
107
72
  codes = []
108
- pow2 = [1 << k for k in range(m)] # weights for bit positions (LSB at index 0)
73
+ pow2 = [2**k for k in range(m)]
109
74
  for i, line in enumerate(model_vars):
110
- code = self.model.NewIntVar(0, (1 << m) - 1, f"code_{i}")
111
- # Sum 2^k * r[k] == code
112
- self.model.Add(code == sum(pow2[k] * line[k] for k in range(m)))
75
+ code = self.model.NewIntVar(0, 2**m, f"code_{i}")
76
+ self.model.Add(code == lxp.weighted_sum(line, pow2)) # Sum 2^k * r[k] == code
113
77
  codes.append(code)
114
-
115
78
  self.model.AddAllDifferent(codes)
116
79
 
80
+ def force_arithmetic(self, arith_board: np.array, direction: Direction, V: int, H: int):
81
+ assert arith_board.shape == (V, H), f'arith_board going {direction} expected shape {V}x{H}, got {arith_board.shape}'
82
+ for pos in get_all_pos(V, H):
83
+ c = get_char(arith_board, pos).strip()
84
+ if c == 'x':
85
+ self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, direction)])
86
+ elif c == '=':
87
+ self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, direction)])
88
+
117
89
  def solve_and_print(self, verbose: bool = True):
118
90
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
119
91
  assignment: dict[Pos, int] = {}
@@ -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