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.
- {multi_puzzle_solver-0.8.6.dist-info → multi_puzzle_solver-0.9.0.dist-info}/METADATA +126 -45
- {multi_puzzle_solver-0.8.6.dist-info → multi_puzzle_solver-0.9.0.dist-info}/RECORD +9 -6
- puzzle_solver/__init__.py +4 -2
- puzzle_solver/puzzles/chess_range/chess_melee.py +7 -0
- puzzle_solver/puzzles/chess_range/chess_range.py +401 -0
- 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.6.dist-info → multi_puzzle_solver-0.9.0.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.8.6.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
|
@@ -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-
|
210
|
-
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/
|
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
|
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
|
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
|
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
|
-
|
1950
|
-
|
1951
|
-
|
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/
|
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
|
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:
|
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/
|
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/
|
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=
|
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=
|
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.
|
30
|
-
multi_puzzle_solver-0.
|
31
|
-
multi_puzzle_solver-0.
|
32
|
-
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
|
-
from puzzle_solver.puzzles.
|
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'
|
@@ -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
|
-
|
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
|