multi-puzzle-solver 0.8.7__py3-none-any.whl → 0.9.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-puzzle-solver
3
- Version: 0.8.7
3
+ Version: 0.9.1
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
@@ -210,8 +210,16 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
210
210
  <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_range_unsolved.png" alt="Chess range" height="120">
211
211
  </a>
212
212
  </td>
213
-
214
- </td><td></td><td></td>
213
+ <td align="center">
214
+ <a href="#chess-solo-puzzle-type-24"><b>Chess Solo</b><br><br>
215
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_solo_unsolved.png" alt="Chess solo" height="120">
216
+ </a>
217
+ </td>
218
+ <td align="center">
219
+ <a href="#chess-melee-puzzle-type-25"><b>Chess Melee</b><br><br>
220
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_melee_unsolved.png" alt="Chess melee" height="120">
221
+ </a>
222
+ </td>
215
223
  </tr>
216
224
  </table>
217
225
 
@@ -249,9 +257,10 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
249
257
  - [Bridges (Puzzle Type #20)](#bridges-puzzle-type-20)
250
258
  - [Inertia (Puzzle Type #21)](#inertia-puzzle-type-21)
251
259
  - [Guess (Puzzle Type #22)](#guess-puzzle-type-22)
252
- - [Chess Range(Puzzle Type #23)](#chess-rangepuzzle-type-23)
260
+ - [Chess Range (Puzzle Type #23)](#chess-range-puzzle-type-23)
261
+ - [Chess Solo (Puzzle Type #24)](#chess-solo-puzzle-type-24)
262
+ - [Chess Melee (Puzzle Type #25)](#chess-melee-puzzle-type-25)
253
263
  - [Why SAT / CP-SAT?](#why-sat--cp-sat)
254
- - [What’s Inside](#whats-inside)
255
264
  - [Testing](#testing)
256
265
  - [Contributing](#contributing)
257
266
  - [Build and push to PyPI](#build-and-push-to-pypi)
@@ -1935,7 +1944,7 @@ In the case when there's only one possible choice left, the solver will inform y
1935
1944
 
1936
1945
  ---
1937
1946
 
1938
- ## Chess Range(Puzzle Type #23)
1947
+ ## Chess Range (Puzzle Type #23)
1939
1948
 
1940
1949
  * [**Play online**](https://www.puzzle-chess.com/chess-ranger-11/)
1941
1950
 
@@ -1944,7 +1953,7 @@ In the case when there's only one possible choice left, the solver will inform y
1944
1953
  <details>
1945
1954
  <summary><strong>Rules</strong></summary>
1946
1955
 
1947
- You are given a chess board with $N$ pieces distributed on it. Your aim is to make $N-1$ range of moves where each move is a legal chess move and captures another piece.
1956
+ You are given a chess board with $N$ pieces distributed on it. Your aim is to make $N-1$ sequence of moves where each move is a legal chess move and captures another piece.
1948
1957
 
1949
1958
  - Pieces move as standard chess pieces.
1950
1959
  - You can perform only capture moves. A move that does not capture another piece is not allowed.
@@ -1977,7 +1986,7 @@ Solution found
1977
1986
  ['Rg2->Pc2', 'Rc2->Bc3', 'Rc3->Pd3', 'Kb6->Pb5', 'Pg4->Nf5', 'Rd3->Rb3', 'Rb3->Kb5', 'Nc6->Qe7', 'Ne7->Pf5', 'Rb5->Nf5']
1978
1987
  Solutions found: 1
1979
1988
  status: FEASIBLE
1980
- Time taken: 6.27 seconds
1989
+ Time taken: 1.16 seconds
1981
1990
  ```
1982
1991
 
1983
1992
  **Solved puzzle**
@@ -1986,6 +1995,108 @@ Time taken: 6.27 seconds
1986
1995
 
1987
1996
  ---
1988
1997
 
1998
+ ## Chess Solo (Puzzle Type #24)
1999
+
2000
+ * [**Play online**](https://www.puzzle-chess.com/solo-chess-11/)
2001
+
2002
+ * [**Solver Code**][24]
2003
+
2004
+ <details>
2005
+ <summary><strong>Rules</strong></summary>
2006
+
2007
+ You are given a chess board with $N$ pieces distributed on it. Your aim is to make $N-1$ sequence of moves where each move is a legal chess move and captures another piece and end up with the king as the only piece on the board. You are not allowed to move a piece more than twice.
2008
+
2009
+ - Pieces move as standard chess pieces.
2010
+ - You can perform only capture moves. A move that does not capture another piece is not allowed.
2011
+ - You can move a piece only twice.
2012
+ - You are NOT allowed to capture the king.
2013
+ - The goal is to end up with one single piece (the king) on the board.
2014
+
2015
+ </details>
2016
+
2017
+ **Unsolved puzzle**
2018
+
2019
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_solo_unsolved.png" alt="Chess solo unsolved" width="500">
2020
+
2021
+ Code to utilize this package and solve the puzzle:
2022
+
2023
+ (Note that this puzzle does not typically have a unique solution. Thus, we specify here that we only want the first valid solution that the solver finds.)
2024
+
2025
+ ```python
2026
+ # algebraic notation
2027
+ board = ['Kc6', 'Rc5', 'Rc4', 'Pb3', 'Bd3', 'Pd2', 'Pe3', 'Nf2', 'Ng2', 'Qg3', 'Pg6']
2028
+ binst = solver.Board(board)
2029
+ solutions = binst.solve_and_print(max_solutions=1)
2030
+ ```
2031
+ **Script Output**
2032
+
2033
+ The output is in the form of "pos -> pos" where "pos" is the algebraic notation of the position.
2034
+
2035
+ ```python
2036
+ Solution found
2037
+ ['Qg3->Pg6', 'Qg6->Bd3', 'Pd2->Pe3', 'Ng2->Pe3', 'Nf2->Qd3', 'Ne3->Rc4', 'Pb3->Nc4', 'Nd3->Rc5', 'Kc6->Nc5', 'Kc5->Pc4']
2038
+ Solutions found: 1
2039
+ status: FEASIBLE
2040
+ Time taken: 0.47 seconds
2041
+ ```
2042
+
2043
+ **Solved puzzle**
2044
+
2045
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_solo_solved.png" alt="Chess solo solved" width="500">
2046
+
2047
+ ---
2048
+
2049
+ ## Chess Melee (Puzzle Type #25)
2050
+
2051
+ * [**Play online**](https://www.puzzle-chess.com/chess-melee-13/)
2052
+
2053
+ * [**Solver Code**][25]
2054
+
2055
+ <details>
2056
+ <summary><strong>Rules</strong></summary>
2057
+
2058
+ You are given a chess board with $N$ pieces distributed on it (equal white and black pieces, one more black if $N$ is odd). Your aim is to make $N-1$ sequence of moves where each move is a legal chess move and captures another piece of the opposite color and end up with a single piece on the board. White starts and colors alternate as usual.
2059
+
2060
+ - Pieces move as standard chess pieces.
2061
+ - White moves first.
2062
+ - You can perform only capture moves. A move that does not capture another piece of the opposite color is not allowed.
2063
+ - The goal is to end up with one single piece on the board.
2064
+
2065
+ </details>
2066
+
2067
+ **Unsolved puzzle**
2068
+
2069
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_melee_unsolved.png" alt="Chess melee unsolved" width="500">
2070
+
2071
+ Code to utilize this package and solve the puzzle:
2072
+
2073
+ (Note that this puzzle does not typically have a unique solution. Thus, we specify here that we only want the first valid solution that the solver finds.)
2074
+
2075
+ ```python
2076
+ # algebraic notation
2077
+ board = ['Pb7', 'Nc7', 'Bc6', 'Ne6', 'Pb5', 'Rc4', 'Qb3', 'Rf7', 'Rb6', 'Pe5', 'Nc3', 'Pd3', 'Nf3']
2078
+ colors = ['B', 'B', 'B', 'B', 'B', 'B', 'B', 'W', 'W', 'W', 'W', 'W', 'W']
2079
+ binst = solver.Board(board, colors)
2080
+ solutions = binst.solve_and_print()
2081
+ ```
2082
+ **Script Output**
2083
+
2084
+ The output is in the form of "pos -> pos" where "pos" is the algebraic notation of the position.
2085
+
2086
+ ```python
2087
+ Solution found
2088
+ ['Rf7->Nc7', 'Ne6->Rc7', 'Pd3->Rc4', 'Qb3->Nc3', 'Pc4->Pb5', 'Qc3->Pe5', 'Nf3->Qe5', 'Nc7->Pb5', 'Ne5->Bc6', 'Pb7->Nc6', 'Rb6->Nb5', 'Pc6->Rb5']
2089
+ Solutions found: 1
2090
+ status: OPTIMAL
2091
+ Time taken: 6.24 seconds
2092
+ ```
2093
+
2094
+ **Solved puzzle**
2095
+
2096
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_melee_solved.png" alt="Chess melee solved" width="500">
2097
+
2098
+ ---
2099
+
1989
2100
  ---
1990
2101
 
1991
2102
  ## Why SAT / CP-SAT?
@@ -2000,36 +2111,6 @@ This repo builds those constraints in Python and uses SAT/CP-SAT (e.g., OR-Tools
2000
2111
 
2001
2112
  ---
2002
2113
 
2003
- ## What’s Inside
2004
-
2005
- Each sub directory in `src/puzzle_solver/puzzles/` targets a different puzzle type. The following are the sub directories:
2006
-
2007
- * `nonograms` — Picross/Griddlers (run-length constraints). ([Chapter 10][1])
2008
- * `sudoku` — Sudoku (rows/cols/blocks all-different). ([Chapter 11][2])
2009
- * `minesweeper` — Minesweeper (mines + counts). ([Chapter 12][3])
2010
- * `guess` — Guess (similar to wordle, guess the colored circles). ([Chapter 15][22])
2011
- * `dominosa` — Dominosa (dominoes + counts). ([Chapter 17][4])
2012
- * `light_up` — *Akari* / Light Up (lighting & adjacency). ([Chapter 21][5])
2013
- * `map` — Map (region coloring). ([Chapter 22][18])
2014
- * `inertia` — Inertia (collect all gems without dying (with least number of moves; my addition)). ([Chapter 24][21])
2015
- * `tents` — Tents (tree-tent matching). ([Chapter 25][6])
2016
- * `bridges` — Bridges (island connections). ([Chapter 26][20])
2017
- * `filling` — Filling (Fillomino-style), region sizes. ([Chapter 29][7])
2018
- * `keen` — Keen (arithmetic operations). ([Chapter 30][8])
2019
- * `towers` — Skyscrapers (permutation + visibility). ([Chapter 31][9])
2020
- * `singles` — Singles (hiding numbers). ([Chapter 32][10])
2021
- * `magnets` — Magnets (polarized dominoes + counts). ([Chapter 33][11])
2022
- * `signpost` — Signpost (visible dominoes + counts). ([Chapter 34][12])
2023
- * `range` — Range (rays & totals). ([Chapter 35][13])
2024
- * `pearl` — Pearl (pearl game). ([Chapter 36][19])
2025
- * `undead` — UnDead (Vampires/Zombies/Ghosts). ([Chapter 37][14])
2026
- * `unruly` — Unruly (no triples + balance). ([Chapter 38][15])
2027
- * `tracks` — Tracks (connected components). ([Chapter 40][16])
2028
- * `mosaic` — Mosaic (Tapa-like tiling). ([Chapter 42][17])
2029
- * `chess_range` — Chess Range (chess moves). ([Puzzle-Chess][23])
2030
-
2031
- ---
2032
-
2033
2114
  ## Testing
2034
2115
 
2035
2116
  To run the tests, simply run the following (to create a fresh conda environment and install the dev dependencies):
@@ -2090,3 +2171,5 @@ Issues and PRs welcome!
2090
2171
  [16]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/tracks "puzzle_solver/src/puzzle_solver/puzzles/tracks at master · Ar-Kareem/puzzle_solver · GitHub"
2091
2172
  [17]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/mosaic "puzzle_solver/src/puzzle_solver/puzzles/mosaic at master · Ar-Kareem/puzzle_solver · GitHub"
2092
2173
  [23]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/chess_range "puzzle_solver/src/puzzle_solver/puzzles/chess_range at master · Ar-Kareem/puzzle_solver · GitHub"
2174
+ [24]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/chess_range#chess-solo-puzzle-type-24 "puzzle_solver/src/puzzle_solver/puzzles/chess_range at master · Ar-Kareem/puzzle_solver · GitHub"
2175
+ [25]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/chess_range#chess-melee-puzzle-type-25 "puzzle_solver/src/puzzle_solver/puzzles/chess_range at master · Ar-Kareem/puzzle_solver · GitHub"
@@ -1,15 +1,17 @@
1
- puzzle_solver/__init__.py,sha256=E6zLTG4lv93Tr8Emb5LBfzRLNJOSe7AfTrW4LT7DTak,1655
1
+ puzzle_solver/__init__.py,sha256=SnID9DFBEgsMeeEJlk9ielo5xbMxM55Ce_4ZGQnA55s,1813
2
2
  puzzle_solver/core/utils.py,sha256=3LlBDuie_G0uSlzibpQS2ULmEYSZmpJXh1kawj7rjkg,3396
3
3
  puzzle_solver/core/utils_ortools.py,sha256=qLTIzmITqmgGZvg8XpYAZ4c-lhD5sEDQfS8ECdQ_dkM,3005
4
4
  puzzle_solver/puzzles/bridges/bridges.py,sha256=zUT0TMIu8l982fqDMJfsTnTgqm48nG0iH8flsGT45_E,5489
5
- puzzle_solver/puzzles/chess_range/chess_range.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
5
+ puzzle_solver/puzzles/chess_range/chess_melee.py,sha256=KnfD_Sxd8bso46eQYpIemp4MIqOUNoonyRVe6soK8kc,231
6
+ puzzle_solver/puzzles/chess_range/chess_range.py,sha256=IaldwJR4d0VAUxME2QyvtJdUNzGzDV0FGs1iq9KqsRU,21072
7
+ puzzle_solver/puzzles/chess_range/chess_solo.py,sha256=U3v766UsZHx_dC3gxqU90VbjAXn-OlYhtrnnvJYFvrQ,401
6
8
  puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
7
9
  puzzle_solver/puzzles/dominosa/dominosa.py,sha256=uh2vsba9HdSHGnYiYE8R_TZzQh5kge51Y1TMRyQlwek,7246
8
10
  puzzle_solver/puzzles/filling/filling.py,sha256=UAkNYjlfxOrYGrRVmuElhWPeW10xD6kiWuB8oELzy3w,9141
9
11
  puzzle_solver/puzzles/guess/guess.py,sha256=w8ZE-0MswR1_e7_MX622OiCJ8fGTloRHblYFvSA4yHc,10812
10
12
  puzzle_solver/puzzles/inertia/inertia.py,sha256=xHtQ09sI7hqPcs20foz6YVMuYCsw59TZQxTumAKJuBs,5658
11
13
  puzzle_solver/puzzles/inertia/tsp.py,sha256=qMyT_W5vBmmaFUB7Cl9rC5xJNdAdQvIJEx6yxlbG9hw,15240
12
- puzzle_solver/puzzles/inertia/parse_map/parse_map.py,sha256=DCV8oI4lAp4CbRVrxeki51t_uJCx1hcCxxpdka1J8AA,8382
14
+ puzzle_solver/puzzles/inertia/parse_map/parse_map.py,sha256=A9JQTNqamUdzlwqks0XQp3Hge3mzyTIVK6YtDJvqpL4,8422
13
15
  puzzle_solver/puzzles/keen/keen.py,sha256=tDb6C5S3Q0JAKPsdw-84WQ6PxRADELZHr_BK8FDH-NA,5039
14
16
  puzzle_solver/puzzles/light_up/light_up.py,sha256=iSA1rjZMFsnI0V0Nxivxox4qZkB7PvUrROSHXcoUXds,4541
15
17
  puzzle_solver/puzzles/magnets/magnets.py,sha256=-Wl49JD_PKeq735zQVMQ3XSQX6gdHiY-7PKw-Sh16jw,6474
@@ -27,7 +29,7 @@ puzzle_solver/puzzles/towers/towers.py,sha256=QvL0Pp-Z2ewCeq9ZkNrh8MShKOh-Y52sFB
27
29
  puzzle_solver/puzzles/tracks/tracks.py,sha256=VnAtxBkuUTHJYNXr1JGg0yYzJj3kRMBi8Nz7NHKS94A,9089
28
30
  puzzle_solver/puzzles/undead/undead.py,sha256=ygNugW5SOlYLy6d740gZ2IW9UJQ3SAr9vuMm0ZFr2nY,6630
29
31
  puzzle_solver/puzzles/unruly/unruly.py,sha256=sDF0oKT50G-NshyW2DYrvAgD9q9Ku9ANUyNhGSAu7cQ,3827
30
- multi_puzzle_solver-0.8.7.dist-info/METADATA,sha256=WEFmdDkVqTMuKK1rDqX5r378UpPvtA8SriZ6ZZcFGJ4,101834
31
- multi_puzzle_solver-0.8.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
- multi_puzzle_solver-0.8.7.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
33
- multi_puzzle_solver-0.8.7.dist-info/RECORD,,
32
+ multi_puzzle_solver-0.9.1.dist-info/METADATA,sha256=LmgK-1usuWuTNzO1ffTSVqtZQbeiD8ZKwg6zD81KEtw,104983
33
+ multi_puzzle_solver-0.9.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
+ multi_puzzle_solver-0.9.1.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
35
+ multi_puzzle_solver-0.9.1.dist-info/RECORD,,
puzzle_solver/__init__.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from puzzle_solver.puzzles.bridges import bridges as bridges_solver
2
2
  from puzzle_solver.puzzles.chess_range import chess_range as chess_range_solver
3
+ from puzzle_solver.puzzles.chess_range import chess_solo as chess_solo_solver
4
+ from puzzle_solver.puzzles.chess_range import chess_melee as chess_melee_solver
3
5
  from puzzle_solver.puzzles.dominosa import dominosa as dominosa_solver
4
6
  from puzzle_solver.puzzles.filling import filling as filling_solver
5
7
  from puzzle_solver.puzzles.guess import guess as guess_solver
@@ -24,4 +26,4 @@ from puzzle_solver.puzzles.unruly import unruly as unruly_solver
24
26
 
25
27
  from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
26
28
 
27
- __version__ = '0.8.7'
29
+ __version__ = '0.9.1'
@@ -0,0 +1,7 @@
1
+ from .chess_range import Board as RangeBoard
2
+ from .chess_range import PieceType
3
+
4
+ class Board(RangeBoard):
5
+ def __init__(self, pieces: list[str], colors: list[str]):
6
+ super().__init__(pieces=pieces, colors=colors)
7
+
@@ -3,10 +3,9 @@ from dataclasses import dataclass
3
3
  from typing import Union
4
4
  from enum import Enum
5
5
 
6
- import numpy as np
7
6
  from ortools.sat.python import cp_model
8
7
 
9
- from puzzle_solver.core.utils import Pos, get_pos, get_all_pos, get_char, set_char, get_row_pos, get_col_pos, Direction, get_next_pos, in_bounds
8
+ from puzzle_solver.core.utils import Pos, get_pos
10
9
  from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, or_constraint
11
10
 
12
11
 
@@ -22,6 +21,7 @@ class PieceType(Enum):
22
21
  @dataclass(frozen=True)
23
22
  class SingleSolution:
24
23
  assignment: dict[int, tuple[str, Pos, Pos, str]] # every time step a single piece moves from one position to another and eats another piece
24
+ position_occupied: dict[Pos, int]
25
25
  # pos_assignment: dict[tuple[int, int, Union[Pos, str]], int]
26
26
  # mover: dict[int, tuple[int, PieceType]]
27
27
  # victim: dict[int, tuple[int, PieceType]]
@@ -37,6 +37,7 @@ class SingleSolution:
37
37
 
38
38
 
39
39
  def parse_algebraic_notation(algebraic: str) -> tuple[PieceType, Pos]:
40
+ assert isinstance(algebraic, str), f'algebraic notation must be a string, got {type(algebraic)}'
40
41
  assert len(algebraic) == 3, 'algebraic notation must be 3 characters'
41
42
  p = {'K': PieceType.KING, 'Q': PieceType.QUEEN, 'R': PieceType.ROOK, 'B': PieceType.BISHOP, 'N': PieceType.KNIGHT, 'P': PieceType.PAWN}
42
43
  assert algebraic[0] in p, 'invalid piece type'
@@ -49,6 +50,7 @@ def parse_algebraic_notation(algebraic: str) -> tuple[PieceType, Pos]:
49
50
  pos = get_pos(x=file, y=rank)
50
51
  return (piece_type, pos)
51
52
 
53
+
52
54
  def to_algebraic_notation_single_move(piece_type: str, from_pos: Pos, to_pos: Pos, victim_type: str) -> str:
53
55
  letter = {PieceType.KING.name: 'K', PieceType.QUEEN.name: 'Q', PieceType.ROOK.name: 'R', PieceType.BISHOP.name: 'B', PieceType.KNIGHT.name: 'N', PieceType.PAWN.name: 'P'}
54
56
  from_file_letter = chr(from_pos.x + ord('a'))
@@ -57,7 +59,9 @@ def to_algebraic_notation_single_move(piece_type: str, from_pos: Pos, to_pos: Po
57
59
  to_rank_letter = str(to_pos.y + 1)
58
60
  return f'{letter[piece_type]}{from_file_letter}{from_rank_letter}->{letter[victim_type]}{to_file_letter}{to_rank_letter}'
59
61
 
60
- def to_algebraic_notation(move_sequence: dict[int, tuple[str, Pos, Pos, str]]) -> list[str]:
62
+
63
+ def to_algebraic_notation(single_solution: SingleSolution) -> list[str]:
64
+ move_sequence = single_solution.assignment
61
65
  move_sequence = sorted(move_sequence.items(), key=lambda x: x[0])
62
66
  move_sequence = [x[1] for x in move_sequence]
63
67
  return [to_algebraic_notation_single_move(piece_type, from_pos, to_pos, victim_type) for piece_type, from_pos, to_pos, victim_type in move_sequence]
@@ -66,50 +70,123 @@ def to_algebraic_notation(move_sequence: dict[int, tuple[str, Pos, Pos, str]]) -
66
70
  def is_same_row_col(from_pos: Pos, to_pos: Pos) -> bool:
67
71
  return from_pos.x == to_pos.x or from_pos.y == to_pos.y
68
72
 
73
+
69
74
  def is_diagonal(from_pos: Pos, to_pos: Pos) -> bool:
70
75
  return abs(from_pos.x - to_pos.x) == abs(from_pos.y - to_pos.y)
71
76
 
72
- def is_move_valid(from_pos: Pos, to_pos: Pos, piece_type: PieceType) -> bool:
77
+
78
+ def squares_between_rook(from_pos: Pos, to_pos: Pos) -> list[Pos]:
79
+ if not is_same_row_col(from_pos, to_pos):
80
+ return []
81
+ if abs(from_pos.x - to_pos.x) <= 1 and abs(from_pos.y - to_pos.y) <= 1: # within 2x2 thus no intermediate squares
82
+ return []
83
+ squares: list[Pos] = []
84
+ if from_pos.x == to_pos.x:
85
+ x = from_pos.x
86
+ step = 1 if to_pos.y > from_pos.y else -1
87
+ for y in range(from_pos.y + step, to_pos.y, step):
88
+ squares.append(get_pos(x=x, y=y))
89
+ else:
90
+ y = from_pos.y
91
+ step = 1 if to_pos.x > from_pos.x else -1
92
+ for x in range(from_pos.x + step, to_pos.x, step):
93
+ squares.append(get_pos(x=x, y=y))
94
+ return squares
95
+
96
+
97
+ def squares_between_bishop(from_pos: Pos, to_pos: Pos) -> list[Pos]:
98
+ if not is_diagonal(from_pos, to_pos):
99
+ return []
100
+ if abs(from_pos.x - to_pos.x) <= 1 and abs(from_pos.y - to_pos.y) <= 1: # within 2x2 thus no intermediate squares
101
+ return []
102
+ squares: list[Pos] = []
103
+ step_x = 1 if to_pos.x > from_pos.x else -1
104
+ step_y = 1 if to_pos.y > from_pos.y else -1
105
+ x = from_pos.x + step_x
106
+ y = from_pos.y + step_y
107
+ while x != to_pos.x and y != to_pos.y:
108
+ squares.append(get_pos(x=x, y=y))
109
+ x += step_x
110
+ y += step_y
111
+ return squares
112
+
113
+
114
+
115
+ def is_move_valid(from_pos: Pos, to_pos: Pos, piece_type: PieceType, color=None) -> tuple[bool, list[Pos]]:
116
+ """Returns: (is_valid, list of positions that must be empty for the move to be valid)
117
+ For Kings, Pawns, and Knights, no positions must be empty for the move to be valid.
118
+ A Queen is equivalent to a Rook and a Bishop.
119
+ A Rook needs all positions directly between the from and to position to be empty for the move to be valid.
120
+ Similarly, a Bishop needs all positions diagonally between the from and to position to be empty for the move to be valid.
121
+
122
+ Args:
123
+ from_pos (Pos): from position
124
+ to_pos (Pos): to position
125
+ piece_type (PieceType): piece type
126
+ color (str, optional): color of the piece (default: None, all pieces are assumed white)
127
+
128
+ Returns:
129
+ tuple[bool, list[Pos]]: (is_valid, list of positions that must be empty for the move to be valid)
130
+ """
73
131
  if piece_type == PieceType.KING:
74
132
  dx = abs(from_pos.x - to_pos.x)
75
133
  dy = abs(from_pos.y - to_pos.y)
76
- return dx <= 1 and dy <= 1
77
- elif piece_type == PieceType.QUEEN:
78
- return is_same_row_col(from_pos, to_pos) or is_diagonal(from_pos, to_pos)
134
+ is_valid = dx <= 1 and dy <= 1
135
+ return is_valid, []
136
+ elif piece_type == PieceType.QUEEN: # rook + bishop
137
+ rook_valid = is_move_valid(from_pos, to_pos, PieceType.ROOK, color)
138
+ if rook_valid[0]:
139
+ return rook_valid
140
+ return is_move_valid(from_pos, to_pos, PieceType.BISHOP, color)
79
141
  elif piece_type == PieceType.ROOK:
80
- return is_same_row_col(from_pos, to_pos)
142
+ return is_same_row_col(from_pos, to_pos), squares_between_rook(from_pos, to_pos)
81
143
  elif piece_type == PieceType.BISHOP:
82
- return is_diagonal(from_pos, to_pos)
144
+ return is_diagonal(from_pos, to_pos), squares_between_bishop(from_pos, to_pos)
83
145
  elif piece_type == PieceType.KNIGHT:
84
146
  dx = abs(from_pos.x - to_pos.x)
85
147
  dy = abs(from_pos.y - to_pos.y)
86
- return (dx == 2 and dy == 1) or (dx == 1 and dy == 2)
148
+ is_valid = (dx == 2 and dy == 1) or (dx == 1 and dy == 2)
149
+ return is_valid, []
87
150
  elif piece_type == PieceType.PAWN: # will always eat because the this is how the puzzle works
88
151
  dx = to_pos.x - from_pos.x
89
152
  dy = to_pos.y - from_pos.y
90
- return abs(dx) == 1 and dy == 1
153
+ is_valid = abs(dx) == 1 and dy == (1 if color != 'B' else -1)
154
+ return is_valid, []
91
155
 
92
156
 
93
157
  class Board:
94
- def __init__(self, pieces: list[str]):
158
+ def __init__(self, pieces: list[str], colors: list[str] = None, max_moves_per_piece: int = None, last_piece_alive: Union[PieceType, str] = None):
159
+ """
160
+ Args:
161
+ pieces: list of algebraic notation of the pieces
162
+ colors: list of colors of the pieces (default: None, all pieces are assumed white (i.e. all pieces are the same except for pawns move only up))
163
+ max_moves_per_piece: maximum number of moves per piece (default: None, no limit)
164
+ last_piece_alive: force the last piece alive to be of this type (default: None, any piece can be last man standing)
165
+ """
95
166
  self.pieces: dict[int, tuple[PieceType, Pos]] = {i: parse_algebraic_notation(p) for i, p in enumerate(pieces)}
167
+ assert colors is None or (len(colors) == len(self.pieces) and all(c in ['B', 'W'] for c in colors)), f'if provided, colors must be a list of length {len(self.pieces)} with elements B or W, got {colors}'
168
+ self.colors = colors
96
169
  self.N = len(self.pieces) # number of pieces
97
170
  self.T = self.N # (N-1) moves + 1 initial state
98
-
171
+ self.max_moves_per_piece = max_moves_per_piece
172
+ self.last_piece_alive = last_piece_alive
99
173
  self.V = 8 # board size
100
174
  self.H = 8 # board size
101
- self.num_positions = self.V * self.H # 8x8 board
175
+ # the puzzle rules mean the only legal positions are the starting positions of the pieces
176
+ self.all_legal_positions: set[Pos] = {pos for _, pos in self.pieces.values()}
177
+ assert len(self.all_legal_positions) == len(self.pieces), f'positions are not unique'
102
178
 
103
179
  self.model = cp_model.CpModel()
104
- # Input numbers: N is number of piece, T is number of time steps (=N here), B is board size (=64 here):
180
+ # Input numbers: N is number of piece, T is number of time steps (=N here), B is board size (=N here because the only legal positions are the starting positions of the pieces):
105
181
  # Number of variables
106
182
  # piece_positions: O(NTB)
107
183
  # is_dead: O(NT)
108
184
  # mover: O(NT)
109
185
  # victim: O(NT)
186
+ # position_occupied: O(TB)
110
187
  # dies_this_timestep: O(NT)
111
188
  # pos_is_p_star: O(NTB)
112
- # Total: ~ (2*64)N^2 + 5N^2 = 132N^2
189
+ # Total: ~ (2*N)N^2 + 6N^2 = 2N^3 + 6N^2
113
190
 
114
191
  # (piece_index, time_step, position) -> boolean variable (possible all false if i'm dead)
115
192
  self.piece_positions: dict[tuple[int, int, Pos], cp_model.IntVar] = {}
@@ -119,13 +196,24 @@ class Board:
119
196
  self.mover: dict[tuple[int, int], cp_model.IntVar] = {} # did i move this timestep?
120
197
  self.victim: dict[tuple[int, int], cp_model.IntVar] = {} # did i die this timestep?
121
198
 
199
+ # (time_step, position) -> boolean variable indicating if the position is occupied by any piece at this timestep
200
+ self.position_occupied: dict[tuple[int, Pos], cp_model.IntVar] = {}
201
+
122
202
  self.create_vars()
123
203
  self.add_all_constraints()
124
-
204
+
205
+ def can_move(self, p: int, t: int) -> bool:
206
+ c = self.colors[p]
207
+ return (c == 'W' and t % 2 == 0) or (c == 'B' and t % 2 == 1)
208
+
209
+ def can_be_victim(self, p: int, t: int) -> bool:
210
+ c = self.colors[p]
211
+ return (c == 'W' and t % 2 == 1) or (c == 'B' and t % 2 == 0)
212
+
125
213
  def create_vars(self):
126
214
  for p in range(self.N):
127
215
  for t in range(self.T):
128
- for pos in get_all_pos(self.V, self.H):
216
+ for pos in self.all_legal_positions:
129
217
  self.piece_positions[(p, t, pos)] = self.model.NewBoolVar(f'piece_positions[{p},{t},{pos}]')
130
218
  self.is_dead[(p, t)] = self.model.NewBoolVar(f'is_dead[{p},{t}]')
131
219
  for p in range(self.N):
@@ -133,10 +221,15 @@ class Board:
133
221
  self.mover[(p, t)] = self.model.NewIntVar(0, 1, f'mover[{p},{t}]')
134
222
  self.victim[(p, t)] = self.model.NewIntVar(0, 1, f'victim[{p},{t}]')
135
223
 
224
+ for t in range(self.T):
225
+ for pos in self.all_legal_positions:
226
+ self.position_occupied[(t, pos)] = self.model.NewBoolVar(f'position_occupied[{t},{pos}]')
227
+
136
228
  def add_all_constraints(self):
137
229
  self.enforce_initial_state()
138
230
  self.enforce_board_state_constraints()
139
231
  self.enforce_mover_victim_constraints()
232
+ self.enforce_position_occupied_constraints()
140
233
 
141
234
  def enforce_initial_state(self):
142
235
  # initial state
@@ -145,7 +238,7 @@ class Board:
145
238
  # cant be initially dead
146
239
  self.model.Add(self.is_dead[(p, 0)] == 0)
147
240
  # all others are blank
148
- for pos in get_all_pos(self.V, self.H):
241
+ for pos in self.all_legal_positions:
149
242
  if pos == initial_pos:
150
243
  continue
151
244
  self.model.Add(self.piece_positions[(p, 0, pos)] == 0)
@@ -154,7 +247,7 @@ class Board:
154
247
  # at each timestep and each piece, it can only be at exactly one position or dead
155
248
  for p in range(self.N):
156
249
  for t in range(self.T):
157
- pos_vars = [self.piece_positions[(p, t, pos)] for pos in get_all_pos(self.V, self.H)]
250
+ pos_vars = [self.piece_positions[(p, t, pos)] for pos in self.all_legal_positions]
158
251
  pos_vars.append(self.is_dead[(p, t)])
159
252
  self.model.AddExactlyOne(pos_vars)
160
253
  # if im dead this timestep then im also dead next timestep
@@ -163,22 +256,55 @@ class Board:
163
256
  self.model.Add(self.is_dead[(p, t + 1)] == 1).OnlyEnforceIf(self.is_dead[(p, t)])
164
257
  # every move must be legal chess move
165
258
  for p in range(self.N):
259
+ color = self.colors[p] if self.colors is not None else None
166
260
  for t in range(self.T - 1):
167
- for from_pos in get_all_pos(self.V, self.H):
168
- for to_pos in get_all_pos(self.V, self.H):
261
+ for from_pos in self.all_legal_positions:
262
+ for to_pos in self.all_legal_positions:
169
263
  if from_pos == to_pos:
170
264
  continue
171
- if not is_move_valid(from_pos, to_pos, self.pieces[p][0]):
265
+ is_valid, need_to_be_empty = is_move_valid(from_pos, to_pos, self.pieces[p][0], color=color)
266
+ # remove non legal moves
267
+ need_to_be_empty = set(need_to_be_empty) & self.all_legal_positions
268
+ if not is_valid:
172
269
  self.model.Add(self.piece_positions[(p, t + 1, to_pos)] == 0).OnlyEnforceIf([self.piece_positions[(p, t, from_pos)]])
270
+ elif len(need_to_be_empty) > 0:
271
+ occupied_between = self.model.NewBoolVar(f'occupied_between[{from_pos},{to_pos},{t},{p}]')
272
+ or_constraint(self.model, occupied_between, [self.position_occupied[(t, pos)] for pos in need_to_be_empty])
273
+ self.model.Add(self.piece_positions[(p, t + 1, to_pos)] == 0).OnlyEnforceIf([self.piece_positions[(p, t, from_pos)], occupied_between])
274
+
173
275
  # if mover is i and victim is j then i HAS to be at the position of j at the next timestep
174
276
  for p_mover in range(self.N):
175
277
  for p_victim in range(self.N):
176
278
  if p_mover == p_victim:
177
279
  continue
178
280
  for t in range(self.T - 1):
179
- for pos in get_all_pos(self.V, self.H):
281
+ for pos in self.all_legal_positions:
180
282
  self.model.Add(self.piece_positions[(p_mover, t + 1, pos)] == self.piece_positions[(p_victim, t, pos)]).OnlyEnforceIf([self.mover[(p_mover, t)], self.victim[(p_victim, t)]])
181
283
 
284
+ # optional parameter to force last piece alive
285
+ if self.last_piece_alive is not None:
286
+ target_ps = [p for p in range(self.N) if self.pieces[p][0] == self.last_piece_alive]
287
+ assert len(target_ps) == 1, f'multiple pieces of type {self.last_piece_alive} found'
288
+ target_p = target_ps[0]
289
+ # target piece is force to be last man standing
290
+ self.model.Add(self.is_dead[(target_p, self.T - 1)] == 0)
291
+ for p in range(self.N):
292
+ if p == target_p:
293
+ continue
294
+ self.model.Add(self.is_dead[(p, self.T - 1)] == 1)
295
+
296
+ if self.colors is not None:
297
+ # t=0 and even timesteps are white, odd timesteps are black
298
+ for p in range(self.N):
299
+ for t in range(self.T - 1):
300
+ if not self.can_move(p, t):
301
+ self.model.Add(self.mover[(p, t)] == 0)
302
+ # t=0 and even timesteps only black victims, odd timesteps only white victims
303
+ for p in range(self.N):
304
+ for t in range(self.T - 1):
305
+ if not self.can_be_victim(p, t):
306
+ self.model.Add(self.victim[(p, t)] == 0)
307
+
182
308
  def enforce_mover_victim_constraints(self):
183
309
  for p in range(self.N):
184
310
  for t in range(self.T - 1):
@@ -201,7 +327,7 @@ class Board:
201
327
  # if next timestep im somewhere else then i was the mover
202
328
  # i.e. there exists a position p* s.t. (piece_positions[p, t + 1, p*] AND NOT piece_positions[p, t, p*])
203
329
  pos_is_p_star = []
204
- for pos in get_all_pos(self.V, self.H):
330
+ for pos in self.all_legal_positions:
205
331
  v = self.model.NewBoolVar(f'pos_is_p_star[{p},{t},{pos}]')
206
332
  self.model.Add(v == 1).OnlyEnforceIf([self.piece_positions[(p, t + 1, pos)], self.piece_positions[(p, t, pos)].Not()])
207
333
  self.model.Add(v == 0).OnlyEnforceIf([self.piece_positions[(p, t + 1, pos)].Not()])
@@ -216,13 +342,23 @@ class Board:
216
342
  for t in range(self.T - 1):
217
343
  self.model.AddExactlyOne([self.victim[(p, t)] for p in range(self.N)])
218
344
 
345
+ # optional parameter to force
346
+ if self.max_moves_per_piece is not None:
347
+ for p in range(self.N):
348
+ self.model.Add(sum([self.mover[(p, t)] for t in range(self.T - 1)]) <= self.max_moves_per_piece)
349
+
350
+ def enforce_position_occupied_constraints(self):
351
+ for t in range(self.T):
352
+ for pos in self.all_legal_positions:
353
+ self.model.Add(self.position_occupied[(t, pos)] == sum([self.piece_positions[(p, t, pos)] for p in range(self.N)]))
354
+
219
355
 
220
356
  def solve_and_print(self, verbose: bool = True, max_solutions: int = None):
221
357
  def board_to_solution(board: "Board", solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
222
358
  pos_assignment: dict[tuple[int, int, Union[Pos, str]], int] = {}
223
359
  for t in range(board.T):
224
360
  for i in range(board.N):
225
- for pos in get_all_pos(board.V, board.H):
361
+ for pos in board.all_legal_positions:
226
362
  pos_assignment[(i, t, pos)] = solver.Value(board.piece_positions[(i, t, pos)])
227
363
  pos_assignment[(i, t, 'DEAD')] = solver.Value(board.is_dead[(i, t)])
228
364
  mover = {}
@@ -240,11 +376,12 @@ class Board:
240
376
  for t in range(board.T - 1):
241
377
  mover_i = mover[t][0]
242
378
  victim_i = victim[t][0]
243
- from_pos = next(pos for pos in get_all_pos(board.V, board.H) if pos_assignment[(mover_i, t, pos)])
244
- to_pos = next(pos for pos in get_all_pos(board.V, board.H) if pos_assignment[(mover_i, t + 1, pos)])
379
+ from_pos = next(pos for pos in board.all_legal_positions if pos_assignment[(mover_i, t, pos)])
380
+ to_pos = next(pos for pos in board.all_legal_positions if pos_assignment[(mover_i, t + 1, pos)])
245
381
  assignment[t] = (board.pieces[mover_i][0].name, from_pos, to_pos, board.pieces[victim_i][0].name)
246
382
  # return SingleSolution(assignment=assignment, pos_assignment=pos_assignment, mover=mover, victim=victim)
247
- return SingleSolution(assignment=assignment)
383
+ position_occupied = {(t, pos): int(solver.Value(board.position_occupied[(t, pos)])) for t in range(board.T) for pos in board.all_legal_positions}
384
+ return SingleSolution(assignment=assignment, position_occupied=position_occupied)
248
385
 
249
386
  def callback(single_res: SingleSolution):
250
387
  print("Solution found")
@@ -257,6 +394,8 @@ class Board:
257
394
  # print('victims:', single_res.victim)
258
395
  # print('movers:', single_res.mover)
259
396
  # print()
260
- move_sequence = to_algebraic_notation(single_res.assignment)
397
+ # for t in range(self.T):
398
+ # print('at timestep', t, 'the following positions are occupied', [pos for pos in self.all_legal_positions if single_res.position_occupied[(t, pos)] == 1])
399
+ move_sequence = to_algebraic_notation(single_res)
261
400
  print(move_sequence)
262
401
  return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=max_solutions)
@@ -0,0 +1,9 @@
1
+ from .chess_range import Board as RangeBoard
2
+ from .chess_range import PieceType
3
+
4
+ class Board(RangeBoard):
5
+ def __init__(self, pieces: list[str]):
6
+ king_pieces = [p for p in range(len(pieces)) if pieces[p][0] == 'K']
7
+ assert len(king_pieces) == 1, f'exactly one king piece is required'
8
+ super().__init__(pieces, max_moves_per_piece=2, last_piece_alive=PieceType.KING)
9
+
@@ -6,14 +6,8 @@
6
6
  from pathlib import Path
7
7
  import numpy as np
8
8
  import numpy as np
9
- try:
10
- import cv2 as cv
11
- except ImportError:
12
- cv = None
13
- try:
14
- from PIL import Image
15
- except ImportError:
16
- Image = None
9
+ cv = None
10
+ Image = None
17
11
 
18
12
  def load_cell_templates(p: Path) -> dict[str, dict]:
19
13
  img = Image.open(p)
@@ -137,6 +131,12 @@ def show_wait_destroy(winname, img):
137
131
 
138
132
 
139
133
  def main(image):
134
+ global Image
135
+ global cv
136
+ from PIL import Image as Image_module
137
+ import cv2 as cv_module
138
+ Image = Image_module
139
+ cv = cv_module
140
140
  CELL_BLANK = load_cell_templates(Path(__file__).parent / 'cells' / 'cell_blank.png')
141
141
  CELL_WALL = load_cell_templates(Path(__file__).parent / 'cells' / 'cell_wall.png')
142
142
  CELL_GEM = load_cell_templates(Path(__file__).parent / 'cells' / 'cell_gem.png')