multi-puzzle-solver 0.9.31__py3-none-any.whl → 1.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of multi-puzzle-solver might be problematic. Click here for more details.

Files changed (46) hide show
  1. {multi_puzzle_solver-0.9.31.dist-info → multi_puzzle_solver-1.0.3.dist-info}/METADATA +335 -1
  2. multi_puzzle_solver-1.0.3.dist-info/RECORD +70 -0
  3. puzzle_solver/__init__.py +60 -1
  4. puzzle_solver/core/utils_ortools.py +8 -6
  5. puzzle_solver/core/utils_visualizer.py +12 -11
  6. puzzle_solver/puzzles/binairo/binairo.py +4 -4
  7. puzzle_solver/puzzles/black_box/black_box.py +5 -11
  8. puzzle_solver/puzzles/bridges/bridges.py +1 -1
  9. puzzle_solver/puzzles/chess_range/chess_range.py +3 -3
  10. puzzle_solver/puzzles/chess_range/chess_solo.py +1 -1
  11. puzzle_solver/puzzles/filling/filling.py +3 -3
  12. puzzle_solver/puzzles/flood_it/flood_it.py +174 -0
  13. puzzle_solver/puzzles/flood_it/parse_map/parse_map.py +198 -0
  14. puzzle_solver/puzzles/galaxies/galaxies.py +1 -1
  15. puzzle_solver/puzzles/galaxies/parse_map/parse_map.py +3 -3
  16. puzzle_solver/puzzles/guess/guess.py +1 -1
  17. puzzle_solver/puzzles/heyawake/heyawake.py +3 -3
  18. puzzle_solver/puzzles/inertia/inertia.py +1 -1
  19. puzzle_solver/puzzles/inertia/parse_map/parse_map.py +13 -10
  20. puzzle_solver/puzzles/inertia/tsp.py +5 -7
  21. puzzle_solver/puzzles/kakuro/kakuro.py +1 -1
  22. puzzle_solver/puzzles/keen/keen.py +2 -2
  23. puzzle_solver/puzzles/minesweeper/minesweeper.py +2 -3
  24. puzzle_solver/puzzles/nonograms/nonograms.py +3 -3
  25. puzzle_solver/puzzles/norinori/norinori.py +2 -2
  26. puzzle_solver/puzzles/nurikabe/nurikabe.py +2 -2
  27. puzzle_solver/puzzles/pipes/pipes.py +81 -0
  28. puzzle_solver/puzzles/range/range.py +1 -1
  29. puzzle_solver/puzzles/rectangles/rectangles.py +2 -6
  30. puzzle_solver/puzzles/shingoki/shingoki.py +1 -1
  31. puzzle_solver/puzzles/signpost/signpost.py +2 -2
  32. puzzle_solver/puzzles/slant/parse_map/parse_map.py +7 -5
  33. puzzle_solver/puzzles/slitherlink/slitherlink.py +1 -1
  34. puzzle_solver/puzzles/stitches/parse_map/parse_map.py +6 -5
  35. puzzle_solver/puzzles/stitches/stitches.py +1 -1
  36. puzzle_solver/puzzles/sudoku/sudoku.py +91 -20
  37. puzzle_solver/puzzles/tents/tents.py +2 -2
  38. puzzle_solver/puzzles/thermometers/thermometers.py +1 -1
  39. puzzle_solver/puzzles/towers/towers.py +1 -1
  40. puzzle_solver/puzzles/undead/undead.py +1 -1
  41. puzzle_solver/puzzles/unruly/unruly.py +1 -1
  42. puzzle_solver/puzzles/yin_yang/yin_yang.py +1 -1
  43. puzzle_solver/utils/visualizer.py +1 -1
  44. multi_puzzle_solver-0.9.31.dist-info/RECORD +0 -67
  45. {multi_puzzle_solver-0.9.31.dist-info → multi_puzzle_solver-1.0.3.dist-info}/WHEEL +0 -0
  46. {multi_puzzle_solver-0.9.31.dist-info → multi_puzzle_solver-1.0.3.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-puzzle-solver
3
- Version: 0.9.31
3
+ Version: 1.0.3
4
4
  Summary: Efficient solvers for numerous popular and esoteric logic puzzles using CP-SAT
5
5
  Author: Ar-Kareem
6
6
  Project-URL: Homepage, https://github.com/Ar-Kareem/puzzle_solver
@@ -369,6 +369,33 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
369
369
  </a>
370
370
  </td>
371
371
  </tr>
372
+ <tr>
373
+ <td align="center">
374
+ <a href="#kakuro-puzzle-type-51"><b>Kakuro</b><br><br>
375
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/kakuro_solved.png" alt="Kakuro" width="140">
376
+ </a>
377
+ </td>
378
+ <td align="center">
379
+ <a href="#sudoku-jigsaw-puzzle-type-52"><b>Sudoku Jigsaw</b><br><br>
380
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/sudoku_jigsaw_solved.png" alt="Sudoku Jigsaw" width="140">
381
+ </a>
382
+ </td>
383
+ <td align="center">
384
+ <a href="#sudoku-killer-puzzle-type-53"><b>Sudoku Killer</b><br><br>
385
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/sudoku_killer_solved.png" alt="Sudoku Killer" width="140">
386
+ </a>
387
+ </td>
388
+ <td align="center">
389
+ <a href="#flood-it-puzzle-type-54"><b>Flood It</b><br><br>
390
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flood_it_unsolved.png" alt="Flood It" width="140">
391
+ </a>
392
+ </td>
393
+ <td align="center">
394
+ <a href="#pipes-puzzle-type-55"><b>Pipes</b><br><br>
395
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/pipes_solved.png" alt="Pipes" width="140">
396
+ </a>
397
+ </td>
398
+ </tr>
372
399
  </table>
373
400
 
374
401
  </div>
@@ -434,6 +461,10 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
434
461
  - [Binairo Plus (Puzzle Type #49)](#binairo-plus-puzzle-type-49)
435
462
  - [Shakashaka (Puzzle Type #50)](#shakashaka-puzzle-type-50)
436
463
  - [Kakuro (Puzzle Type #51)](#kakuro-puzzle-type-51)
464
+ - [Sudoku Jigsaw (Puzzle Type #52)](#sudoku-jigsaw-puzzle-type-52)
465
+ - [Sudoku Killer (Puzzle Type #53)](#sudoku-killer-puzzle-type-53)
466
+ - [Flood It (Puzzle Type #54)](#flood-it-puzzle-type-54)
467
+ - [Pipes (Puzzle Type #55)](#pipes-puzzle-type-55)
437
468
  - [Why SAT / CP-SAT?](#why-sat--cp-sat)
438
469
  - [Testing](#testing)
439
470
  - [Contributing](#contributing)
@@ -4823,6 +4854,305 @@ Time taken: 0.00 seconds
4823
4854
 
4824
4855
  ---
4825
4856
 
4857
+ ## Sudoku Jigsaw (Puzzle Type #52)
4858
+
4859
+ * [**Play online**](https://www.puzzle-jigsaw-sudoku.com/)
4860
+
4861
+ * [**Solver Code**][52]
4862
+
4863
+ <details>
4864
+ <summary><strong>Rules</strong></summary>
4865
+
4866
+ 1. The basic Sudoku rules apply.
4867
+ 2. The difference is that instead of having 3x3 rectangular blocks these blocks have irregular shapes
4868
+
4869
+ </details>
4870
+
4871
+ **Unsolved puzzle**
4872
+
4873
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/sudoku_jigsaw_unsolved.png" alt="Sudoku Jigsaw unsolved" width="500">
4874
+
4875
+ Code to utilize this package and solve the puzzle:
4876
+
4877
+ (Note: the ids are arbitrary and simply represent cells that share a block)
4878
+
4879
+ ```python
4880
+ import numpy as np
4881
+ from puzzle_solver import sudoku_solver as solver
4882
+ board = np.array([
4883
+ [ '1', ' ', ' ', '2', ' ', ' ', '8', '5', ' ' ],
4884
+ [ '2', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' ],
4885
+ [ ' ', ' ', '8', ' ', ' ', ' ', ' ', ' ', ' ' ],
4886
+ [ '7', ' ', ' ', '5', ' ', '1', ' ', ' ', ' ' ],
4887
+ [ ' ', ' ', ' ', '1', ' ', '3', ' ', ' ', ' ' ],
4888
+ [ ' ', ' ', ' ', '8', ' ', '4', ' ', ' ', '6' ],
4889
+ [ ' ', ' ', ' ', ' ', ' ', ' ', '5', ' ', ' ' ],
4890
+ [ ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '5' ],
4891
+ [ ' ', '2', '6', ' ', ' ', '9', ' ', ' ', '1' ],
4892
+ ])
4893
+ jigsaw_ids = np.array([
4894
+ ['00', '00', '01', '01', '01', '01', '02', '02', '02'],
4895
+ ['00', '00', '01', '01', '03', '01', '01', '02', '02'],
4896
+ ['00', '00', '01', '04', '03', '03', '02', '02', '02'],
4897
+ ['00', '04', '04', '04', '03', '03', '03', '03', '02'],
4898
+ ['00', '00', '04', '04', '03', '03', '05', '05', '05'],
4899
+ ['06', '04', '04', '04', '05', '05', '05', '07', '05'],
4900
+ ['06', '08', '08', '08', '08', '05', '05', '07', '07'],
4901
+ ['06', '06', '06', '06', '08', '07', '07', '07', '07'],
4902
+ ['06', '06', '06', '08', '08', '08', '08', '07', '07'],
4903
+ ])
4904
+ binst = solver.Board(board=board, jigsaw=jigsaw_ids, constrain_blocks=False)
4905
+ solutions = binst.solve_and_print()
4906
+ ```
4907
+
4908
+ **Script Output**
4909
+
4910
+ ```python
4911
+ Solution found
4912
+ [['1' '9' '4' '2' '3' '6' '8' '5' '7']
4913
+ ['2' '8' '5' '9' '4' '7' '1' '6' '3']
4914
+ ['6' '3' '8' '4' '7' '5' '9' '1' '2']
4915
+ ['7' '6' '3' '5' '9' '1' '2' '8' '4']
4916
+ ['4' '5' '2' '1' '6' '3' '7' '9' '8']
4917
+ ['5' '7' '9' '8' '1' '4' '3' '2' '6']
4918
+ ['3' '1' '7' '6' '8' '2' '5' '4' '9']
4919
+ ['9' '4' '1' '7' '2' '8' '6' '3' '5']
4920
+ ['8' '2' '6' '3' '5' '9' '4' '7' '1']]
4921
+ Solutions found: 1
4922
+ status: OPTIMAL
4923
+ Time taken: 0.01 seconds
4924
+ ```
4925
+
4926
+ **Solved puzzle**
4927
+
4928
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/sudoku_jigsaw_solved.png" alt="Sudoku Jigsaw solved" width="500">
4929
+
4930
+ ---
4931
+
4932
+ ## Sudoku Killer (Puzzle Type #53)
4933
+
4934
+ * [**Play online**](https://www.puzzle-killer-sudoku.com/)
4935
+
4936
+ * [**Solver Code**][53]
4937
+
4938
+ <details>
4939
+ <summary><strong>Rules</strong></summary>
4940
+
4941
+ 1. The basic Sudoku rules apply.
4942
+ 2. The sum of all numbers in a cage must match the small number printed in its corner.
4943
+ 3. No number appears more than once in a cage.
4944
+
4945
+ </details>
4946
+
4947
+ **Unsolved puzzle**
4948
+
4949
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/sudoku_killer_unsolved.png" alt="Sudoku Killer unsolved" width="500">
4950
+
4951
+ Code to utilize this package and solve the puzzle:
4952
+
4953
+ (Note: the ids are arbitrary and simply represent cells that share a cage)
4954
+
4955
+ ```python
4956
+ import numpy as np
4957
+ from puzzle_solver import sudoku_solver as solver
4958
+ board = np.full((9, 9), ' ')
4959
+ killer_board = np.array([
4960
+ ['01', '01', '03', '03', '03', '12', '12', '13', '14'],
4961
+ ['02', '01', '04', '16', '16', '17', '17', '13', '14'],
4962
+ ['02', '02', '04', '18', '19', '19', '15', '15', '14'],
4963
+ ['11', '11', '05', '18', '19', '19', '20', '15', '23'],
4964
+ ['10', '10', '05', '30', '31', '32', '20', '22', '23'],
4965
+ ['08', '07', '06', '30', '31', '32', '21', '22', '24'],
4966
+ ['08', '07', '06', '29', '31', '33', '21', '24', '24'],
4967
+ ['09', '34', '34', '29', '28', '33', '26', '26', '25'],
4968
+ ['09', '34', '34', '28', '28', '27', '27', '25', '25'],
4969
+ ])
4970
+ killer_clues = {
4971
+ '01': 16, '02': 11, '03': 24, '04': 10, '05': 11, '06': 7, '07': 10, '08': 10, '09': 16,
4972
+ '10': 11, '11': 10, '12': 7, '13': 11, '14': 16, '15': 16, '16': 8, '17': 12, '18': 8, '19': 15,
4973
+ '20': 7, '21': 10, '22': 5, '23': 13, '24': 16, '25': 9, '26': 14, '27': 15, '28': 13, '29': 11,
4974
+ '30': 9, '31': 15, '32': 13, '33': 11, '34': 15,
4975
+ }
4976
+ binst = solver.Board(board=board, block_size=(3, 3), killer=(killer_board, killer_clues))
4977
+ solutions = binst.solve_and_print()
4978
+ ```
4979
+
4980
+ **Script Output**
4981
+
4982
+ ```python
4983
+ Solution found
4984
+ [['5' '4' '8' '7' '9' '1' '6' '2' '3']
4985
+ ['3' '7' '1' '6' '2' '8' '4' '9' '5']
4986
+ ['2' '6' '9' '5' '4' '3' '1' '7' '8']
4987
+ ['1' '9' '4' '3' '6' '2' '5' '8' '7']
4988
+ ['8' '3' '7' '1' '5' '9' '2' '4' '6']
4989
+ ['6' '2' '5' '8' '7' '4' '3' '1' '9']
4990
+ ['4' '8' '2' '9' '3' '5' '7' '6' '1']
4991
+ ['7' '1' '3' '2' '8' '6' '9' '5' '4']
4992
+ ['9' '5' '6' '4' '1' '7' '8' '3' '2']]
4993
+ Solutions found: 1
4994
+ status: OPTIMAL
4995
+ Time taken: 0.01 seconds
4996
+ ```
4997
+
4998
+ **Solved puzzle**
4999
+
5000
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/sudoku_killer_solved.png" alt="Sudoku Killer solved" width="500">
5001
+
5002
+ ---
5003
+
5004
+ ## Flood It (Puzzle Type #54)
5005
+
5006
+ * [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/flood.html)
5007
+
5008
+ * [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/flood.html#flood)
5009
+
5010
+ * [**Solver Code**][54]
5011
+
5012
+ <details>
5013
+ <summary><strong>Rules</strong></summary>
5014
+
5015
+ The game is a combinatorial puzzle played on a colored N by N grid where the goal is to make the entire grid a single color using the minimum number of moves.
5016
+
5017
+ A move consists of picking a new color, which then floods the connected component of the player's current area that has that chosen color.
5018
+
5019
+ The player's current area is the top-leftmost corner of the grid along with any similarly colored orthogonal cells connected to the current area.
5020
+
5021
+ </details>
5022
+
5023
+ This game has a lot of interesting mathematical properties related to Graph Theory (for example many details are referenced in this [2022 Graph Theory paper](https://arxiv.org/pdf/1101.5876))
5024
+
5025
+ Finding an optimal solution for any graph is NP-hard.
5026
+
5027
+ **Unsolved puzzle**
5028
+
5029
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flood_it_unsolved.png" alt="Flood It unsolved" width="500">
5030
+
5031
+ Code to utilize this package and solve the puzzle:
5032
+
5033
+ (Note: the ids are arbitrary and simply represent cells that share a cage)
5034
+
5035
+ ```python
5036
+ import numpy as np
5037
+ from puzzle_solver import flood_it_solver as solver
5038
+ board = np.array([
5039
+ ['B', 'Y', 'G', 'Y', 'R', 'B', 'Y', 'Y', 'G', 'B', 'R', 'P'],
5040
+ ['P', 'G', 'G', 'Y', 'B', 'O', 'Y', 'O', 'B', 'Y', 'R', 'O'],
5041
+ ['B', 'R', 'P', 'Y', 'O', 'R', 'G', 'G', 'G', 'R', 'R', 'Y'],
5042
+ ['O', 'G', 'P', 'G', 'Y', 'Y', 'P', 'P', 'O', 'Y', 'B', 'B'],
5043
+ ['G', 'Y', 'G', 'O', 'R', 'G', 'R', 'P', 'G', 'O', 'B', 'R'],
5044
+ ['R', 'G', 'B', 'G', 'O', 'B', 'O', 'G', 'B', 'O', 'O', 'B'],
5045
+ ['G', 'B', 'P', 'R', 'Y', 'P', 'R', 'B', 'Y', 'B', 'Y', 'P'],
5046
+ ['G', 'B', 'G', 'P', 'O', 'Y', 'R', 'Y', 'P', 'P', 'O', 'G'],
5047
+ ['R', 'P', 'B', 'O', 'B', 'G', 'Y', 'O', 'Y', 'R', 'P', 'O'],
5048
+ ['G', 'P', 'P', 'P', 'P', 'Y', 'G', 'P', 'O', 'G', 'O', 'R'],
5049
+ ['Y', 'Y', 'B', 'B', 'R', 'B', 'O', 'R', 'O', 'O', 'R', 'O'],
5050
+ ['B', 'G', 'B', 'G', 'R', 'B', 'P', 'Y', 'P', 'B', 'R', 'G']
5051
+ ])
5052
+ solution = solver.solve_minimum_steps(board=board)
5053
+ ```
5054
+
5055
+ **Script Output**
5056
+
5057
+ ```python
5058
+ Trying with exactly 16 moves... Not possible!
5059
+ Trying with exactly 32 moves... Possible!
5060
+ Solution: ['Y', 'G', 'B', 'Y', 'B', 'R', 'B', 'Y', 'G', 'Y', 'G', 'B', 'B', 'G', 'Y', 'Y', 'R', 'B', 'Y', 'G', 'Y', 'B', 'Y', 'B', 'Y', 'B', 'G', 'Y', 'B', 'G', 'R', 'Y']
5061
+ Trying with exactly 24 moves... Possible!
5062
+ Solution: ['Y', 'G', 'B', 'Y', 'B', 'R', 'B', 'R', 'Y', 'G', 'R', 'Y', 'R', 'B', 'G', 'B', 'G', 'Y', 'G', 'B', 'Y', 'G', 'R', 'Y']
5063
+ Trying with exactly 20 moves... Possible!
5064
+ Solution: ['Y', 'G', 'B', 'Y', 'B', 'R', 'B', 'G', 'Y', 'G', 'B', 'G', 'Y', 'B', 'Y', 'R', 'B', 'G', 'R', 'Y']
5065
+ Trying with exactly 18 moves... Possible!
5066
+ Solution: ['Y', 'G', 'B', 'Y', 'R', 'B', 'R', 'Y', 'B', 'G', 'R', 'Y', 'R', 'B', 'G', 'R', 'Y', 'B']
5067
+ Trying with exactly 17 moves... Not possible!
5068
+ Best Horizon is: T=18
5069
+ Best solution is: ['Y', 'G', 'B', 'Y', 'R', 'B', 'R', 'Y', 'B', 'G', 'R', 'Y', 'R', 'B', 'G', 'R', 'Y', 'B']
5070
+ Time taken: 3.10 seconds
5071
+ ```
5072
+
5073
+ **Solved puzzle**
5074
+
5075
+ This picture won't mean much as the game is about the sequence and number of moves not the final frame as shown here.
5076
+
5077
+ Note that the solved solution on the bottom left says that only 18 moves were used (based on the above output) despite the website saying 20 total moves are permitted (and the puzzle settings specified 0 extra moves permitted). Thus the solver managed to find a more optimal solution than the website.
5078
+
5079
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flood_it_solved.png" alt="Flood It solved" width="500">
5080
+
5081
+ ---
5082
+
5083
+ ## Pipes (Puzzle Type #55)
5084
+
5085
+ Also called "Net"
5086
+
5087
+ * [**Play online 1**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/net.html)
5088
+
5089
+ * [**Play online 2**](https://www.puzzle-pipes.com/)
5090
+
5091
+ * [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/net.html#net)
5092
+
5093
+ * [**Solver Code**][55]
5094
+
5095
+ <details>
5096
+ <summary><strong>Rules</strong></summary>
5097
+
5098
+ You are given a grid of cells where each cell has 1, 2, 3, or 4 connections to its neighbors. Each cell can be freely rotated in multiple of 90 degrees, thus your can rotate the cells to be one of four possible states.
5099
+
5100
+ The goal is to create a single fully connected graph where each cell's connection must be towards another cell's connection. No loose ends or loops are allowed.
5101
+
5102
+ </details>
5103
+
5104
+ **Unsolved puzzle**
5105
+
5106
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/pipes_unsolved.png" alt="Pipes unsolved" width="500">
5107
+
5108
+ Code to utilize this package and solve the puzzle:
5109
+
5110
+ (Note: cells with 1 or 3 active connections only have 1 unique orientation under rotational symmetry. However, cells with 2 active connections can be either a straight line (2I) or curved line (2L))
5111
+
5112
+ ```python
5113
+ import numpy as np
5114
+ from puzzle_solver import pipes_solver as solver
5115
+ board=np.array([
5116
+ [ '1 ', '3 ', '3 ', '3 ', '1 ', '1 ', '2L', '2L', '2I', '1 ' ],
5117
+ [ '1 ', '1 ', '1 ', '3 ', '2I', '1 ', '2I', '3 ', '2I', '1 ' ],
5118
+ [ '2I', '1 ', '1 ', '3 ', '2L', '1 ', '3 ', '2I', '1 ', '1 ' ],
5119
+ [ '2I', '2I', '1 ', '3 ', '3 ', '3 ', '2L', '3 ', '3 ', '2L' ],
5120
+ [ '3 ', '3 ', '2I', '3 ', '1 ', '3 ', '2I', '2L', '1 ', '2L' ],
5121
+ [ '1 ', '1 ', '3 ', '2I', '3 ', '2L', '1 ', '1 ', '2L', '2L' ],
5122
+ [ '1 ', '1 ', '3 ', '1 ', '1 ', '1 ', '3 ', '3 ', '3 ', '2L' ],
5123
+ [ '3 ', '2I', '3 ', '3 ', '2L', '3 ', '3 ', '2I', '2L', '1 ' ],
5124
+ [ '1 ', '1 ', '3 ', '3 ', '3 ', '3 ', '1 ', '2L', '3 ', '2L' ],
5125
+ [ '1 ', '2I', '3 ', '2I', '1 ', '1 ', '1 ', '3 ', '1 ', '1 ' ],
5126
+ ])
5127
+ binst = solver.Board(board=board)
5128
+ solutions = binst.solve_and_print()
5129
+ ```
5130
+
5131
+ **Script Output**
5132
+
5133
+ ```python
5134
+ Solution found
5135
+ [['R' 'DLR' 'DLR' 'DLR' 'L' 'R' 'DL' 'DR' 'LR' 'L']
5136
+ ['D' 'U' 'U' 'UDR' 'LR' 'L' 'UD' 'UDR' 'LR' 'L']
5137
+ ['UD' 'D' 'R' 'ULR' 'DL' 'R' 'UDL' 'UD' 'D' 'D']
5138
+ ['UD' 'UD' 'R' 'DLR' 'ULR' 'DLR' 'UL' 'UDR' 'ULR' 'UL']
5139
+ ['UDR' 'ULR' 'LR' 'ULR' 'L' 'UDR' 'LR' 'UL' 'R' 'DL']
5140
+ ['U' 'R' 'DLR' 'LR' 'DLR' 'UL' 'D' 'D' 'DR' 'UL']
5141
+ ['D' 'R' 'UDL' 'D' 'U' 'D' 'UDR' 'ULR' 'ULR' 'DL']
5142
+ ['UDR' 'LR' 'ULR' 'UDL' 'DR' 'ULR' 'ULR' 'LR' 'DL' 'U']
5143
+ ['U' 'R' 'DLR' 'ULR' 'ULR' 'DLR' 'L' 'DR' 'ULR' 'DL']
5144
+ ['R' 'LR' 'ULR' 'LR' 'L' 'U' 'R' 'ULR' 'L' 'U']]
5145
+ Solutions found: 1
5146
+ status: OPTIMAL
5147
+ Time taken: 5.65 seconds
5148
+ ```
5149
+
5150
+ **Solved puzzle**
5151
+
5152
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/pipes_solved.png" alt="Pipes solved" width="500">
5153
+
5154
+ ---
5155
+
4826
5156
  ---
4827
5157
 
4828
5158
  ## Why SAT / CP-SAT?
@@ -4925,3 +5255,7 @@ Issues and PRs welcome!
4925
5255
  [49]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/binairo_plus "puzzle_solver/src/puzzle_solver/puzzles/binairo_plus at master · Ar-Kareem/puzzle_solver · GitHub"
4926
5256
  [50]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/shakashaka "puzzle_solver/src/puzzle_solver/puzzles/shakashaka at master · Ar-Kareem/puzzle_solver · GitHub"
4927
5257
  [51]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/kakuro "puzzle_solver/src/puzzle_solver/puzzles/kakuro at master · Ar-Kareem/puzzle_solver · GitHub"
5258
+ [52]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/sudoku "puzzle_solver/src/puzzle_solver/puzzles/sudoku at master · Ar-Kareem/puzzle_solver · GitHub"
5259
+ [53]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/sudoku "puzzle_solver/src/puzzle_solver/puzzles/sudoku at master · Ar-Kareem/puzzle_solver · GitHub"
5260
+ [54]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/flood_it "puzzle_solver/src/puzzle_solver/puzzles/flood_it at master · Ar-Kareem/puzzle_solver · GitHub"
5261
+ [55]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/pipes "puzzle_solver/src/puzzle_solver/puzzles/pipes at master · Ar-Kareem/puzzle_solver · GitHub"
@@ -0,0 +1,70 @@
1
+ puzzle_solver/__init__.py,sha256=g1rzu3qSDD7oz46Els59z3HCxigzjkkFmoG244ymWro,4966
2
+ puzzle_solver/core/utils.py,sha256=XBW5j-IwtJMPMP-ycmY6SqRCM1NOVl5O6UeoGqNj618,8153
3
+ puzzle_solver/core/utils_ortools.py,sha256=ACV3HgKWpEUTt1lpqsPryK1DeZpu7kdWQKEWTLJ2tfs,10384
4
+ puzzle_solver/core/utils_visualizer.py,sha256=ymuhF75uwJbNhN8XVDYEPqw6sPKoqRaaxlhGeHtXpLs,20201
5
+ puzzle_solver/puzzles/aquarium/aquarium.py,sha256=BUfkAS2d9eG3TdMoe1cOGGeNYgKUebRvn-z9nsC9gvE,5708
6
+ puzzle_solver/puzzles/battleships/battleships.py,sha256=RuYCrs4j0vUjlU139NRYYP-uNPAgO0V7hAzbsHrRwD8,7446
7
+ puzzle_solver/puzzles/binairo/binairo.py,sha256=NmVPIoyVCoMLaSFhsN0TcJQYvav9hi4hSwoAVirYhDU,6835
8
+ puzzle_solver/puzzles/binairo/binairo_plus.py,sha256=TvLG3olwANtft3LuCF-y4OofpU9PNa4IXDqgZqsD-g0,267
9
+ puzzle_solver/puzzles/black_box/black_box.py,sha256=EiCVkbhUP0x94otvQirv7MrggTu0ok8MIUPbxv6jkIU,15544
10
+ puzzle_solver/puzzles/bridges/bridges.py,sha256=QwOhZyO5urbatkNyPmQxZ_lGM01ZejndMr_eoiBkr7g,5394
11
+ puzzle_solver/puzzles/chess_range/chess_melee.py,sha256=D-_Oi8OyxsVe1j3dIKYwRlxgeb3NWLmDWGcv-oclY0c,195
12
+ puzzle_solver/puzzles/chess_range/chess_range.py,sha256=_VHlpUPnqeBstvSIt9RtTV-w2etSK7UrEHg6sErNqtU,21068
13
+ puzzle_solver/puzzles/chess_range/chess_solo.py,sha256=ByDfcRsk5FVmFicpU_DpLoLTJ99Kr___vX4y8ln8_EQ,400
14
+ puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
15
+ puzzle_solver/puzzles/dominosa/dominosa.py,sha256=Nmb7pn8U27QJwGy9F3wo8ylqo2_U51OAo3GN2soaNpc,7195
16
+ puzzle_solver/puzzles/filling/filling.py,sha256=R8UIbztk3zNCeNbVClBJoKZHKeHwK_pesjGmMaEEQO0,5536
17
+ puzzle_solver/puzzles/flip/flip.py,sha256=ZngJLUhRNc7qqo2wtNLdMPx4u9w9JTUge27PmdXyDCw,3985
18
+ puzzle_solver/puzzles/flood_it/flood_it.py,sha256=jnCtH1sZIt6K4hbQDSsiM1Cd8FjQNP7cfw2ObUW5fEQ,7948
19
+ puzzle_solver/puzzles/flood_it/parse_map/parse_map.py,sha256=0aw1TbiyxknY2hUAXaP3nXqT6I6mT9BIiERJSCj57xw,8245
20
+ puzzle_solver/puzzles/galaxies/galaxies.py,sha256=36X9jaQfvLIWFkBY1VZH6I59eCDkc77U06NDtKRUECY,5571
21
+ puzzle_solver/puzzles/galaxies/parse_map/parse_map.py,sha256=XmFqVN_oRfq9AZFWy5ViUJ2Szjgx-srrRkFPJXEEyFo,9358
22
+ puzzle_solver/puzzles/guess/guess.py,sha256=MpyrF6YVu0S1fzX-BllwxGKRGacWJpeLbNn5GetuEyo,10792
23
+ puzzle_solver/puzzles/heyawake/heyawake.py,sha256=L_y44dHArOvO_tDyO35dwkvqdk9eEGItO7n4FDfzNDc,5586
24
+ puzzle_solver/puzzles/inertia/inertia.py,sha256=-Y5fr7aK20zwmGHsZql7pYCq1kyMZglvkVZ6uIDf1HA,5658
25
+ puzzle_solver/puzzles/inertia/tsp.py,sha256=mAhlSjCWespASeN8uLZ0JkYDw-ZqFEpal6NM-ubpCXw,15313
26
+ puzzle_solver/puzzles/inertia/parse_map/parse_map.py,sha256=x0d64gTBd0HC2lO5uOpX2VKWfwj8rRiz0mQM_lqNmWs,8457
27
+ puzzle_solver/puzzles/kakurasu/kakurasu.py,sha256=VNGMJnBHDi6WkghLObRLhUvkmrPaGphTTUDMC0TkQvQ,2064
28
+ puzzle_solver/puzzles/kakuro/kakuro.py,sha256=m22Ju-V2BdQl2Ng_pjVUSrxPCtIfqezdpebutURlhvg,4348
29
+ puzzle_solver/puzzles/keen/keen.py,sha256=adSA_pc1m6F6jV7a-PpQxdci1bv4psCNRNt9hMIQdSY,5034
30
+ puzzle_solver/puzzles/light_up/light_up.py,sha256=iSA1rjZMFsnI0V0Nxivxox4qZkB7PvUrROSHXcoUXds,4541
31
+ puzzle_solver/puzzles/lits/lits.py,sha256=3fPIkhAIUz8JokcfaE_ZM3b0AFEnf5xPzGJ2qnm8SWY,7099
32
+ puzzle_solver/puzzles/magnets/magnets.py,sha256=-Wl49JD_PKeq735zQVMQ3XSQX6gdHiY-7PKw-Sh16jw,6474
33
+ puzzle_solver/puzzles/map/map.py,sha256=sxc57tapB8Tsgam-yoDitln1o-EB_SbIYvO6WEYy3us,2582
34
+ puzzle_solver/puzzles/minesweeper/minesweeper.py,sha256=gSdFsuZ-KrwVxgI1HF2q_pYleZ6vBm9jjRTFlboVnLY,5871
35
+ puzzle_solver/puzzles/mosaic/mosaic.py,sha256=QX_nVpVKQg8OfaUcqFk9tKqsDyVqvZc6-XWvfI3YcSw,2175
36
+ puzzle_solver/puzzles/nonograms/nonograms.py,sha256=dTKfMwBL49hW3bNd34ETXW7lBRPuQeSPNSCHqHmfybg,6066
37
+ puzzle_solver/puzzles/norinori/norinori.py,sha256=qR7V7NbZRN_ME90R2jL47AkGik1CY6JlAPhLBMXP2Gw,4714
38
+ puzzle_solver/puzzles/nurikabe/nurikabe.py,sha256=3cbW7X4kAMQK8PkH_t65fzT5cI0O6tWWOqpQUVyuGT4,6501
39
+ puzzle_solver/puzzles/palisade/palisade.py,sha256=T-LXlaLU5OwUQ24QWJWhBUFUktg0qDODTilNmBaXs4I,5014
40
+ puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
41
+ puzzle_solver/puzzles/pipes/pipes.py,sha256=SPPgmYXeqjdzijLqdIb_TtlGmxzIad6MHQ31pyDcgUc,4448
42
+ puzzle_solver/puzzles/range/range.py,sha256=q0J3crlGfjYZSA6Dh4iMCwP_gRMWid-_8KPgggOrFKk,4410
43
+ puzzle_solver/puzzles/rectangles/rectangles.py,sha256=MgOhZJGr9DVHb9bB8EAuwus0_8frBqRWqMwrOvMezHQ,6918
44
+ puzzle_solver/puzzles/shakashaka/shakashaka.py,sha256=PRpg_qI7XA3ysAo_g1TRJsT3VwB5Vial2UcFyBOMwKQ,9571
45
+ puzzle_solver/puzzles/shingoki/shingoki.py,sha256=heMuL9sm3jBegItjnqX05ttmDNiHSLB77BRljpeLLWk,7417
46
+ puzzle_solver/puzzles/signpost/signpost.py,sha256=38LlMvP5Fx4qrTXmw4aNCt3yUbG3fhdSk6-YXmhAHFg,3861
47
+ puzzle_solver/puzzles/singles/singles.py,sha256=KKn_Yl-eW874Bl1UmmcqoQ5vhNiO1JbM7fxKczOV5M4,2847
48
+ puzzle_solver/puzzles/slant/slant.py,sha256=xF-N4PuXYfx638NP1f1mi6YncIZB4mLtXtdS79XyPbg,6122
49
+ puzzle_solver/puzzles/slant/parse_map/parse_map.py,sha256=8thQxWbq0qjehKb2VzgUP22PGj-9n9djwbt3LGMVLJw,4811
50
+ puzzle_solver/puzzles/slitherlink/slitherlink.py,sha256=JpyNQk8K4nUziwWKxSvWEkF1RRBGLnCppCWK1Yf5bt0,7052
51
+ puzzle_solver/puzzles/star_battle/star_battle.py,sha256=IX6w4H3sifN01kPPtrAVRCK0Nl_xlXXSHvJKw8K1EuE,3718
52
+ puzzle_solver/puzzles/star_battle/star_battle_shapeless.py,sha256=lj05V0Y3A3NjMo1boMkPIwBhMtm6SWydjgAMeCf5EIo,225
53
+ puzzle_solver/puzzles/stitches/stitches.py,sha256=bb5JXyclkbKq350MQ9d8AuGteQwSF8knaJ0DU9M92Uw,6515
54
+ puzzle_solver/puzzles/stitches/parse_map/parse_map.py,sha256=b21SQvlnDM6wOl_1iUhZ7X6akpBZoOnj3kEzImBCh8Q,10497
55
+ puzzle_solver/puzzles/sudoku/sudoku.py,sha256=rLq0N34v3Hb10CiptXtKxX-37OlQIyjIle9Es1FAtpM,13378
56
+ puzzle_solver/puzzles/tapa/tapa.py,sha256=TsOQhnEvlC1JxaWiEjQg2KxRXJR49GrN71DsMvPpia8,5337
57
+ puzzle_solver/puzzles/tents/tents.py,sha256=jccUXWA7KWAtPKpVJJYNI6masTYWQgx0eitcQw0-6Fc,6281
58
+ puzzle_solver/puzzles/thermometers/thermometers.py,sha256=bGcVmpPeqL5AJtj8jkK8gYThzv9aGCd_QrWEiYBCA2s,4011
59
+ puzzle_solver/puzzles/towers/towers.py,sha256=OLyTf9nTFR5L32-S_fhVyBmpz4i5YUNJotwOwbw_Fjg,6500
60
+ puzzle_solver/puzzles/tracks/tracks.py,sha256=98xds9SKNqtOLFTRUX_KSMC7XYmZo567LOFeqotVQaM,7237
61
+ puzzle_solver/puzzles/undead/undead.py,sha256=IGFQysgoaKZH8rKjqlrkoHsH28ve4_hKor2f0QOsWY0,6596
62
+ puzzle_solver/puzzles/unequal/unequal.py,sha256=ExY2XDCrqROCDpRLfHo8uVr1zuli1QvbCdNCiDhlCac,6978
63
+ puzzle_solver/puzzles/unruly/unruly.py,sha256=xwOUpC12uHbmlDj2guN60VaaHpLr1Y-WmMD5TKeHbZE,3826
64
+ puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=5WixT_7K1HwfQ_dWbuBlQfpU8p69zB2KvOg32XJ8vno,5255
65
+ puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
66
+ puzzle_solver/utils/visualizer.py,sha256=T2g5We9J3tkhyXWoN2OrIDIJDjt6w5sDd2ksOub0ZI8,6819
67
+ multi_puzzle_solver-1.0.3.dist-info/METADATA,sha256=XxSydqNr_sbU-cmZcqvrI5ODi-vrpx7KuLWNmK9q4W4,350517
68
+ multi_puzzle_solver-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
69
+ multi_puzzle_solver-1.0.3.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
70
+ multi_puzzle_solver-1.0.3.dist-info/RECORD,,
puzzle_solver/__init__.py CHANGED
@@ -9,6 +9,7 @@ from puzzle_solver.puzzles.chess_range import chess_solo as chess_solo_solver
9
9
  from puzzle_solver.puzzles.chess_range import chess_melee as chess_melee_solver
10
10
  from puzzle_solver.puzzles.dominosa import dominosa as dominosa_solver
11
11
  from puzzle_solver.puzzles.filling import filling as filling_solver
12
+ from puzzle_solver.puzzles.flood_it import flood_it as flood_it_solver
12
13
  from puzzle_solver.puzzles.flip import flip as flip_solver
13
14
  from puzzle_solver.puzzles.galaxies import galaxies as galaxies_solver
14
15
  from puzzle_solver.puzzles.guess import guess as guess_solver
@@ -28,6 +29,7 @@ from puzzle_solver.puzzles.nurikabe import nurikabe as nurikabe_solver
28
29
  from puzzle_solver.puzzles.palisade import palisade as palisade_solver
29
30
  from puzzle_solver.puzzles.lits import lits as lits_solver
30
31
  from puzzle_solver.puzzles.pearl import pearl as pearl_solver
32
+ from puzzle_solver.puzzles.pipes import pipes as pipes_solver
31
33
  from puzzle_solver.puzzles.range import range as range_solver
32
34
  from puzzle_solver.puzzles.rectangles import rectangles as rectangles_solver
33
35
  from puzzle_solver.puzzles.shakashaka import shakashaka as shakashaka_solver
@@ -52,4 +54,61 @@ from puzzle_solver.puzzles.yin_yang import yin_yang as yin_yang_solver
52
54
 
53
55
  from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
54
56
 
55
- __version__ = '0.9.31'
57
+ __all__ = [
58
+ aquarium_solver,
59
+ battleships_solver,
60
+ binairo_solver,
61
+ binairo_plus_solver,
62
+ black_box_solver,
63
+ bridges_solver,
64
+ chess_range_solver,
65
+ chess_solo_solver,
66
+ chess_melee_solver,
67
+ dominosa_solver,
68
+ filling_solver,
69
+ flood_it_solver,
70
+ flip_solver,
71
+ galaxies_solver,
72
+ guess_solver,
73
+ heyawake_solver,
74
+ inertia_solver,
75
+ kakurasu_solver,
76
+ kakuro_solver,
77
+ keen_solver,
78
+ light_up_solver,
79
+ magnets_solver,
80
+ map_solver,
81
+ minesweeper_solver,
82
+ mosaic_solver,
83
+ nonograms_solver,
84
+ norinori_solver,
85
+ nurikabe_solver,
86
+ palisade_solver,
87
+ lits_solver,
88
+ pearl_solver,
89
+ pipes_solver,
90
+ range_solver,
91
+ rectangles_solver,
92
+ shakashaka_solver,
93
+ shingoki_solver,
94
+ signpost_solver,
95
+ singles_solver,
96
+ slant_solver,
97
+ slitherlink_solver,
98
+ star_battle_solver,
99
+ star_battle_shapeless_solver,
100
+ stitches_solver,
101
+ sudoku_solver,
102
+ tapa_solver,
103
+ tents_solver,
104
+ thermometers_solver,
105
+ towers_solver,
106
+ tracks_solver,
107
+ undead_solver,
108
+ unequal_solver,
109
+ unruly_solver,
110
+ yin_yang_solver,
111
+ inertia_image_parser,
112
+ ]
113
+
114
+ __version__ = '1.0.3'
@@ -14,6 +14,8 @@ class SingleSolution:
14
14
  assignment: dict[Pos, Union[str, int]]
15
15
 
16
16
  def get_hashable_solution(self) -> str:
17
+ if isinstance(self.assignment, list):
18
+ return json.dumps(self.assignment)
17
19
  result = []
18
20
  for pos, v in self.assignment.items():
19
21
  result.append((pos.x, pos.y, v))
@@ -93,14 +95,14 @@ def force_connected_component(model: cp_model.CpModel, vars_to_force: dict[Any,
93
95
  Total new variables: =4V [for N by M 2D grid total is 4NM]
94
96
  """
95
97
  if is_neighbor is None:
96
- is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1
98
+ is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1 # noqa: E731
97
99
 
98
100
  vs = vars_to_force
99
101
  v_count = len(vs)
100
102
  if v_count <= 2: # graph must have at least 3 nodes to possibly be disconnected
101
103
  return {}
102
104
  # =V model variables, one for each variable
103
- is_root: dict[Pos, cp_model.IntVar] = {} # =V, defines the unique root
105
+ is_root: dict[Pos, cp_model.IntVar] = {} # =V, defines the unique
104
106
  prefix_zero: dict[Pos, cp_model.IntVar] = {} # =V, used for picking the unique root
105
107
  node_height: dict[Pos, cp_model.IntVar] = {} # =V, trickles down from the root
106
108
  max_neighbor_height: dict[Pos, cp_model.IntVar] = {} # =V, the height of the tallest neighbor
@@ -141,7 +143,7 @@ def force_connected_component(model: cp_model.CpModel, vars_to_force: dict[Any,
141
143
  model.Add(node_height[pi] == max_neighbor_height[pi] - 1).OnlyEnforceIf([vs[pi], is_root[pi].Not()])
142
144
  model.Add(node_height[pi] == v_count).OnlyEnforceIf(is_root[pi])
143
145
  model.Add(node_height[pi] == 0).OnlyEnforceIf(vs[pi].Not())
144
-
146
+
145
147
  # final check: all active nodes have height > 0
146
148
  for p in keys_in_order:
147
149
  model.Add(node_height[p] > 0).OnlyEnforceIf(vs[p])
@@ -161,7 +163,7 @@ def force_no_loops(model: cp_model.CpModel, vars_to_force: dict[Any, cp_model.In
161
163
  Returns a dictionary of new variables that can be used to enforce the no component constraint.
162
164
  """
163
165
  if is_neighbor is None:
164
- is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1
166
+ is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1 # noqa: E731
165
167
 
166
168
  vs = vars_to_force
167
169
  v_count = len(vs)
@@ -220,8 +222,8 @@ def force_no_loops(model: cp_model.CpModel, vars_to_force: dict[Any, cp_model.In
220
222
  model.Add(tree_edge[(p_parent, p)] == 0).OnlyEnforceIf([is_root[p]])
221
223
  # every active node has exactly 1 parent except root has none
222
224
  model.AddExactlyOne([tree_edge[(p_parent, p)] for p_parent in parent_of[p]] + [vs[p].Not(), is_root[p]])
223
-
224
- # now each subgraph has directions where each non-root points to a single parent (and its value is parent+1).
225
+
226
+ # now each subgraph has directions where each non-root points to a single parent (and its value is parent+1).
225
227
  # to break cycles, every non-root active node must be > all neighbors that arent children
226
228
 
227
229
  all_new_vars: dict[str, cp_model.IntVar] = {}
@@ -94,7 +94,7 @@ def render_grid(cell_flags: np.ndarray,
94
94
  for r in range(R):
95
95
  rr = 2*r + 1
96
96
  for c in range(C):
97
- val = center_char if isinstance(center_char, str) else center_char[r, c]
97
+ val = center_char if isinstance(center_char, str) else (center_char(r, c) if callable(center_char) else center_char[r, c])
98
98
  put_center_text(rr, c, '' if val is None else str(val))
99
99
 
100
100
  # rows -> strings
@@ -159,13 +159,7 @@ def id_board_to_wall_board(id_board: np.array, border_is_wall = True) -> np.arra
159
159
  def render_shaded_grid(V: int,
160
160
  H: int,
161
161
  is_shaded: Callable[[int, int], bool],
162
- *,
163
- scale_x: int = 2,
164
- scale_y: int = 1,
165
- fill_char: str = '▒',
166
- empty_char: str = ' ',
167
- empty_text: Optional[Union[str, Callable[[int, int], Optional[str]]]] = None,
168
- show_axes: bool = True) -> str:
162
+ empty_text: Optional[Union[str, Callable[[int, int], Optional[str]]]] = None,) -> str:
169
163
  """
170
164
  Most of this function was AI generated then modified by me, I don't currently care about the details of rendering to the terminal this looked good enough during my testing.
171
165
  Visualize a V x H grid where each cell is shaded if is_shaded(r, c) is True.
@@ -179,6 +173,11 @@ def render_shaded_grid(V: int,
179
173
  cells. If a callable (r, c) -> str|None, used per cell. Text is
180
174
  centered within the interior row and truncated to fit.
181
175
  """
176
+ scale_x: int = 2
177
+ scale_y: int = 1
178
+ fill_char: str = '▒'
179
+ empty_char: str = ' '
180
+ show_axes: bool = True
182
181
  assert V >= 1 and H >= 1
183
182
  assert scale_x >= 1 and scale_y >= 1
184
183
  assert len(fill_char) == 1 and len(empty_char) == 1
@@ -349,8 +348,10 @@ def render_bw_tiles_split(
349
348
  if not use_color:
350
349
  return ""
351
350
  parts = []
352
- if fg is not None: parts.append(str(fg))
353
- if bg is not None: parts.append(str(bg))
351
+ if fg is not None:
352
+ parts.append(str(fg))
353
+ if bg is not None:
354
+ parts.append(str(bg))
354
355
  return ("\x1b[" + ";".join(parts) + "m") if parts else ""
355
356
 
356
357
  RESET = "\x1b[0m" if use_color else ""
@@ -501,4 +502,4 @@ def render_bw_tiles_split(
501
502
  # mode="text", # ← key change
502
503
  # text_palette="solid" # try "solid" for stark black/white
503
504
  # )
504
- # print("```text\n" + art + "\n```")
505
+ # print("```text\n" + art + "\n```")