multi-puzzle-solver 0.9.22__py3-none-any.whl → 0.9.24__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-0.9.22.dist-info → multi_puzzle_solver-0.9.24.dist-info}/METADATA +221 -2
- {multi_puzzle_solver-0.9.22.dist-info → multi_puzzle_solver-0.9.24.dist-info}/RECORD +10 -8
- puzzle_solver/__init__.py +3 -1
- puzzle_solver/core/utils.py +135 -0
- puzzle_solver/puzzles/binairo/binairo.py +98 -0
- puzzle_solver/puzzles/rectangles/rectangles.py +130 -0
- puzzle_solver/puzzles/slitherlink/slitherlink.py +4 -128
- puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py +4 -1
- {multi_puzzle_solver-0.9.22.dist-info → multi_puzzle_solver-0.9.24.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.9.22.dist-info → multi_puzzle_solver-0.9.24.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multi-puzzle-solver
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.24
|
|
4
4
|
Summary: Efficient solvers for numerous popular and esoteric logic puzzles using CP-SAT
|
|
5
5
|
Author: Ar-Kareem
|
|
6
6
|
Project-URL: Homepage, https://github.com/Ar-Kareem/puzzle_solver
|
|
@@ -315,6 +315,18 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
315
315
|
</a>
|
|
316
316
|
</td>
|
|
317
317
|
</tr>
|
|
318
|
+
<tr>
|
|
319
|
+
<td align="center">
|
|
320
|
+
<a href="#binairo-puzzle-type-41"><b>Binairo</b><br><br>
|
|
321
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/binairo_solved.png" alt="Binairo" width="140">
|
|
322
|
+
</a>
|
|
323
|
+
</td>
|
|
324
|
+
<td align="center">
|
|
325
|
+
<a href="#rectangles-puzzle-type-42"><b>Rectangles</b><br><br>
|
|
326
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/rectangles_solved.png" alt="Rectangles" width="140">
|
|
327
|
+
</a>
|
|
328
|
+
</td>
|
|
329
|
+
</tr>
|
|
318
330
|
</table>
|
|
319
331
|
|
|
320
332
|
</div>
|
|
@@ -369,6 +381,8 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
369
381
|
- [Norinori (Puzzle Type #38)](#norinori-puzzle-type-38)
|
|
370
382
|
- [Slitherlink (Puzzle Type #39)](#slitherlink-puzzle-type-39)
|
|
371
383
|
- [Yin-Yang (Puzzle Type #40)](#yin-yang-puzzle-type-40)
|
|
384
|
+
- [Binairo (Puzzle Type #41)](#binairo-puzzle-type-41)
|
|
385
|
+
- [Rectangles (Puzzle Type #42)](#rectangles-puzzle-type-42)
|
|
372
386
|
- [Why SAT / CP-SAT?](#why-sat--cp-sat)
|
|
373
387
|
- [Testing](#testing)
|
|
374
388
|
- [Contributing](#contributing)
|
|
@@ -3412,7 +3426,6 @@ Applying the solution to the puzzle visually:
|
|
|
3412
3426
|
|
|
3413
3427
|
---
|
|
3414
3428
|
|
|
3415
|
-
|
|
3416
3429
|
## Yin-Yang (Puzzle Type #40)
|
|
3417
3430
|
|
|
3418
3431
|
* [**Play online**](https://www.puzzle-yin-yang.com)
|
|
@@ -3503,6 +3516,210 @@ Applying the solution to the puzzle visually:
|
|
|
3503
3516
|
|
|
3504
3517
|
---
|
|
3505
3518
|
|
|
3519
|
+
## Binairo (Puzzle Type #41)
|
|
3520
|
+
|
|
3521
|
+
* [**Play online**](https://www.puzzle-binairo.com)
|
|
3522
|
+
|
|
3523
|
+
* [**Solver Code**][41]
|
|
3524
|
+
|
|
3525
|
+
<details>
|
|
3526
|
+
<summary><strong>Rules</strong></summary>
|
|
3527
|
+
|
|
3528
|
+
Binairo is played on a rectangular grid with no standard size. Some cells start out filled with black or white circles. The rest of the cells are empty. The goal is to place circles in all cells in such a way that:
|
|
3529
|
+
|
|
3530
|
+
1. Each row and each column must contain an equal number of white and black circles.
|
|
3531
|
+
2. More than two circles of the same color can't be adjacent.
|
|
3532
|
+
3. Each row and column is unique.
|
|
3533
|
+
|
|
3534
|
+
</details>
|
|
3535
|
+
|
|
3536
|
+
**Unsolved puzzle**
|
|
3537
|
+
|
|
3538
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/binairo_unsolved.png" alt="Binairo unsolved" width="500">
|
|
3539
|
+
|
|
3540
|
+
Code to utilize this package and solve the puzzle:
|
|
3541
|
+
|
|
3542
|
+
```python
|
|
3543
|
+
import numpy as np
|
|
3544
|
+
from puzzle_solver import binairo_solver as solver
|
|
3545
|
+
board = np.array([
|
|
3546
|
+
[' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'B', ' ', ' ', 'W'],
|
|
3547
|
+
[' ', 'W', ' ', ' ', 'B', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'B', ' ', ' ', ' ', 'B', ' '],
|
|
3548
|
+
[' ', 'W', ' ', ' ', ' ', 'W', ' ', 'W', 'W', ' ', ' ', ' ', 'B', ' ', ' ', 'W', ' ', ' ', ' ', ' '],
|
|
3549
|
+
['B', ' ', ' ', 'W', ' ', ' ', 'B', ' ', ' ', ' ', ' ', ' ', 'B', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
3550
|
+
['B', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', ' ', 'W', ' ', ' ', ' ', 'B', ' ', ' ', ' ', 'W'],
|
|
3551
|
+
[' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' '],
|
|
3552
|
+
['W', ' ', ' ', ' ', ' ', ' ', 'B', ' ', ' ', 'B', ' ', ' ', 'B', 'B', ' ', ' ', 'W', ' ', 'B', ' '],
|
|
3553
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'B', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' '],
|
|
3554
|
+
[' ', ' ', ' ', ' ', 'W', ' ', 'B', ' ', 'W', ' ', ' ', ' ', 'B', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
3555
|
+
[' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'W', 'W', ' ', ' ', ' '],
|
|
3556
|
+
[' ', ' ', 'B', ' ', ' ', ' ', 'B', ' ', 'B', ' ', ' ', ' ', 'B', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
3557
|
+
[' ', 'W', 'B', ' ', 'W', ' ', 'B', ' ', ' ', ' ', ' ', ' ', 'W', 'W', ' ', 'B', ' ', ' ', 'B', ' '],
|
|
3558
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'B', ' ', ' ', ' ', 'B', 'B'],
|
|
3559
|
+
[' ', 'B', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', 'W', ' ', ' ', ' '],
|
|
3560
|
+
[' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', ' ', ' ', 'W', 'W', ' '],
|
|
3561
|
+
[' ', 'B', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'B', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
3562
|
+
[' ', 'B', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'W', ' ', ' ', 'B', ' ', 'B', ' ', ' ', ' ', ' ', ' '],
|
|
3563
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'B', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'W'],
|
|
3564
|
+
[' ', ' ', ' ', 'B', 'B', ' ', ' ', 'W', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'B', ' ', ' '],
|
|
3565
|
+
['B', ' ', 'B', 'B', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'B', ' ', ' ', 'B', ' ', ' ', ' ', ' ', ' ']
|
|
3566
|
+
])
|
|
3567
|
+
binst = solver.Board(board=board)
|
|
3568
|
+
solutions = binst.solve_and_print()
|
|
3569
|
+
```
|
|
3570
|
+
|
|
3571
|
+
**Script Output**
|
|
3572
|
+
|
|
3573
|
+
```python
|
|
3574
|
+
Solution found
|
|
3575
|
+
[
|
|
3576
|
+
[ 'B', 'B', 'W', 'B', 'W', 'B', 'W', 'B', 'W', 'W', 'B', 'W', 'W', 'B', 'W', 'B', 'B', 'W', 'B', 'W' ],
|
|
3577
|
+
[ 'W', 'W', 'B', 'W', 'B', 'B', 'W', 'B', 'B', 'W', 'B', 'W', 'W', 'B', 'B', 'W', 'W', 'B', 'B', 'W' ],
|
|
3578
|
+
[ 'W', 'W', 'B', 'B', 'W', 'W', 'B', 'W', 'W', 'B', 'W', 'B', 'B', 'W', 'B', 'W', 'B', 'B', 'W', 'B' ],
|
|
3579
|
+
[ 'B', 'B', 'W', 'W', 'B', 'W', 'B', 'W', 'B', 'W', 'W', 'B', 'B', 'W', 'W', 'B', 'W', 'W', 'B', 'B' ],
|
|
3580
|
+
[ 'B', 'W', 'B', 'W', 'W', 'B', 'W', 'B', 'W', 'B', 'B', 'W', 'W', 'B', 'W', 'B', 'W', 'B', 'B', 'W' ],
|
|
3581
|
+
[ 'W', 'B', 'W', 'B', 'B', 'W', 'W', 'B', 'B', 'W', 'W', 'B', 'B', 'W', 'B', 'W', 'B', 'W', 'W', 'B' ],
|
|
3582
|
+
[ 'W', 'B', 'W', 'W', 'B', 'W', 'B', 'W', 'W', 'B', 'B', 'W', 'B', 'B', 'W', 'B', 'W', 'B', 'B', 'W' ],
|
|
3583
|
+
[ 'B', 'W', 'B', 'W', 'W', 'B', 'W', 'B', 'B', 'W', 'B', 'B', 'W', 'W', 'B', 'W', 'W', 'B', 'W', 'B' ],
|
|
3584
|
+
[ 'W', 'B', 'W', 'B', 'W', 'B', 'B', 'W', 'W', 'B', 'W', 'B', 'B', 'W', 'W', 'B', 'B', 'W', 'B', 'W' ],
|
|
3585
|
+
[ 'B', 'W', 'W', 'B', 'B', 'W', 'W', 'B', 'W', 'B', 'B', 'W', 'W', 'B', 'B', 'W', 'W', 'B', 'W', 'B' ],
|
|
3586
|
+
[ 'W', 'B', 'B', 'W', 'B', 'W', 'B', 'W', 'B', 'W', 'B', 'W', 'B', 'B', 'W', 'W', 'B', 'B', 'W', 'W' ],
|
|
3587
|
+
[ 'B', 'W', 'B', 'W', 'W', 'B', 'B', 'W', 'B', 'B', 'W', 'B', 'W', 'W', 'B', 'B', 'W', 'W', 'B', 'W' ],
|
|
3588
|
+
[ 'B', 'W', 'W', 'B', 'B', 'W', 'W', 'B', 'W', 'B', 'W', 'W', 'B', 'W', 'B', 'W', 'B', 'W', 'B', 'B' ],
|
|
3589
|
+
[ 'W', 'B', 'B', 'W', 'B', 'B', 'W', 'B', 'W', 'W', 'B', 'B', 'W', 'B', 'W', 'B', 'W', 'B', 'W', 'W' ],
|
|
3590
|
+
[ 'B', 'W', 'W', 'B', 'W', 'B', 'B', 'W', 'B', 'B', 'W', 'W', 'B', 'W', 'W', 'B', 'B', 'W', 'W', 'B' ],
|
|
3591
|
+
[ 'W', 'B', 'W', 'B', 'W', 'W', 'B', 'W', 'B', 'B', 'W', 'B', 'W', 'B', 'B', 'W', 'B', 'W', 'B', 'W' ],
|
|
3592
|
+
[ 'W', 'B', 'B', 'W', 'B', 'W', 'W', 'B', 'W', 'W', 'B', 'W', 'B', 'W', 'B', 'B', 'W', 'B', 'W', 'B' ],
|
|
3593
|
+
[ 'B', 'W', 'B', 'W', 'W', 'B', 'B', 'W', 'B', 'B', 'W', 'W', 'B', 'B', 'W', 'W', 'B', 'W', 'B', 'W' ],
|
|
3594
|
+
[ 'W', 'B', 'W', 'B', 'B', 'W', 'B', 'W', 'B', 'W', 'W', 'B', 'W', 'B', 'W', 'B', 'W', 'B', 'W', 'B' ],
|
|
3595
|
+
[ 'B', 'W', 'B', 'B', 'W', 'B', 'W', 'B', 'W', 'W', 'B', 'B', 'W', 'W', 'B', 'W', 'B', 'W', 'W', 'B' ],
|
|
3596
|
+
]
|
|
3597
|
+
Solutions found: 1
|
|
3598
|
+
status: OPTIMAL
|
|
3599
|
+
Time taken: 0.03 seconds
|
|
3600
|
+
```
|
|
3601
|
+
|
|
3602
|
+
**Solved puzzle**
|
|
3603
|
+
|
|
3604
|
+
Applying the solution to the puzzle visually:
|
|
3605
|
+
|
|
3606
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/binairo_solved.png" alt="Binairo solved" width="500">
|
|
3607
|
+
|
|
3608
|
+
---
|
|
3609
|
+
|
|
3610
|
+
## Rectangles (Puzzle Type #42)
|
|
3611
|
+
|
|
3612
|
+
Also called "Shikaku".
|
|
3613
|
+
|
|
3614
|
+
* [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/rect.html)
|
|
3615
|
+
|
|
3616
|
+
* [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/rect.html#rect)
|
|
3617
|
+
|
|
3618
|
+
* [**Solver Code**][42]
|
|
3619
|
+
|
|
3620
|
+
<details>
|
|
3621
|
+
<summary><strong>Rules</strong></summary>
|
|
3622
|
+
|
|
3623
|
+
You have a grid of squares, with numbers written in some (but not all) of the squares. Your task is to subdivide the grid into rectangles of various sizes, such that both:
|
|
3624
|
+
|
|
3625
|
+
- (a) every rectangle contains exactly one numbered square
|
|
3626
|
+
- (b) the area of each rectangle is equal to the number written in its numbered square.
|
|
3627
|
+
|
|
3628
|
+
|
|
3629
|
+
</details>
|
|
3630
|
+
|
|
3631
|
+
**Unsolved puzzle**
|
|
3632
|
+
|
|
3633
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/rectangles_unsolved.png" alt="Rectangles unsolved" width="500">
|
|
3634
|
+
|
|
3635
|
+
Code to utilize this package and solve the puzzle:
|
|
3636
|
+
|
|
3637
|
+
```python
|
|
3638
|
+
import numpy as np
|
|
3639
|
+
from puzzle_solver import rectangles_solver as solver
|
|
3640
|
+
board = np.array([
|
|
3641
|
+
['3', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '15',' ', ' ', ' ', ' ' ],
|
|
3642
|
+
[' ', ' ', '2', '2', ' ', ' ', ' ', ' ', ' ', ' ', '11',' ', ' ', ' ', ' ', ' ', ' ', '3', '2' ],
|
|
3643
|
+
[' ', ' ', ' ', ' ', '2', ' ', ' ', ' ', '11',' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '2', ' ' ],
|
|
3644
|
+
[' ', ' ', ' ', '2', ' ', ' ', ' ', ' ', ' ', ' ', '6', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' ],
|
|
3645
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '3', ' ', ' ', ' ', ' ', ' ', ' ', ' ' ],
|
|
3646
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '2', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' ],
|
|
3647
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '2', ' ', ' ', ' ', ' ', ' ', ' ', ' ' ],
|
|
3648
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '28','4', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' ],
|
|
3649
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '10',' ', '10',' ', ' ', ' ', ' ', '45',' ' ],
|
|
3650
|
+
[' ', ' ', '3', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' ],
|
|
3651
|
+
[' ', '22',' ', ' ', ' ', ' ', ' ', '28',' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '17'],
|
|
3652
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' ],
|
|
3653
|
+
[' ', '8', '3', ' ', ' ', '2', '2', ' ', ' ', ' ', '5', ' ', ' ', '4', ' ', ' ', ' ', ' ', ' ' ],
|
|
3654
|
+
[' ', ' ', ' ', ' ', '4', ' ', ' ', '8', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '2', ' ', ' ', ' ' ],
|
|
3655
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '3', ' ' ],
|
|
3656
|
+
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' ],
|
|
3657
|
+
['2', ' ', ' ', ' ', '12',' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' ],
|
|
3658
|
+
['2', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' ],
|
|
3659
|
+
[' ', ' ', '3', '2', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '60',' ', ' ', ' ', ' ', ' ', '4', ' ' ],
|
|
3660
|
+
])
|
|
3661
|
+
binst = solver.Board(board=board)
|
|
3662
|
+
solutions = binst.solve_and_print()
|
|
3663
|
+
```
|
|
3664
|
+
|
|
3665
|
+
**Script Output**
|
|
3666
|
+
|
|
3667
|
+
```python
|
|
3668
|
+
Solution found
|
|
3669
|
+
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1
|
|
3670
|
+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
|
|
3671
|
+
┌───────────┬───────────────────────────────────────────────────────────┬───┐
|
|
3672
|
+
0│ 3 │ 15 │ │
|
|
3673
|
+
├───────┬───┼───┬───────────────────────────────────────────┬───────────┤ │
|
|
3674
|
+
1│ │ 2 │ 2 │ 11 │ 3 │ 2 │
|
|
3675
|
+
│ │ │ ├───┬───────────────────────────────────────┴───┬───────┼───┤
|
|
3676
|
+
2│ │ │ │ 2 │ 11 │ 2 │ │
|
|
3677
|
+
│ ├───┴───┤ ├───────────────────────┬───┬───┬───────────┴───────┤ │
|
|
3678
|
+
3│ │ 2 │ │ 6 │ │ │ │ │
|
|
3679
|
+
│ ├───────┴───┴───────────────┬───┬───┤ │ │ │ │
|
|
3680
|
+
4│ │ │ │ │ 3 │ │ │ │
|
|
3681
|
+
│ │ │ │ │ │ │ │ │
|
|
3682
|
+
5│ │ │ │ 2 │ │ │ │ │
|
|
3683
|
+
│ │ │ ├───┴───┤ │ │ │
|
|
3684
|
+
6│ │ │ │ 2 │ │ │ │
|
|
3685
|
+
│ │ │ ├───────┤ │ │ │
|
|
3686
|
+
7│ │ 28 │ 4 │ │ │ │ │
|
|
3687
|
+
│ ├───┬───────────────────────┴───┤ │ │ │ │
|
|
3688
|
+
8│ │ │ │10 │10 │ 45 │ │
|
|
3689
|
+
│ │ │ │ │ │ │ │
|
|
3690
|
+
9│ │ 3 │ │ │ │ │ │
|
|
3691
|
+
│ │ │ │ │ │ │ │
|
|
3692
|
+
10│ 22 │ │ 28 │ │ │ │17 │
|
|
3693
|
+
│ ├───┤ │ │ │ │ │
|
|
3694
|
+
11│ │ │ │ │ │ │ │
|
|
3695
|
+
├───────┤ ├───────┬───┬───┬───────────┴───────┤ ├───────────────┬───┤ │
|
|
3696
|
+
12│ 8 │ 3 │ │ 2 │ 2 │ 5 │ │ 4 │ │ │
|
|
3697
|
+
│ │ │ │ │ ├───────────────────┴───┴───────┬───────┤ │ │
|
|
3698
|
+
13│ │ │ 4 │ │ │ 8 │ 2 │ │ │
|
|
3699
|
+
│ ├───┴───────┼───┴───┴───────────────────────────────┴───────┤ │ │
|
|
3700
|
+
14│ │ │ │ 3 │ │
|
|
3701
|
+
│ │ │ ├───┤ │
|
|
3702
|
+
15│ │ │ │ │ │
|
|
3703
|
+
├───────┤ │ │ │ │
|
|
3704
|
+
16│ 2 │ 12 │ │ │ │
|
|
3705
|
+
├───────┤ │ │ │ │
|
|
3706
|
+
17│ 2 │ │ │ │ │
|
|
3707
|
+
├───────┴───┬───────┤ │ │ │
|
|
3708
|
+
18│ 3 │ 2 │ 60 │ 4 │ │
|
|
3709
|
+
└───────────┴───────┴───────────────────────────────────────────────┴───┴───┘
|
|
3710
|
+
Solutions found: 1
|
|
3711
|
+
status: OPTIMAL
|
|
3712
|
+
Time taken: 0.01 seconds
|
|
3713
|
+
```
|
|
3714
|
+
|
|
3715
|
+
**Solved puzzle**
|
|
3716
|
+
|
|
3717
|
+
Applying the solution to the puzzle visually:
|
|
3718
|
+
|
|
3719
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/rectangles_solved.png" alt="Rectangles solved" width="500">
|
|
3720
|
+
|
|
3721
|
+
---
|
|
3722
|
+
|
|
3506
3723
|
---
|
|
3507
3724
|
|
|
3508
3725
|
## Why SAT / CP-SAT?
|
|
@@ -3594,3 +3811,5 @@ Issues and PRs welcome!
|
|
|
3594
3811
|
[38]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/norinori "puzzle_solver/src/puzzle_solver/puzzles/norinori at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
3595
3812
|
[39]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/slitherlink "puzzle_solver/src/puzzle_solver/puzzles/slitherlink at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
3596
3813
|
[40]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/yin_yang "puzzle_solver/src/puzzle_solver/puzzles/yin_yang at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
3814
|
+
[41]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/binairo "puzzle_solver/src/puzzle_solver/puzzles/binairo at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
3815
|
+
[42]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/rectangles "puzzle_solver/src/puzzle_solver/puzzles/rectangles at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
puzzle_solver/__init__.py,sha256=
|
|
2
|
-
puzzle_solver/core/utils.py,sha256=
|
|
1
|
+
puzzle_solver/__init__.py,sha256=y6x9mMvglHDRKhkRXIudfXFgJ0KGBPo3jqi1YT_B6wc,3071
|
|
2
|
+
puzzle_solver/core/utils.py,sha256=BH4b-GZLfYWWZ4QPt1UcuwSX3ntE3bInJtwHd7RnVf4,13459
|
|
3
3
|
puzzle_solver/core/utils_ortools.py,sha256=2xEL9cMEKmNhRD9lhr2nGdZ3Lbmc9cnHY8xv6iLhUr0,10542
|
|
4
4
|
puzzle_solver/puzzles/aquarium/aquarium.py,sha256=BUfkAS2d9eG3TdMoe1cOGGeNYgKUebRvn-z9nsC9gvE,5708
|
|
5
5
|
puzzle_solver/puzzles/battleships/battleships.py,sha256=RuYCrs4j0vUjlU139NRYYP-uNPAgO0V7hAzbsHrRwD8,7446
|
|
6
|
+
puzzle_solver/puzzles/binairo/binairo.py,sha256=sRtflnlGrN8xQ64beRZBGr74R8KptzxYDdFgXuW27pM,4595
|
|
6
7
|
puzzle_solver/puzzles/black_box/black_box.py,sha256=ZnHDVt6PFS_r1kMNSsbz9hav1hxIrNDUvPyERGPjLjM,15635
|
|
7
8
|
puzzle_solver/puzzles/bridges/bridges.py,sha256=15A9uV4xjoqPRo_9CTnoKeGRxS3z2aMF619T1n0dTOQ,5402
|
|
8
9
|
puzzle_solver/puzzles/chess_range/chess_melee.py,sha256=D-_Oi8OyxsVe1j3dIKYwRlxgeb3NWLmDWGcv-oclY0c,195
|
|
@@ -29,11 +30,12 @@ puzzle_solver/puzzles/nonograms/nonograms.py,sha256=1jmDTOCnmivmBlwtMDyyk3TVqH5I
|
|
|
29
30
|
puzzle_solver/puzzles/norinori/norinori.py,sha256=uC8vXAw35xsTmpmTeKqYW7tbcssms9LCcXFBONtV2Ng,4743
|
|
30
31
|
puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
|
|
31
32
|
puzzle_solver/puzzles/range/range.py,sha256=rruvD5ZSaOgvQuX6uGV_Dkr82nSiWZ5kDz03_j7Tt24,4425
|
|
33
|
+
puzzle_solver/puzzles/rectangles/rectangles.py,sha256=V7p6GSCwYrFfILDWiLLUbX08WlnPbQKdhQm8bMa2Mgw,7060
|
|
32
34
|
puzzle_solver/puzzles/signpost/signpost.py,sha256=-0_S6ycwzwlUf9-ZhP127Rgo5gMBOHiTM6t08dLLDac,3869
|
|
33
35
|
puzzle_solver/puzzles/singles/singles.py,sha256=3wACiUa1Vmh2ce6szQ2hPjyBuH7aHiQ888p4R2jFkW4,3342
|
|
34
36
|
puzzle_solver/puzzles/slant/slant.py,sha256=xF-N4PuXYfx638NP1f1mi6YncIZB4mLtXtdS79XyPbg,6122
|
|
35
37
|
puzzle_solver/puzzles/slant/parse_map/parse_map.py,sha256=dxnALSDXe9wU0uSD0QEXnzoh1q801mj1ePTNLtG0n60,4796
|
|
36
|
-
puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=
|
|
38
|
+
puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=_P5IyDKs8gP9aubCW5QStOv4TGf0Hkq7ybyjkxw5n_U,6856
|
|
37
39
|
puzzle_solver/puzzles/star_battle/star_battle.py,sha256=IX6w4H3sifN01kPPtrAVRCK0Nl_xlXXSHvJKw8K1EuE,3718
|
|
38
40
|
puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
|
|
39
41
|
puzzle_solver/puzzles/stitches/stitches.py,sha256=iK8t02q43gH3FPbuIDn4dK0sbaOgZOnw8yHNRNvNuIU,6534
|
|
@@ -47,9 +49,9 @@ puzzle_solver/puzzles/undead/undead.py,sha256=IrCUfzQFBem658P5KKqldG7vd2TugTHehc
|
|
|
47
49
|
puzzle_solver/puzzles/unequal/unequal.py,sha256=ExY2XDCrqROCDpRLfHo8uVr1zuli1QvbCdNCiDhlCac,6978
|
|
48
50
|
puzzle_solver/puzzles/unruly/unruly.py,sha256=sDF0oKT50G-NshyW2DYrvAgD9q9Ku9ANUyNhGSAu7cQ,3827
|
|
49
51
|
puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=WrRdNhmKhIARdGOt_36gpRxRzrfLGv3wl7igBpPFM64,5259
|
|
50
|
-
puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=
|
|
52
|
+
puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
|
|
51
53
|
puzzle_solver/utils/visualizer.py,sha256=tsX1yEKwmwXBYuBJpx_oZGe2UUt1g5yV73G3UbtmvtE,6817
|
|
52
|
-
multi_puzzle_solver-0.9.
|
|
53
|
-
multi_puzzle_solver-0.9.
|
|
54
|
-
multi_puzzle_solver-0.9.
|
|
55
|
-
multi_puzzle_solver-0.9.
|
|
54
|
+
multi_puzzle_solver-0.9.24.dist-info/METADATA,sha256=mcSsOot7iHAhhc2fangljbrIpGvDsuwc80_ifFRXJ7s,202273
|
|
55
|
+
multi_puzzle_solver-0.9.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
56
|
+
multi_puzzle_solver-0.9.24.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
|
|
57
|
+
multi_puzzle_solver-0.9.24.dist-info/RECORD,,
|
puzzle_solver/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from puzzle_solver.puzzles.aquarium import aquarium as aquarium_solver
|
|
2
2
|
from puzzle_solver.puzzles.battleships import battleships as battleships_solver
|
|
3
|
+
from puzzle_solver.puzzles.binairo import binairo as binairo_solver
|
|
3
4
|
from puzzle_solver.puzzles.black_box import black_box as black_box_solver
|
|
4
5
|
from puzzle_solver.puzzles.bridges import bridges as bridges_solver
|
|
5
6
|
from puzzle_solver.puzzles.chess_range import chess_range as chess_range_solver
|
|
@@ -22,6 +23,7 @@ from puzzle_solver.puzzles.norinori import norinori as norinori_solver
|
|
|
22
23
|
from puzzle_solver.puzzles.lits import lits as lits_solver
|
|
23
24
|
from puzzle_solver.puzzles.pearl import pearl as pearl_solver
|
|
24
25
|
from puzzle_solver.puzzles.range import range as range_solver
|
|
26
|
+
from puzzle_solver.puzzles.rectangles import rectangles as rectangles_solver
|
|
25
27
|
from puzzle_solver.puzzles.signpost import signpost as signpost_solver
|
|
26
28
|
from puzzle_solver.puzzles.singles import singles as singles_solver
|
|
27
29
|
from puzzle_solver.puzzles.slant import slant as slant_solver
|
|
@@ -41,4 +43,4 @@ from puzzle_solver.puzzles.yin_yang import yin_yang as yin_yang_solver
|
|
|
41
43
|
|
|
42
44
|
from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
|
|
43
45
|
|
|
44
|
-
__version__ = '0.9.
|
|
46
|
+
__version__ = '0.9.24'
|
puzzle_solver/core/utils.py
CHANGED
|
@@ -188,6 +188,7 @@ def polyominoes(N):
|
|
|
188
188
|
shapes = {frozenset(Pos(x, y) for x, y in s) for s in shapes} # regular class, not the dirty-fast one
|
|
189
189
|
return shapes
|
|
190
190
|
|
|
191
|
+
|
|
191
192
|
def polyominoes_with_shape_id(N):
|
|
192
193
|
"""Refer to polyominoes() for more details. This function returns a set of all polyominoes of size N (rotated and reflected up to D4 symmetry) along with a unique ID for each polyomino that is unique up to D4 symmetry.
|
|
193
194
|
Args:
|
|
@@ -226,3 +227,137 @@ def polyominoes_with_shape_id(N):
|
|
|
226
227
|
result = {(s, canon_to_id[shape_to_canon[s]]) for s in shapes}
|
|
227
228
|
result = {(frozenset(Pos(x, y) for x, y in s), _id) for s, _id in result}
|
|
228
229
|
return result
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def render_grid(cell_flags: np.ndarray,
|
|
233
|
+
center_char: Union[np.ndarray, str, None] = None,
|
|
234
|
+
show_axes: bool = True,
|
|
235
|
+
scale_x: int = 2) -> str:
|
|
236
|
+
"""
|
|
237
|
+
most of this function was AI generated then modified by me, I don't currently care about the details of rendering to the terminal this looked good enough during my testing.
|
|
238
|
+
cell_flags: np.ndarray of shape (N, N) with characters 'U', 'D', 'L', 'R' to represent the edges of the cells.
|
|
239
|
+
center_char: np.ndarray of shape (N, N) with the center of the cells, or a string to use for all cells, or None to not show centers.
|
|
240
|
+
scale_x: horizontal stretch factor (>=1). Try 2 or 3 for squarer cells.
|
|
241
|
+
"""
|
|
242
|
+
if cell_flags is not None:
|
|
243
|
+
N = cell_flags.shape[0]
|
|
244
|
+
H = np.zeros((N+1, N), dtype=bool)
|
|
245
|
+
V = np.zeros((N, N+1), dtype=bool)
|
|
246
|
+
for r in range(N):
|
|
247
|
+
for c in range(N):
|
|
248
|
+
s = cell_flags[r, c]
|
|
249
|
+
if 'U' in s: H[r, c] = True # edge between (r,c) and (r, c+1) above the cell
|
|
250
|
+
if 'D' in s: H[r+1, c] = True # edge below the cell
|
|
251
|
+
if 'L' in s: V[r, c] = True # edge left of the cell
|
|
252
|
+
if 'R' in s: V[r, c+1] = True # edge right of the cell
|
|
253
|
+
assert H is not None and V is not None, 'H and V must be provided'
|
|
254
|
+
# Bitmask for corner connections
|
|
255
|
+
U, R, D, L = 1, 2, 4, 8
|
|
256
|
+
JUNCTION = {
|
|
257
|
+
0: ' ',
|
|
258
|
+
U: '│', D: '│', U|D: '│',
|
|
259
|
+
L: '─', R: '─', L|R: '─',
|
|
260
|
+
U|R: '└', R|D: '┌', D|L: '┐', L|U: '┘',
|
|
261
|
+
U|D|L: '┤', U|D|R: '├', L|R|U: '┴', L|R|D: '┬',
|
|
262
|
+
U|R|D|L: '┼',
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
assert scale_x >= 1
|
|
266
|
+
N = V.shape[0]
|
|
267
|
+
assert H.shape == (N+1, N) and V.shape == (N, N+1)
|
|
268
|
+
|
|
269
|
+
rows = 2*N + 1
|
|
270
|
+
cols = 2*N*scale_x + 1 # stretched width
|
|
271
|
+
canvas = [[' ']*cols for _ in range(rows)]
|
|
272
|
+
|
|
273
|
+
def x_corner(c): # x of corner column c
|
|
274
|
+
return (2*c) * scale_x
|
|
275
|
+
def x_between(c,k): # kth in-between column (1..scale_x) between c and c+1 corners
|
|
276
|
+
return (2*c) * scale_x + k
|
|
277
|
+
|
|
278
|
+
# horizontal edges: fill the stretched band between corners with '─'
|
|
279
|
+
for r in range(N+1):
|
|
280
|
+
rr = 2*r
|
|
281
|
+
for c in range(N):
|
|
282
|
+
if H[r, c]:
|
|
283
|
+
# previously: for k in range(1, scale_x*2, 2):
|
|
284
|
+
for k in range(1, scale_x*2): # 1..(2*scale_x-1), no gaps
|
|
285
|
+
canvas[rr][x_between(c, k)] = '─'
|
|
286
|
+
|
|
287
|
+
# vertical edges: draw at the corner columns (no horizontal stretching needed)
|
|
288
|
+
for r in range(N):
|
|
289
|
+
rr = 2*r + 1
|
|
290
|
+
for c in range(N+1):
|
|
291
|
+
if V[r, c]:
|
|
292
|
+
canvas[rr][x_corner(c)] = '│'
|
|
293
|
+
|
|
294
|
+
# junctions at corners
|
|
295
|
+
for r in range(N+1):
|
|
296
|
+
rr = 2*r
|
|
297
|
+
for c in range(N+1):
|
|
298
|
+
m = 0
|
|
299
|
+
if r > 0 and V[r-1, c]: m |= U
|
|
300
|
+
if c < N and H[r, c]: m |= R
|
|
301
|
+
if r < N and V[r, c]: m |= D
|
|
302
|
+
if c > 0 and H[r, c-1]: m |= L
|
|
303
|
+
canvas[rr][x_corner(c)] = JUNCTION[m]
|
|
304
|
+
|
|
305
|
+
# centers
|
|
306
|
+
# ── Centers (now safe for multi-character strings) ──────────────────────
|
|
307
|
+
# We render center text within the interior span (between corner columns),
|
|
308
|
+
# centered if it fits; otherwise we truncate to the span width.
|
|
309
|
+
def put_center_text(rr: int, c: int, text: str):
|
|
310
|
+
# interior span (exclusive of the corner columns)
|
|
311
|
+
left = x_corner(c) + 1
|
|
312
|
+
right = x_corner(c+1) - 1
|
|
313
|
+
if right < left:
|
|
314
|
+
return # no interior space (shouldn’t happen when scale_x>=1)
|
|
315
|
+
span_width = right - left + 1
|
|
316
|
+
|
|
317
|
+
s = str(text)
|
|
318
|
+
if len(s) > span_width:
|
|
319
|
+
s = s[:span_width] # hard truncate if it doesn't fit
|
|
320
|
+
# center within the span
|
|
321
|
+
start = left + (span_width - len(s)) // 2
|
|
322
|
+
for i, ch in enumerate(s):
|
|
323
|
+
canvas[rr][start + i] = ch
|
|
324
|
+
|
|
325
|
+
if center_char is not None:
|
|
326
|
+
for r in range(N):
|
|
327
|
+
rr = 2*r + 1
|
|
328
|
+
for c in range(N):
|
|
329
|
+
val = center_char if isinstance(center_char, str) else center_char[r, c]
|
|
330
|
+
put_center_text(rr, c, '' if val is None else str(val))
|
|
331
|
+
|
|
332
|
+
# turn canvas rows into strings
|
|
333
|
+
art_rows = [''.join(row) for row in canvas]
|
|
334
|
+
|
|
335
|
+
if not show_axes:
|
|
336
|
+
return '\n'.join(art_rows)
|
|
337
|
+
|
|
338
|
+
# ── Axes ────────────────────────────────────────────────────────────────
|
|
339
|
+
gut = max(2, len(str(N-1))) # left gutter width
|
|
340
|
+
gutter = ' ' * gut
|
|
341
|
+
top_tens = list(gutter + ' ' * cols)
|
|
342
|
+
top_ones = list(gutter + ' ' * cols)
|
|
343
|
+
|
|
344
|
+
for c in range(N):
|
|
345
|
+
xc_center = x_corner(c) + scale_x
|
|
346
|
+
if N >= 10:
|
|
347
|
+
top_tens[gut + xc_center] = str((c // 10) % 10)
|
|
348
|
+
top_ones[gut + xc_center] = str(c % 10)
|
|
349
|
+
|
|
350
|
+
# tiny corner labels
|
|
351
|
+
if gut >= 2:
|
|
352
|
+
top_tens[gut-2:gut] = list(' ')
|
|
353
|
+
top_ones[gut-2:gut] = list(' ')
|
|
354
|
+
|
|
355
|
+
labeled = []
|
|
356
|
+
for r, line in enumerate(art_rows):
|
|
357
|
+
if r % 2 == 1: # cell-center row
|
|
358
|
+
label = str(r//2).rjust(gut)
|
|
359
|
+
else:
|
|
360
|
+
label = ' ' * gut
|
|
361
|
+
labeled.append(label + line)
|
|
362
|
+
|
|
363
|
+
return ''.join(top_tens) + '\n' + ''.join(top_ones) + '\n' + '\n'.join(labeled)
|
|
@@ -0,0 +1,98 @@
|
|
|
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 Direction, Pos, get_all_pos, get_next_pos, in_bounds, set_char, get_char, get_neighbors8, get_row_pos, get_col_pos
|
|
6
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Board:
|
|
10
|
+
def __init__(self, board: np.array):
|
|
11
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
12
|
+
assert all(c.item() in [' ', 'B', 'W'] for c in np.nditer(board)), 'board must contain only space or B'
|
|
13
|
+
self.board = board
|
|
14
|
+
self.V, self.H = board.shape
|
|
15
|
+
self.model = cp_model.CpModel()
|
|
16
|
+
self.model_vars: dict[Pos, cp_model.IntVar] = {}
|
|
17
|
+
|
|
18
|
+
self.create_vars()
|
|
19
|
+
self.add_all_constraints()
|
|
20
|
+
|
|
21
|
+
def create_vars(self):
|
|
22
|
+
for pos in get_all_pos(self.V, self.H):
|
|
23
|
+
self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
|
|
24
|
+
|
|
25
|
+
def add_all_constraints(self):
|
|
26
|
+
for pos in get_all_pos(self.V, self.H): # force clues
|
|
27
|
+
c = get_char(self.board, pos)
|
|
28
|
+
if c == 'B':
|
|
29
|
+
self.model.Add(self.model_vars[pos] == 1)
|
|
30
|
+
elif c == 'W':
|
|
31
|
+
self.model.Add(self.model_vars[pos] == 0)
|
|
32
|
+
# 1. Each row and each column must contain an equal number of white and black circles.
|
|
33
|
+
for row in range(self.V):
|
|
34
|
+
row_vars = [self.model_vars[pos] for pos in get_row_pos(row, self.H)]
|
|
35
|
+
self.model.Add(lxp.sum(row_vars) == len(row_vars) // 2)
|
|
36
|
+
for col in range(self.H):
|
|
37
|
+
col_vars = [self.model_vars[pos] for pos in get_col_pos(col, self.V)]
|
|
38
|
+
self.model.Add(lxp.sum(col_vars) == len(col_vars) // 2)
|
|
39
|
+
# 2. More than two circles of the same color can't be adjacent.
|
|
40
|
+
for pos in get_all_pos(self.V, self.H):
|
|
41
|
+
self.disallow_three_in_a_row(pos, Direction.RIGHT)
|
|
42
|
+
self.disallow_three_in_a_row(pos, Direction.DOWN)
|
|
43
|
+
# 3. Each row and column is unique.
|
|
44
|
+
# a list per row
|
|
45
|
+
self.force_unique([[self.model_vars[pos] for pos in get_row_pos(row, self.H)] for row in range(self.V)])
|
|
46
|
+
# a list per column
|
|
47
|
+
self.force_unique([[self.model_vars[pos] for pos in get_col_pos(col, self.V)] for col in range(self.H)])
|
|
48
|
+
|
|
49
|
+
def disallow_three_in_a_row(self, p1: Pos, direction: Direction):
|
|
50
|
+
p2 = get_next_pos(p1, direction)
|
|
51
|
+
p3 = get_next_pos(p2, direction)
|
|
52
|
+
if any(not in_bounds(p, self.V, self.H) for p in [p1, p2, p3]):
|
|
53
|
+
return
|
|
54
|
+
self.model.AddBoolOr([
|
|
55
|
+
self.model_vars[p1],
|
|
56
|
+
self.model_vars[p2],
|
|
57
|
+
self.model_vars[p3],
|
|
58
|
+
])
|
|
59
|
+
self.model.AddBoolOr([
|
|
60
|
+
self.model_vars[p1].Not(),
|
|
61
|
+
self.model_vars[p2].Not(),
|
|
62
|
+
self.model_vars[p3].Not(),
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
def force_unique(self, model_vars: list[list[cp_model.IntVar]]):
|
|
66
|
+
if not model_vars or len(model_vars) < 2:
|
|
67
|
+
return
|
|
68
|
+
m = len(model_vars[0])
|
|
69
|
+
assert m <= 61, f"Too many cells for binary encoding in int64: m={m}, model_vars={model_vars}"
|
|
70
|
+
|
|
71
|
+
codes = []
|
|
72
|
+
pow2 = [1 << k for k in range(m)] # weights for bit positions (LSB at index 0)
|
|
73
|
+
for i, l in enumerate(model_vars):
|
|
74
|
+
code = self.model.NewIntVar(0, (1 << m) - 1, f"code_{i}")
|
|
75
|
+
# Sum 2^k * r[k] == code
|
|
76
|
+
self.model.Add(code == sum(pow2[k] * l[k] for k in range(m)))
|
|
77
|
+
codes.append(code)
|
|
78
|
+
|
|
79
|
+
self.model.AddAllDifferent(codes)
|
|
80
|
+
|
|
81
|
+
def solve_and_print(self, verbose: bool = True):
|
|
82
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
83
|
+
assignment: dict[Pos, int] = {}
|
|
84
|
+
for pos, var in board.model_vars.items():
|
|
85
|
+
assignment[pos] = solver.Value(var)
|
|
86
|
+
return SingleSolution(assignment=assignment)
|
|
87
|
+
def callback(single_res: SingleSolution):
|
|
88
|
+
print("Solution found")
|
|
89
|
+
res = np.full((self.V, self.H), ' ', dtype=object)
|
|
90
|
+
for pos in get_all_pos(self.V, self.H):
|
|
91
|
+
c = get_char(self.board, pos)
|
|
92
|
+
c = 'B' if single_res.assignment[pos] == 1 else 'W'
|
|
93
|
+
set_char(res, pos, c)
|
|
94
|
+
print('[')
|
|
95
|
+
for row in res:
|
|
96
|
+
print(" [ '" + "', '".join(row.tolist()) + "' ],")
|
|
97
|
+
print(']')
|
|
98
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
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, in_bounds, set_char, get_char, get_neighbors8, Direction, get_next_pos, render_grid
|
|
9
|
+
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def factor_pairs(N: int, upper_limit_i: int, upper_limit_j: int):
|
|
13
|
+
"""Return all unique pairs (a, b) such that a * b == N, with a, b <= upper_limit."""
|
|
14
|
+
if N <= 0 or upper_limit_i <= 0 or upper_limit_j <= 0:
|
|
15
|
+
return []
|
|
16
|
+
|
|
17
|
+
pairs = []
|
|
18
|
+
i = 1
|
|
19
|
+
while i * i <= N:
|
|
20
|
+
if N % i == 0:
|
|
21
|
+
j = N // i
|
|
22
|
+
if i <= upper_limit_i and j <= upper_limit_j:
|
|
23
|
+
pairs.append((i, j))
|
|
24
|
+
if i != j and j <= upper_limit_i and i <= upper_limit_j:
|
|
25
|
+
pairs.append((j, i))
|
|
26
|
+
i += 1
|
|
27
|
+
return pairs
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Rectangle:
|
|
32
|
+
active: cp_model.IntVar
|
|
33
|
+
N: int
|
|
34
|
+
clue_id: int
|
|
35
|
+
width: int
|
|
36
|
+
height: int
|
|
37
|
+
body: set[Pos]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Board:
|
|
41
|
+
def __init__(self, board: np.array):
|
|
42
|
+
assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
|
|
43
|
+
assert all((c.item() == ' ') or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or digits'
|
|
44
|
+
self.board = board
|
|
45
|
+
self.V, self.H = board.shape
|
|
46
|
+
self.clue_pos: list[Pos] = [pos for pos in get_all_pos(self.V, self.H) if str(get_char(self.board, pos)).isdecimal()]
|
|
47
|
+
self.clue_pos_to_id: dict[Pos, int] = {pos: i for i, pos in enumerate(self.clue_pos)}
|
|
48
|
+
self.clue_pos_to_value: dict[Pos, int] = {pos: int(get_char(self.board, pos)) for pos in self.clue_pos}
|
|
49
|
+
|
|
50
|
+
self.model = cp_model.CpModel()
|
|
51
|
+
self.model_vars: dict[tuple[Pos, Pos], cp_model.IntVar] = {}
|
|
52
|
+
self.rectangles: list[Rectangle] = []
|
|
53
|
+
|
|
54
|
+
self.create_vars()
|
|
55
|
+
self.add_all_constraints()
|
|
56
|
+
|
|
57
|
+
def create_vars(self):
|
|
58
|
+
self.init_rectangles()
|
|
59
|
+
# for each position it belongs to exactly 1 clue
|
|
60
|
+
# instead of iterating over all clues, we only look at the clues that are possible for this position (by looking at the rectangles that contain this position)
|
|
61
|
+
for pos in get_all_pos(self.V, self.H):
|
|
62
|
+
possible_clue_here = {rectangle.clue_id for rectangle in self.rectangles if pos in rectangle.body} # get the clue position for any rectangle that contains this position
|
|
63
|
+
for possible_clue in possible_clue_here:
|
|
64
|
+
self.model_vars[(pos, possible_clue)] = self.model.NewBoolVar(f'{pos}:{possible_clue}')
|
|
65
|
+
|
|
66
|
+
def init_rectangles(self) -> list[Rectangle]:
|
|
67
|
+
self.fixed_pos: set[Pos] = set(self.clue_pos)
|
|
68
|
+
for pos in self.clue_pos: # for each clue on the board
|
|
69
|
+
clue_id = self.clue_pos_to_id[pos]
|
|
70
|
+
clue_num = self.clue_pos_to_value[pos]
|
|
71
|
+
other_fixed_pos = self.fixed_pos - {pos}
|
|
72
|
+
for width, height in factor_pairs(clue_num, self.V, self.H): # for each possible width x height rectangle that can fit the clue
|
|
73
|
+
# if the digit is at pos and we have a width x height rectangle then we can translate the rectangle "0 to width" to the left and "0 to height" to the top
|
|
74
|
+
for dx in range(width):
|
|
75
|
+
for dy in range(height):
|
|
76
|
+
body = {Pos(x=pos.x - dx + i, y=pos.y - dy + j) for i in range(width) for j in range(height)}
|
|
77
|
+
if any(not in_bounds(p, self.V, self.H) for p in body): # a rectangle cannot be out of bounds
|
|
78
|
+
continue
|
|
79
|
+
if any(p in other_fixed_pos for p in body): # a rectangle cannot contain a different clue; each clue is 1 rectangle only
|
|
80
|
+
continue
|
|
81
|
+
rectangle = Rectangle(active=self.model.NewBoolVar(f'{clue_id}'), N=clue_num, clue_id=clue_id, width=width, height=height, body=body)
|
|
82
|
+
self.rectangles.append(rectangle)
|
|
83
|
+
|
|
84
|
+
def add_all_constraints(self):
|
|
85
|
+
# each pos has only 1 rectangle active
|
|
86
|
+
for pos in get_all_pos(self.V, self.H):
|
|
87
|
+
self.model.AddExactlyOne(rectangle.active for rectangle in self.rectangles if pos in rectangle.body)
|
|
88
|
+
# each pos has only 1 clue active
|
|
89
|
+
for pos in get_all_pos(self.V, self.H):
|
|
90
|
+
self.model.AddExactlyOne(self.model_vars[(pos, clue_id)] for clue_id in self.clue_pos_to_id.values() if (pos, clue_id) in self.model_vars)
|
|
91
|
+
# a rectangle being active means all its body ponts to the clue
|
|
92
|
+
for rectangle in self.rectangles:
|
|
93
|
+
is_active = rectangle.active
|
|
94
|
+
for pos in rectangle.body:
|
|
95
|
+
self.model.Add(self.model_vars[(pos, rectangle.clue_id)] == 1).OnlyEnforceIf(is_active)
|
|
96
|
+
|
|
97
|
+
def solve_and_print(self, verbose: bool = True):
|
|
98
|
+
def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
99
|
+
assignment: dict[Pos, int] = {}
|
|
100
|
+
for (i, rectangle) in enumerate(self.rectangles):
|
|
101
|
+
if solver.Value(rectangle.active) == 1:
|
|
102
|
+
for pos in rectangle.body:
|
|
103
|
+
assignment[pos] = f'id{rectangle.clue_id}:N={rectangle.N}:{rectangle.height}x{rectangle.width}'
|
|
104
|
+
return SingleSolution(assignment=assignment)
|
|
105
|
+
def callback(single_res: SingleSolution):
|
|
106
|
+
print("Solution found")
|
|
107
|
+
res = np.full((self.V, self.H), '', dtype=object)
|
|
108
|
+
id_board = np.full((self.V, self.H), '', dtype=object)
|
|
109
|
+
for pos in get_all_pos(self.V, self.H):
|
|
110
|
+
cur = single_res.assignment[pos]
|
|
111
|
+
set_char(id_board, pos, cur)
|
|
112
|
+
left_pos = get_next_pos(pos, Direction.LEFT)
|
|
113
|
+
right_pos = get_next_pos(pos, Direction.RIGHT)
|
|
114
|
+
top_pos = get_next_pos(pos, Direction.UP)
|
|
115
|
+
bottom_pos = get_next_pos(pos, Direction.DOWN)
|
|
116
|
+
if left_pos not in single_res.assignment or single_res.assignment[left_pos] != cur:
|
|
117
|
+
set_char(res, pos, get_char(res, pos) + 'L')
|
|
118
|
+
if right_pos not in single_res.assignment or single_res.assignment[right_pos] != cur:
|
|
119
|
+
set_char(res, pos, get_char(res, pos) + 'R')
|
|
120
|
+
if top_pos not in single_res.assignment or single_res.assignment[top_pos] != cur:
|
|
121
|
+
set_char(res, pos, get_char(res, pos) + 'U')
|
|
122
|
+
if bottom_pos not in single_res.assignment or single_res.assignment[bottom_pos] != cur:
|
|
123
|
+
set_char(res, pos, get_char(res, pos) + 'D')
|
|
124
|
+
# print('[')
|
|
125
|
+
# for row in id_board:
|
|
126
|
+
# print(' ', row.tolist(), end=',\n')
|
|
127
|
+
# print(' ])')
|
|
128
|
+
print(render_grid(res, center_char=self.board))
|
|
129
|
+
|
|
130
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -2,7 +2,7 @@ import numpy as np
|
|
|
2
2
|
from collections import defaultdict
|
|
3
3
|
from ortools.sat.python import cp_model
|
|
4
4
|
|
|
5
|
-
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_pos, Direction, get_row_pos, get_col_pos, get_next_pos, in_bounds, get_opposite_direction
|
|
5
|
+
from puzzle_solver.core.utils import Pos, get_all_pos, get_char, set_char, get_pos, Direction, get_row_pos, get_col_pos, get_next_pos, in_bounds, get_opposite_direction, render_grid
|
|
6
6
|
from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
|
|
7
7
|
|
|
8
8
|
|
|
@@ -118,131 +118,7 @@ class Board:
|
|
|
118
118
|
continue
|
|
119
119
|
c = ''.join(sorted(single_res.assignment[pos]))
|
|
120
120
|
set_char(res, pos, c)
|
|
121
|
-
|
|
121
|
+
# replace " " with "·"
|
|
122
|
+
board = np.where(self.board == ' ', '·', self.board)
|
|
123
|
+
print(render_grid(cell_flags=res, center_char=board))
|
|
122
124
|
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=999)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def render_grid(cell_flags: np.ndarray = None,
|
|
129
|
-
H: np.ndarray = None,
|
|
130
|
-
V: np.ndarray = None,
|
|
131
|
-
mark_centers: bool = True,
|
|
132
|
-
center_char: str = '·',
|
|
133
|
-
show_axes: bool = True,
|
|
134
|
-
scale_x: int = 2) -> str:
|
|
135
|
-
"""
|
|
136
|
-
AI generated this because I don't currently care about the details of rendering to the terminal and I did it in a quick and dirty way while the AI made it in a pretty way, and this looks good during my development.
|
|
137
|
-
cell_flags: np.ndarray of shape (N, N) with characters 'U', 'D', 'L', 'R' to represent the edges of the cells.
|
|
138
|
-
OR:
|
|
139
|
-
H: (N+1, N) horizontal edges between corners
|
|
140
|
-
V: (N, N+1) vertical edges between corners
|
|
141
|
-
scale_x: horizontal stretch factor (>=1). Try 2 or 3 for squarer cells.
|
|
142
|
-
"""
|
|
143
|
-
if cell_flags is not None:
|
|
144
|
-
N = cell_flags.shape[0]
|
|
145
|
-
H = np.zeros((N+1, N), dtype=bool)
|
|
146
|
-
V = np.zeros((N, N+1), dtype=bool)
|
|
147
|
-
for r in range(N):
|
|
148
|
-
for c in range(N):
|
|
149
|
-
s = cell_flags[r, c]
|
|
150
|
-
if 'U' in s: H[r, c] = True # edge between (r,c) and (r, c+1) above the cell
|
|
151
|
-
if 'D' in s: H[r+1, c] = True # edge below the cell
|
|
152
|
-
if 'L' in s: V[r, c] = True # edge left of the cell
|
|
153
|
-
if 'R' in s: V[r, c+1] = True # edge right of the cell
|
|
154
|
-
assert H is not None and V is not None, 'H and V must be provided'
|
|
155
|
-
# Bitmask for corner connections
|
|
156
|
-
U, R, D, L = 1, 2, 4, 8
|
|
157
|
-
JUNCTION = {
|
|
158
|
-
0: ' ',
|
|
159
|
-
U: '│', D: '│', U|D: '│',
|
|
160
|
-
L: '─', R: '─', L|R: '─',
|
|
161
|
-
U|R: '└', R|D: '┌', D|L: '┐', L|U: '┘',
|
|
162
|
-
U|D|L: '┤', U|D|R: '├', L|R|U: '┴', L|R|D: '┬',
|
|
163
|
-
U|R|D|L: '┼',
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
assert scale_x >= 1
|
|
167
|
-
N = V.shape[0]
|
|
168
|
-
assert H.shape == (N+1, N) and V.shape == (N, N+1)
|
|
169
|
-
|
|
170
|
-
rows = 2*N + 1
|
|
171
|
-
cols = 2*N*scale_x + 1 # stretched width
|
|
172
|
-
canvas = [[' ']*cols for _ in range(rows)]
|
|
173
|
-
|
|
174
|
-
def x_corner(c): # x of corner column c
|
|
175
|
-
return (2*c) * scale_x
|
|
176
|
-
def x_between(c,k): # kth in-between column (1..scale_x) between c and c+1 corners
|
|
177
|
-
return (2*c) * scale_x + k
|
|
178
|
-
|
|
179
|
-
# horizontal edges: fill the stretched band between corners with '─'
|
|
180
|
-
for r in range(N+1):
|
|
181
|
-
rr = 2*r
|
|
182
|
-
for c in range(N):
|
|
183
|
-
if H[r, c]:
|
|
184
|
-
# previously: for k in range(1, scale_x*2, 2):
|
|
185
|
-
for k in range(1, scale_x*2): # 1..(2*scale_x-1), no gaps
|
|
186
|
-
canvas[rr][x_between(c, k)] = '─'
|
|
187
|
-
|
|
188
|
-
# vertical edges: draw at the corner columns (no horizontal stretching needed)
|
|
189
|
-
for r in range(N):
|
|
190
|
-
rr = 2*r + 1
|
|
191
|
-
for c in range(N+1):
|
|
192
|
-
if V[r, c]:
|
|
193
|
-
canvas[rr][x_corner(c)] = '│'
|
|
194
|
-
|
|
195
|
-
# junctions at corners
|
|
196
|
-
for r in range(N+1):
|
|
197
|
-
rr = 2*r
|
|
198
|
-
for c in range(N+1):
|
|
199
|
-
m = 0
|
|
200
|
-
if r > 0 and V[r-1, c]: m |= U
|
|
201
|
-
if c < N and H[r, c]: m |= R
|
|
202
|
-
if r < N and V[r, c]: m |= D
|
|
203
|
-
if c > 0 and H[r, c-1]: m |= L
|
|
204
|
-
canvas[rr][x_corner(c)] = JUNCTION[m]
|
|
205
|
-
|
|
206
|
-
# centers (help count exact widths/heights)
|
|
207
|
-
if mark_centers:
|
|
208
|
-
for r in range(N):
|
|
209
|
-
rr = 2*r + 1
|
|
210
|
-
for c in range(N):
|
|
211
|
-
# center lies midway across the stretched span
|
|
212
|
-
xc = x_corner(c) + scale_x # middle-ish; works for any integer scale_x
|
|
213
|
-
canvas[rr][xc] = center_char if isinstance(center_char, str) else center_char(c, r)
|
|
214
|
-
|
|
215
|
-
# turn canvas rows into strings
|
|
216
|
-
art_rows = [''.join(row) for row in canvas]
|
|
217
|
-
|
|
218
|
-
if not show_axes:
|
|
219
|
-
return '\n'.join(art_rows)
|
|
220
|
-
|
|
221
|
-
# ── Axes ────────────────────────────────────────────────────────────────
|
|
222
|
-
gut = max(2, len(str(N-1))) # left gutter width
|
|
223
|
-
gutter = ' ' * gut
|
|
224
|
-
top_tens = list(gutter + ' ' * cols)
|
|
225
|
-
top_ones = list(gutter + ' ' * cols)
|
|
226
|
-
|
|
227
|
-
for c in range(N):
|
|
228
|
-
xc_center = x_corner(c) + scale_x
|
|
229
|
-
if N >= 10:
|
|
230
|
-
top_tens[gut + xc_center] = str((c // 10) % 10)
|
|
231
|
-
top_ones[gut + xc_center] = str(c % 10)
|
|
232
|
-
|
|
233
|
-
# tiny corner labels
|
|
234
|
-
if gut >= 2:
|
|
235
|
-
top_tens[gut-2:gut] = list(' ')
|
|
236
|
-
top_ones[gut-2:gut] = list(' ')
|
|
237
|
-
|
|
238
|
-
labeled = []
|
|
239
|
-
for r, line in enumerate(art_rows):
|
|
240
|
-
if r % 2 == 1: # cell-center row
|
|
241
|
-
label = str(r//2).rjust(gut)
|
|
242
|
-
else:
|
|
243
|
-
label = ' ' * gut
|
|
244
|
-
labeled.append(label + line)
|
|
245
|
-
|
|
246
|
-
return ''.join(top_tens) + '\n' + ''.join(top_ones) + '\n' + '\n'.join(labeled)
|
|
247
|
-
|
|
248
|
-
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# THIS PARSER IS STILL VERY BUGGY
|
|
1
2
|
|
|
2
3
|
def extract_lines(bw):
|
|
3
4
|
horizontal = np.copy(bw)
|
|
@@ -157,11 +158,13 @@ def extract_yinyang_board(image_path, debug=False):
|
|
|
157
158
|
return board
|
|
158
159
|
|
|
159
160
|
if __name__ == "__main__":
|
|
161
|
+
# THIS PARSER IS STILL VERY BUGGY
|
|
160
162
|
# python .\src\puzzle_solver\puzzles\yin_yang\parse_map\parse_map.py | python .\src\puzzle_solver\utils\visualizer.py --read_stdin
|
|
161
163
|
import cv2
|
|
162
164
|
import numpy as np
|
|
163
165
|
from pathlib import Path
|
|
164
|
-
image_path = Path(__file__).parent / "input_output" / "
|
|
166
|
+
image_path = Path(__file__).parent / "input_output" / "OTozLDY2MSw3MjE=.png"
|
|
167
|
+
# image_path = Path(__file__).parent / "input_output" / "MzoyLDcwMSw2NTY=.png"
|
|
165
168
|
# image_path = Path(__file__).parent / "input_output" / "Njo5MDcsNDk4.png"
|
|
166
169
|
# image_path = Path(__file__).parent / "input_output" / "MTE6Niw0NjEsMTIx.png"
|
|
167
170
|
assert image_path.exists(), f"Image file does not exist: {image_path}"
|
|
File without changes
|
|
File without changes
|