multi-puzzle-solver 0.8.7__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.
- {multi_puzzle_solver-0.8.7.dist-info → multi_puzzle_solver-0.9.0.dist-info}/METADATA +117 -37
- {multi_puzzle_solver-0.8.7.dist-info → multi_puzzle_solver-0.9.0.dist-info}/RECORD +9 -7
- puzzle_solver/__init__.py +3 -1
- puzzle_solver/puzzles/chess_range/chess_melee.py +7 -0
- puzzle_solver/puzzles/chess_range/chess_range.py +169 -30
- puzzle_solver/puzzles/chess_range/chess_solo.py +9 -0
- puzzle_solver/puzzles/inertia/parse_map/parse_map.py +8 -8
- {multi_puzzle_solver-0.8.7.dist-info → multi_puzzle_solver-0.9.0.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.8.7.dist-info → multi_puzzle_solver-0.9.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: multi-puzzle-solver
|
3
|
-
Version: 0.
|
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
|
@@ -210,8 +210,14 @@ 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
|
+
<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
|
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 Range(Puzzle Type #23)](#chess-
|
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 Range(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
|
|
@@ -1944,7 +1951,7 @@ In the case when there's only one possible choice left, the solver will inform y
|
|
1944
1951
|
<details>
|
1945
1952
|
<summary><strong>Rules</strong></summary>
|
1946
1953
|
|
1947
|
-
You are given a chess board with $N$ pieces distributed on it. Your aim is to make $N-1$
|
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
1956
|
- Pieces move as standard chess pieces.
|
1950
1957
|
- You can perform only capture moves. A move that does not capture another piece is not allowed.
|
@@ -1977,7 +1984,7 @@ Solution found
|
|
1977
1984
|
['Rg2->Pc2', 'Rc2->Bc3', 'Rc3->Pd3', 'Kb6->Pb5', 'Pg4->Nf5', 'Rd3->Rb3', 'Rb3->Kb5', 'Nc6->Qe7', 'Ne7->Pf5', 'Rb5->Nf5']
|
1978
1985
|
Solutions found: 1
|
1979
1986
|
status: FEASIBLE
|
1980
|
-
Time taken:
|
1987
|
+
Time taken: 1.16 seconds
|
1981
1988
|
```
|
1982
1989
|
|
1983
1990
|
**Solved puzzle**
|
@@ -1986,6 +1993,108 @@ Time taken: 6.27 seconds
|
|
1986
1993
|
|
1987
1994
|
---
|
1988
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
|
2039
|
+
```
|
2040
|
+
|
2041
|
+
**Solved puzzle**
|
2042
|
+
|
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">
|
2095
|
+
|
2096
|
+
---
|
2097
|
+
|
1989
2098
|
---
|
1990
2099
|
|
1991
2100
|
## Why SAT / CP-SAT?
|
@@ -2000,36 +2109,6 @@ This repo builds those constraints in Python and uses SAT/CP-SAT (e.g., OR-Tools
|
|
2000
2109
|
|
2001
2110
|
---
|
2002
2111
|
|
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
2112
|
## Testing
|
2034
2113
|
|
2035
2114
|
To run the tests, simply run the following (to create a fresh conda environment and install the dev dependencies):
|
@@ -2090,3 +2169,4 @@ Issues and PRs welcome!
|
|
2090
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"
|
2091
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"
|
2092
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,15 +1,17 @@
|
|
1
|
-
puzzle_solver/__init__.py,sha256=
|
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/
|
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=
|
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.
|
31
|
-
multi_puzzle_solver-0.
|
32
|
-
multi_puzzle_solver-0.
|
33
|
-
multi_puzzle_solver-0.
|
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
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.
|
29
|
+
__version__ = '0.9.0'
|
@@ -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
|
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
|
-
|
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
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 (=
|
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*
|
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
|
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
|
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
|
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
|
168
|
-
for to_pos in
|
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
|
-
|
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
|
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
|
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
|
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
|
244
|
-
to_pos = next(pos for pos in
|
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
|
-
|
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
|
-
|
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
|
-
|
10
|
-
|
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')
|
File without changes
|
File without changes
|