multi-puzzle-solver 1.0.8__py3-none-any.whl → 1.0.9__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.8
3
+ Version: 1.0.9
4
4
  Summary: Efficient solvers for countless (50+) types of puzzles (like Sudoku, Minesweeper, etc.) with a simple python API.
5
5
  Author: Ar-Kareem
6
6
  Project-URL: Homepage, https://github.com/Ar-Kareem/puzzle_solver
@@ -610,7 +610,15 @@ Time taken: 0.04 seconds
610
610
 
611
611
  ## Sudoku (Puzzle Type #2)
612
612
 
613
- Also known as Number Place and Solo.
613
+ Also known as Number Place and Solo.
614
+
615
+ The code can:
616
+
617
+ 1. Solve arbitrarily sized valid board sizes, thus can be used to solve:
618
+ - Hex Sudoku (a 16x16 variant)
619
+ - Kidoku (a kid-friendly sudoku variant)
620
+ 2. Solve the ["Sandwich" sudoku variant](https://dkmgames.com/SandwichSudoku/) using the optional parameter `sandwich={'side': [...], 'bottom': [...]}`
621
+ 3. Solve the ["Sudoku-X" variant](https://www.sudopedia.org/wiki/Sudoku-X) using the optional parameter `unique_diagonal=True`
614
622
 
615
623
  * [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/solo.html)
616
624
 
@@ -635,12 +643,6 @@ You are given some of the numbers as clues; your aim is to place the rest of the
635
643
 
636
644
  Code to utilize this package and solve the puzzle:
637
645
 
638
- Note:
639
-
640
- - The solver also supports solving the ["Sandwich" sudoku variant](https://dkmgames.com/SandwichSudoku/) through the optional parameter ``sandwich={'side': [...], 'bottom': [...] }``。
641
-
642
- - The solver also supports solving the ["Sudoku-X" variant](https://www.sudopedia.org/wiki/Sudoku-X) through the optional parameter ``unique_diagonal=True``。
643
-
644
646
  ```python
645
647
  import numpy as np
646
648
  from puzzle_solver import sudoku_solver as solver
@@ -1105,7 +1107,7 @@ Time taken: 0.15 seconds
1105
1107
 
1106
1108
  ## Keen (Puzzle Type #8)
1107
1109
 
1108
- Also known as KenKen or CalcuDoku.
1110
+ Also known as KenKen, CalcuDoku, Inkies, or Inky.
1109
1111
 
1110
1112
  * [**Play online**](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/js/keen.html)
1111
1113
 
@@ -4043,6 +4045,8 @@ Applying the solution to the puzzle visually:
4043
4045
 
4044
4046
  ## Binairo (Puzzle Type #41)
4045
4047
 
4048
+ Also known as Takuzu, Binero, Tohu-Wa-Vohu (Formless and Empty), Eins und Zwei (One and Two), Binary Puzzles, Binoxxo, Binox, Zernero, Tic-Tac-Logic, and Sudoku Binary.
4049
+
4046
4050
  * [**Play online**](https://www.puzzle-binairo.com)
4047
4051
 
4048
4052
  * [**Solver Code**][41]
@@ -1,11 +1,11 @@
1
- puzzle_solver/__init__.py,sha256=0jnYSJY4kJBhX5PU4UMmMcbTg8PsyFjiX0y23r5sv5k,5502
1
+ puzzle_solver/__init__.py,sha256=mCXXxGttWhDOTXSDFI2CrplOLmSFVF9r_XygisFqiAY,5502
2
2
  puzzle_solver/core/utils.py,sha256=FE0106dfQRsgCn2FRBvRq5zILLK7-Z3cPHkAlBWUX0w,8785
3
3
  puzzle_solver/core/utils_ortools.py,sha256=ACV3HgKWpEUTt1lpqsPryK1DeZpu7kdWQKEWTLJ2tfs,10384
4
4
  puzzle_solver/core/utils_visualizer.py,sha256=3EJ7V8rHyasj1peAzplDJfKkPy6Yj9j7BXqMBWQ3eNg,22834
5
5
  puzzle_solver/puzzles/abc_view/abc_view.py,sha256=Qr0rZKmKQ2teStHjQ5VPQ4k-XptsjJAlZ1WXWk5Aax4,4570
6
6
  puzzle_solver/puzzles/aquarium/aquarium.py,sha256=dGqYEWMoh4di5DN4sd-GtYb6QeTpVYFQJHBkrrmrudQ,5649
7
7
  puzzle_solver/puzzles/battleships/battleships.py,sha256=U4xJ_NJC2baHvfaAfJ01YEBjixq9gD0h8GP9L1V-_oM,7223
8
- puzzle_solver/puzzles/binairo/binairo.py,sha256=qKvpixLIBUcugAyJgpBGHV-9q_4nzA1ZOxeDFnltsXA,6843
8
+ puzzle_solver/puzzles/binairo/binairo.py,sha256=EBpXYD9Mxuig4uJl3xkcQ6_tbnoG13mVV7RZpQEXm38,5790
9
9
  puzzle_solver/puzzles/binairo/binairo_plus.py,sha256=TvLG3olwANtft3LuCF-y4OofpU9PNa4IXDqgZqsD-g0,267
10
10
  puzzle_solver/puzzles/black_box/black_box.py,sha256=RXTXQhMAb_Oce9Mk1XpouniYIyy9k3kYGdey-SEeRMU,12559
11
11
  puzzle_solver/puzzles/bridges/bridges.py,sha256=QwOhZyO5urbatkNyPmQxZ_lGM01ZejndMr_eoiBkr7g,5394
@@ -69,7 +69,7 @@ puzzle_solver/puzzles/unruly/unruly.py,sha256=_C6FhYm9rqwhlQa6TMTxYr3rWcP_QS-E93
69
69
  puzzle_solver/puzzles/yin_yang/yin_yang.py,sha256=D0JacUdK5yPrfScmGqX-p8144VbwxfDgIaqF8hwLXlM,5039
70
70
  puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py,sha256=drjfoHqmFf6U-ZQUwrBbfGINRxDQpgbvy4U3D9QyMhM,6617
71
71
  puzzle_solver/utils/visualizer.py,sha256=T2g5We9J3tkhyXWoN2OrIDIJDjt6w5sDd2ksOub0ZI8,6819
72
- multi_puzzle_solver-1.0.8.dist-info/METADATA,sha256=E6zK6c0vVluGrFEvS459AuNa33dET9IWNdmqtIbYJwA,456018
73
- multi_puzzle_solver-1.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
74
- multi_puzzle_solver-1.0.8.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
75
- multi_puzzle_solver-1.0.8.dist-info/RECORD,,
72
+ multi_puzzle_solver-1.0.9.dist-info/METADATA,sha256=DsMyTWC0gI51MbgX6UecmA6jdP_pbGE-RLrHCK9Cyhk,456299
73
+ multi_puzzle_solver-1.0.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
74
+ multi_puzzle_solver-1.0.9.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
75
+ multi_puzzle_solver-1.0.9.dist-info/RECORD,,
puzzle_solver/__init__.py CHANGED
@@ -121,4 +121,4 @@ __all__ = [
121
121
  inertia_image_parser,
122
122
  ]
123
123
 
124
- __version__ = '1.0.8'
124
+ __version__ = '1.0.9'
@@ -10,26 +10,21 @@ from puzzle_solver.core.utils_visualizer import combined_function
10
10
 
11
11
 
12
12
  class Board:
13
- def __init__(self, board: np.array, arith_rows: Optional[np.array] = None, arith_cols: Optional[np.array] = None, force_unique: bool = True):
13
+ def __init__(self, board: np.array, arith_rows: Optional[np.array] = None, arith_cols: Optional[np.array] = None, force_unique: bool = True, disallow_three: bool = True):
14
14
  assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
15
+ assert board.shape[0] % 2 == 0 and board.shape[1] % 2 == 0, f'board must have even number of rows and columns, got {board.shape[0]}x{board.shape[1]}'
15
16
  assert all(c.item() in [' ', 'B', 'W'] for c in np.nditer(board)), 'board must contain only space or B'
17
+ assert arith_rows is None or all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_rows)), 'arith_rows must contain only space, x, or ='
18
+ assert arith_cols is None or all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_cols)), 'arith_cols must contain only space, x, or ='
16
19
  self.board = board
17
20
  self.V, self.H = board.shape
18
- if arith_rows is not None:
19
- assert arith_rows.ndim == 2, f'arith_rows must be 2d, got {arith_rows.ndim}'
20
- assert arith_rows.shape == (self.V, self.H-1), f'arith_rows must be one column less than board, got {arith_rows.shape} for {board.shape}'
21
- assert all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_rows)), 'arith_rows must contain only space, x, or ='
22
- if arith_cols is not None:
23
- assert arith_cols.ndim == 2, f'arith_cols must be 2d, got {arith_cols.ndim}'
24
- assert arith_cols.shape == (self.V-1, self.H), f'arith_cols must be one column and row less than board, got {arith_cols.shape} for {board.shape}'
25
- assert all(isinstance(c.item(), str) and c.item() in [' ', 'x', '='] for c in np.nditer(arith_cols)), 'arith_cols must contain only space, x, or ='
26
21
  self.arith_rows = arith_rows
27
22
  self.arith_cols = arith_cols
28
23
  self.force_unique = force_unique
24
+ self.disallow_three = disallow_three
29
25
 
30
26
  self.model = cp_model.CpModel()
31
27
  self.model_vars: dict[Pos, cp_model.IntVar] = {}
32
-
33
28
  self.create_vars()
34
29
  self.add_all_constraints()
35
30
 
@@ -39,11 +34,9 @@ class Board:
39
34
 
40
35
  def add_all_constraints(self):
41
36
  for pos in get_all_pos(self.V, self.H): # force clues
42
- c = get_char(self.board, pos)
43
- if c == 'B':
44
- self.model.Add(self.model_vars[pos] == 1)
45
- elif c == 'W':
46
- self.model.Add(self.model_vars[pos] == 0)
37
+ c = get_char(self.board, pos).strip()
38
+ if c:
39
+ self.model.Add(self.model_vars[pos] == (c == 'B'))
47
40
  # 1. Each row and each column must contain an equal number of white and black circles.
48
41
  for row in range(self.V):
49
42
  row_vars = [self.model_vars[pos] for pos in get_row_pos(row, self.H)]
@@ -51,69 +44,48 @@ class Board:
51
44
  for col in range(self.H):
52
45
  col_vars = [self.model_vars[pos] for pos in get_col_pos(col, self.V)]
53
46
  self.model.Add(lxp.sum(col_vars) == len(col_vars) // 2)
54
- # 2. More than two circles of the same color can't be adjacent.
55
- for pos in get_all_pos(self.V, self.H):
56
- self.disallow_three_in_a_row(pos, Direction.RIGHT)
57
- self.disallow_three_in_a_row(pos, Direction.DOWN)
58
-
47
+ # 2. No three consecutive cells of the same color
48
+ if self.disallow_three:
49
+ for pos in get_all_pos(self.V, self.H):
50
+ self.disallow_three_in_a_row(pos, Direction.RIGHT)
51
+ self.disallow_three_in_a_row(pos, Direction.DOWN)
59
52
  # 3. Each row and column is unique.
60
53
  if self.force_unique:
61
- # a list per row
62
54
  self.force_unique_double_list([[self.model_vars[pos] for pos in get_row_pos(row, self.H)] for row in range(self.V)])
63
- # a list per column
64
55
  self.force_unique_double_list([[self.model_vars[pos] for pos in get_col_pos(col, self.V)] for col in range(self.H)])
65
-
66
56
  # if arithmetic is provided, add constraints for it
67
57
  if self.arith_rows is not None:
68
- assert self.arith_rows.shape == (self.V, self.H-1), f'arith_rows must be one column less than board, got {self.arith_rows.shape} for {self.board.shape}'
69
- for pos in get_all_pos(self.V, self.H-1):
70
- c = get_char(self.arith_rows, pos)
71
- if c == 'x':
72
- self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, Direction.RIGHT)])
73
- elif c == '=':
74
- self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, Direction.RIGHT)])
58
+ self.force_arithmetic(self.arith_rows, Direction.RIGHT, self.V, self.H-1)
75
59
  if self.arith_cols is not None:
76
- assert self.arith_cols.shape == (self.V-1, self.H), f'arith_cols must be one row less than board, got {self.arith_cols.shape} for {self.board.shape}'
77
- for pos in get_all_pos(self.V-1, self.H):
78
- c = get_char(self.arith_cols, pos)
79
- if c == 'x':
80
- self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, Direction.DOWN)])
81
- elif c == '=':
82
- self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, Direction.DOWN)])
83
-
60
+ self.force_arithmetic(self.arith_cols, Direction.DOWN, self.V-1, self.H)
84
61
 
85
62
  def disallow_three_in_a_row(self, p1: Pos, direction: Direction):
86
63
  p2 = get_next_pos(p1, direction)
87
64
  p3 = get_next_pos(p2, direction)
88
- if any(not in_bounds(p, self.V, self.H) for p in [p1, p2, p3]):
89
- return
90
- self.model.AddBoolOr([
91
- self.model_vars[p1],
92
- self.model_vars[p2],
93
- self.model_vars[p3],
94
- ])
95
- self.model.AddBoolOr([
96
- self.model_vars[p1].Not(),
97
- self.model_vars[p2].Not(),
98
- self.model_vars[p3].Not(),
99
- ])
65
+ if all(in_bounds(p, self.V, self.H) for p in [p1, p2, p3]):
66
+ self.model.AddBoolOr([self.model_vars[p1], self.model_vars[p2], self.model_vars[p3]])
67
+ self.model.AddBoolOr([self.model_vars[p1].Not(), self.model_vars[p2].Not(), self.model_vars[p3].Not()])
100
68
 
101
69
  def force_unique_double_list(self, model_vars: list[list[cp_model.IntVar]]):
102
- if not model_vars or len(model_vars) < 2:
103
- return
104
70
  m = len(model_vars[0])
105
- assert m <= 61, f"Too many cells for binary encoding in int64: m={m}, model_vars={model_vars}"
106
-
71
+ assert m <= 61, f'Too many cells for binary encoding in int64: m={m}, model_vars={model_vars}'
107
72
  codes = []
108
- pow2 = [1 << k for k in range(m)] # weights for bit positions (LSB at index 0)
73
+ pow2 = [2**k for k in range(m)]
109
74
  for i, line in enumerate(model_vars):
110
- code = self.model.NewIntVar(0, (1 << m) - 1, f"code_{i}")
111
- # Sum 2^k * r[k] == code
112
- self.model.Add(code == sum(pow2[k] * line[k] for k in range(m)))
75
+ code = self.model.NewIntVar(0, 2**m, f"code_{i}")
76
+ self.model.Add(code == lxp.weighted_sum(line, pow2)) # Sum 2^k * r[k] == code
113
77
  codes.append(code)
114
-
115
78
  self.model.AddAllDifferent(codes)
116
79
 
80
+ def force_arithmetic(self, arith_board: np.array, direction: Direction, V: int, H: int):
81
+ assert arith_board.shape == (V, H), f'arith_board going {direction} expected shape {V}x{H}, got {arith_board.shape}'
82
+ for pos in get_all_pos(V, H):
83
+ c = get_char(arith_board, pos).strip()
84
+ if c == 'x':
85
+ self.model.Add(self.model_vars[pos] != self.model_vars[get_next_pos(pos, direction)])
86
+ elif c == '=':
87
+ self.model.Add(self.model_vars[pos] == self.model_vars[get_next_pos(pos, direction)])
88
+
117
89
  def solve_and_print(self, verbose: bool = True):
118
90
  def board_to_solution(board: Board, solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
119
91
  assignment: dict[Pos, int] = {}