multi-puzzle-solver 0.9.9__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.

Files changed (82) hide show
  1. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/PKG-INFO +97 -1
  2. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/README.md +96 -0
  3. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/multi_puzzle_solver.egg-info/PKG-INFO +97 -1
  4. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/multi_puzzle_solver.egg-info/SOURCES.txt +2 -0
  5. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/__init__.py +2 -1
  6. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/core/utils.py +1 -1
  7. multi_puzzle_solver-0.9.12/src/puzzle_solver/core/utils_ortools.py +172 -0
  8. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/battleships/battleships.py +0 -1
  9. multi_puzzle_solver-0.9.12/src/puzzle_solver/puzzles/norinori/norinori.py +255 -0
  10. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/stitches/parse_map/parse_map.py +4 -3
  11. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/utils/visualizer.py +15 -1
  12. multi_puzzle_solver-0.9.12/tests/test_norinori.py +107 -0
  13. multi_puzzle_solver-0.9.9/src/puzzle_solver/core/utils_ortools.py +0 -82
  14. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/pyproject.toml +0 -0
  15. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/setup.cfg +0 -0
  16. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/multi_puzzle_solver.egg-info/dependency_links.txt +0 -0
  17. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/multi_puzzle_solver.egg-info/requires.txt +0 -0
  18. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/multi_puzzle_solver.egg-info/top_level.txt +0 -0
  19. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/aquarium/aquarium.py +0 -0
  20. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/bridges/bridges.py +0 -0
  21. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/chess_range/chess_melee.py +0 -0
  22. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/chess_range/chess_range.py +0 -0
  23. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/chess_range/chess_solo.py +0 -0
  24. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/dominosa/dominosa.py +0 -0
  25. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/filling/filling.py +0 -0
  26. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/guess/guess.py +0 -0
  27. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/inertia/inertia.py +0 -0
  28. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/inertia/parse_map/parse_map.py +0 -0
  29. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/inertia/tsp.py +0 -0
  30. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/kakurasu/kakurasu.py +0 -0
  31. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/keen/keen.py +0 -0
  32. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/light_up/light_up.py +0 -0
  33. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/magnets/magnets.py +0 -0
  34. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/map/map.py +0 -0
  35. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/minesweeper/minesweeper.py +0 -0
  36. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/mosaic/mosaic.py +0 -0
  37. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/nonograms/nonograms.py +0 -0
  38. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/pearl/pearl.py +0 -0
  39. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/range/range.py +0 -0
  40. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/signpost/signpost.py +0 -0
  41. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/singles/singles.py +0 -0
  42. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/star_battle/star_battle.py +0 -0
  43. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/star_battle/star_battle_shapeless.py +0 -0
  44. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/stitches/stitches.py +0 -0
  45. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/sudoku/sudoku.py +0 -0
  46. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/tents/tents.py +0 -0
  47. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/thermometers/thermometers.py +0 -0
  48. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/towers/towers.py +0 -0
  49. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/tracks/tracks.py +0 -0
  50. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/undead/undead.py +0 -0
  51. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/src/puzzle_solver/puzzles/unruly/unruly.py +0 -0
  52. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_aquarium.py +0 -0
  53. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_battleships.py +0 -0
  54. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_bridges.py +0 -0
  55. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_chess_melee.py +0 -0
  56. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_chess_range.py +0 -0
  57. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_chess_solo.py +0 -0
  58. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_dominosa.py +0 -0
  59. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_filling.py +0 -0
  60. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_guess.py +0 -0
  61. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_inertia.py +0 -0
  62. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_kakurasu.py +0 -0
  63. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_keen.py +0 -0
  64. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_light_up.py +0 -0
  65. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_magnets.py +0 -0
  66. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_map.py +0 -0
  67. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_minesweeper.py +0 -0
  68. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_mosaic.py +0 -0
  69. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_nonograms.py +0 -0
  70. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_pearl.py +0 -0
  71. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_range.py +0 -0
  72. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_signpost.py +0 -0
  73. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_singles.py +0 -0
  74. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_star_battle.py +0 -0
  75. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_stitches.py +0 -0
  76. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_sudoku.py +0 -0
  77. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_tents.py +0 -0
  78. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_thermometers.py +0 -0
  79. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_towers.py +0 -0
  80. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_tracks.py +0 -0
  81. {multi_puzzle_solver-0.9.9 → multi_puzzle_solver-0.9.12}/tests/test_undead.py +0 -0
  82. {multi_puzzle_solver-0.9.9 → 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.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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-puzzle-solver
3
- Version: 0.9.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.9'
37
+ __version__ = '0.9.12'
@@ -21,7 +21,7 @@ class Direction8(Enum):
21
21
  DOWN_LEFT = 7
22
22
  DOWN_RIGHT = 8
23
23
 
24
- @dataclass(frozen=True)
24
+ @dataclass(frozen=True, order=True)
25
25
  class Pos:
26
26
  x: int
27
27
  y: int
@@ -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