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.
- {multi_puzzle_solver-1.0.2.dist-info → multi_puzzle_solver-1.0.3.dist-info}/METADATA +81 -1
- {multi_puzzle_solver-1.0.2.dist-info → multi_puzzle_solver-1.0.3.dist-info}/RECORD +6 -5
- puzzle_solver/__init__.py +3 -1
- puzzle_solver/puzzles/pipes/pipes.py +81 -0
- {multi_puzzle_solver-1.0.2.dist-info → multi_puzzle_solver-1.0.3.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-1.0.2.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: 1.0.
|
|
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=
|
|
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.
|
|
67
|
-
multi_puzzle_solver-1.0.
|
|
68
|
-
multi_puzzle_solver-1.0.
|
|
69
|
-
multi_puzzle_solver-1.0.
|
|
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.
|
|
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)
|
|
File without changes
|
|
File without changes
|