multi-puzzle-solver 0.9.25__py3-none-any.whl → 0.9.26__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: 0.9.25
3
+ Version: 0.9.26
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
@@ -331,6 +331,11 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
331
331
  <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/palisade_solved.png" alt="Palisade" width="140">
332
332
  </a>
333
333
  </td>
334
+ <td align="center">
335
+ <a href="#flip-puzzle-type-44"><b>Flip</b><br><br>
336
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flip_unsolved.png" alt="Flip" width="140">
337
+ </a>
338
+ </td>
334
339
  </tr>
335
340
  </table>
336
341
 
@@ -389,6 +394,7 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
389
394
  - [Binairo (Puzzle Type #41)](#binairo-puzzle-type-41)
390
395
  - [Rectangles (Puzzle Type #42)](#rectangles-puzzle-type-42)
391
396
  - [Palisade (Puzzle Type #43)](#palisade-puzzle-type-43)
397
+ - [Flip (Puzzle Type #44)](#flip-puzzle-type-44)
392
398
  - [Why SAT / CP-SAT?](#why-sat--cp-sat)
393
399
  - [Testing](#testing)
394
400
  - [Contributing](#contributing)
@@ -3730,14 +3736,13 @@ Applying the solution to the puzzle visually:
3730
3736
 
3731
3737
  ---
3732
3738
 
3733
-
3734
3739
  ## Palisade (Puzzle Type #43)
3735
3740
 
3736
3741
  * [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/palisade.html)
3737
3742
 
3738
3743
  * [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/palisade.html#palisade)
3739
3744
 
3740
- * [**Solver Code**][42]
3745
+ * [**Solver Code**][43]
3741
3746
 
3742
3747
  <details>
3743
3748
  <summary><strong>Rules</strong></summary>
@@ -3819,6 +3824,70 @@ Applying the solution to the puzzle visually:
3819
3824
 
3820
3825
  ---
3821
3826
 
3827
+ ## Flip (Puzzle Type #44)
3828
+
3829
+ * [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/flip.html)
3830
+
3831
+ * [**Instructions**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/flip.html#flip)
3832
+
3833
+ * [**Solver Code**][44]
3834
+
3835
+ <details>
3836
+ <summary><strong>Rules</strong></summary>
3837
+
3838
+ You have a grid of squares, some light and some dark. Your aim is to light all the squares up at the same time. You can choose any square and flip its state from light to dark or dark to light, but when you do so, other squares around it change state as well.
3839
+
3840
+ </details>
3841
+
3842
+ **Unsolved puzzle**
3843
+
3844
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flip_unsolved.png" alt="Flip unsolved" width="500">
3845
+
3846
+ Code to utilize this package and solve the puzzle:
3847
+
3848
+ (Note: the solver also supports random mapping of squares to the neighbors they flip, see the test cases in `tests/test_flip.py` for usage examples)
3849
+
3850
+ ```python
3851
+ import numpy as np
3852
+ from puzzle_solver import flip_solver as solver
3853
+ board = np.array([
3854
+ ['B', 'W', 'W', 'W', 'W', 'W', 'W'],
3855
+ ['B', 'B', 'W', 'W', 'W', 'B', 'B'],
3856
+ ['W', 'B', 'W', 'W', 'B', 'B', 'W'],
3857
+ ['B', 'B', 'B', 'W', 'W', 'B', 'W'],
3858
+ ['W', 'W', 'B', 'B', 'W', 'B', 'W'],
3859
+ ['B', 'W', 'B', 'B', 'W', 'W', 'W'],
3860
+ ['B', 'W', 'B', 'W', 'W', 'B', 'B'],
3861
+ ])
3862
+ binst = solver.Board(board=board)
3863
+ solutions = binst.solve_and_print()
3864
+ ```
3865
+
3866
+ **Script Output**
3867
+
3868
+ The output tells you which squares to tap to solve the puzzle.
3869
+
3870
+ ```python
3871
+ Solution found
3872
+ [['T' ' ' 'T' 'T' 'T' ' ' ' ']
3873
+ [' ' ' ' ' ' 'T' ' ' 'T' ' ']
3874
+ [' ' 'T' ' ' ' ' 'T' ' ' ' ']
3875
+ ['T' ' ' 'T' ' ' ' ' 'T' ' ']
3876
+ [' ' ' ' ' ' 'T' ' ' ' ' 'T']
3877
+ ['T' ' ' 'T' ' ' 'T' 'T' 'T']
3878
+ [' ' ' ' ' ' ' ' ' ' 'T' 'T']]
3879
+ Solutions found: 1
3880
+ status: OPTIMAL
3881
+ ```
3882
+
3883
+ **Solved puzzle**
3884
+
3885
+ This picture won't mean much as the game is about the sequence of moves not the final frame as shown here.
3886
+
3887
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/flip_solved.png" alt="Flip solved" width="500">
3888
+
3889
+ ---
3890
+
3822
3891
  ---
3823
3892
 
3824
3893
  ## Why SAT / CP-SAT?
@@ -3913,3 +3982,4 @@ Issues and PRs welcome!
3913
3982
  [41]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/binairo "puzzle_solver/src/puzzle_solver/puzzles/binairo at master · Ar-Kareem/puzzle_solver · GitHub"
3914
3983
  [42]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/rectangles "puzzle_solver/src/puzzle_solver/puzzles/rectangles at master · Ar-Kareem/puzzle_solver · GitHub"
3915
3984
  [43]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/palisade "puzzle_solver/src/puzzle_solver/puzzles/palisade at master · Ar-Kareem/puzzle_solver · GitHub"
3985
+ [44]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/flip "puzzle_solver/src/puzzle_solver/puzzles/flip at master · Ar-Kareem/puzzle_solver · GitHub"
@@ -1,5 +1,5 @@
1
- puzzle_solver/__init__.py,sha256=DsQVO-Eo1odFFFvQB1IpbJw-Yr2MTtON6zMnLUi05P8,3203
2
- puzzle_solver/core/utils.py,sha256=7Wo8_LHLEv8bY5-HsuCuLIjttZMMW09DoL1CcFDiu1Q,14046
1
+ puzzle_solver/__init__.py,sha256=ScIPz0Gi0xGaY-v7N0JcfFgKUvfdSF3lUggAh8yagdg,3201
2
+ puzzle_solver/core/utils.py,sha256=LyrdhExCcdp7jWJJv1cu2urgS2gpcI44OsIipPeBAeQ,14113
3
3
  puzzle_solver/core/utils_ortools.py,sha256=_i8cixHOB5XGqqcr-493bOiZgYJidnvxQMEfj--Trns,10278
4
4
  puzzle_solver/puzzles/aquarium/aquarium.py,sha256=BUfkAS2d9eG3TdMoe1cOGGeNYgKUebRvn-z9nsC9gvE,5708
5
5
  puzzle_solver/puzzles/battleships/battleships.py,sha256=RuYCrs4j0vUjlU139NRYYP-uNPAgO0V7hAzbsHrRwD8,7446
@@ -12,7 +12,7 @@ puzzle_solver/puzzles/chess_range/chess_solo.py,sha256=U3v766UsZHx_dC3gxqU90VbjA
12
12
  puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
13
13
  puzzle_solver/puzzles/dominosa/dominosa.py,sha256=Nmb7pn8U27QJwGy9F3wo8ylqo2_U51OAo3GN2soaNpc,7195
14
14
  puzzle_solver/puzzles/filling/filling.py,sha256=vrOIil285_r3IQ0F4c9mUBWMRVlPH4vowog_z1tCGdI,5567
15
- puzzle_solver/puzzles/flip/flip.py,sha256=4rQ-JsC_f33YKDM7aueKVlcVdDwzeBkTJL51K-Vy0gA,2223
15
+ puzzle_solver/puzzles/flip/flip.py,sha256=ZngJLUhRNc7qqo2wtNLdMPx4u9w9JTUge27PmdXyDCw,3985
16
16
  puzzle_solver/puzzles/galaxies/galaxies.py,sha256=p10lpmW0FjtneFCMEjG1FSiEpQuvD8zZG9FG8zYGoes,5582
17
17
  puzzle_solver/puzzles/galaxies/parse_map/parse_map.py,sha256=v5TCrdREeOB69s9_QFgPHKA7flG69Im1HVzIdxH0qQc,9355
18
18
  puzzle_solver/puzzles/guess/guess.py,sha256=sH-NlYhxM3DNbhk4eGde09kgM0KaDvSbLrpHQiwcFGo,10791
@@ -29,7 +29,7 @@ puzzle_solver/puzzles/minesweeper/minesweeper.py,sha256=LiQVOGkWCsc1WtX8CdPgL_Ww
29
29
  puzzle_solver/puzzles/mosaic/mosaic.py,sha256=QX_nVpVKQg8OfaUcqFk9tKqsDyVqvZc6-XWvfI3YcSw,2175
30
30
  puzzle_solver/puzzles/nonograms/nonograms.py,sha256=1jmDTOCnmivmBlwtMDyyk3TVqH5IjapzLn7zLQ4qubk,6056
31
31
  puzzle_solver/puzzles/norinori/norinori.py,sha256=uC8vXAw35xsTmpmTeKqYW7tbcssms9LCcXFBONtV2Ng,4743
32
- puzzle_solver/puzzles/palisade/palisade.py,sha256=ZFvBnBVbR0iIcQ5Vm3PtHPjdSDvrO5OUbM91YoTKpHI,4962
32
+ puzzle_solver/puzzles/palisade/palisade.py,sha256=GTtzuc1OZCm3D5p-Po7LzK1d-whJkNSZ9G9rWr3vFMc,4966
33
33
  puzzle_solver/puzzles/pearl/pearl.py,sha256=OhzpMYpxqvR3GCd5NH4ETT0NO4X753kRi6p5omYLChM,6798
34
34
  puzzle_solver/puzzles/range/range.py,sha256=rruvD5ZSaOgvQuX6uGV_Dkr82nSiWZ5kDz03_j7Tt24,4425
35
35
  puzzle_solver/puzzles/rectangles/rectangles.py,sha256=V7p6GSCwYrFfILDWiLLUbX08WlnPbQKdhQm8bMa2Mgw,7060
@@ -53,7 +53,7 @@ puzzle_solver/puzzles/unruly/unruly.py,sha256=sDF0oKT50G-NshyW2DYrvAgD9q9Ku9ANUy
53
53
  puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=WrRdNhmKhIARdGOt_36gpRxRzrfLGv3wl7igBpPFM64,5259
54
54
  puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
55
55
  puzzle_solver/utils/visualizer.py,sha256=tsX1yEKwmwXBYuBJpx_oZGe2UUt1g5yV73G3UbtmvtE,6817
56
- multi_puzzle_solver-0.9.25.dist-info/METADATA,sha256=DcVaQpmwyhYN0y0XxOcvomrpoerVjgYdylE8VUFml04,208538
57
- multi_puzzle_solver-0.9.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
- multi_puzzle_solver-0.9.25.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
59
- multi_puzzle_solver-0.9.25.dist-info/RECORD,,
56
+ multi_puzzle_solver-0.9.26.dist-info/METADATA,sha256=iVRpKnMdTJNLrbdh9eP19RsPCloN1iEFwtlvcwy_peg,211081
57
+ multi_puzzle_solver-0.9.26.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
+ multi_puzzle_solver-0.9.26.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
59
+ multi_puzzle_solver-0.9.26.dist-info/RECORD,,
puzzle_solver/__init__.py CHANGED
@@ -8,7 +8,7 @@ from puzzle_solver.puzzles.chess_range import chess_solo as chess_solo_solver
8
8
  from puzzle_solver.puzzles.chess_range import chess_melee as chess_melee_solver
9
9
  from puzzle_solver.puzzles.dominosa import dominosa as dominosa_solver
10
10
  from puzzle_solver.puzzles.filling import filling as filling_solver
11
- # from puzzle_solver.puzzles.flip import flip as flip_solver
11
+ from puzzle_solver.puzzles.flip import flip as flip_solver
12
12
  from puzzle_solver.puzzles.galaxies import galaxies as galaxies_solver
13
13
  from puzzle_solver.puzzles.guess import guess as guess_solver
14
14
  from puzzle_solver.puzzles.inertia import inertia as inertia_solver
@@ -45,4 +45,4 @@ from puzzle_solver.puzzles.yin_yang import yin_yang as yin_yang_solver
45
45
 
46
46
  from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
47
47
 
48
- __version__ = '0.9.25'
48
+ __version__ = '0.9.26'
@@ -42,7 +42,9 @@ def get_next_pos(cur_pos: Pos, direction: Union[Direction, Direction8]) -> Pos:
42
42
  return get_pos(cur_pos.x+delta_x, cur_pos.y+delta_y)
43
43
 
44
44
 
45
- def get_neighbors4(pos: Pos, V: int, H: int) -> Iterable[Pos]:
45
+ def get_neighbors4(pos: Pos, V: int, H: int, include_self: bool = False) -> Iterable[Pos]:
46
+ if include_self:
47
+ yield pos
46
48
  for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)):
47
49
  p2 = get_pos(x=pos.x+dx, y=pos.y+dy)
48
50
  if in_bounds(p2, V, H):
@@ -1,18 +1,41 @@
1
+ from typing import Any, Optional
2
+
1
3
  import numpy as np
2
4
  from ortools.sat.python import cp_model
3
- from ortools.sat.python.cp_model import LinearExpr as lxp
4
5
 
5
- from puzzle_solver.core.utils import Pos, get_all_pos, set_char, get_char, get_neighbors8
6
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_neighbors4, set_char, get_char, Direction, get_next_pos
6
7
  from puzzle_solver.core.utils_ortools import generic_solve_all, SingleSolution
7
8
 
8
9
 
9
10
  class Board:
10
- def __init__(self, board: np.array):
11
+ def __init__(self, board: np.array, random_mapping: Optional[dict[Pos, Any]] = None):
11
12
  assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
12
- assert board.shape[0] == board.shape[1], 'board must be square'
13
- assert all((c.item() == ' ') or str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only space or digits'
13
+ assert all((c.item() in ['B', 'W']) for c in np.nditer(board)), 'board must contain only B or W'
14
14
  self.board = board
15
- self.N = board.shape[0]
15
+ self.V, self.H = board.shape
16
+
17
+ if random_mapping is None:
18
+ self.tap_mapping: dict[Pos, set[Pos]] = {pos: list(get_neighbors4(pos, self.V, self.H, include_self=True)) for pos in get_all_pos(self.V, self.H)}
19
+ else:
20
+ mapping_value = list(random_mapping.values())[0]
21
+ if isinstance(mapping_value, (set, list, tuple)) and isinstance(list(mapping_value)[0], Pos):
22
+ self.tap_mapping: dict[Pos, set[Pos]] = {pos: set(random_mapping[pos]) for pos in get_all_pos(self.V, self.H)}
23
+ elif isinstance(mapping_value, (set, list, tuple)) and isinstance(list(mapping_value)[0], str): # strings like "L", "UR", etc.
24
+ def _to_pos(pos: Pos, s: str) -> Pos:
25
+ d = {'L': Direction.LEFT, 'R': Direction.RIGHT, 'U': Direction.UP, 'D': Direction.DOWN}[s[0]]
26
+ r = get_next_pos(pos, d)
27
+ if len(s) == 1:
28
+ return r
29
+ else:
30
+ return _to_pos(r, s[1:])
31
+ self.tap_mapping: dict[Pos, set[Pos]] = {pos: set(_to_pos(pos, s) for s in random_mapping[pos]) for pos in get_all_pos(self.V, self.H)}
32
+ else:
33
+ raise ValueError(f'invalid random_mapping: {random_mapping}')
34
+ for k, v in self.tap_mapping.items():
35
+ if k not in v:
36
+ v.add(k)
37
+
38
+
16
39
  self.model = cp_model.CpModel()
17
40
  self.model_vars: dict[Pos, cp_model.IntVar] = {}
18
41
 
@@ -20,16 +43,22 @@ class Board:
20
43
  self.add_all_constraints()
21
44
 
22
45
  def create_vars(self):
23
- for pos in get_all_pos(self.N):
24
- self.model_vars[pos] = self.model.NewBoolVar(f'{pos}')
46
+ for pos in get_all_pos(self.V, self.H):
47
+ self.model_vars[pos] = self.model.NewBoolVar(f'tap:{pos}')
25
48
 
26
49
  def add_all_constraints(self):
27
- for pos in get_all_pos(self.N):
50
+ for pos in get_all_pos(self.V, self.H):
51
+ # the state of a position is its starting state + if it is tapped + if any pos pointing to it is tapped
28
52
  c = get_char(self.board, pos)
29
- if not str(c).isdecimal():
30
- continue
31
- neighbour_vars = [self.model_vars[p] for p in get_neighbors8(pos, self.N, include_self=True)]
32
- self.model.Add(lxp.sum(neighbour_vars) == int(c))
53
+ pos_that_will_turn_me = [k for k,v in self.tap_mapping.items() if pos in v]
54
+ literals = [self.model_vars[p] for p in pos_that_will_turn_me]
55
+ if c == 'W': # if started as white then needs an even number of taps while xor checks for odd number
56
+ literals.append(self.model.NewConstant(True))
57
+ elif c == 'B':
58
+ pass
59
+ else:
60
+ raise ValueError(f'invalid character: {c}')
61
+ self.model.AddBoolXOr(literals)
33
62
 
34
63
  def solve_and_print(self, verbose: bool = True):
35
64
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
@@ -39,10 +68,10 @@ class Board:
39
68
  return SingleSolution(assignment=assignment)
40
69
  def callback(single_res: SingleSolution):
41
70
  print("Solution found")
42
- res = np.full((self.N, self.N), ' ', dtype=object)
43
- for pos in get_all_pos(self.N):
71
+ res = np.full((self.V, self.H), ' ', dtype=object)
72
+ for pos in get_all_pos(self.V, self.H):
44
73
  c = get_char(self.board, pos)
45
- c = 'B' if single_res.assignment[pos] == 1 else ' '
74
+ c = 'T' if single_res.assignment[pos] == 1 else ' '
46
75
  set_char(res, pos, c)
47
76
  print(res)
48
77
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose)
@@ -44,7 +44,7 @@ def get_valid_translations(shape: Shape, board: np.array) -> set[Pos]:
44
44
  if c != ' ' and c != str(shape_borders[i]): # there is a clue and it doesn't match my translated shape, skip
45
45
  break
46
46
  else:
47
- yield tuple(get_pos(x=p[0], y=p[1]) for p in body)
47
+ yield frozenset(get_pos(x=p[0], y=p[1]) for p in body)
48
48
 
49
49
 
50
50