multi-puzzle-solver 0.9.31__py3-none-any.whl → 1.0.2__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.31.dist-info → multi_puzzle_solver-1.0.2.dist-info}/METADATA +255 -1
- multi_puzzle_solver-1.0.2.dist-info/RECORD +69 -0
- puzzle_solver/__init__.py +58 -1
- puzzle_solver/core/utils_ortools.py +8 -6
- puzzle_solver/core/utils_visualizer.py +12 -11
- puzzle_solver/puzzles/binairo/binairo.py +4 -4
- puzzle_solver/puzzles/black_box/black_box.py +5 -11
- puzzle_solver/puzzles/bridges/bridges.py +1 -1
- puzzle_solver/puzzles/chess_range/chess_range.py +3 -3
- puzzle_solver/puzzles/chess_range/chess_solo.py +1 -1
- puzzle_solver/puzzles/filling/filling.py +3 -3
- puzzle_solver/puzzles/flood_it/flood_it.py +174 -0
- puzzle_solver/puzzles/flood_it/parse_map/parse_map.py +198 -0
- puzzle_solver/puzzles/galaxies/galaxies.py +1 -1
- puzzle_solver/puzzles/galaxies/parse_map/parse_map.py +3 -3
- puzzle_solver/puzzles/guess/guess.py +1 -1
- puzzle_solver/puzzles/heyawake/heyawake.py +3 -3
- puzzle_solver/puzzles/inertia/inertia.py +1 -1
- puzzle_solver/puzzles/inertia/parse_map/parse_map.py +13 -10
- puzzle_solver/puzzles/inertia/tsp.py +5 -7
- puzzle_solver/puzzles/kakuro/kakuro.py +1 -1
- puzzle_solver/puzzles/keen/keen.py +2 -2
- puzzle_solver/puzzles/minesweeper/minesweeper.py +2 -3
- puzzle_solver/puzzles/nonograms/nonograms.py +3 -3
- puzzle_solver/puzzles/norinori/norinori.py +2 -2
- puzzle_solver/puzzles/nurikabe/nurikabe.py +2 -2
- puzzle_solver/puzzles/range/range.py +1 -1
- puzzle_solver/puzzles/rectangles/rectangles.py +2 -6
- puzzle_solver/puzzles/shingoki/shingoki.py +1 -1
- puzzle_solver/puzzles/signpost/signpost.py +2 -2
- puzzle_solver/puzzles/slant/parse_map/parse_map.py +7 -5
- puzzle_solver/puzzles/slitherlink/slitherlink.py +1 -1
- puzzle_solver/puzzles/stitches/parse_map/parse_map.py +6 -5
- puzzle_solver/puzzles/stitches/stitches.py +1 -1
- puzzle_solver/puzzles/sudoku/sudoku.py +91 -20
- puzzle_solver/puzzles/tents/tents.py +2 -2
- puzzle_solver/puzzles/thermometers/thermometers.py +1 -1
- puzzle_solver/puzzles/towers/towers.py +1 -1
- puzzle_solver/puzzles/undead/undead.py +1 -1
- puzzle_solver/puzzles/unruly/unruly.py +1 -1
- puzzle_solver/puzzles/yin_yang/yin_yang.py +1 -1
- puzzle_solver/utils/visualizer.py +1 -1
- multi_puzzle_solver-0.9.31.dist-info/RECORD +0 -67
- {multi_puzzle_solver-0.9.31.dist-info → multi_puzzle_solver-1.0.2.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.9.31.dist-info → multi_puzzle_solver-1.0.2.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.
|
|
3
|
+
Version: 1.0.2
|
|
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
|
|
@@ -369,6 +369,28 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
369
369
|
</a>
|
|
370
370
|
</td>
|
|
371
371
|
</tr>
|
|
372
|
+
<tr>
|
|
373
|
+
<td align="center">
|
|
374
|
+
<a href="#kakuro-puzzle-type-51"><b>Kakuro</b><br><br>
|
|
375
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/kakuro_solved.png" alt="Kakuro" width="140">
|
|
376
|
+
</a>
|
|
377
|
+
</td>
|
|
378
|
+
<td align="center">
|
|
379
|
+
<a href="#sudoku-jigsaw-puzzle-type-52"><b>Sudoku Jigsaw</b><br><br>
|
|
380
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/sudoku_jigsaw_solved.png" alt="Sudoku Jigsaw" width="140">
|
|
381
|
+
</a>
|
|
382
|
+
</td>
|
|
383
|
+
<td align="center">
|
|
384
|
+
<a href="#sudoku-killer-puzzle-type-53"><b>Sudoku Killer</b><br><br>
|
|
385
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/sudoku_killer_solved.png" alt="Sudoku Killer" width="140">
|
|
386
|
+
</a>
|
|
387
|
+
</td>
|
|
388
|
+
<td align="center">
|
|
389
|
+
<a href="#flood-it-puzzle-type-54"><b>Flood It</b><br><br>
|
|
390
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flood_it_unsolved.png" alt="Flood It" width="140">
|
|
391
|
+
</a>
|
|
392
|
+
</td>
|
|
393
|
+
</tr>
|
|
372
394
|
</table>
|
|
373
395
|
|
|
374
396
|
</div>
|
|
@@ -434,6 +456,9 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
434
456
|
- [Binairo Plus (Puzzle Type #49)](#binairo-plus-puzzle-type-49)
|
|
435
457
|
- [Shakashaka (Puzzle Type #50)](#shakashaka-puzzle-type-50)
|
|
436
458
|
- [Kakuro (Puzzle Type #51)](#kakuro-puzzle-type-51)
|
|
459
|
+
- [Sudoku Jigsaw (Puzzle Type #52)](#sudoku-jigsaw-puzzle-type-52)
|
|
460
|
+
- [Sudoku Killer (Puzzle Type #53)](#sudoku-killer-puzzle-type-53)
|
|
461
|
+
- [Flood It (Puzzle Type #54)](#flood-it-puzzle-type-54)
|
|
437
462
|
- [Why SAT / CP-SAT?](#why-sat--cp-sat)
|
|
438
463
|
- [Testing](#testing)
|
|
439
464
|
- [Contributing](#contributing)
|
|
@@ -4823,6 +4848,232 @@ Time taken: 0.00 seconds
|
|
|
4823
4848
|
|
|
4824
4849
|
---
|
|
4825
4850
|
|
|
4851
|
+
## Sudoku Jigsaw (Puzzle Type #52)
|
|
4852
|
+
|
|
4853
|
+
* [**Play online**](https://www.puzzle-jigsaw-sudoku.com/)
|
|
4854
|
+
|
|
4855
|
+
* [**Solver Code**][52]
|
|
4856
|
+
|
|
4857
|
+
<details>
|
|
4858
|
+
<summary><strong>Rules</strong></summary>
|
|
4859
|
+
|
|
4860
|
+
1. The basic Sudoku rules apply.
|
|
4861
|
+
2. The difference is that instead of having 3x3 rectangular blocks these blocks have irregular shapes
|
|
4862
|
+
|
|
4863
|
+
</details>
|
|
4864
|
+
|
|
4865
|
+
**Unsolved puzzle**
|
|
4866
|
+
|
|
4867
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/sudoku_jigsaw_unsolved.png" alt="Sudoku Jigsaw unsolved" width="500">
|
|
4868
|
+
|
|
4869
|
+
Code to utilize this package and solve the puzzle:
|
|
4870
|
+
|
|
4871
|
+
(Note: the ids are arbitrary and simply represent cells that share a block)
|
|
4872
|
+
|
|
4873
|
+
```python
|
|
4874
|
+
import numpy as np
|
|
4875
|
+
from puzzle_solver import sudoku_solver as solver
|
|
4876
|
+
board = np.array([
|
|
4877
|
+
[ '1', ' ', ' ', '2', ' ', ' ', '8', '5', ' ' ],
|
|
4878
|
+
[ '2', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' ],
|
|
4879
|
+
[ ' ', ' ', '8', ' ', ' ', ' ', ' ', ' ', ' ' ],
|
|
4880
|
+
[ '7', ' ', ' ', '5', ' ', '1', ' ', ' ', ' ' ],
|
|
4881
|
+
[ ' ', ' ', ' ', '1', ' ', '3', ' ', ' ', ' ' ],
|
|
4882
|
+
[ ' ', ' ', ' ', '8', ' ', '4', ' ', ' ', '6' ],
|
|
4883
|
+
[ ' ', ' ', ' ', ' ', ' ', ' ', '5', ' ', ' ' ],
|
|
4884
|
+
[ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '5' ],
|
|
4885
|
+
[ ' ', '2', '6', ' ', ' ', '9', ' ', ' ', '1' ],
|
|
4886
|
+
])
|
|
4887
|
+
jigsaw_ids = np.array([
|
|
4888
|
+
['00', '00', '01', '01', '01', '01', '02', '02', '02'],
|
|
4889
|
+
['00', '00', '01', '01', '03', '01', '01', '02', '02'],
|
|
4890
|
+
['00', '00', '01', '04', '03', '03', '02', '02', '02'],
|
|
4891
|
+
['00', '04', '04', '04', '03', '03', '03', '03', '02'],
|
|
4892
|
+
['00', '00', '04', '04', '03', '03', '05', '05', '05'],
|
|
4893
|
+
['06', '04', '04', '04', '05', '05', '05', '07', '05'],
|
|
4894
|
+
['06', '08', '08', '08', '08', '05', '05', '07', '07'],
|
|
4895
|
+
['06', '06', '06', '06', '08', '07', '07', '07', '07'],
|
|
4896
|
+
['06', '06', '06', '08', '08', '08', '08', '07', '07'],
|
|
4897
|
+
])
|
|
4898
|
+
binst = solver.Board(board=board, jigsaw=jigsaw_ids, constrain_blocks=False)
|
|
4899
|
+
solutions = binst.solve_and_print()
|
|
4900
|
+
```
|
|
4901
|
+
|
|
4902
|
+
**Script Output**
|
|
4903
|
+
|
|
4904
|
+
```python
|
|
4905
|
+
Solution found
|
|
4906
|
+
[['1' '9' '4' '2' '3' '6' '8' '5' '7']
|
|
4907
|
+
['2' '8' '5' '9' '4' '7' '1' '6' '3']
|
|
4908
|
+
['6' '3' '8' '4' '7' '5' '9' '1' '2']
|
|
4909
|
+
['7' '6' '3' '5' '9' '1' '2' '8' '4']
|
|
4910
|
+
['4' '5' '2' '1' '6' '3' '7' '9' '8']
|
|
4911
|
+
['5' '7' '9' '8' '1' '4' '3' '2' '6']
|
|
4912
|
+
['3' '1' '7' '6' '8' '2' '5' '4' '9']
|
|
4913
|
+
['9' '4' '1' '7' '2' '8' '6' '3' '5']
|
|
4914
|
+
['8' '2' '6' '3' '5' '9' '4' '7' '1']]
|
|
4915
|
+
Solutions found: 1
|
|
4916
|
+
status: OPTIMAL
|
|
4917
|
+
Time taken: 0.01 seconds
|
|
4918
|
+
```
|
|
4919
|
+
|
|
4920
|
+
**Solved puzzle**
|
|
4921
|
+
|
|
4922
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/sudoku_jigsaw_solved.png" alt="Sudoku Jigsaw solved" width="500">
|
|
4923
|
+
|
|
4924
|
+
---
|
|
4925
|
+
|
|
4926
|
+
## Sudoku Killer (Puzzle Type #53)
|
|
4927
|
+
|
|
4928
|
+
* [**Play online**](https://www.puzzle-killer-sudoku.com/)
|
|
4929
|
+
|
|
4930
|
+
* [**Solver Code**][53]
|
|
4931
|
+
|
|
4932
|
+
<details>
|
|
4933
|
+
<summary><strong>Rules</strong></summary>
|
|
4934
|
+
|
|
4935
|
+
1. The basic Sudoku rules apply.
|
|
4936
|
+
2. The sum of all numbers in a cage must match the small number printed in its corner.
|
|
4937
|
+
3. No number appears more than once in a cage.
|
|
4938
|
+
|
|
4939
|
+
</details>
|
|
4940
|
+
|
|
4941
|
+
**Unsolved puzzle**
|
|
4942
|
+
|
|
4943
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/sudoku_killer_unsolved.png" alt="Sudoku Killer unsolved" width="500">
|
|
4944
|
+
|
|
4945
|
+
Code to utilize this package and solve the puzzle:
|
|
4946
|
+
|
|
4947
|
+
(Note: the ids are arbitrary and simply represent cells that share a cage)
|
|
4948
|
+
|
|
4949
|
+
```python
|
|
4950
|
+
import numpy as np
|
|
4951
|
+
from puzzle_solver import sudoku_solver as solver
|
|
4952
|
+
board = np.full((9, 9), ' ')
|
|
4953
|
+
killer_board = np.array([
|
|
4954
|
+
['01', '01', '03', '03', '03', '12', '12', '13', '14'],
|
|
4955
|
+
['02', '01', '04', '16', '16', '17', '17', '13', '14'],
|
|
4956
|
+
['02', '02', '04', '18', '19', '19', '15', '15', '14'],
|
|
4957
|
+
['11', '11', '05', '18', '19', '19', '20', '15', '23'],
|
|
4958
|
+
['10', '10', '05', '30', '31', '32', '20', '22', '23'],
|
|
4959
|
+
['08', '07', '06', '30', '31', '32', '21', '22', '24'],
|
|
4960
|
+
['08', '07', '06', '29', '31', '33', '21', '24', '24'],
|
|
4961
|
+
['09', '34', '34', '29', '28', '33', '26', '26', '25'],
|
|
4962
|
+
['09', '34', '34', '28', '28', '27', '27', '25', '25'],
|
|
4963
|
+
])
|
|
4964
|
+
killer_clues = {
|
|
4965
|
+
'01': 16, '02': 11, '03': 24, '04': 10, '05': 11, '06': 7, '07': 10, '08': 10, '09': 16,
|
|
4966
|
+
'10': 11, '11': 10, '12': 7, '13': 11, '14': 16, '15': 16, '16': 8, '17': 12, '18': 8, '19': 15,
|
|
4967
|
+
'20': 7, '21': 10, '22': 5, '23': 13, '24': 16, '25': 9, '26': 14, '27': 15, '28': 13, '29': 11,
|
|
4968
|
+
'30': 9, '31': 15, '32': 13, '33': 11, '34': 15,
|
|
4969
|
+
}
|
|
4970
|
+
binst = solver.Board(board=board, block_size=(3, 3), killer=(killer_board, killer_clues))
|
|
4971
|
+
solutions = binst.solve_and_print()
|
|
4972
|
+
```
|
|
4973
|
+
|
|
4974
|
+
**Script Output**
|
|
4975
|
+
|
|
4976
|
+
```python
|
|
4977
|
+
Solution found
|
|
4978
|
+
[['5' '4' '8' '7' '9' '1' '6' '2' '3']
|
|
4979
|
+
['3' '7' '1' '6' '2' '8' '4' '9' '5']
|
|
4980
|
+
['2' '6' '9' '5' '4' '3' '1' '7' '8']
|
|
4981
|
+
['1' '9' '4' '3' '6' '2' '5' '8' '7']
|
|
4982
|
+
['8' '3' '7' '1' '5' '9' '2' '4' '6']
|
|
4983
|
+
['6' '2' '5' '8' '7' '4' '3' '1' '9']
|
|
4984
|
+
['4' '8' '2' '9' '3' '5' '7' '6' '1']
|
|
4985
|
+
['7' '1' '3' '2' '8' '6' '9' '5' '4']
|
|
4986
|
+
['9' '5' '6' '4' '1' '7' '8' '3' '2']]
|
|
4987
|
+
Solutions found: 1
|
|
4988
|
+
status: OPTIMAL
|
|
4989
|
+
Time taken: 0.01 seconds
|
|
4990
|
+
```
|
|
4991
|
+
|
|
4992
|
+
**Solved puzzle**
|
|
4993
|
+
|
|
4994
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/sudoku_killer_solved.png" alt="Sudoku Killer solved" width="500">
|
|
4995
|
+
|
|
4996
|
+
---
|
|
4997
|
+
|
|
4998
|
+
## Flood It (Puzzle Type #54)
|
|
4999
|
+
|
|
5000
|
+
* [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/flood.html)
|
|
5001
|
+
|
|
5002
|
+
* [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/flood.html#flood)
|
|
5003
|
+
|
|
5004
|
+
* [**Solver Code**][54]
|
|
5005
|
+
|
|
5006
|
+
<details>
|
|
5007
|
+
<summary><strong>Rules</strong></summary>
|
|
5008
|
+
|
|
5009
|
+
The game is a combinatorial puzzle played on a colored N by N grid where the goal is to make the entire grid a single color using the minimum number of moves.
|
|
5010
|
+
|
|
5011
|
+
A move consists of picking a new color, which then floods the connected component of the player's current area that has that chosen color.
|
|
5012
|
+
|
|
5013
|
+
The player's current area is the top-leftmost corner of the grid along with any similarly colored orthogonal cells connected to the current area.
|
|
5014
|
+
|
|
5015
|
+
</details>
|
|
5016
|
+
|
|
5017
|
+
This game has a lot of interesting mathematical properties related to Graph Theory (for example many details are referenced in this [2022 Graph Theory paper](https://arxiv.org/pdf/1101.5876))
|
|
5018
|
+
|
|
5019
|
+
Finding an optimal solution for any graph is NP-hard.
|
|
5020
|
+
|
|
5021
|
+
**Unsolved puzzle**
|
|
5022
|
+
|
|
5023
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flood_it_unsolved.png" alt="Flood It unsolved" width="500">
|
|
5024
|
+
|
|
5025
|
+
Code to utilize this package and solve the puzzle:
|
|
5026
|
+
|
|
5027
|
+
(Note: the ids are arbitrary and simply represent cells that share a cage)
|
|
5028
|
+
|
|
5029
|
+
```python
|
|
5030
|
+
import numpy as np
|
|
5031
|
+
from puzzle_solver import flood_it_solver as solver
|
|
5032
|
+
board = np.array([
|
|
5033
|
+
['B', 'Y', 'G', 'Y', 'R', 'B', 'Y', 'Y', 'G', 'B', 'R', 'P'],
|
|
5034
|
+
['P', 'G', 'G', 'Y', 'B', 'O', 'Y', 'O', 'B', 'Y', 'R', 'O'],
|
|
5035
|
+
['B', 'R', 'P', 'Y', 'O', 'R', 'G', 'G', 'G', 'R', 'R', 'Y'],
|
|
5036
|
+
['O', 'G', 'P', 'G', 'Y', 'Y', 'P', 'P', 'O', 'Y', 'B', 'B'],
|
|
5037
|
+
['G', 'Y', 'G', 'O', 'R', 'G', 'R', 'P', 'G', 'O', 'B', 'R'],
|
|
5038
|
+
['R', 'G', 'B', 'G', 'O', 'B', 'O', 'G', 'B', 'O', 'O', 'B'],
|
|
5039
|
+
['G', 'B', 'P', 'R', 'Y', 'P', 'R', 'B', 'Y', 'B', 'Y', 'P'],
|
|
5040
|
+
['G', 'B', 'G', 'P', 'O', 'Y', 'R', 'Y', 'P', 'P', 'O', 'G'],
|
|
5041
|
+
['R', 'P', 'B', 'O', 'B', 'G', 'Y', 'O', 'Y', 'R', 'P', 'O'],
|
|
5042
|
+
['G', 'P', 'P', 'P', 'P', 'Y', 'G', 'P', 'O', 'G', 'O', 'R'],
|
|
5043
|
+
['Y', 'Y', 'B', 'B', 'R', 'B', 'O', 'R', 'O', 'O', 'R', 'O'],
|
|
5044
|
+
['B', 'G', 'B', 'G', 'R', 'B', 'P', 'Y', 'P', 'B', 'R', 'G']
|
|
5045
|
+
])
|
|
5046
|
+
solution = solver.solve_minimum_steps(board=board)
|
|
5047
|
+
```
|
|
5048
|
+
|
|
5049
|
+
**Script Output**
|
|
5050
|
+
|
|
5051
|
+
```python
|
|
5052
|
+
Trying with exactly 16 moves... Not possible!
|
|
5053
|
+
Trying with exactly 32 moves... Possible!
|
|
5054
|
+
Solution: ['Y', 'G', 'B', 'Y', 'B', 'R', 'B', 'Y', 'G', 'Y', 'G', 'B', 'B', 'G', 'Y', 'Y', 'R', 'B', 'Y', 'G', 'Y', 'B', 'Y', 'B', 'Y', 'B', 'G', 'Y', 'B', 'G', 'R', 'Y']
|
|
5055
|
+
Trying with exactly 24 moves... Possible!
|
|
5056
|
+
Solution: ['Y', 'G', 'B', 'Y', 'B', 'R', 'B', 'R', 'Y', 'G', 'R', 'Y', 'R', 'B', 'G', 'B', 'G', 'Y', 'G', 'B', 'Y', 'G', 'R', 'Y']
|
|
5057
|
+
Trying with exactly 20 moves... Possible!
|
|
5058
|
+
Solution: ['Y', 'G', 'B', 'Y', 'B', 'R', 'B', 'G', 'Y', 'G', 'B', 'G', 'Y', 'B', 'Y', 'R', 'B', 'G', 'R', 'Y']
|
|
5059
|
+
Trying with exactly 18 moves... Possible!
|
|
5060
|
+
Solution: ['Y', 'G', 'B', 'Y', 'R', 'B', 'R', 'Y', 'B', 'G', 'R', 'Y', 'R', 'B', 'G', 'R', 'Y', 'B']
|
|
5061
|
+
Trying with exactly 17 moves... Not possible!
|
|
5062
|
+
Best Horizon is: T=18
|
|
5063
|
+
Best solution is: ['Y', 'G', 'B', 'Y', 'R', 'B', 'R', 'Y', 'B', 'G', 'R', 'Y', 'R', 'B', 'G', 'R', 'Y', 'B']
|
|
5064
|
+
Time taken: 3.10 seconds
|
|
5065
|
+
```
|
|
5066
|
+
|
|
5067
|
+
**Solved puzzle**
|
|
5068
|
+
|
|
5069
|
+
This picture won't mean much as the game is about the sequence and number of moves not the final frame as shown here.
|
|
5070
|
+
|
|
5071
|
+
Note that the solved solution on the bottom left says that only 18 moves were used (based on the above output) despite the website saying 20 total moves are permitted (and the puzzle settings specified 0 extra moves permitted). Thus the solver managed to find a more optimal solution than the website.
|
|
5072
|
+
|
|
5073
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flood_it_solved.png" alt="Flood It solved" width="500">
|
|
5074
|
+
|
|
5075
|
+
---
|
|
5076
|
+
|
|
4826
5077
|
---
|
|
4827
5078
|
|
|
4828
5079
|
## Why SAT / CP-SAT?
|
|
@@ -4925,3 +5176,6 @@ Issues and PRs welcome!
|
|
|
4925
5176
|
[49]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/binairo_plus "puzzle_solver/src/puzzle_solver/puzzles/binairo_plus at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
4926
5177
|
[50]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/shakashaka "puzzle_solver/src/puzzle_solver/puzzles/shakashaka at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
4927
5178
|
[51]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/kakuro "puzzle_solver/src/puzzle_solver/puzzles/kakuro at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
5179
|
+
[52]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/sudoku "puzzle_solver/src/puzzle_solver/puzzles/sudoku at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
5180
|
+
[53]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/sudoku "puzzle_solver/src/puzzle_solver/puzzles/sudoku at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
5181
|
+
[54]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/flood_it "puzzle_solver/src/puzzle_solver/puzzles/flood_it at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
puzzle_solver/__init__.py,sha256=Ll-qN1ElCTTILeun1u4t5dU0CdI3DkCX0ZNf0Q2UJtE,4886
|
|
2
|
+
puzzle_solver/core/utils.py,sha256=XBW5j-IwtJMPMP-ycmY6SqRCM1NOVl5O6UeoGqNj618,8153
|
|
3
|
+
puzzle_solver/core/utils_ortools.py,sha256=ACV3HgKWpEUTt1lpqsPryK1DeZpu7kdWQKEWTLJ2tfs,10384
|
|
4
|
+
puzzle_solver/core/utils_visualizer.py,sha256=ymuhF75uwJbNhN8XVDYEPqw6sPKoqRaaxlhGeHtXpLs,20201
|
|
5
|
+
puzzle_solver/puzzles/aquarium/aquarium.py,sha256=BUfkAS2d9eG3TdMoe1cOGGeNYgKUebRvn-z9nsC9gvE,5708
|
|
6
|
+
puzzle_solver/puzzles/battleships/battleships.py,sha256=RuYCrs4j0vUjlU139NRYYP-uNPAgO0V7hAzbsHrRwD8,7446
|
|
7
|
+
puzzle_solver/puzzles/binairo/binairo.py,sha256=NmVPIoyVCoMLaSFhsN0TcJQYvav9hi4hSwoAVirYhDU,6835
|
|
8
|
+
puzzle_solver/puzzles/binairo/binairo_plus.py,sha256=TvLG3olwANtft3LuCF-y4OofpU9PNa4IXDqgZqsD-g0,267
|
|
9
|
+
puzzle_solver/puzzles/black_box/black_box.py,sha256=EiCVkbhUP0x94otvQirv7MrggTu0ok8MIUPbxv6jkIU,15544
|
|
10
|
+
puzzle_solver/puzzles/bridges/bridges.py,sha256=QwOhZyO5urbatkNyPmQxZ_lGM01ZejndMr_eoiBkr7g,5394
|
|
11
|
+
puzzle_solver/puzzles/chess_range/chess_melee.py,sha256=D-_Oi8OyxsVe1j3dIKYwRlxgeb3NWLmDWGcv-oclY0c,195
|
|
12
|
+
puzzle_solver/puzzles/chess_range/chess_range.py,sha256=_VHlpUPnqeBstvSIt9RtTV-w2etSK7UrEHg6sErNqtU,21068
|
|
13
|
+
puzzle_solver/puzzles/chess_range/chess_solo.py,sha256=ByDfcRsk5FVmFicpU_DpLoLTJ99Kr___vX4y8ln8_EQ,400
|
|
14
|
+
puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
|
|
15
|
+
puzzle_solver/puzzles/dominosa/dominosa.py,sha256=Nmb7pn8U27QJwGy9F3wo8ylqo2_U51OAo3GN2soaNpc,7195
|
|
16
|
+
puzzle_solver/puzzles/filling/filling.py,sha256=R8UIbztk3zNCeNbVClBJoKZHKeHwK_pesjGmMaEEQO0,5536
|
|
17
|
+
puzzle_solver/puzzles/flip/flip.py,sha256=ZngJLUhRNc7qqo2wtNLdMPx4u9w9JTUge27PmdXyDCw,3985
|
|
18
|
+
puzzle_solver/puzzles/flood_it/flood_it.py,sha256=jnCtH1sZIt6K4hbQDSsiM1Cd8FjQNP7cfw2ObUW5fEQ,7948
|
|
19
|
+
puzzle_solver/puzzles/flood_it/parse_map/parse_map.py,sha256=0aw1TbiyxknY2hUAXaP3nXqT6I6mT9BIiERJSCj57xw,8245
|
|
20
|
+
puzzle_solver/puzzles/galaxies/galaxies.py,sha256=36X9jaQfvLIWFkBY1VZH6I59eCDkc77U06NDtKRUECY,5571
|
|
21
|
+
puzzle_solver/puzzles/galaxies/parse_map/parse_map.py,sha256=XmFqVN_oRfq9AZFWy5ViUJ2Szjgx-srrRkFPJXEEyFo,9358
|
|
22
|
+
puzzle_solver/puzzles/guess/guess.py,sha256=MpyrF6YVu0S1fzX-BllwxGKRGacWJpeLbNn5GetuEyo,10792
|
|
23
|
+
puzzle_solver/puzzles/heyawake/heyawake.py,sha256=L_y44dHArOvO_tDyO35dwkvqdk9eEGItO7n4FDfzNDc,5586
|
|
24
|
+
puzzle_solver/puzzles/inertia/inertia.py,sha256=-Y5fr7aK20zwmGHsZql7pYCq1kyMZglvkVZ6uIDf1HA,5658
|
|
25
|
+
puzzle_solver/puzzles/inertia/tsp.py,sha256=mAhlSjCWespASeN8uLZ0JkYDw-ZqFEpal6NM-ubpCXw,15313
|
|
26
|
+
puzzle_solver/puzzles/inertia/parse_map/parse_map.py,sha256=x0d64gTBd0HC2lO5uOpX2VKWfwj8rRiz0mQM_lqNmWs,8457
|
|
27
|
+
puzzle_solver/puzzles/kakurasu/kakurasu.py,sha256=VNGMJnBHDi6WkghLObRLhUvkmrPaGphTTUDMC0TkQvQ,2064
|
|
28
|
+
puzzle_solver/puzzles/kakuro/kakuro.py,sha256=m22Ju-V2BdQl2Ng_pjVUSrxPCtIfqezdpebutURlhvg,4348
|
|
29
|
+
puzzle_solver/puzzles/keen/keen.py,sha256=adSA_pc1m6F6jV7a-PpQxdci1bv4psCNRNt9hMIQdSY,5034
|
|
30
|
+
puzzle_solver/puzzles/light_up/light_up.py,sha256=iSA1rjZMFsnI0V0Nxivxox4qZkB7PvUrROSHXcoUXds,4541
|
|
31
|
+
puzzle_solver/puzzles/lits/lits.py,sha256=3fPIkhAIUz8JokcfaE_ZM3b0AFEnf5xPzGJ2qnm8SWY,7099
|
|
32
|
+
puzzle_solver/puzzles/magnets/magnets.py,sha256=-Wl49JD_PKeq735zQVMQ3XSQX6gdHiY-7PKw-Sh16jw,6474
|
|
33
|
+
puzzle_solver/puzzles/map/map.py,sha256=sxc57tapB8Tsgam-yoDitln1o-EB_SbIYvO6WEYy3us,2582
|
|
34
|
+
puzzle_solver/puzzles/minesweeper/minesweeper.py,sha256=gSdFsuZ-KrwVxgI1HF2q_pYleZ6vBm9jjRTFlboVnLY,5871
|
|
35
|
+
puzzle_solver/puzzles/mosaic/mosaic.py,sha256=QX_nVpVKQg8OfaUcqFk9tKqsDyVqvZc6-XWvfI3YcSw,2175
|
|
36
|
+
puzzle_solver/puzzles/nonograms/nonograms.py,sha256=dTKfMwBL49hW3bNd34ETXW7lBRPuQeSPNSCHqHmfybg,6066
|
|
37
|
+
puzzle_solver/puzzles/norinori/norinori.py,sha256=qR7V7NbZRN_ME90R2jL47AkGik1CY6JlAPhLBMXP2Gw,4714
|
|
38
|
+
puzzle_solver/puzzles/nurikabe/nurikabe.py,sha256=3cbW7X4kAMQK8PkH_t65fzT5cI0O6tWWOqpQUVyuGT4,6501
|
|
39
|
+
puzzle_solver/puzzles/palisade/palisade.py,sha256=T-LXlaLU5OwUQ24QWJWhBUFUktg0qDODTilNmBaXs4I,5014
|
|
40
|
+
puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
|
|
41
|
+
puzzle_solver/puzzles/range/range.py,sha256=q0J3crlGfjYZSA6Dh4iMCwP_gRMWid-_8KPgggOrFKk,4410
|
|
42
|
+
puzzle_solver/puzzles/rectangles/rectangles.py,sha256=MgOhZJGr9DVHb9bB8EAuwus0_8frBqRWqMwrOvMezHQ,6918
|
|
43
|
+
puzzle_solver/puzzles/shakashaka/shakashaka.py,sha256=PRpg_qI7XA3ysAo_g1TRJsT3VwB5Vial2UcFyBOMwKQ,9571
|
|
44
|
+
puzzle_solver/puzzles/shingoki/shingoki.py,sha256=heMuL9sm3jBegItjnqX05ttmDNiHSLB77BRljpeLLWk,7417
|
|
45
|
+
puzzle_solver/puzzles/signpost/signpost.py,sha256=38LlMvP5Fx4qrTXmw4aNCt3yUbG3fhdSk6-YXmhAHFg,3861
|
|
46
|
+
puzzle_solver/puzzles/singles/singles.py,sha256=KKn_Yl-eW874Bl1UmmcqoQ5vhNiO1JbM7fxKczOV5M4,2847
|
|
47
|
+
puzzle_solver/puzzles/slant/slant.py,sha256=xF-N4PuXYfx638NP1f1mi6YncIZB4mLtXtdS79XyPbg,6122
|
|
48
|
+
puzzle_solver/puzzles/slant/parse_map/parse_map.py,sha256=8thQxWbq0qjehKb2VzgUP22PGj-9n9djwbt3LGMVLJw,4811
|
|
49
|
+
puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=JpyNQk8K4nUziwWKxSvWEkF1RRBGLnCppCWK1Yf5bt0,7052
|
|
50
|
+
puzzle_solver/puzzles/star_battle/star_battle.py,sha256=IX6w4H3sifN01kPPtrAVRCK0Nl_xlXXSHvJKw8K1EuE,3718
|
|
51
|
+
puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
|
|
52
|
+
puzzle_solver/puzzles/stitches/stitches.py,sha256=bb5JXyclkbKq350MQ9d8AuGteQwSF8knaJ0DU9M92Uw,6515
|
|
53
|
+
puzzle_solver/puzzles/stitches/parse_map/parse_map.py,sha256=b21SQvlnDM6wOl_1iUhZ7X6akpBZoOnj3kEzImBCh8Q,10497
|
|
54
|
+
puzzle_solver/puzzles/sudoku/sudoku.py,sha256=rLq0N34v3Hb10CiptXtKxX-37OlQIyjIle9Es1FAtpM,13378
|
|
55
|
+
puzzle_solver/puzzles/tapa/tapa.py,sha256=TsOQhnEvlC1JxaWiEjQg2KxRXJR49GrN71DsMvPpia8,5337
|
|
56
|
+
puzzle_solver/puzzles/tents/tents.py,sha256=jccUXWA7KWAtPKpVJJYNI6masTYWQgx0eitcQw0-6Fc,6281
|
|
57
|
+
puzzle_solver/puzzles/thermometers/thermometers.py,sha256=bGcVmpPeqL5AJtj8jkK8gYThzv9aGCd_QrWEiYBCA2s,4011
|
|
58
|
+
puzzle_solver/puzzles/towers/towers.py,sha256=OLyTf9nTFR5L32-S_fhVyBmpz4i5YUNJotwOwbw_Fjg,6500
|
|
59
|
+
puzzle_solver/puzzles/tracks/tracks.py,sha256=98xds9SKNqtOLFTRUX_KSMC7XYmZo567LOFeqotVQaM,7237
|
|
60
|
+
puzzle_solver/puzzles/undead/undead.py,sha256=IGFQysgoaKZH8rKjqlrkoHsH28ve4_hKor2f0QOsWY0,6596
|
|
61
|
+
puzzle_solver/puzzles/unequal/unequal.py,sha256=ExY2XDCrqROCDpRLfHo8uVr1zuli1QvbCdNCiDhlCac,6978
|
|
62
|
+
puzzle_solver/puzzles/unruly/unruly.py,sha256=xwOUpC12uHbmlDj2guN60VaaHpLr1Y-WmMD5TKeHbZE,3826
|
|
63
|
+
puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=5WixT_7K1HwfQ_dWbuBlQfpU8p69zB2KvOg32XJ8vno,5255
|
|
64
|
+
puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
|
|
65
|
+
puzzle_solver/utils/visualizer.py,sha256=T2g5We9J3tkhyXWoN2OrIDIJDjt6w5sDd2ksOub0ZI8,6819
|
|
66
|
+
multi_puzzle_solver-1.0.2.dist-info/METADATA,sha256=LCKeSEhi50eG0kd-PUEbBBrpY7ZPuWau5Kz4csMTN84,347154
|
|
67
|
+
multi_puzzle_solver-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
68
|
+
multi_puzzle_solver-1.0.2.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
|
|
69
|
+
multi_puzzle_solver-1.0.2.dist-info/RECORD,,
|
puzzle_solver/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from puzzle_solver.puzzles.chess_range import chess_solo as chess_solo_solver
|
|
|
9
9
|
from puzzle_solver.puzzles.chess_range import chess_melee as chess_melee_solver
|
|
10
10
|
from puzzle_solver.puzzles.dominosa import dominosa as dominosa_solver
|
|
11
11
|
from puzzle_solver.puzzles.filling import filling as filling_solver
|
|
12
|
+
from puzzle_solver.puzzles.flood_it import flood_it as flood_it_solver
|
|
12
13
|
from puzzle_solver.puzzles.flip import flip as flip_solver
|
|
13
14
|
from puzzle_solver.puzzles.galaxies import galaxies as galaxies_solver
|
|
14
15
|
from puzzle_solver.puzzles.guess import guess as guess_solver
|
|
@@ -52,4 +53,60 @@ from puzzle_solver.puzzles.yin_yang import yin_yang as yin_yang_solver
|
|
|
52
53
|
|
|
53
54
|
from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
__all__ = [
|
|
57
|
+
aquarium_solver,
|
|
58
|
+
battleships_solver,
|
|
59
|
+
binairo_solver,
|
|
60
|
+
binairo_plus_solver,
|
|
61
|
+
black_box_solver,
|
|
62
|
+
bridges_solver,
|
|
63
|
+
chess_range_solver,
|
|
64
|
+
chess_solo_solver,
|
|
65
|
+
chess_melee_solver,
|
|
66
|
+
dominosa_solver,
|
|
67
|
+
filling_solver,
|
|
68
|
+
flood_it_solver,
|
|
69
|
+
flip_solver,
|
|
70
|
+
galaxies_solver,
|
|
71
|
+
guess_solver,
|
|
72
|
+
heyawake_solver,
|
|
73
|
+
inertia_solver,
|
|
74
|
+
kakurasu_solver,
|
|
75
|
+
kakuro_solver,
|
|
76
|
+
keen_solver,
|
|
77
|
+
light_up_solver,
|
|
78
|
+
magnets_solver,
|
|
79
|
+
map_solver,
|
|
80
|
+
minesweeper_solver,
|
|
81
|
+
mosaic_solver,
|
|
82
|
+
nonograms_solver,
|
|
83
|
+
norinori_solver,
|
|
84
|
+
nurikabe_solver,
|
|
85
|
+
palisade_solver,
|
|
86
|
+
lits_solver,
|
|
87
|
+
pearl_solver,
|
|
88
|
+
range_solver,
|
|
89
|
+
rectangles_solver,
|
|
90
|
+
shakashaka_solver,
|
|
91
|
+
shingoki_solver,
|
|
92
|
+
signpost_solver,
|
|
93
|
+
singles_solver,
|
|
94
|
+
slant_solver,
|
|
95
|
+
slitherlink_solver,
|
|
96
|
+
star_battle_solver,
|
|
97
|
+
star_battle_shapeless_solver,
|
|
98
|
+
stitches_solver,
|
|
99
|
+
sudoku_solver,
|
|
100
|
+
tapa_solver,
|
|
101
|
+
tents_solver,
|
|
102
|
+
thermometers_solver,
|
|
103
|
+
towers_solver,
|
|
104
|
+
tracks_solver,
|
|
105
|
+
undead_solver,
|
|
106
|
+
unequal_solver,
|
|
107
|
+
unruly_solver,
|
|
108
|
+
yin_yang_solver,
|
|
109
|
+
inertia_image_parser,
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
__version__ = '1.0.2'
|
|
@@ -14,6 +14,8 @@ class SingleSolution:
|
|
|
14
14
|
assignment: dict[Pos, Union[str, int]]
|
|
15
15
|
|
|
16
16
|
def get_hashable_solution(self) -> str:
|
|
17
|
+
if isinstance(self.assignment, list):
|
|
18
|
+
return json.dumps(self.assignment)
|
|
17
19
|
result = []
|
|
18
20
|
for pos, v in self.assignment.items():
|
|
19
21
|
result.append((pos.x, pos.y, v))
|
|
@@ -93,14 +95,14 @@ def force_connected_component(model: cp_model.CpModel, vars_to_force: dict[Any,
|
|
|
93
95
|
Total new variables: =4V [for N by M 2D grid total is 4NM]
|
|
94
96
|
"""
|
|
95
97
|
if is_neighbor is None:
|
|
96
|
-
is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1
|
|
98
|
+
is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1 # noqa: E731
|
|
97
99
|
|
|
98
100
|
vs = vars_to_force
|
|
99
101
|
v_count = len(vs)
|
|
100
102
|
if v_count <= 2: # graph must have at least 3 nodes to possibly be disconnected
|
|
101
103
|
return {}
|
|
102
104
|
# =V model variables, one for each variable
|
|
103
|
-
is_root: dict[Pos, cp_model.IntVar] = {} # =V, defines the unique
|
|
105
|
+
is_root: dict[Pos, cp_model.IntVar] = {} # =V, defines the unique
|
|
104
106
|
prefix_zero: dict[Pos, cp_model.IntVar] = {} # =V, used for picking the unique root
|
|
105
107
|
node_height: dict[Pos, cp_model.IntVar] = {} # =V, trickles down from the root
|
|
106
108
|
max_neighbor_height: dict[Pos, cp_model.IntVar] = {} # =V, the height of the tallest neighbor
|
|
@@ -141,7 +143,7 @@ def force_connected_component(model: cp_model.CpModel, vars_to_force: dict[Any,
|
|
|
141
143
|
model.Add(node_height[pi] == max_neighbor_height[pi] - 1).OnlyEnforceIf([vs[pi], is_root[pi].Not()])
|
|
142
144
|
model.Add(node_height[pi] == v_count).OnlyEnforceIf(is_root[pi])
|
|
143
145
|
model.Add(node_height[pi] == 0).OnlyEnforceIf(vs[pi].Not())
|
|
144
|
-
|
|
146
|
+
|
|
145
147
|
# final check: all active nodes have height > 0
|
|
146
148
|
for p in keys_in_order:
|
|
147
149
|
model.Add(node_height[p] > 0).OnlyEnforceIf(vs[p])
|
|
@@ -161,7 +163,7 @@ def force_no_loops(model: cp_model.CpModel, vars_to_force: dict[Any, cp_model.In
|
|
|
161
163
|
Returns a dictionary of new variables that can be used to enforce the no component constraint.
|
|
162
164
|
"""
|
|
163
165
|
if is_neighbor is None:
|
|
164
|
-
is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1
|
|
166
|
+
is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1 # noqa: E731
|
|
165
167
|
|
|
166
168
|
vs = vars_to_force
|
|
167
169
|
v_count = len(vs)
|
|
@@ -220,8 +222,8 @@ def force_no_loops(model: cp_model.CpModel, vars_to_force: dict[Any, cp_model.In
|
|
|
220
222
|
model.Add(tree_edge[(p_parent, p)] == 0).OnlyEnforceIf([is_root[p]])
|
|
221
223
|
# every active node has exactly 1 parent except root has none
|
|
222
224
|
model.AddExactlyOne([tree_edge[(p_parent, p)] for p_parent in parent_of[p]] + [vs[p].Not(), is_root[p]])
|
|
223
|
-
|
|
224
|
-
# now each subgraph has directions where each non-root points to a single parent (and its value is parent+1).
|
|
225
|
+
|
|
226
|
+
# now each subgraph has directions where each non-root points to a single parent (and its value is parent+1).
|
|
225
227
|
# to break cycles, every non-root active node must be > all neighbors that arent children
|
|
226
228
|
|
|
227
229
|
all_new_vars: dict[str, cp_model.IntVar] = {}
|
|
@@ -94,7 +94,7 @@ def render_grid(cell_flags: np.ndarray,
|
|
|
94
94
|
for r in range(R):
|
|
95
95
|
rr = 2*r + 1
|
|
96
96
|
for c in range(C):
|
|
97
|
-
val = center_char if isinstance(center_char, str) else center_char[r, c]
|
|
97
|
+
val = center_char if isinstance(center_char, str) else (center_char(r, c) if callable(center_char) else center_char[r, c])
|
|
98
98
|
put_center_text(rr, c, '' if val is None else str(val))
|
|
99
99
|
|
|
100
100
|
# rows -> strings
|
|
@@ -159,13 +159,7 @@ def id_board_to_wall_board(id_board: np.array, border_is_wall = True) -> np.arra
|
|
|
159
159
|
def render_shaded_grid(V: int,
|
|
160
160
|
H: int,
|
|
161
161
|
is_shaded: Callable[[int, int], bool],
|
|
162
|
-
|
|
163
|
-
scale_x: int = 2,
|
|
164
|
-
scale_y: int = 1,
|
|
165
|
-
fill_char: str = '▒',
|
|
166
|
-
empty_char: str = ' ',
|
|
167
|
-
empty_text: Optional[Union[str, Callable[[int, int], Optional[str]]]] = None,
|
|
168
|
-
show_axes: bool = True) -> str:
|
|
162
|
+
empty_text: Optional[Union[str, Callable[[int, int], Optional[str]]]] = None,) -> str:
|
|
169
163
|
"""
|
|
170
164
|
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.
|
|
171
165
|
Visualize a V x H grid where each cell is shaded if is_shaded(r, c) is True.
|
|
@@ -179,6 +173,11 @@ def render_shaded_grid(V: int,
|
|
|
179
173
|
cells. If a callable (r, c) -> str|None, used per cell. Text is
|
|
180
174
|
centered within the interior row and truncated to fit.
|
|
181
175
|
"""
|
|
176
|
+
scale_x: int = 2
|
|
177
|
+
scale_y: int = 1
|
|
178
|
+
fill_char: str = '▒'
|
|
179
|
+
empty_char: str = ' '
|
|
180
|
+
show_axes: bool = True
|
|
182
181
|
assert V >= 1 and H >= 1
|
|
183
182
|
assert scale_x >= 1 and scale_y >= 1
|
|
184
183
|
assert len(fill_char) == 1 and len(empty_char) == 1
|
|
@@ -349,8 +348,10 @@ def render_bw_tiles_split(
|
|
|
349
348
|
if not use_color:
|
|
350
349
|
return ""
|
|
351
350
|
parts = []
|
|
352
|
-
if fg is not None:
|
|
353
|
-
|
|
351
|
+
if fg is not None:
|
|
352
|
+
parts.append(str(fg))
|
|
353
|
+
if bg is not None:
|
|
354
|
+
parts.append(str(bg))
|
|
354
355
|
return ("\x1b[" + ";".join(parts) + "m") if parts else ""
|
|
355
356
|
|
|
356
357
|
RESET = "\x1b[0m" if use_color else ""
|
|
@@ -501,4 +502,4 @@ def render_bw_tiles_split(
|
|
|
501
502
|
# mode="text", # ← key change
|
|
502
503
|
# text_palette="solid" # try "solid" for stark black/white
|
|
503
504
|
# )
|
|
504
|
-
# print("```text\n" + art + "\n```")
|
|
505
|
+
# print("```text\n" + art + "\n```")
|
|
@@ -56,13 +56,13 @@ class Board:
|
|
|
56
56
|
self.disallow_three_in_a_row(pos, Direction.RIGHT)
|
|
57
57
|
self.disallow_three_in_a_row(pos, Direction.DOWN)
|
|
58
58
|
|
|
59
|
-
# 3. Each row and column is unique.
|
|
59
|
+
# 3. Each row and column is unique.
|
|
60
60
|
if self.force_unique:
|
|
61
61
|
# a list per row
|
|
62
62
|
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
63
|
# a list per column
|
|
64
64
|
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
|
-
|
|
65
|
+
|
|
66
66
|
# if arithmetic is provided, add constraints for it
|
|
67
67
|
if self.arith_rows is not None:
|
|
68
68
|
assert self.arith_rows.shape == (self.V, self.H-1), f'arith_rows must be one column less than board, got {self.arith_rows.shape} for {self.board.shape}'
|
|
@@ -106,10 +106,10 @@ class Board:
|
|
|
106
106
|
|
|
107
107
|
codes = []
|
|
108
108
|
pow2 = [1 << k for k in range(m)] # weights for bit positions (LSB at index 0)
|
|
109
|
-
for i,
|
|
109
|
+
for i, line in enumerate(model_vars):
|
|
110
110
|
code = self.model.NewIntVar(0, (1 << m) - 1, f"code_{i}")
|
|
111
111
|
# Sum 2^k * r[k] == code
|
|
112
|
-
self.model.Add(code == sum(pow2[k] *
|
|
112
|
+
self.model.Add(code == sum(pow2[k] * line[k] for k in range(m)))
|
|
113
113
|
codes.append(code)
|
|
114
114
|
|
|
115
115
|
self.model.AddAllDifferent(codes)
|
|
@@ -50,7 +50,7 @@ class Board:
|
|
|
50
50
|
self.right_values = right
|
|
51
51
|
self.bottom_values = bottom
|
|
52
52
|
self.left_values = left
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
self.model = cp_model.CpModel()
|
|
55
55
|
self.ball_states: dict[Pos, cp_model.IntVar] = {}
|
|
56
56
|
# (entry_pos, T, cell_pos, direction) -> True if the beam that entered from the board at "entry_pos" is present in "cell_pos" and is going in the direction "direction" at time T
|
|
@@ -86,7 +86,7 @@ class Board:
|
|
|
86
86
|
for cell in self.get_all_pos_extended():
|
|
87
87
|
for direction in Direction:
|
|
88
88
|
self.beam_states[(entry_pos, t, cell, direction)] = self.model.NewBoolVar(f'beam:{entry_pos}:{t}:{cell}:{direction}')
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
for (entry_pos, t, cell, direction) in self.beam_states.keys():
|
|
91
91
|
if t not in self.beam_states_at_t:
|
|
92
92
|
self.beam_states_at_t[t] = {}
|
|
@@ -110,7 +110,7 @@ class Board:
|
|
|
110
110
|
beam_ids.extend((beam_id, Direction.LEFT) for beam_id in self.right_cells)
|
|
111
111
|
beam_ids.extend((beam_id, Direction.UP) for beam_id in self.bottom_cells)
|
|
112
112
|
beam_ids.extend((beam_id, Direction.RIGHT) for beam_id in self.left_cells)
|
|
113
|
-
|
|
113
|
+
|
|
114
114
|
for (beam_id, direction) in beam_ids:
|
|
115
115
|
# beam at t=0 is present at beam_id and facing direction
|
|
116
116
|
self.model.Add(self.beam_states[(beam_id, 0, beam_id, direction)] == 1)
|
|
@@ -189,7 +189,7 @@ class Board:
|
|
|
189
189
|
else:
|
|
190
190
|
ball_right = False
|
|
191
191
|
ball_right_not = True
|
|
192
|
-
|
|
192
|
+
|
|
193
193
|
pos_left = get_next_pos(cur_pos, direction_left)
|
|
194
194
|
pos_right = get_next_pos(cur_pos, direction_right)
|
|
195
195
|
pos_reflected = get_next_pos(cur_pos, reflected)
|
|
@@ -304,10 +304,4 @@ class Board:
|
|
|
304
304
|
ball_state = 'O' if single_res.assignment[pos] else ' '
|
|
305
305
|
res[pos.y][pos.x] = ball_state
|
|
306
306
|
print(res)
|
|
307
|
-
|
|
308
|
-
# print('non unique count:', count)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
307
|
+
generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
|
|
@@ -73,7 +73,7 @@ class Board:
|
|
|
73
73
|
xhoriz_min = min(horiz_bridge[0].x, horiz_bridge[1].x)
|
|
74
74
|
xhoriz_max = max(horiz_bridge[0].x, horiz_bridge[1].x)
|
|
75
75
|
yhoriz = horiz_bridge[0].y
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
# no equals because thats what the puzzle says
|
|
78
78
|
x_contained = xhoriz_min < xvert < xhoriz_max
|
|
79
79
|
y_contained = yvert_min < yhoriz < yvert_max
|
|
@@ -173,11 +173,11 @@ class Board:
|
|
|
173
173
|
self.H = 8 # board size
|
|
174
174
|
# the puzzle rules mean the only legal positions are the starting positions of the pieces
|
|
175
175
|
self.all_legal_positions: set[Pos] = {pos for _, pos in self.pieces.values()}
|
|
176
|
-
assert len(self.all_legal_positions) == len(self.pieces),
|
|
176
|
+
assert len(self.all_legal_positions) == len(self.pieces), 'positions are not unique'
|
|
177
177
|
|
|
178
178
|
self.model = cp_model.CpModel()
|
|
179
179
|
# Input numbers: N is number of piece, T is number of time steps (=N here), B is board size (=N here because the only legal positions are the starting positions of the pieces):
|
|
180
|
-
# Number of variables
|
|
180
|
+
# Number of variables
|
|
181
181
|
# piece_positions: O(NTB)
|
|
182
182
|
# is_dead: O(NT)
|
|
183
183
|
# mover: O(NT)
|
|
@@ -341,7 +341,7 @@ class Board:
|
|
|
341
341
|
for t in range(self.T - 1):
|
|
342
342
|
self.model.AddExactlyOne([self.victim[(p, t)] for p in range(self.N)])
|
|
343
343
|
|
|
344
|
-
# optional parameter to force
|
|
344
|
+
# optional parameter to force
|
|
345
345
|
if self.max_moves_per_piece is not None:
|
|
346
346
|
for p in range(self.N):
|
|
347
347
|
self.model.Add(sum([self.mover[(p, t)] for t in range(self.T - 1)]) <= self.max_moves_per_piece)
|