multi-puzzle-solver 0.8.6__py3-none-any.whl → 0.9.0__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.6
3
+ Version: 0.9.0
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
@@ -206,12 +206,18 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
206
206
  </a>
207
207
  </td>
208
208
  <td align="center">
209
- <a href="#chess-sequence-puzzle-type-23"><b>Chess Sequence</b><br><br>
210
- <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_sequence_unsolved.png" alt="Chess sequence" height="120">
209
+ <a href="#chess-range-puzzle-type-23"><b>Chess Range</b><br><br>
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
+ <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">
213
219
 
214
- </td><td></td><td></td>
220
+ </td>
215
221
  </tr>
216
222
  </table>
217
223
 
@@ -249,9 +255,10 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
249
255
  - [Bridges (Puzzle Type #20)](#bridges-puzzle-type-20)
250
256
  - [Inertia (Puzzle Type #21)](#inertia-puzzle-type-21)
251
257
  - [Guess (Puzzle Type #22)](#guess-puzzle-type-22)
252
- - [Chess Sequence(Puzzle Type #23)](#chess-sequencepuzzle-type-23)
258
+ - [Chess Range (Puzzle Type #23)](#chess-range-puzzle-type-23)
259
+ - [Chess Solo (Puzzle Type #24)](#chess-solo-puzzle-type-24)
260
+ - [Chess Melee (Puzzle Type #25)](#chess-melee-puzzle-type-25)
253
261
  - [Why SAT / CP-SAT?](#why-sat--cp-sat)
254
- - [What’s Inside](#whats-inside)
255
262
  - [Testing](#testing)
256
263
  - [Contributing](#contributing)
257
264
  - [Build and push to PyPI](#build-and-push-to-pypi)
@@ -1935,7 +1942,7 @@ In the case when there's only one possible choice left, the solver will inform y
1935
1942
 
1936
1943
  ---
1937
1944
 
1938
- ## Chess Sequence(Puzzle Type #23)
1945
+ ## Chess Range (Puzzle Type #23)
1939
1946
 
1940
1947
  * [**Play online**](https://www.puzzle-chess.com/chess-ranger-11/)
1941
1948
 
@@ -1946,22 +1953,23 @@ In the case when there's only one possible choice left, the solver will inform y
1946
1953
 
1947
1954
  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
1955
 
1949
- This means that at the end of the $N-1$ moves, there is only one piece left alive on the board.
1950
-
1951
- A move that does not capture another piece is not allowed.
1956
+ - Pieces move as standard chess pieces.
1957
+ - You can perform only capture moves. A move that does not capture another piece is not allowed.
1958
+ - You are allowed to capture the king.
1959
+ - The goal is to end up with one single piece on the board.
1952
1960
 
1953
1961
  </details>
1954
1962
 
1955
1963
  **Unsolved puzzle**
1956
1964
 
1957
- <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_sequence_unsolved.png" alt="Chess sequence unsolved" width="500">
1965
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_range_unsolved.png" alt="Chess range unsolved" width="500">
1958
1966
 
1959
1967
  Code to utilize this package and solve the puzzle:
1960
1968
 
1961
1969
  (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.)
1962
1970
 
1963
1971
  ```python
1964
- from puzzle_solver import chess_sequence_solver as solver
1972
+ from puzzle_solver import chess_range_solver as solver
1965
1973
  # algebraic notation
1966
1974
  board = ['Qe7', 'Nc6', 'Kb6', 'Pb5', 'Nf5', 'Pg4', 'Rb3', 'Bc3', 'Pd3', 'Pc2', 'Rg2']
1967
1975
  binst = solver.Board(board)
@@ -1976,12 +1984,114 @@ Solution found
1976
1984
  ['Rg2->Pc2', 'Rc2->Bc3', 'Rc3->Pd3', 'Kb6->Pb5', 'Pg4->Nf5', 'Rd3->Rb3', 'Rb3->Kb5', 'Nc6->Qe7', 'Ne7->Pf5', 'Rb5->Nf5']
1977
1985
  Solutions found: 1
1978
1986
  status: FEASIBLE
1979
- Time taken: 6.27 seconds
1987
+ Time taken: 1.16 seconds
1988
+ ```
1989
+
1990
+ **Solved puzzle**
1991
+
1992
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_range_solved.png" alt="Chess range solved" width="500">
1993
+
1994
+ ---
1995
+
1996
+ ## Chess Solo (Puzzle Type #24)
1997
+
1998
+ * [**Play online**](https://www.puzzle-chess.com/solo-chess-11/)
1999
+
2000
+ * [**Solver Code**][24]
2001
+
2002
+ <details>
2003
+ <summary><strong>Rules</strong></summary>
2004
+
2005
+ 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.
2006
+
2007
+ - Pieces move as standard chess pieces.
2008
+ - You can perform only capture moves. A move that does not capture another piece is not allowed.
2009
+ - You can move a piece only twice.
2010
+ - You are NOT allowed to capture the king.
2011
+ - The goal is to end up with one single piece (the king) on the board.
2012
+
2013
+ </details>
2014
+
2015
+ **Unsolved puzzle**
2016
+
2017
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_solo_unsolved.png" alt="Chess solo unsolved" width="500">
2018
+
2019
+ Code to utilize this package and solve the puzzle:
2020
+
2021
+ (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.)
2022
+
2023
+ ```python
2024
+ # algebraic notation
2025
+ board = ['Kc6', 'Rc5', 'Rc4', 'Pb3', 'Bd3', 'Pd2', 'Pe3', 'Nf2', 'Ng2', 'Qg3', 'Pg6']
2026
+ binst = solver.Board(board)
2027
+ solutions = binst.solve_and_print(max_solutions=1)
2028
+ ```
2029
+ **Script Output**
2030
+
2031
+ The output is in the form of "pos -> pos" where "pos" is the algebraic notation of the position.
2032
+
2033
+ ```python
2034
+ Solution found
2035
+ ['Qg3->Pg6', 'Qg6->Bd3', 'Pd2->Pe3', 'Ng2->Pe3', 'Nf2->Qd3', 'Ne3->Rc4', 'Pb3->Nc4', 'Nd3->Rc5', 'Kc6->Nc5', 'Kc5->Pc4']
2036
+ Solutions found: 1
2037
+ status: FEASIBLE
2038
+ Time taken: 0.47 seconds
1980
2039
  ```
1981
2040
 
1982
2041
  **Solved puzzle**
1983
2042
 
1984
- <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_sequence_solved.png" alt="Chess sequence solved" width="500">
2043
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_solo_solved.png" alt="Chess solo solved" width="500">
2044
+
2045
+ ---
2046
+
2047
+ ## Chess Melee (Puzzle Type #25)
2048
+
2049
+ * [**Play online**](https://www.puzzle-chess.com/chess-melee-13/)
2050
+
2051
+ * [**Solver Code**][25]
2052
+
2053
+ <details>
2054
+ <summary><strong>Rules</strong></summary>
2055
+
2056
+ 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.
2057
+
2058
+ - Pieces move as standard chess pieces.
2059
+ - White moves first.
2060
+ - You can perform only capture moves. A move that does not capture another piece of the opposite color is not allowed.
2061
+ - The goal is to end up with one single piece on the board.
2062
+
2063
+ </details>
2064
+
2065
+ **Unsolved puzzle**
2066
+
2067
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_melee_unsolved.png" alt="Chess melee unsolved" width="500">
2068
+
2069
+ Code to utilize this package and solve the puzzle:
2070
+
2071
+ (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.)
2072
+
2073
+ ```python
2074
+ # algebraic notation
2075
+ board = ['Pb7', 'Nc7', 'Bc6', 'Ne6', 'Pb5', 'Rc4', 'Qb3', 'Rf7', 'Rb6', 'Pe5', 'Nc3', 'Pd3', 'Nf3']
2076
+ colors = ['B', 'B', 'B', 'B', 'B', 'B', 'B', 'W', 'W', 'W', 'W', 'W', 'W']
2077
+ binst = solver.Board(board, colors)
2078
+ solutions = binst.solve_and_print()
2079
+ ```
2080
+ **Script Output**
2081
+
2082
+ The output is in the form of "pos -> pos" where "pos" is the algebraic notation of the position.
2083
+
2084
+ ```python
2085
+ Solution found
2086
+ ['Rf7->Nc7', 'Ne6->Rc7', 'Pd3->Rc4', 'Qb3->Nc3', 'Pc4->Pb5', 'Qc3->Pe5', 'Nf3->Qe5', 'Nc7->Pb5', 'Ne5->Bc6', 'Pb7->Nc6', 'Rb6->Nb5', 'Pc6->Rb5']
2087
+ Solutions found: 1
2088
+ status: OPTIMAL
2089
+ Time taken: 6.24 seconds
2090
+ ```
2091
+
2092
+ **Solved puzzle**
2093
+
2094
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_melee_solved.png" alt="Chess melee solved" width="500">
1985
2095
 
1986
2096
  ---
1987
2097
 
@@ -1999,36 +2109,6 @@ This repo builds those constraints in Python and uses SAT/CP-SAT (e.g., OR-Tools
1999
2109
 
2000
2110
  ---
2001
2111
 
2002
- ## What’s Inside
2003
-
2004
- Each sub directory in `src/puzzle_solver/puzzles/` targets a different puzzle type. The following are the sub directories:
2005
-
2006
- * `nonograms` — Picross/Griddlers (run-length constraints). ([Chapter 10][1])
2007
- * `sudoku` — Sudoku (rows/cols/blocks all-different). ([Chapter 11][2])
2008
- * `minesweeper` — Minesweeper (mines + counts). ([Chapter 12][3])
2009
- * `guess` — Guess (similar to wordle, guess the colored circles). ([Chapter 15][22])
2010
- * `dominosa` — Dominosa (dominoes + counts). ([Chapter 17][4])
2011
- * `light_up` — *Akari* / Light Up (lighting & adjacency). ([Chapter 21][5])
2012
- * `map` — Map (region coloring). ([Chapter 22][18])
2013
- * `inertia` — Inertia (collect all gems without dying (with least number of moves; my addition)). ([Chapter 24][21])
2014
- * `tents` — Tents (tree-tent matching). ([Chapter 25][6])
2015
- * `bridges` — Bridges (island connections). ([Chapter 26][20])
2016
- * `filling` — Filling (Fillomino-style), region sizes. ([Chapter 29][7])
2017
- * `keen` — Keen (arithmetic operations). ([Chapter 30][8])
2018
- * `towers` — Skyscrapers (permutation + visibility). ([Chapter 31][9])
2019
- * `singles` — Singles (hiding numbers). ([Chapter 32][10])
2020
- * `magnets` — Magnets (polarized dominoes + counts). ([Chapter 33][11])
2021
- * `signpost` — Signpost (visible dominoes + counts). ([Chapter 34][12])
2022
- * `range` — Range (rays & totals). ([Chapter 35][13])
2023
- * `pearl` — Pearl (pearl game). ([Chapter 36][19])
2024
- * `undead` — UnDead (Vampires/Zombies/Ghosts). ([Chapter 37][14])
2025
- * `unruly` — Unruly (no triples + balance). ([Chapter 38][15])
2026
- * `tracks` — Tracks (connected components). ([Chapter 40][16])
2027
- * `mosaic` — Mosaic (Tapa-like tiling). ([Chapter 42][17])
2028
- * `chess_sequence` — Chess Sequence (chess moves). ([Puzzle-Chess][23])
2029
-
2030
- ---
2031
-
2032
2112
  ## Testing
2033
2113
 
2034
2114
  To run the tests, simply run the following (to create a fresh conda environment and install the dev dependencies):
@@ -2088,4 +2168,5 @@ Issues and PRs welcome!
2088
2168
  [15]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/unruly "puzzle_solver/src/puzzle_solver/puzzles/unruly at master · Ar-Kareem/puzzle_solver · GitHub"
2089
2169
  [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"
2090
2170
  [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"
2091
- [23]: https://github.com/Ar-Kareem/puzzle_solver/tree/master/src/puzzle_solver/puzzles/chess_sequence "puzzle_solver/src/puzzle_solver/puzzles/chess_sequence at master · Ar-Kareem/puzzle_solver · GitHub"
2171
+ [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"
2172
+ [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"
@@ -1,14 +1,17 @@
1
- puzzle_solver/__init__.py,sha256=uc7Anf6yvOAigUSOX5Rrhq8xnwWlZk9BRJb4EuoL_ew,1664
1
+ puzzle_solver/__init__.py,sha256=bPtr98uTvRDOU7v5H1iXRfEHuZa5qSE3coOgZjT8Ea4,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_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
5
8
  puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
6
9
  puzzle_solver/puzzles/dominosa/dominosa.py,sha256=uh2vsba9HdSHGnYiYE8R_TZzQh5kge51Y1TMRyQlwek,7246
7
10
  puzzle_solver/puzzles/filling/filling.py,sha256=UAkNYjlfxOrYGrRVmuElhWPeW10xD6kiWuB8oELzy3w,9141
8
11
  puzzle_solver/puzzles/guess/guess.py,sha256=w8ZE-0MswR1_e7_MX622OiCJ8fGTloRHblYFvSA4yHc,10812
9
12
  puzzle_solver/puzzles/inertia/inertia.py,sha256=xHtQ09sI7hqPcs20foz6YVMuYCsw59TZQxTumAKJuBs,5658
10
13
  puzzle_solver/puzzles/inertia/tsp.py,sha256=qMyT_W5vBmmaFUB7Cl9rC5xJNdAdQvIJEx6yxlbG9hw,15240
11
- 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
12
15
  puzzle_solver/puzzles/keen/keen.py,sha256=tDb6C5S3Q0JAKPsdw-84WQ6PxRADELZHr_BK8FDH-NA,5039
13
16
  puzzle_solver/puzzles/light_up/light_up.py,sha256=iSA1rjZMFsnI0V0Nxivxox4qZkB7PvUrROSHXcoUXds,4541
14
17
  puzzle_solver/puzzles/magnets/magnets.py,sha256=-Wl49JD_PKeq735zQVMQ3XSQX6gdHiY-7PKw-Sh16jw,6474
@@ -26,7 +29,7 @@ puzzle_solver/puzzles/towers/towers.py,sha256=QvL0Pp-Z2ewCeq9ZkNrh8MShKOh-Y52sFB
26
29
  puzzle_solver/puzzles/tracks/tracks.py,sha256=VnAtxBkuUTHJYNXr1JGg0yYzJj3kRMBi8Nz7NHKS94A,9089
27
30
  puzzle_solver/puzzles/undead/undead.py,sha256=ygNugW5SOlYLy6d740gZ2IW9UJQ3SAr9vuMm0ZFr2nY,6630
28
31
  puzzle_solver/puzzles/unruly/unruly.py,sha256=sDF0oKT50G-NshyW2DYrvAgD9q9Ku9ANUyNhGSAu7cQ,3827
29
- multi_puzzle_solver-0.8.6.dist-info/METADATA,sha256=2dFiJVRUPijxzl7Ie2vpS1S7DXJfQJl7DQpDToFGNVo,101803
30
- multi_puzzle_solver-0.8.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- multi_puzzle_solver-0.8.6.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
32
- multi_puzzle_solver-0.8.6.dist-info/RECORD,,
32
+ multi_puzzle_solver-0.9.0.dist-info/METADATA,sha256=vN-LmOU4spC46p31NuuT1stsyR6fGzqm1E77gt8hECc,104532
33
+ multi_puzzle_solver-0.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
+ multi_puzzle_solver-0.9.0.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
35
+ multi_puzzle_solver-0.9.0.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
- from puzzle_solver.puzzles.chess_sequence import chess_sequence as chess_sequence_solver
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.6'
29
+ __version__ = '0.9.0'
@@ -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
+
@@ -0,0 +1,401 @@
1
+ import json
2
+ from dataclasses import dataclass
3
+ from typing import Union
4
+ from enum import Enum
5
+
6
+ from ortools.sat.python import cp_model
7
+
8
+ from puzzle_solver.core.utils import Pos, get_pos
9
+ from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, or_constraint
10
+
11
+
12
+
13
+ class PieceType(Enum):
14
+ KING = 1
15
+ QUEEN = 2
16
+ ROOK = 3
17
+ BISHOP = 4
18
+ KNIGHT = 5
19
+ PAWN = 6
20
+
21
+ @dataclass(frozen=True)
22
+ class SingleSolution:
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
+ # pos_assignment: dict[tuple[int, int, Union[Pos, str]], int]
26
+ # mover: dict[int, tuple[int, PieceType]]
27
+ # victim: dict[int, tuple[int, PieceType]]
28
+
29
+ def get_hashable_solution(self) -> str:
30
+ # only hash assignment
31
+ result = []
32
+ for _, (_, from_pos, to_pos, _) in sorted(self.assignment.items()):
33
+ result.append((from_pos.x, from_pos.y, to_pos.x, to_pos.y))
34
+ # order doesnt matter for uniqueness
35
+ result = sorted(result)
36
+ return json.dumps(result)
37
+
38
+
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)}'
41
+ assert len(algebraic) == 3, 'algebraic notation must be 3 characters'
42
+ p = {'K': PieceType.KING, 'Q': PieceType.QUEEN, 'R': PieceType.ROOK, 'B': PieceType.BISHOP, 'N': PieceType.KNIGHT, 'P': PieceType.PAWN}
43
+ assert algebraic[0] in p, 'invalid piece type'
44
+ assert algebraic[1] in 'abcdefgh', f'invalid file: {algebraic[1]}'
45
+ assert algebraic[2] in '12345678', f'invalid rank: {algebraic[2]}'
46
+ piece_type = p[algebraic[0]]
47
+ file, rank = algebraic[1:]
48
+ file = ord(file) - ord('a')
49
+ rank = int(rank) - 1
50
+ pos = get_pos(x=file, y=rank)
51
+ return (piece_type, pos)
52
+
53
+
54
+ def to_algebraic_notation_single_move(piece_type: str, from_pos: Pos, to_pos: Pos, victim_type: str) -> str:
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'}
56
+ from_file_letter = chr(from_pos.x + ord('a'))
57
+ from_rank_letter = str(from_pos.y + 1)
58
+ to_file_letter = chr(to_pos.x + ord('a'))
59
+ to_rank_letter = str(to_pos.y + 1)
60
+ return f'{letter[piece_type]}{from_file_letter}{from_rank_letter}->{letter[victim_type]}{to_file_letter}{to_rank_letter}'
61
+
62
+
63
+ def to_algebraic_notation(single_solution: SingleSolution) -> list[str]:
64
+ move_sequence = single_solution.assignment
65
+ move_sequence = sorted(move_sequence.items(), key=lambda x: x[0])
66
+ move_sequence = [x[1] for x in move_sequence]
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]
68
+
69
+
70
+ def is_same_row_col(from_pos: Pos, to_pos: Pos) -> bool:
71
+ return from_pos.x == to_pos.x or from_pos.y == to_pos.y
72
+
73
+
74
+ def is_diagonal(from_pos: Pos, to_pos: Pos) -> bool:
75
+ return abs(from_pos.x - to_pos.x) == abs(from_pos.y - to_pos.y)
76
+
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
+ """
131
+ if piece_type == PieceType.KING:
132
+ dx = abs(from_pos.x - to_pos.x)
133
+ dy = abs(from_pos.y - to_pos.y)
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)
141
+ elif piece_type == PieceType.ROOK:
142
+ return is_same_row_col(from_pos, to_pos), squares_between_rook(from_pos, to_pos)
143
+ elif piece_type == PieceType.BISHOP:
144
+ return is_diagonal(from_pos, to_pos), squares_between_bishop(from_pos, to_pos)
145
+ elif piece_type == PieceType.KNIGHT:
146
+ dx = abs(from_pos.x - to_pos.x)
147
+ dy = abs(from_pos.y - to_pos.y)
148
+ is_valid = (dx == 2 and dy == 1) or (dx == 1 and dy == 2)
149
+ return is_valid, []
150
+ elif piece_type == PieceType.PAWN: # will always eat because the this is how the puzzle works
151
+ dx = to_pos.x - from_pos.x
152
+ dy = to_pos.y - from_pos.y
153
+ is_valid = abs(dx) == 1 and dy == (1 if color != 'B' else -1)
154
+ return is_valid, []
155
+
156
+
157
+ class Board:
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
+ """
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
169
+ self.N = len(self.pieces) # number of pieces
170
+ self.T = self.N # (N-1) moves + 1 initial state
171
+ self.max_moves_per_piece = max_moves_per_piece
172
+ self.last_piece_alive = last_piece_alive
173
+ self.V = 8 # board size
174
+ self.H = 8 # board size
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'
178
+
179
+ self.model = cp_model.CpModel()
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):
181
+ # Number of variables
182
+ # piece_positions: O(NTB)
183
+ # is_dead: O(NT)
184
+ # mover: O(NT)
185
+ # victim: O(NT)
186
+ # position_occupied: O(TB)
187
+ # dies_this_timestep: O(NT)
188
+ # pos_is_p_star: O(NTB)
189
+ # Total: ~ (2*N)N^2 + 6N^2 = 2N^3 + 6N^2
190
+
191
+ # (piece_index, time_step, position) -> boolean variable (possible all false if i'm dead)
192
+ self.piece_positions: dict[tuple[int, int, Pos], cp_model.IntVar] = {}
193
+ self.is_dead: dict[tuple[int, int], cp_model.IntVar] = {} # Am I currently dead
194
+
195
+ # (piece_index, time_step) -> boolean variable indicating if the piece [moved/died]
196
+ self.mover: dict[tuple[int, int], cp_model.IntVar] = {} # did i move this timestep?
197
+ self.victim: dict[tuple[int, int], cp_model.IntVar] = {} # did i die this timestep?
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
+
202
+ self.create_vars()
203
+ self.add_all_constraints()
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
+
213
+ def create_vars(self):
214
+ for p in range(self.N):
215
+ for t in range(self.T):
216
+ for pos in self.all_legal_positions:
217
+ self.piece_positions[(p, t, pos)] = self.model.NewBoolVar(f'piece_positions[{p},{t},{pos}]')
218
+ self.is_dead[(p, t)] = self.model.NewBoolVar(f'is_dead[{p},{t}]')
219
+ for p in range(self.N):
220
+ for t in range(self.T - 1): # final state does not have a mover or victim
221
+ self.mover[(p, t)] = self.model.NewIntVar(0, 1, f'mover[{p},{t}]')
222
+ self.victim[(p, t)] = self.model.NewIntVar(0, 1, f'victim[{p},{t}]')
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
+
228
+ def add_all_constraints(self):
229
+ self.enforce_initial_state()
230
+ self.enforce_board_state_constraints()
231
+ self.enforce_mover_victim_constraints()
232
+ self.enforce_position_occupied_constraints()
233
+
234
+ def enforce_initial_state(self):
235
+ # initial state
236
+ for p, (_, initial_pos) in self.pieces.items():
237
+ self.model.Add(self.piece_positions[(p, 0, initial_pos)] == 1)
238
+ # cant be initially dead
239
+ self.model.Add(self.is_dead[(p, 0)] == 0)
240
+ # all others are blank
241
+ for pos in self.all_legal_positions:
242
+ if pos == initial_pos:
243
+ continue
244
+ self.model.Add(self.piece_positions[(p, 0, pos)] == 0)
245
+
246
+ def enforce_board_state_constraints(self):
247
+ # at each timestep and each piece, it can only be at exactly one position or dead
248
+ for p in range(self.N):
249
+ for t in range(self.T):
250
+ pos_vars = [self.piece_positions[(p, t, pos)] for pos in self.all_legal_positions]
251
+ pos_vars.append(self.is_dead[(p, t)])
252
+ self.model.AddExactlyOne(pos_vars)
253
+ # if im dead this timestep then im also dead next timestep
254
+ for p in range(self.N):
255
+ for t in range(self.T - 1):
256
+ self.model.Add(self.is_dead[(p, t + 1)] == 1).OnlyEnforceIf(self.is_dead[(p, t)])
257
+ # every move must be legal chess move
258
+ for p in range(self.N):
259
+ color = self.colors[p] if self.colors is not None else None
260
+ for t in range(self.T - 1):
261
+ for from_pos in self.all_legal_positions:
262
+ for to_pos in self.all_legal_positions:
263
+ if from_pos == to_pos:
264
+ continue
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:
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
+
275
+ # if mover is i and victim is j then i HAS to be at the position of j at the next timestep
276
+ for p_mover in range(self.N):
277
+ for p_victim in range(self.N):
278
+ if p_mover == p_victim:
279
+ continue
280
+ for t in range(self.T - 1):
281
+ for pos in self.all_legal_positions:
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)]])
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
+
308
+ def enforce_mover_victim_constraints(self):
309
+ for p in range(self.N):
310
+ for t in range(self.T - 1):
311
+ # if i'm dead at time step t then I did not move nor victimized
312
+ self.model.Add(self.mover[(p, t)] == 0).OnlyEnforceIf(self.is_dead[(p, t)])
313
+ self.model.Add(self.victim[(p, t)] == 0).OnlyEnforceIf(self.is_dead[(p, t)])
314
+ # if I was the mover or victim at time step t then I was not dead
315
+ self.model.Add(self.is_dead[(p, t)] == 0).OnlyEnforceIf(self.mover[(p, t)])
316
+ self.model.Add(self.is_dead[(p, t)] == 0).OnlyEnforceIf(self.victim[(p, t)])
317
+ # a victim cannot be the mover and vice versa
318
+ self.model.Add(self.mover[(p, t)] == 0).OnlyEnforceIf(self.victim[(p, t)])
319
+ self.model.Add(self.victim[(p, t)] == 0).OnlyEnforceIf(self.mover[(p, t)])
320
+
321
+ # if im dead next timestep and i was alive this timestep then im the victim
322
+ # cant rely on victim var here because the goal it to constrain it
323
+ dies_this_timestep = self.model.NewBoolVar(f'dies_this_timestep[{p},{t}]')
324
+ and_constraint(self.model, dies_this_timestep, [self.is_dead[(p, t + 1)], self.is_dead[(p, t)].Not()])
325
+ self.model.Add(self.victim[(p, t)] == dies_this_timestep)
326
+
327
+ # if next timestep im somewhere else then i was the mover
328
+ # i.e. there exists a position p* s.t. (piece_positions[p, t + 1, p*] AND NOT piece_positions[p, t, p*])
329
+ pos_is_p_star = []
330
+ for pos in self.all_legal_positions:
331
+ v = self.model.NewBoolVar(f'pos_is_p_star[{p},{t},{pos}]')
332
+ self.model.Add(v == 1).OnlyEnforceIf([self.piece_positions[(p, t + 1, pos)], self.piece_positions[(p, t, pos)].Not()])
333
+ self.model.Add(v == 0).OnlyEnforceIf([self.piece_positions[(p, t + 1, pos)].Not()])
334
+ self.model.Add(v == 0).OnlyEnforceIf([self.piece_positions[(p, t, pos)]])
335
+ pos_is_p_star.append(v)
336
+ or_constraint(self.model, self.mover[(p, t)], pos_is_p_star)
337
+
338
+ # at each timestep only one piece can be the mover
339
+ for t in range(self.T - 1):
340
+ self.model.AddExactlyOne([self.mover[(p, t)] for p in range(self.N)])
341
+ # at each timestep only one piece can be victimized
342
+ for t in range(self.T - 1):
343
+ self.model.AddExactlyOne([self.victim[(p, t)] for p in range(self.N)])
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
+
355
+
356
+ def solve_and_print(self, verbose: bool = True, max_solutions: int = None):
357
+ def board_to_solution(board: "Board", solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
358
+ pos_assignment: dict[tuple[int, int, Union[Pos, str]], int] = {}
359
+ for t in range(board.T):
360
+ for i in range(board.N):
361
+ for pos in board.all_legal_positions:
362
+ pos_assignment[(i, t, pos)] = solver.Value(board.piece_positions[(i, t, pos)])
363
+ pos_assignment[(i, t, 'DEAD')] = solver.Value(board.is_dead[(i, t)])
364
+ mover = {}
365
+ for t in range(board.T - 1):
366
+ for i in range(board.N):
367
+ if solver.Value(board.mover[(i, t)]):
368
+ mover[t] = (i, board.pieces[i][0].name)
369
+ victim = {}
370
+ for t in range(board.T - 1):
371
+ for i in range(board.N):
372
+ if solver.Value(board.victim[(i, t)]):
373
+ victim[t] = (i, board.pieces[i][0].name)
374
+
375
+ assignment: dict[int, tuple[int, Pos, Pos]] = {} # final result
376
+ for t in range(board.T - 1):
377
+ mover_i = mover[t][0]
378
+ victim_i = victim[t][0]
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)])
381
+ assignment[t] = (board.pieces[mover_i][0].name, from_pos, to_pos, board.pieces[victim_i][0].name)
382
+ # return SingleSolution(assignment=assignment, pos_assignment=pos_assignment, mover=mover, victim=victim)
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)
385
+
386
+ def callback(single_res: SingleSolution):
387
+ print("Solution found")
388
+ # pieces = sorted(set(i for (i, _, _) in single_res.assignment.keys()))
389
+ # for piece in pieces:
390
+ # print(f"Piece {piece} type: {single_res.piece_types[piece]}")
391
+ # # at each timestep a piece can only be in one position
392
+ # t_to_pos = {t: pos for (i, t, pos), v in single_res.assignment.items() if i == piece and v == 1}
393
+ # print(t_to_pos)
394
+ # print('victims:', single_res.victim)
395
+ # print('movers:', single_res.mover)
396
+ # print()
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)
400
+ print(move_sequence)
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')