multi-puzzle-solver 0.9.10__tar.gz → 0.9.12__tar.gz
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.10 → multi_puzzle_solver-0.9.12}/PKG-INFO +97 -1
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/README.md +96 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/multi_puzzle_solver.egg-info/PKG-INFO +97 -1
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/multi_puzzle_solver.egg-info/SOURCES.txt +2 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/__init__.py +2 -1
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/core/utils.py +1 -1
- multi_puzzle_solver-0.9.12/src/puzzle_solver/core/utils_ortools.py +172 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/battleships/battleships.py +0 -1
- multi_puzzle_solver-0.9.12/src/puzzle_solver/puzzles/norinori/norinori.py +255 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/stitches/parse_map/parse_map.py +4 -3
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/utils/visualizer.py +15 -1
- multi_puzzle_solver-0.9.12/tests/test_norinori.py +107 -0
- multi_puzzle_solver-0.9.10/src/puzzle_solver/core/utils_ortools.py +0 -82
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/pyproject.toml +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/setup.cfg +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/multi_puzzle_solver.egg-info/dependency_links.txt +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/multi_puzzle_solver.egg-info/requires.txt +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/multi_puzzle_solver.egg-info/top_level.txt +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/aquarium/aquarium.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/bridges/bridges.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/chess_range/chess_melee.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/chess_range/chess_range.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/chess_range/chess_solo.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/dominosa/dominosa.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/filling/filling.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/guess/guess.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/inertia/inertia.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/inertia/parse_map/parse_map.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/inertia/tsp.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/kakurasu/kakurasu.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/keen/keen.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/light_up/light_up.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/magnets/magnets.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/map/map.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/minesweeper/minesweeper.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/mosaic/mosaic.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/nonograms/nonograms.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/pearl/pearl.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/range/range.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/signpost/signpost.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/singles/singles.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/star_battle/star_battle.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/star_battle/star_battle_shapeless.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/stitches/stitches.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/sudoku/sudoku.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/tents/tents.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/thermometers/thermometers.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/towers/towers.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/tracks/tracks.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/undead/undead.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/unruly/unruly.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_aquarium.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_battleships.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_bridges.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_chess_melee.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_chess_range.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_chess_solo.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_dominosa.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_filling.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_guess.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_inertia.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_kakurasu.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_keen.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_light_up.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_magnets.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_map.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_minesweeper.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_mosaic.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_nonograms.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_pearl.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_range.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_signpost.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_singles.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_star_battle.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_stitches.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_sudoku.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_tents.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_thermometers.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_towers.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_tracks.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_undead.py +0 -0
- {multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/tests/test_unruly.py +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.12
|
|
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
|
|
@@ -259,6 +259,11 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
259
259
|
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/star_battle_shapeless_solved.png" alt="Star Battle Shapeless" width="140">
|
|
260
260
|
</a>
|
|
261
261
|
</td>
|
|
262
|
+
<td align="center">
|
|
263
|
+
<a href="#norinori-puzzle-type-33"><b>Norinori</b><br><br>
|
|
264
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/norinori_solved.png" alt="Norinori" width="140">
|
|
265
|
+
</a>
|
|
266
|
+
</td>
|
|
262
267
|
</tr>
|
|
263
268
|
</table>
|
|
264
269
|
|
|
@@ -306,6 +311,7 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
306
311
|
- [Kakurasu (Puzzle Type #30)](#kakurasu-puzzle-type-30)
|
|
307
312
|
- [Star Battle (Puzzle Type #31)](#star-battle-puzzle-type-31)
|
|
308
313
|
- [Star Battle Shapeless (Puzzle Type #32)](#star-battle-shapeless-puzzle-type-32)
|
|
314
|
+
- [Norinori (Puzzle Type #33)](#norinori-puzzle-type-33)
|
|
309
315
|
- [Why SAT / CP-SAT?](#why-sat--cp-sat)
|
|
310
316
|
- [Testing](#testing)
|
|
311
317
|
- [Contributing](#contributing)
|
|
@@ -2687,6 +2693,95 @@ Time taken: 0.02 seconds
|
|
|
2687
2693
|
|
|
2688
2694
|
---
|
|
2689
2695
|
|
|
2696
|
+
## Norinori (Puzzle Type #33)
|
|
2697
|
+
|
|
2698
|
+
* [**Play online**](https://www.puzzle-norinori.com/)
|
|
2699
|
+
|
|
2700
|
+
* [**Solver Code**][33]
|
|
2701
|
+
|
|
2702
|
+
<details>
|
|
2703
|
+
<summary><strong>Rules</strong></summary>
|
|
2704
|
+
|
|
2705
|
+
You have to place one tetromino in each region in such a way that:
|
|
2706
|
+
- 2 tetrominoes of matching types cannot touch each other horizontally or vertically. Rotations and reflections count as matching.
|
|
2707
|
+
- The shaded cells should form a single connected area.
|
|
2708
|
+
- 2x2 shaded areas are not allowed.
|
|
2709
|
+
|
|
2710
|
+
* Tetromino is a shape made of 4 connected cells. There are 5 types of tetrominoes, which are usually named L, I, T, S and O, based on their shape. The O tetromino is not used in this puzzle because it is a 2x2 shape, which is not allowed.
|
|
2711
|
+
|
|
2712
|
+
</details>
|
|
2713
|
+
|
|
2714
|
+
Note: The solver is capable of solving variations where the puzzle pieces the made up of more than 4 cells (e.g., pentominoes for 5 with `polyomino_degrees=5`, or hexominoes for 6 with `polyomino_degrees=6`, etc.). By default the degree is set to 4 thus only tetrominoes are used.
|
|
2715
|
+
|
|
2716
|
+
**Unsolved puzzle**
|
|
2717
|
+
|
|
2718
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/norinori_unsolved.png" alt="Norinori unsolved" width="500">
|
|
2719
|
+
|
|
2720
|
+
Code to utilize this package and solve the puzzle:
|
|
2721
|
+
|
|
2722
|
+
```python
|
|
2723
|
+
board = np.array([
|
|
2724
|
+
['00', '00', '00', '01', '01', '02', '02', '02', '03', '03', '03', '04', '04', '05', '06', '07', '07', '08', '08', '09'],
|
|
2725
|
+
['00', '00', '00', '00', '01', '02', '03', '03', '03', '10', '04', '04', '05', '05', '06', '07', '08', '08', '09', '09'],
|
|
2726
|
+
['11', '11', '11', '01', '01', '02', '02', '03', '10', '10', '04', '04', '05', '06', '06', '07', '07', '07', '09', '12'],
|
|
2727
|
+
['11', '13', '13', '13', '01', '02', '03', '03', '03', '10', '04', '04', '06', '06', '06', '07', '12', '09', '09', '12'],
|
|
2728
|
+
['11', '11', '11', '13', '14', '14', '03', '15', '15', '10', '04', '04', '06', '16', '16', '12', '12', '09', '12', '12'],
|
|
2729
|
+
['17', '13', '13', '13', '14', '14', '03', '03', '15', '15', '04', '04', '16', '16', '16', '12', '12', '12', '12', '18'],
|
|
2730
|
+
['17', '13', '19', '13', '20', '14', '03', '03', '15', '04', '04', '16', '16', '21', '21', '22', '23', '23', '23', '18'],
|
|
2731
|
+
['17', '17', '19', '19', '20', '20', '03', '03', '24', '24', '24', '25', '25', '25', '21', '22', '23', '23', '18', '18'],
|
|
2732
|
+
['17', '26', '19', '19', '20', '20', '20', '24', '24', '20', '20', '25', '25', '21', '21', '22', '22', '23', '23', '18'],
|
|
2733
|
+
['26', '26', '26', '19', '19', '20', '20', '20', '20', '20', '25', '25', '21', '21', '21', '21', '21', '23', '27', '18'],
|
|
2734
|
+
['28', '28', '28', '29', '29', '29', '29', '20', '20', '30', '30', '25', '31', '32', '32', '32', '21', '27', '27', '27'],
|
|
2735
|
+
['28', '33', '28', '28', '28', '28', '29', '34', '34', '35', '30', '30', '31', '31', '31', '32', '32', '36', '36', '27'],
|
|
2736
|
+
['28', '33', '33', '28', '28', '29', '29', '34', '34', '35', '35', '30', '31', '31', '31', '32', '36', '36', '27', '27'],
|
|
2737
|
+
['28', '33', '37', '37', '28', '29', '34', '34', '35', '35', '38', '38', '39', '39', '40', '40', '40', '40', '27', '41'],
|
|
2738
|
+
['28', '37', '37', '37', '42', '34', '34', '34', '43', '38', '38', '38', '39', '39', '44', '44', '40', '40', '27', '41'],
|
|
2739
|
+
['37', '37', '42', '42', '42', '34', '34', '43', '43', '43', '38', '39', '39', '39', '44', '44', '27', '27', '27', '41'],
|
|
2740
|
+
['45', '45', '45', '42', '46', '34', '34', '34', '34', '38', '38', '47', '47', '47', '44', '44', '44', '27', '27', '41'],
|
|
2741
|
+
['48', '45', '45', '46', '46', '46', '46', '34', '49', '49', '49', '47', '44', '44', '44', '27', '44', '50', '27', '27'],
|
|
2742
|
+
['48', '48', '45', '46', '46', '51', '46', '52', '52', '49', '49', '53', '44', '53', '44', '27', '50', '50', '50', '27'],
|
|
2743
|
+
['48', '51', '51', '51', '51', '51', '52', '52', '52', '49', '53', '53', '53', '53', '44', '27', '27', '27', '27', '27']
|
|
2744
|
+
])
|
|
2745
|
+
binst = solver.Board(board)
|
|
2746
|
+
solutions = binst.solve_then_constrain() # solve_then_constrain NOT solve_and_print (to use #1 instead of #2 in https://github.com/google/or-tools/discussions/3347, its faster in this case)
|
|
2747
|
+
```
|
|
2748
|
+
|
|
2749
|
+
**Script Output**
|
|
2750
|
+
|
|
2751
|
+
```python
|
|
2752
|
+
Solution found
|
|
2753
|
+
[
|
|
2754
|
+
['X', 'X', 'X', ' ', ' ', 'X', 'X', 'X', ' ', ' ', ' ', ' ', ' ', 'X', ' ', 'X', ' ', 'X', 'X', ' '],
|
|
2755
|
+
[' ', 'X', ' ', ' ', 'X', 'X', ' ', ' ', ' ', 'X', ' ', ' ', 'X', 'X', ' ', 'X', 'X', 'X', ' ', ' '],
|
|
2756
|
+
['X', 'X', 'X', 'X', 'X', ' ', ' ', 'X', 'X', 'X', ' ', ' ', 'X', ' ', ' ', 'X', ' ', ' ', 'X', ' '],
|
|
2757
|
+
['X', ' ', ' ', ' ', 'X', ' ', 'X', 'X', ' ', 'X', 'X', ' ', 'X', 'X', 'X', 'X', ' ', 'X', 'X', ' '],
|
|
2758
|
+
[' ', ' ', ' ', 'X', 'X', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' ', ' ', ' ', ' ', 'X', ' ', ' '],
|
|
2759
|
+
['X', ' ', 'X', 'X', ' ', 'X', ' ', ' ', 'X', 'X', 'X', ' ', 'X', 'X', ' ', 'X', 'X', 'X', 'X', ' '],
|
|
2760
|
+
['X', ' ', ' ', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', ' ', ' ', 'X', ' ', ' ', ' ', 'X'],
|
|
2761
|
+
['X', 'X', ' ', 'X', ' ', 'X', ' ', ' ', 'X', 'X', ' ', 'X', ' ', ' ', ' ', 'X', ' ', 'X', ' ', 'X'],
|
|
2762
|
+
[' ', 'X', ' ', 'X', ' ', 'X', 'X', 'X', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', 'X', 'X', 'X', 'X'],
|
|
2763
|
+
['X', 'X', 'X', 'X', 'X', 'X', ' ', ' ', ' ', ' ', 'X', 'X', 'X', 'X', ' ', ' ', ' ', 'X', ' ', 'X'],
|
|
2764
|
+
[' ', ' ', ' ', ' ', ' ', 'X', 'X', ' ', ' ', ' ', 'X', ' ', ' ', 'X', 'X', 'X', ' ', ' ', ' ', ' '],
|
|
2765
|
+
[' ', 'X', ' ', 'X', 'X', ' ', 'X', ' ', ' ', 'X', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X', 'X', ' '],
|
|
2766
|
+
[' ', 'X', 'X', ' ', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', 'X', ' ', 'X', 'X', ' ', ' '],
|
|
2767
|
+
[' ', 'X', ' ', ' ', 'X', ' ', 'X', ' ', 'X', 'X', 'X', ' ', 'X', ' ', 'X', 'X', 'X', ' ', ' ', 'X'],
|
|
2768
|
+
[' ', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' ', 'X', 'X', ' ', ' ', 'X', ' ', ' ', 'X'],
|
|
2769
|
+
['X', 'X', ' ', 'X', 'X', ' ', 'X', 'X', 'X', 'X', 'X', ' ', ' ', 'X', ' ', ' ', 'X', 'X', ' ', 'X'],
|
|
2770
|
+
[' ', 'X', 'X', 'X', ' ', ' ', 'X', ' ', ' ', ' ', 'X', 'X', 'X', 'X', 'X', ' ', ' ', 'X', 'X', 'X'],
|
|
2771
|
+
['X', ' ', 'X', ' ', 'X', 'X', 'X', ' ', 'X', 'X', ' ', 'X', ' ', ' ', 'X', ' ', ' ', 'X', ' ', ' '],
|
|
2772
|
+
['X', 'X', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', ' '],
|
|
2773
|
+
['X', ' ', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', ' ', 'X', ' ', ' ', ' ', ' ', ' '],
|
|
2774
|
+
]
|
|
2775
|
+
Solutions found: 1
|
|
2776
|
+
Time taken: 0.38 seconds
|
|
2777
|
+
```
|
|
2778
|
+
|
|
2779
|
+
**Solved puzzle**
|
|
2780
|
+
|
|
2781
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/norinori_solved.png" alt="Norinori solved" width="500">
|
|
2782
|
+
|
|
2783
|
+
---
|
|
2784
|
+
|
|
2690
2785
|
---
|
|
2691
2786
|
|
|
2692
2787
|
## Why SAT / CP-SAT?
|
|
@@ -2770,3 +2865,4 @@ Issues and PRs welcome!
|
|
|
2770
2865
|
[30]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/kakurasu "puzzle_solver/src/puzzle_solver/puzzles/kakurasu at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
2771
2866
|
[31]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/star_battle "puzzle_solver/src/puzzle_solver/puzzles/star_battle at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
2772
2867
|
[32]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/star_battle_shapeless "puzzle_solver/src/puzzle_solver/puzzles/star_battle_shapeless at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
2868
|
+
[33]: 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"
|
|
@@ -233,6 +233,11 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
233
233
|
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/star_battle_shapeless_solved.png" alt="Star Battle Shapeless" width="140">
|
|
234
234
|
</a>
|
|
235
235
|
</td>
|
|
236
|
+
<td align="center">
|
|
237
|
+
<a href="#norinori-puzzle-type-33"><b>Norinori</b><br><br>
|
|
238
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/norinori_solved.png" alt="Norinori" width="140">
|
|
239
|
+
</a>
|
|
240
|
+
</td>
|
|
236
241
|
</tr>
|
|
237
242
|
</table>
|
|
238
243
|
|
|
@@ -280,6 +285,7 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
280
285
|
- [Kakurasu (Puzzle Type #30)](#kakurasu-puzzle-type-30)
|
|
281
286
|
- [Star Battle (Puzzle Type #31)](#star-battle-puzzle-type-31)
|
|
282
287
|
- [Star Battle Shapeless (Puzzle Type #32)](#star-battle-shapeless-puzzle-type-32)
|
|
288
|
+
- [Norinori (Puzzle Type #33)](#norinori-puzzle-type-33)
|
|
283
289
|
- [Why SAT / CP-SAT?](#why-sat--cp-sat)
|
|
284
290
|
- [Testing](#testing)
|
|
285
291
|
- [Contributing](#contributing)
|
|
@@ -2661,6 +2667,95 @@ Time taken: 0.02 seconds
|
|
|
2661
2667
|
|
|
2662
2668
|
---
|
|
2663
2669
|
|
|
2670
|
+
## Norinori (Puzzle Type #33)
|
|
2671
|
+
|
|
2672
|
+
* [**Play online**](https://www.puzzle-norinori.com/)
|
|
2673
|
+
|
|
2674
|
+
* [**Solver Code**][33]
|
|
2675
|
+
|
|
2676
|
+
<details>
|
|
2677
|
+
<summary><strong>Rules</strong></summary>
|
|
2678
|
+
|
|
2679
|
+
You have to place one tetromino in each region in such a way that:
|
|
2680
|
+
- 2 tetrominoes of matching types cannot touch each other horizontally or vertically. Rotations and reflections count as matching.
|
|
2681
|
+
- The shaded cells should form a single connected area.
|
|
2682
|
+
- 2x2 shaded areas are not allowed.
|
|
2683
|
+
|
|
2684
|
+
* Tetromino is a shape made of 4 connected cells. There are 5 types of tetrominoes, which are usually named L, I, T, S and O, based on their shape. The O tetromino is not used in this puzzle because it is a 2x2 shape, which is not allowed.
|
|
2685
|
+
|
|
2686
|
+
</details>
|
|
2687
|
+
|
|
2688
|
+
Note: The solver is capable of solving variations where the puzzle pieces the made up of more than 4 cells (e.g., pentominoes for 5 with `polyomino_degrees=5`, or hexominoes for 6 with `polyomino_degrees=6`, etc.). By default the degree is set to 4 thus only tetrominoes are used.
|
|
2689
|
+
|
|
2690
|
+
**Unsolved puzzle**
|
|
2691
|
+
|
|
2692
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/norinori_unsolved.png" alt="Norinori unsolved" width="500">
|
|
2693
|
+
|
|
2694
|
+
Code to utilize this package and solve the puzzle:
|
|
2695
|
+
|
|
2696
|
+
```python
|
|
2697
|
+
board = np.array([
|
|
2698
|
+
['00', '00', '00', '01', '01', '02', '02', '02', '03', '03', '03', '04', '04', '05', '06', '07', '07', '08', '08', '09'],
|
|
2699
|
+
['00', '00', '00', '00', '01', '02', '03', '03', '03', '10', '04', '04', '05', '05', '06', '07', '08', '08', '09', '09'],
|
|
2700
|
+
['11', '11', '11', '01', '01', '02', '02', '03', '10', '10', '04', '04', '05', '06', '06', '07', '07', '07', '09', '12'],
|
|
2701
|
+
['11', '13', '13', '13', '01', '02', '03', '03', '03', '10', '04', '04', '06', '06', '06', '07', '12', '09', '09', '12'],
|
|
2702
|
+
['11', '11', '11', '13', '14', '14', '03', '15', '15', '10', '04', '04', '06', '16', '16', '12', '12', '09', '12', '12'],
|
|
2703
|
+
['17', '13', '13', '13', '14', '14', '03', '03', '15', '15', '04', '04', '16', '16', '16', '12', '12', '12', '12', '18'],
|
|
2704
|
+
['17', '13', '19', '13', '20', '14', '03', '03', '15', '04', '04', '16', '16', '21', '21', '22', '23', '23', '23', '18'],
|
|
2705
|
+
['17', '17', '19', '19', '20', '20', '03', '03', '24', '24', '24', '25', '25', '25', '21', '22', '23', '23', '18', '18'],
|
|
2706
|
+
['17', '26', '19', '19', '20', '20', '20', '24', '24', '20', '20', '25', '25', '21', '21', '22', '22', '23', '23', '18'],
|
|
2707
|
+
['26', '26', '26', '19', '19', '20', '20', '20', '20', '20', '25', '25', '21', '21', '21', '21', '21', '23', '27', '18'],
|
|
2708
|
+
['28', '28', '28', '29', '29', '29', '29', '20', '20', '30', '30', '25', '31', '32', '32', '32', '21', '27', '27', '27'],
|
|
2709
|
+
['28', '33', '28', '28', '28', '28', '29', '34', '34', '35', '30', '30', '31', '31', '31', '32', '32', '36', '36', '27'],
|
|
2710
|
+
['28', '33', '33', '28', '28', '29', '29', '34', '34', '35', '35', '30', '31', '31', '31', '32', '36', '36', '27', '27'],
|
|
2711
|
+
['28', '33', '37', '37', '28', '29', '34', '34', '35', '35', '38', '38', '39', '39', '40', '40', '40', '40', '27', '41'],
|
|
2712
|
+
['28', '37', '37', '37', '42', '34', '34', '34', '43', '38', '38', '38', '39', '39', '44', '44', '40', '40', '27', '41'],
|
|
2713
|
+
['37', '37', '42', '42', '42', '34', '34', '43', '43', '43', '38', '39', '39', '39', '44', '44', '27', '27', '27', '41'],
|
|
2714
|
+
['45', '45', '45', '42', '46', '34', '34', '34', '34', '38', '38', '47', '47', '47', '44', '44', '44', '27', '27', '41'],
|
|
2715
|
+
['48', '45', '45', '46', '46', '46', '46', '34', '49', '49', '49', '47', '44', '44', '44', '27', '44', '50', '27', '27'],
|
|
2716
|
+
['48', '48', '45', '46', '46', '51', '46', '52', '52', '49', '49', '53', '44', '53', '44', '27', '50', '50', '50', '27'],
|
|
2717
|
+
['48', '51', '51', '51', '51', '51', '52', '52', '52', '49', '53', '53', '53', '53', '44', '27', '27', '27', '27', '27']
|
|
2718
|
+
])
|
|
2719
|
+
binst = solver.Board(board)
|
|
2720
|
+
solutions = binst.solve_then_constrain() # solve_then_constrain NOT solve_and_print (to use #1 instead of #2 in https://github.com/google/or-tools/discussions/3347, its faster in this case)
|
|
2721
|
+
```
|
|
2722
|
+
|
|
2723
|
+
**Script Output**
|
|
2724
|
+
|
|
2725
|
+
```python
|
|
2726
|
+
Solution found
|
|
2727
|
+
[
|
|
2728
|
+
['X', 'X', 'X', ' ', ' ', 'X', 'X', 'X', ' ', ' ', ' ', ' ', ' ', 'X', ' ', 'X', ' ', 'X', 'X', ' '],
|
|
2729
|
+
[' ', 'X', ' ', ' ', 'X', 'X', ' ', ' ', ' ', 'X', ' ', ' ', 'X', 'X', ' ', 'X', 'X', 'X', ' ', ' '],
|
|
2730
|
+
['X', 'X', 'X', 'X', 'X', ' ', ' ', 'X', 'X', 'X', ' ', ' ', 'X', ' ', ' ', 'X', ' ', ' ', 'X', ' '],
|
|
2731
|
+
['X', ' ', ' ', ' ', 'X', ' ', 'X', 'X', ' ', 'X', 'X', ' ', 'X', 'X', 'X', 'X', ' ', 'X', 'X', ' '],
|
|
2732
|
+
[' ', ' ', ' ', 'X', 'X', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' ', ' ', ' ', ' ', 'X', ' ', ' '],
|
|
2733
|
+
['X', ' ', 'X', 'X', ' ', 'X', ' ', ' ', 'X', 'X', 'X', ' ', 'X', 'X', ' ', 'X', 'X', 'X', 'X', ' '],
|
|
2734
|
+
['X', ' ', ' ', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', ' ', ' ', 'X', ' ', ' ', ' ', 'X'],
|
|
2735
|
+
['X', 'X', ' ', 'X', ' ', 'X', ' ', ' ', 'X', 'X', ' ', 'X', ' ', ' ', ' ', 'X', ' ', 'X', ' ', 'X'],
|
|
2736
|
+
[' ', 'X', ' ', 'X', ' ', 'X', 'X', 'X', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', 'X', 'X', 'X', 'X'],
|
|
2737
|
+
['X', 'X', 'X', 'X', 'X', 'X', ' ', ' ', ' ', ' ', 'X', 'X', 'X', 'X', ' ', ' ', ' ', 'X', ' ', 'X'],
|
|
2738
|
+
[' ', ' ', ' ', ' ', ' ', 'X', 'X', ' ', ' ', ' ', 'X', ' ', ' ', 'X', 'X', 'X', ' ', ' ', ' ', ' '],
|
|
2739
|
+
[' ', 'X', ' ', 'X', 'X', ' ', 'X', ' ', ' ', 'X', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X', 'X', ' '],
|
|
2740
|
+
[' ', 'X', 'X', ' ', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', 'X', ' ', 'X', 'X', ' ', ' '],
|
|
2741
|
+
[' ', 'X', ' ', ' ', 'X', ' ', 'X', ' ', 'X', 'X', 'X', ' ', 'X', ' ', 'X', 'X', 'X', ' ', ' ', 'X'],
|
|
2742
|
+
[' ', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' ', 'X', 'X', ' ', ' ', 'X', ' ', ' ', 'X'],
|
|
2743
|
+
['X', 'X', ' ', 'X', 'X', ' ', 'X', 'X', 'X', 'X', 'X', ' ', ' ', 'X', ' ', ' ', 'X', 'X', ' ', 'X'],
|
|
2744
|
+
[' ', 'X', 'X', 'X', ' ', ' ', 'X', ' ', ' ', ' ', 'X', 'X', 'X', 'X', 'X', ' ', ' ', 'X', 'X', 'X'],
|
|
2745
|
+
['X', ' ', 'X', ' ', 'X', 'X', 'X', ' ', 'X', 'X', ' ', 'X', ' ', ' ', 'X', ' ', ' ', 'X', ' ', ' '],
|
|
2746
|
+
['X', 'X', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', ' '],
|
|
2747
|
+
['X', ' ', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', ' ', 'X', ' ', ' ', ' ', ' ', ' '],
|
|
2748
|
+
]
|
|
2749
|
+
Solutions found: 1
|
|
2750
|
+
Time taken: 0.38 seconds
|
|
2751
|
+
```
|
|
2752
|
+
|
|
2753
|
+
**Solved puzzle**
|
|
2754
|
+
|
|
2755
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/norinori_solved.png" alt="Norinori solved" width="500">
|
|
2756
|
+
|
|
2757
|
+
---
|
|
2758
|
+
|
|
2664
2759
|
---
|
|
2665
2760
|
|
|
2666
2761
|
## Why SAT / CP-SAT?
|
|
@@ -2744,3 +2839,4 @@ Issues and PRs welcome!
|
|
|
2744
2839
|
[30]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/kakurasu "puzzle_solver/src/puzzle_solver/puzzles/kakurasu at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
2745
2840
|
[31]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/star_battle "puzzle_solver/src/puzzle_solver/puzzles/star_battle at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
2746
2841
|
[32]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/star_battle_shapeless "puzzle_solver/src/puzzle_solver/puzzles/star_battle_shapeless at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
2842
|
+
[33]: 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"
|
{multi_puzzle_solver-0.9.10 → multi_puzzle_solver-0.9.12}/src/multi_puzzle_solver.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multi-puzzle-solver
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.12
|
|
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
|
|
@@ -259,6 +259,11 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
259
259
|
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/star_battle_shapeless_solved.png" alt="Star Battle Shapeless" width="140">
|
|
260
260
|
</a>
|
|
261
261
|
</td>
|
|
262
|
+
<td align="center">
|
|
263
|
+
<a href="#norinori-puzzle-type-33"><b>Norinori</b><br><br>
|
|
264
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/norinori_solved.png" alt="Norinori" width="140">
|
|
265
|
+
</a>
|
|
266
|
+
</td>
|
|
262
267
|
</tr>
|
|
263
268
|
</table>
|
|
264
269
|
|
|
@@ -306,6 +311,7 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
|
306
311
|
- [Kakurasu (Puzzle Type #30)](#kakurasu-puzzle-type-30)
|
|
307
312
|
- [Star Battle (Puzzle Type #31)](#star-battle-puzzle-type-31)
|
|
308
313
|
- [Star Battle Shapeless (Puzzle Type #32)](#star-battle-shapeless-puzzle-type-32)
|
|
314
|
+
- [Norinori (Puzzle Type #33)](#norinori-puzzle-type-33)
|
|
309
315
|
- [Why SAT / CP-SAT?](#why-sat--cp-sat)
|
|
310
316
|
- [Testing](#testing)
|
|
311
317
|
- [Contributing](#contributing)
|
|
@@ -2687,6 +2693,95 @@ Time taken: 0.02 seconds
|
|
|
2687
2693
|
|
|
2688
2694
|
---
|
|
2689
2695
|
|
|
2696
|
+
## Norinori (Puzzle Type #33)
|
|
2697
|
+
|
|
2698
|
+
* [**Play online**](https://www.puzzle-norinori.com/)
|
|
2699
|
+
|
|
2700
|
+
* [**Solver Code**][33]
|
|
2701
|
+
|
|
2702
|
+
<details>
|
|
2703
|
+
<summary><strong>Rules</strong></summary>
|
|
2704
|
+
|
|
2705
|
+
You have to place one tetromino in each region in such a way that:
|
|
2706
|
+
- 2 tetrominoes of matching types cannot touch each other horizontally or vertically. Rotations and reflections count as matching.
|
|
2707
|
+
- The shaded cells should form a single connected area.
|
|
2708
|
+
- 2x2 shaded areas are not allowed.
|
|
2709
|
+
|
|
2710
|
+
* Tetromino is a shape made of 4 connected cells. There are 5 types of tetrominoes, which are usually named L, I, T, S and O, based on their shape. The O tetromino is not used in this puzzle because it is a 2x2 shape, which is not allowed.
|
|
2711
|
+
|
|
2712
|
+
</details>
|
|
2713
|
+
|
|
2714
|
+
Note: The solver is capable of solving variations where the puzzle pieces the made up of more than 4 cells (e.g., pentominoes for 5 with `polyomino_degrees=5`, or hexominoes for 6 with `polyomino_degrees=6`, etc.). By default the degree is set to 4 thus only tetrominoes are used.
|
|
2715
|
+
|
|
2716
|
+
**Unsolved puzzle**
|
|
2717
|
+
|
|
2718
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/norinori_unsolved.png" alt="Norinori unsolved" width="500">
|
|
2719
|
+
|
|
2720
|
+
Code to utilize this package and solve the puzzle:
|
|
2721
|
+
|
|
2722
|
+
```python
|
|
2723
|
+
board = np.array([
|
|
2724
|
+
['00', '00', '00', '01', '01', '02', '02', '02', '03', '03', '03', '04', '04', '05', '06', '07', '07', '08', '08', '09'],
|
|
2725
|
+
['00', '00', '00', '00', '01', '02', '03', '03', '03', '10', '04', '04', '05', '05', '06', '07', '08', '08', '09', '09'],
|
|
2726
|
+
['11', '11', '11', '01', '01', '02', '02', '03', '10', '10', '04', '04', '05', '06', '06', '07', '07', '07', '09', '12'],
|
|
2727
|
+
['11', '13', '13', '13', '01', '02', '03', '03', '03', '10', '04', '04', '06', '06', '06', '07', '12', '09', '09', '12'],
|
|
2728
|
+
['11', '11', '11', '13', '14', '14', '03', '15', '15', '10', '04', '04', '06', '16', '16', '12', '12', '09', '12', '12'],
|
|
2729
|
+
['17', '13', '13', '13', '14', '14', '03', '03', '15', '15', '04', '04', '16', '16', '16', '12', '12', '12', '12', '18'],
|
|
2730
|
+
['17', '13', '19', '13', '20', '14', '03', '03', '15', '04', '04', '16', '16', '21', '21', '22', '23', '23', '23', '18'],
|
|
2731
|
+
['17', '17', '19', '19', '20', '20', '03', '03', '24', '24', '24', '25', '25', '25', '21', '22', '23', '23', '18', '18'],
|
|
2732
|
+
['17', '26', '19', '19', '20', '20', '20', '24', '24', '20', '20', '25', '25', '21', '21', '22', '22', '23', '23', '18'],
|
|
2733
|
+
['26', '26', '26', '19', '19', '20', '20', '20', '20', '20', '25', '25', '21', '21', '21', '21', '21', '23', '27', '18'],
|
|
2734
|
+
['28', '28', '28', '29', '29', '29', '29', '20', '20', '30', '30', '25', '31', '32', '32', '32', '21', '27', '27', '27'],
|
|
2735
|
+
['28', '33', '28', '28', '28', '28', '29', '34', '34', '35', '30', '30', '31', '31', '31', '32', '32', '36', '36', '27'],
|
|
2736
|
+
['28', '33', '33', '28', '28', '29', '29', '34', '34', '35', '35', '30', '31', '31', '31', '32', '36', '36', '27', '27'],
|
|
2737
|
+
['28', '33', '37', '37', '28', '29', '34', '34', '35', '35', '38', '38', '39', '39', '40', '40', '40', '40', '27', '41'],
|
|
2738
|
+
['28', '37', '37', '37', '42', '34', '34', '34', '43', '38', '38', '38', '39', '39', '44', '44', '40', '40', '27', '41'],
|
|
2739
|
+
['37', '37', '42', '42', '42', '34', '34', '43', '43', '43', '38', '39', '39', '39', '44', '44', '27', '27', '27', '41'],
|
|
2740
|
+
['45', '45', '45', '42', '46', '34', '34', '34', '34', '38', '38', '47', '47', '47', '44', '44', '44', '27', '27', '41'],
|
|
2741
|
+
['48', '45', '45', '46', '46', '46', '46', '34', '49', '49', '49', '47', '44', '44', '44', '27', '44', '50', '27', '27'],
|
|
2742
|
+
['48', '48', '45', '46', '46', '51', '46', '52', '52', '49', '49', '53', '44', '53', '44', '27', '50', '50', '50', '27'],
|
|
2743
|
+
['48', '51', '51', '51', '51', '51', '52', '52', '52', '49', '53', '53', '53', '53', '44', '27', '27', '27', '27', '27']
|
|
2744
|
+
])
|
|
2745
|
+
binst = solver.Board(board)
|
|
2746
|
+
solutions = binst.solve_then_constrain() # solve_then_constrain NOT solve_and_print (to use #1 instead of #2 in https://github.com/google/or-tools/discussions/3347, its faster in this case)
|
|
2747
|
+
```
|
|
2748
|
+
|
|
2749
|
+
**Script Output**
|
|
2750
|
+
|
|
2751
|
+
```python
|
|
2752
|
+
Solution found
|
|
2753
|
+
[
|
|
2754
|
+
['X', 'X', 'X', ' ', ' ', 'X', 'X', 'X', ' ', ' ', ' ', ' ', ' ', 'X', ' ', 'X', ' ', 'X', 'X', ' '],
|
|
2755
|
+
[' ', 'X', ' ', ' ', 'X', 'X', ' ', ' ', ' ', 'X', ' ', ' ', 'X', 'X', ' ', 'X', 'X', 'X', ' ', ' '],
|
|
2756
|
+
['X', 'X', 'X', 'X', 'X', ' ', ' ', 'X', 'X', 'X', ' ', ' ', 'X', ' ', ' ', 'X', ' ', ' ', 'X', ' '],
|
|
2757
|
+
['X', ' ', ' ', ' ', 'X', ' ', 'X', 'X', ' ', 'X', 'X', ' ', 'X', 'X', 'X', 'X', ' ', 'X', 'X', ' '],
|
|
2758
|
+
[' ', ' ', ' ', 'X', 'X', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' ', ' ', ' ', ' ', 'X', ' ', ' '],
|
|
2759
|
+
['X', ' ', 'X', 'X', ' ', 'X', ' ', ' ', 'X', 'X', 'X', ' ', 'X', 'X', ' ', 'X', 'X', 'X', 'X', ' '],
|
|
2760
|
+
['X', ' ', ' ', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', ' ', ' ', 'X', ' ', ' ', ' ', 'X'],
|
|
2761
|
+
['X', 'X', ' ', 'X', ' ', 'X', ' ', ' ', 'X', 'X', ' ', 'X', ' ', ' ', ' ', 'X', ' ', 'X', ' ', 'X'],
|
|
2762
|
+
[' ', 'X', ' ', 'X', ' ', 'X', 'X', 'X', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', 'X', 'X', 'X', 'X'],
|
|
2763
|
+
['X', 'X', 'X', 'X', 'X', 'X', ' ', ' ', ' ', ' ', 'X', 'X', 'X', 'X', ' ', ' ', ' ', 'X', ' ', 'X'],
|
|
2764
|
+
[' ', ' ', ' ', ' ', ' ', 'X', 'X', ' ', ' ', ' ', 'X', ' ', ' ', 'X', 'X', 'X', ' ', ' ', ' ', ' '],
|
|
2765
|
+
[' ', 'X', ' ', 'X', 'X', ' ', 'X', ' ', ' ', 'X', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X', 'X', ' '],
|
|
2766
|
+
[' ', 'X', 'X', ' ', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', 'X', ' ', 'X', 'X', ' ', ' '],
|
|
2767
|
+
[' ', 'X', ' ', ' ', 'X', ' ', 'X', ' ', 'X', 'X', 'X', ' ', 'X', ' ', 'X', 'X', 'X', ' ', ' ', 'X'],
|
|
2768
|
+
[' ', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' ', 'X', ' ', 'X', 'X', ' ', ' ', 'X', ' ', ' ', 'X'],
|
|
2769
|
+
['X', 'X', ' ', 'X', 'X', ' ', 'X', 'X', 'X', 'X', 'X', ' ', ' ', 'X', ' ', ' ', 'X', 'X', ' ', 'X'],
|
|
2770
|
+
[' ', 'X', 'X', 'X', ' ', ' ', 'X', ' ', ' ', ' ', 'X', 'X', 'X', 'X', 'X', ' ', ' ', 'X', 'X', 'X'],
|
|
2771
|
+
['X', ' ', 'X', ' ', 'X', 'X', 'X', ' ', 'X', 'X', ' ', 'X', ' ', ' ', 'X', ' ', ' ', 'X', ' ', ' '],
|
|
2772
|
+
['X', 'X', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', ' ', 'X', ' ', ' ', 'X', ' ', 'X', 'X', 'X', ' '],
|
|
2773
|
+
['X', ' ', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', ' ', 'X', ' ', ' ', ' ', ' ', ' '],
|
|
2774
|
+
]
|
|
2775
|
+
Solutions found: 1
|
|
2776
|
+
Time taken: 0.38 seconds
|
|
2777
|
+
```
|
|
2778
|
+
|
|
2779
|
+
**Solved puzzle**
|
|
2780
|
+
|
|
2781
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/norinori_solved.png" alt="Norinori solved" width="500">
|
|
2782
|
+
|
|
2783
|
+
---
|
|
2784
|
+
|
|
2690
2785
|
---
|
|
2691
2786
|
|
|
2692
2787
|
## Why SAT / CP-SAT?
|
|
@@ -2770,3 +2865,4 @@ Issues and PRs welcome!
|
|
|
2770
2865
|
[30]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/kakurasu "puzzle_solver/src/puzzle_solver/puzzles/kakurasu at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
2771
2866
|
[31]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/star_battle "puzzle_solver/src/puzzle_solver/puzzles/star_battle at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
2772
2867
|
[32]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/star_battle_shapeless "puzzle_solver/src/puzzle_solver/puzzles/star_battle_shapeless at master · Ar-Kareem/puzzle_solver · GitHub"
|
|
2868
|
+
[33]: 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"
|
|
@@ -28,6 +28,7 @@ src/puzzle_solver/puzzles/map/map.py
|
|
|
28
28
|
src/puzzle_solver/puzzles/minesweeper/minesweeper.py
|
|
29
29
|
src/puzzle_solver/puzzles/mosaic/mosaic.py
|
|
30
30
|
src/puzzle_solver/puzzles/nonograms/nonograms.py
|
|
31
|
+
src/puzzle_solver/puzzles/norinori/norinori.py
|
|
31
32
|
src/puzzle_solver/puzzles/pearl/pearl.py
|
|
32
33
|
src/puzzle_solver/puzzles/range/range.py
|
|
33
34
|
src/puzzle_solver/puzzles/signpost/signpost.py
|
|
@@ -62,6 +63,7 @@ tests/test_map.py
|
|
|
62
63
|
tests/test_minesweeper.py
|
|
63
64
|
tests/test_mosaic.py
|
|
64
65
|
tests/test_nonograms.py
|
|
66
|
+
tests/test_norinori.py
|
|
65
67
|
tests/test_pearl.py
|
|
66
68
|
tests/test_range.py
|
|
67
69
|
tests/test_signpost.py
|
|
@@ -16,6 +16,7 @@ from puzzle_solver.puzzles.map import map as map_solver
|
|
|
16
16
|
from puzzle_solver.puzzles.minesweeper import minesweeper as minesweeper_solver
|
|
17
17
|
from puzzle_solver.puzzles.mosaic import mosaic as mosaic_solver
|
|
18
18
|
from puzzle_solver.puzzles.nonograms import nonograms as nonograms_solver
|
|
19
|
+
from puzzle_solver.puzzles.norinori import norinori as norinori_solver
|
|
19
20
|
from puzzle_solver.puzzles.pearl import pearl as pearl_solver
|
|
20
21
|
from puzzle_solver.puzzles.range import range as range_solver
|
|
21
22
|
from puzzle_solver.puzzles.signpost import signpost as signpost_solver
|
|
@@ -33,4 +34,4 @@ from puzzle_solver.puzzles.unruly import unruly as unruly_solver
|
|
|
33
34
|
|
|
34
35
|
from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
|
|
35
36
|
|
|
36
|
-
__version__ = '0.9.
|
|
37
|
+
__version__ = '0.9.12'
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import json
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, Callable, Any, Union
|
|
5
|
+
|
|
6
|
+
from ortools.sat.python import cp_model
|
|
7
|
+
from ortools.sat.python.cp_model import CpSolverSolutionCallback
|
|
8
|
+
|
|
9
|
+
from puzzle_solver.core.utils import Pos
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class SingleSolution:
|
|
14
|
+
assignment: dict[Pos, Union[str, int]]
|
|
15
|
+
|
|
16
|
+
def get_hashable_solution(self) -> str:
|
|
17
|
+
result = []
|
|
18
|
+
for pos, v in self.assignment.items():
|
|
19
|
+
result.append((pos.x, pos.y, v))
|
|
20
|
+
return json.dumps(result, sort_keys=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def and_constraint(model: cp_model.CpModel, target: cp_model.IntVar, cs: list[cp_model.IntVar]):
|
|
24
|
+
for c in cs:
|
|
25
|
+
model.Add(target <= c)
|
|
26
|
+
model.Add(target >= sum(cs) - len(cs) + 1)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def or_constraint(model: cp_model.CpModel, target: cp_model.IntVar, cs: list[cp_model.IntVar]):
|
|
30
|
+
for c in cs:
|
|
31
|
+
model.Add(target >= c)
|
|
32
|
+
model.Add(target <= sum(cs))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AllSolutionsCollector(CpSolverSolutionCallback):
|
|
37
|
+
def __init__(self,
|
|
38
|
+
board: Any,
|
|
39
|
+
board_to_solution: Callable[Any, SingleSolution],
|
|
40
|
+
max_solutions: Optional[int] = None,
|
|
41
|
+
callback: Optional[Callable[SingleSolution, None]] = None
|
|
42
|
+
):
|
|
43
|
+
super().__init__()
|
|
44
|
+
self.board = board
|
|
45
|
+
self.board_to_solution = board_to_solution
|
|
46
|
+
self.max_solutions = max_solutions
|
|
47
|
+
self.callback = callback
|
|
48
|
+
self.solutions = []
|
|
49
|
+
self.unique_solutions = set()
|
|
50
|
+
|
|
51
|
+
def on_solution_callback(self):
|
|
52
|
+
try:
|
|
53
|
+
result = self.board_to_solution(self.board, self)
|
|
54
|
+
result_json = result.get_hashable_solution()
|
|
55
|
+
if result_json in self.unique_solutions:
|
|
56
|
+
return
|
|
57
|
+
self.unique_solutions.add(result_json)
|
|
58
|
+
self.solutions.append(result)
|
|
59
|
+
if self.callback is not None:
|
|
60
|
+
self.callback(result)
|
|
61
|
+
if self.max_solutions is not None and len(self.solutions) >= self.max_solutions:
|
|
62
|
+
self.StopSearch()
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(e)
|
|
65
|
+
raise e
|
|
66
|
+
|
|
67
|
+
def generic_solve_all(board: Any, board_to_solution: Callable[Any, SingleSolution], max_solutions: Optional[int] = None, callback: Optional[Callable[[SingleSolution], None]] = None, verbose: bool = True) -> list[SingleSolution]:
|
|
68
|
+
try:
|
|
69
|
+
solver = cp_model.CpSolver()
|
|
70
|
+
solver.parameters.enumerate_all_solutions = True
|
|
71
|
+
collector = AllSolutionsCollector(board, board_to_solution, max_solutions=max_solutions, callback=callback)
|
|
72
|
+
tic = time.time()
|
|
73
|
+
solver.solve(board.model, collector)
|
|
74
|
+
if verbose:
|
|
75
|
+
print("Solutions found:", len(collector.solutions))
|
|
76
|
+
print("status:", solver.StatusName())
|
|
77
|
+
toc = time.time()
|
|
78
|
+
print(f"Time taken: {toc - tic:.2f} seconds")
|
|
79
|
+
return collector.solutions
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(e)
|
|
82
|
+
raise e
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def manhattan_distance(p1: Pos, p2: Pos) -> int:
|
|
86
|
+
return abs(p1.x - p2.x) + abs(p1.y - p2.y)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def force_connected_component(model: cp_model.CpModel, vars_to_force: dict[Any, cp_model.IntVar], is_neighbor: Callable[[Any, Any], bool] = None):
|
|
90
|
+
"""
|
|
91
|
+
Forces a single connected component of the given variables and any abstract function that defines adjacency.
|
|
92
|
+
Returns a dictionary of new variables that can be used to enforce the connected component constraint.
|
|
93
|
+
Total new variables: =(3+N)V where N is average number of neighbors, ~7*N*M for N by M 2D grid
|
|
94
|
+
WARNING: Will make solutions not unique (because the choice of parent is not unique)
|
|
95
|
+
"""
|
|
96
|
+
if is_neighbor is None:
|
|
97
|
+
is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1
|
|
98
|
+
|
|
99
|
+
vs = vars_to_force
|
|
100
|
+
v_count = len(vs)
|
|
101
|
+
# =V model variables, one for each variable
|
|
102
|
+
is_root: dict[Pos, cp_model.IntVar] = {} # =V
|
|
103
|
+
prefix_zero: dict[Pos, cp_model.IntVar] = {} # =V
|
|
104
|
+
node_mtz: dict[Pos, cp_model.IntVar] = {} # =V
|
|
105
|
+
# =NV model variables where N is average number of neighbors (with double counting)
|
|
106
|
+
# for a N by M 2D grid exactly = 4MN-2M-2N [the correction term (-2M-2N) is because the borders have less neighbors]
|
|
107
|
+
parent: dict[tuple[int, int], cp_model.IntVar] = {} # =NV
|
|
108
|
+
prefix_name = "connected_component_"
|
|
109
|
+
# total = (3+N)V [for N by M 2D grid total is (7MN-2M-2N) or simply ~7*N*M]
|
|
110
|
+
|
|
111
|
+
# must enforce some ordering
|
|
112
|
+
key_to_idx: dict[Pos, int] = {p: i for i, p in enumerate(vs.keys())}
|
|
113
|
+
idx_to_key: dict[int, Pos] = {i: p for p, i in key_to_idx.items()}
|
|
114
|
+
keys_in_order = [idx_to_key[i] for i in range(len(key_to_idx))]
|
|
115
|
+
|
|
116
|
+
for p in keys_in_order:
|
|
117
|
+
is_root[p] = model.NewBoolVar(f"{prefix_name}is_root[{p}]")
|
|
118
|
+
node_mtz[p] = model.NewIntVar(0, v_count - 1, f"{prefix_name}node_mtz[{p}]")
|
|
119
|
+
# Unique root: the smallest index i with x[i] = 1
|
|
120
|
+
# prefix_zero[i] = AND_{k < i} (not x[k])
|
|
121
|
+
prev_p = None
|
|
122
|
+
for p in keys_in_order:
|
|
123
|
+
b = model.NewBoolVar(f"{prefix_name}prefix_zero[{p}]")
|
|
124
|
+
prefix_zero[p] = b
|
|
125
|
+
if prev_p is None: # No earlier cells -> True
|
|
126
|
+
model.Add(b == 1)
|
|
127
|
+
else:
|
|
128
|
+
# b <-> (prefix_zero[i-1] & ~x[i-1])
|
|
129
|
+
and_constraint(model, b, [prefix_zero[prev_p], vs[prev_p].Not()])
|
|
130
|
+
prev_p = p
|
|
131
|
+
|
|
132
|
+
# x[i] & prefix_zero[i] -> root[i]
|
|
133
|
+
for p in keys_in_order:
|
|
134
|
+
and_constraint(model, is_root[p], [vs[p], prefix_zero[p]])
|
|
135
|
+
# Exactly one root:
|
|
136
|
+
model.Add(sum(is_root.values()) == 1)
|
|
137
|
+
|
|
138
|
+
# For each node i, consider only neighbors
|
|
139
|
+
for i, pi in enumerate(keys_in_order):
|
|
140
|
+
cand = sorted([pj for j, pj in enumerate(keys_in_order) if i != j and is_neighbor(pi, pj)])
|
|
141
|
+
# if a node is active and its not root, it must have 1 parent [the first true candidate], otherwise no parent
|
|
142
|
+
ps = []
|
|
143
|
+
for j, pj in enumerate(cand):
|
|
144
|
+
parent_ij = model.NewBoolVar(f"{prefix_name}parent[{pi},{pj}]")
|
|
145
|
+
parent[(pi,pj)] = parent_ij
|
|
146
|
+
am_i_root = is_root[pi]
|
|
147
|
+
am_i_active = vs[pi]
|
|
148
|
+
is_neighbor_active = vs[pj]
|
|
149
|
+
model.AddImplication(parent_ij, am_i_root.Not())
|
|
150
|
+
model.AddImplication(parent_ij, am_i_active)
|
|
151
|
+
model.AddImplication(parent_ij, is_neighbor_active)
|
|
152
|
+
ps.append(parent_ij)
|
|
153
|
+
# if 1 then sum(parents) = 1, if 0 then sum(parents) = 0; thus sum(parents) = var_minus_root
|
|
154
|
+
var_minus_root = vs[pi] - is_root[pi]
|
|
155
|
+
model.Add(sum(ps) == var_minus_root)
|
|
156
|
+
# MTZ constraint to force single connected component
|
|
157
|
+
model.Add(node_mtz[pi] == 0).OnlyEnforceIf(is_root[pi])
|
|
158
|
+
model.Add(node_mtz[pi] == 0).OnlyEnforceIf(vs[pi].Not())
|
|
159
|
+
for pj in cand:
|
|
160
|
+
model.Add(node_mtz[pi] == node_mtz[pj] + 1).OnlyEnforceIf(parent[(pi,pj)])
|
|
161
|
+
|
|
162
|
+
all_new_vars: dict[str, cp_model.IntVar] = {}
|
|
163
|
+
for k, v in is_root.items():
|
|
164
|
+
all_new_vars[f"{prefix_name}is_root[{k}]"] = v
|
|
165
|
+
for k, v in prefix_zero.items():
|
|
166
|
+
all_new_vars[f"{prefix_name}prefix_zero[{k}]"] = v
|
|
167
|
+
for (p1, p2), v in parent.items():
|
|
168
|
+
all_new_vars[f"{prefix_name}parent[{p1},{p2}]"] = v
|
|
169
|
+
for k, v in node_mtz.items():
|
|
170
|
+
all_new_vars[f"{prefix_name}node_mtz[{k}]"] = v
|
|
171
|
+
|
|
172
|
+
return all_new_vars
|