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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-puzzle-solver
3
- Version: 1.0.2
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
@@ -390,6 +390,11 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
390
390
  <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flood_it_unsolved.png" alt="Flood It" width="140">
391
391
  </a>
392
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>
393
398
  </tr>
394
399
  </table>
395
400
 
@@ -459,6 +464,7 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
459
464
  - [Sudoku Jigsaw (Puzzle Type #52)](#sudoku-jigsaw-puzzle-type-52)
460
465
  - [Sudoku Killer (Puzzle Type #53)](#sudoku-killer-puzzle-type-53)
461
466
  - [Flood It (Puzzle Type #54)](#flood-it-puzzle-type-54)
467
+ - [Pipes (Puzzle Type #55)](#pipes-puzzle-type-55)
462
468
  - [Why SAT / CP-SAT?](#why-sat--cp-sat)
463
469
  - [Testing](#testing)
464
470
  - [Contributing](#contributing)
@@ -5074,6 +5080,79 @@ Note that the solved solution on the bottom left says that only 18 moves were us
5074
5080
 
5075
5081
  ---
5076
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
+
5077
5156
  ---
5078
5157
 
5079
5158
  ## Why SAT / CP-SAT?
@@ -5179,3 +5258,4 @@ Issues and PRs welcome!
5179
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"
5180
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"
5181
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"
@@ -1,4 +1,4 @@
1
- puzzle_solver/__init__.py,sha256=Ll-qN1ElCTTILeun1u4t5dU0CdI3DkCX0ZNf0Q2UJtE,4886
1
+ puzzle_solver/__init__.py,sha256=g1rzu3qSDD7oz46Els59z3HCxigzjkkFmoG244ymWro,4966
2
2
  puzzle_solver/core/utils.py,sha256=XBW5j-IwtJMPMP-ycmY6SqRCM1NOVl5O6UeoGqNj618,8153
3
3
  puzzle_solver/core/utils_ortools.py,sha256=ACV3HgKWpEUTt1lpqsPryK1DeZpu7kdWQKEWTLJ2tfs,10384
4
4
  puzzle_solver/core/utils_visualizer.py,sha256=ymuhF75uwJbNhN8XVDYEPqw6sPKoqRaaxlhGeHtXpLs,20201
@@ -38,6 +38,7 @@ puzzle_solver/puzzles/norinori/norinori.py,sha256=qR7V7NbZRN_ME90R2jL47AkGik1CY6
38
38
  puzzle_solver/puzzles/nurikabe/nurikabe.py,sha256=3cbW7X4kAMQK8PkH_t65fzT5cI0O6tWWOqpQUVyuGT4,6501
39
39
  puzzle_solver/puzzles/palisade/palisade.py,sha256=T-LXlaLU5OwUQ24QWJWhBUFUktg0qDODTilNmBaXs4I,5014
40
40
  puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
41
+ puzzle_solver/puzzles/pipes/pipes.py,sha256=SPPgmYXeqjdzijLqdIb_TtlGmxzIad6MHQ31pyDcgUc,4448
41
42
  puzzle_solver/puzzles/range/range.py,sha256=q0J3crlGfjYZSA6Dh4iMCwP_gRMWid-_8KPgggOrFKk,4410
42
43
  puzzle_solver/puzzles/rectangles/rectangles.py,sha256=MgOhZJGr9DVHb9bB8EAuwus0_8frBqRWqMwrOvMezHQ,6918
43
44
  puzzle_solver/puzzles/shakashaka/shakashaka.py,sha256=PRpg_qI7XA3ysAo_g1TRJsT3VwB5Vial2UcFyBOMwKQ,9571
@@ -63,7 +64,7 @@ puzzle_solver/puzzles/unruly/unruly.py,sha256=xwOUpC12uHbmlDj2guN60VaaHpLr1Y-WmM
63
64
  puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=5WixT_7K1HwfQ_dWbuBlQfpU8p69zB2KvOg32XJ8vno,5255
64
65
  puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
65
66
  puzzle_solver/utils/visualizer.py,sha256=T2g5We9J3tkhyXWoN2OrIDIJDjt6w5sDd2ksOub0ZI8,6819
66
- multi_puzzle_solver-1.0.2.dist-info/METADATA,sha256=LCKeSEhi50eG0kd-PUEbBBrpY7ZPuWau5Kz4csMTN84,347154
67
- multi_puzzle_solver-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
68
- multi_puzzle_solver-1.0.2.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
69
- multi_puzzle_solver-1.0.2.dist-info/RECORD,,
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
@@ -29,6 +29,7 @@ from puzzle_solver.puzzles.nurikabe import nurikabe as nurikabe_solver
29
29
  from puzzle_solver.puzzles.palisade import palisade as palisade_solver
30
30
  from puzzle_solver.puzzles.lits import lits as lits_solver
31
31
  from puzzle_solver.puzzles.pearl import pearl as pearl_solver
32
+ from puzzle_solver.puzzles.pipes import pipes as pipes_solver
32
33
  from puzzle_solver.puzzles.range import range as range_solver
33
34
  from puzzle_solver.puzzles.rectangles import rectangles as rectangles_solver
34
35
  from puzzle_solver.puzzles.shakashaka import shakashaka as shakashaka_solver
@@ -85,6 +86,7 @@ __all__ = [
85
86
  palisade_solver,
86
87
  lits_solver,
87
88
  pearl_solver,
89
+ pipes_solver,
88
90
  range_solver,
89
91
  rectangles_solver,
90
92
  shakashaka_solver,
@@ -109,4 +111,4 @@ __all__ = [
109
111
  inertia_image_parser,
110
112
  ]
111
113
 
112
- __version__ = '1.0.2'
114
+ __version__ = '1.0.3'
@@ -0,0 +1,81 @@
1
+ import numpy as np
2
+ from ortools.sat.python import cp_model
3
+ from ortools.sat.python.cp_model import LinearExpr as lxp
4
+
5
+ from puzzle_solver.core.utils import Pos, get_all_pos, set_char, get_char, Direction, get_next_pos, get_opposite_direction
6
+ from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution, force_connected_component
7
+
8
+
9
+ class Board:
10
+ def __init__(self, board: np.array):
11
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
12
+ assert all(c.item().strip() in ['1', '2L', '2I', '3', '4'] for c in np.nditer(board)), 'board must contain only 1, 2L, 2I, 3, 4. Found:' + str(set(c.item().strip() for c in np.nditer(board)) - set(['1', '2L', '2I', '3', '4']))
13
+ self.board = board
14
+ self.V, self.H = board.shape
15
+ self.model = cp_model.CpModel()
16
+ self.model_vars: dict[Pos, cp_model.IntVar] = {}
17
+
18
+ self.create_vars()
19
+ self.add_all_constraints()
20
+
21
+ def create_vars(self):
22
+ for pos in get_all_pos(self.V, self.H):
23
+ for direction in Direction:
24
+ mirrored = (get_next_pos(pos, direction), get_opposite_direction(direction))
25
+ if mirrored in self.model_vars:
26
+ self.model_vars[(pos, direction)] = self.model_vars[mirrored]
27
+ else:
28
+ self.model_vars[(pos, direction)] = self.model.NewBoolVar(f'{pos}:{direction}')
29
+
30
+ def add_all_constraints(self):
31
+ for pos in get_all_pos(self.V, self.H):
32
+ self.force_position(pos, get_char(self.board, pos).strip())
33
+ # single connected component
34
+ self.force_connected_component()
35
+
36
+ def force_connected_component(self):
37
+ def is_neighbor(pd1: tuple[Pos, Direction], pd2: tuple[Pos, Direction]) -> bool:
38
+ p1, d1 = pd1
39
+ p2, d2 = pd2
40
+ if p1 == p2 and d1 != d2: # same position, different direction, is neighbor
41
+ return True
42
+ if get_next_pos(p1, d1) == p2 and d2 == get_opposite_direction(d1):
43
+ return True
44
+ return False
45
+ force_connected_component(self.model, self.model_vars, is_neighbor=is_neighbor)
46
+
47
+ def force_position(self, pos: Pos, value: str):
48
+ # cells with 1 or 3 or 4 neighbors each only have 1 unique state under rotational symmetry
49
+ # cells with 2 neighbors can either be a straight line (2I) or curved line (2L)
50
+ if value == '1':
51
+ self.model.Add(lxp.sum([self.model_vars[(pos, direction)] for direction in Direction]) == 1)
52
+ elif value == '2L':
53
+ self.model.Add(self.model_vars[(pos, Direction.LEFT)] != self.model_vars[(pos, Direction.RIGHT)])
54
+ self.model.Add(self.model_vars[(pos, Direction.UP)] != self.model_vars[(pos, Direction.DOWN)])
55
+ elif value == '2I':
56
+ self.model.Add(self.model_vars[(pos, Direction.LEFT)] == self.model_vars[(pos, Direction.RIGHT)])
57
+ self.model.Add(self.model_vars[(pos, Direction.UP)] == self.model_vars[(pos, Direction.DOWN)])
58
+ self.model.Add(self.model_vars[(pos, Direction.UP)] != self.model_vars[(pos, Direction.RIGHT)])
59
+ elif value == '3':
60
+ self.model.Add(lxp.sum([self.model_vars[(pos, direction)] for direction in Direction]) == 3)
61
+ elif value == '4':
62
+ self.model.Add(lxp.sum([self.model_vars[(pos, direction)] for direction in Direction]) == 4)
63
+ else:
64
+ raise ValueError(f'invalid value: {value}')
65
+
66
+ def solve_and_print(self, verbose: bool = True):
67
+ def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
68
+ assignment = {}
69
+ for pos in get_all_pos(self.V, self.H):
70
+ assignment[pos] = ''
71
+ for direction in Direction:
72
+ if solver.Value(board.model_vars[(pos, direction)]) == 1:
73
+ assignment[pos] += direction.name[0]
74
+ return SingleSolution(assignment=assignment)
75
+ def callback(single_res: SingleSolution):
76
+ print("Solution found")
77
+ res = np.full((self.V, self.H), ' ', dtype=object)
78
+ for pos in get_all_pos(self.V, self.H):
79
+ set_char(res, pos, single_res.assignment[pos])
80
+ print(res)
81
+ return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)