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.
- {multi_puzzle_solver-1.0.8.dist-info → multi_puzzle_solver-1.0.10.dist-info}/METADATA +185 -20
- {multi_puzzle_solver-1.0.8.dist-info → multi_puzzle_solver-1.0.10.dist-info}/RECORD +10 -8
- puzzle_solver/__init__.py +5 -1
- puzzle_solver/puzzles/binairo/binairo.py +31 -59
- puzzle_solver/puzzles/flip/flip.py +14 -27
- puzzle_solver/puzzles/n_queens/n_queens.py +81 -0
- puzzle_solver/puzzles/split_ends/split_ends.py +94 -0
- puzzle_solver/puzzles/stitches/parse_map/parse_map.py +28 -10
- {multi_puzzle_solver-1.0.8.dist-info → multi_puzzle_solver-1.0.10.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-1.0.8.dist-info → multi_puzzle_solver-1.0.10.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multi-puzzle-solver
|
|
3
|
-
Version: 1.0.
|
|
4
|
-
Summary: Efficient solvers for countless (
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
73
|
-
multi_puzzle_solver-1.0.
|
|
74
|
-
multi_puzzle_solver-1.0.
|
|
75
|
-
multi_puzzle_solver-1.0.
|
|
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.
|
|
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
|
|
44
|
-
self.model.Add(self.model_vars[pos] ==
|
|
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.
|
|
55
|
-
|
|
56
|
-
self.
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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 = [
|
|
73
|
+
pow2 = [2**k for k in range(m)]
|
|
109
74
|
for i, line in enumerate(model_vars):
|
|
110
|
-
code = self.model.NewIntVar(0,
|
|
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,
|
|
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
|
|
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:
|
|
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
|
-
|
|
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'][
|
|
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'][
|
|
156
|
+
hists['left'][j_idx, i_idx] = np.sum(left)
|
|
149
157
|
right = cell[mid_x-5:mid_x+5, -10:]
|
|
150
|
-
hists['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'][
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
|
File without changes
|
|
File without changes
|