multi-puzzle-solver 0.8.6__py3-none-any.whl → 0.8.7__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.8.7.dist-info}/METADATA +15 -14
- {multi_puzzle_solver-0.8.6.dist-info → multi_puzzle_solver-0.8.7.dist-info}/RECORD +6 -5
- puzzle_solver/__init__.py +2 -2
- puzzle_solver/puzzles/chess_range/chess_range.py +262 -0
- {multi_puzzle_solver-0.8.6.dist-info → multi_puzzle_solver-0.8.7.dist-info}/WHEEL +0 -0
- {multi_puzzle_solver-0.8.6.dist-info → multi_puzzle_solver-0.8.7.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.8.
|
3
|
+
Version: 0.8.7
|
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,8 +206,8 @@ 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
213
|
|
@@ -249,7 +249,7 @@ These are all the puzzles that are implemented in this repo. <br> Click on any o
|
|
249
249
|
- [Bridges (Puzzle Type #20)](#bridges-puzzle-type-20)
|
250
250
|
- [Inertia (Puzzle Type #21)](#inertia-puzzle-type-21)
|
251
251
|
- [Guess (Puzzle Type #22)](#guess-puzzle-type-22)
|
252
|
-
- [Chess
|
252
|
+
- [Chess Range(Puzzle Type #23)](#chess-rangepuzzle-type-23)
|
253
253
|
- [Why SAT / CP-SAT?](#why-sat--cp-sat)
|
254
254
|
- [What’s Inside](#whats-inside)
|
255
255
|
- [Testing](#testing)
|
@@ -1935,7 +1935,7 @@ In the case when there's only one possible choice left, the solver will inform y
|
|
1935
1935
|
|
1936
1936
|
---
|
1937
1937
|
|
1938
|
-
## Chess
|
1938
|
+
## Chess Range(Puzzle Type #23)
|
1939
1939
|
|
1940
1940
|
* [**Play online**](https://www.puzzle-chess.com/chess-ranger-11/)
|
1941
1941
|
|
@@ -1944,24 +1944,25 @@ In the case when there's only one possible choice left, the solver will inform y
|
|
1944
1944
|
<details>
|
1945
1945
|
<summary><strong>Rules</strong></summary>
|
1946
1946
|
|
1947
|
-
You are given a chess board with $N$ pieces distributed on it. Your aim is to make $N-1$
|
1947
|
+
You are given a chess board with $N$ pieces distributed on it. Your aim is to make $N-1$ range of moves where each move is a legal chess move and captures another piece.
|
1948
1948
|
|
1949
|
-
|
1950
|
-
|
1951
|
-
|
1949
|
+
- Pieces move as standard chess pieces.
|
1950
|
+
- You can perform only capture moves. A move that does not capture another piece is not allowed.
|
1951
|
+
- You are allowed to capture the king.
|
1952
|
+
- The goal is to end up with one single piece on the board.
|
1952
1953
|
|
1953
1954
|
</details>
|
1954
1955
|
|
1955
1956
|
**Unsolved puzzle**
|
1956
1957
|
|
1957
|
-
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/
|
1958
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_range_unsolved.png" alt="Chess range unsolved" width="500">
|
1958
1959
|
|
1959
1960
|
Code to utilize this package and solve the puzzle:
|
1960
1961
|
|
1961
1962
|
(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
1963
|
|
1963
1964
|
```python
|
1964
|
-
from puzzle_solver import
|
1965
|
+
from puzzle_solver import chess_range_solver as solver
|
1965
1966
|
# algebraic notation
|
1966
1967
|
board = ['Qe7', 'Nc6', 'Kb6', 'Pb5', 'Nf5', 'Pg4', 'Rb3', 'Bc3', 'Pd3', 'Pc2', 'Rg2']
|
1967
1968
|
binst = solver.Board(board)
|
@@ -1981,7 +1982,7 @@ Time taken: 6.27 seconds
|
|
1981
1982
|
|
1982
1983
|
**Solved puzzle**
|
1983
1984
|
|
1984
|
-
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/
|
1985
|
+
<img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_range_solved.png" alt="Chess range solved" width="500">
|
1985
1986
|
|
1986
1987
|
---
|
1987
1988
|
|
@@ -2025,7 +2026,7 @@ Each sub directory in `src/puzzle_solver/puzzles/` targets a different puzzle ty
|
|
2025
2026
|
* `unruly` — Unruly (no triples + balance). ([Chapter 38][15])
|
2026
2027
|
* `tracks` — Tracks (connected components). ([Chapter 40][16])
|
2027
2028
|
* `mosaic` — Mosaic (Tapa-like tiling). ([Chapter 42][17])
|
2028
|
-
* `
|
2029
|
+
* `chess_range` — Chess Range (chess moves). ([Puzzle-Chess][23])
|
2029
2030
|
|
2030
2031
|
---
|
2031
2032
|
|
@@ -2088,4 +2089,4 @@ Issues and PRs welcome!
|
|
2088
2089
|
[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
2090
|
[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
2091
|
[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/
|
2092
|
+
[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"
|
@@ -1,7 +1,8 @@
|
|
1
|
-
puzzle_solver/__init__.py,sha256=
|
1
|
+
puzzle_solver/__init__.py,sha256=E6zLTG4lv93Tr8Emb5LBfzRLNJOSe7AfTrW4LT7DTak,1655
|
2
2
|
puzzle_solver/core/utils.py,sha256=3LlBDuie_G0uSlzibpQS2ULmEYSZmpJXh1kawj7rjkg,3396
|
3
3
|
puzzle_solver/core/utils_ortools.py,sha256=qLTIzmITqmgGZvg8XpYAZ4c-lhD5sEDQfS8ECdQ_dkM,3005
|
4
4
|
puzzle_solver/puzzles/bridges/bridges.py,sha256=zUT0TMIu8l982fqDMJfsTnTgqm48nG0iH8flsGT45_E,5489
|
5
|
+
puzzle_solver/puzzles/chess_range/chess_range.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
|
5
6
|
puzzle_solver/puzzles/chess_sequence/chess_sequence.py,sha256=6ap3Wouf2PxHV4P56B9ol1QT98Ym6VHaxorQZWl6LnY,13692
|
6
7
|
puzzle_solver/puzzles/dominosa/dominosa.py,sha256=uh2vsba9HdSHGnYiYE8R_TZzQh5kge51Y1TMRyQlwek,7246
|
7
8
|
puzzle_solver/puzzles/filling/filling.py,sha256=UAkNYjlfxOrYGrRVmuElhWPeW10xD6kiWuB8oELzy3w,9141
|
@@ -26,7 +27,7 @@ puzzle_solver/puzzles/towers/towers.py,sha256=QvL0Pp-Z2ewCeq9ZkNrh8MShKOh-Y52sFB
|
|
26
27
|
puzzle_solver/puzzles/tracks/tracks.py,sha256=VnAtxBkuUTHJYNXr1JGg0yYzJj3kRMBi8Nz7NHKS94A,9089
|
27
28
|
puzzle_solver/puzzles/undead/undead.py,sha256=ygNugW5SOlYLy6d740gZ2IW9UJQ3SAr9vuMm0ZFr2nY,6630
|
28
29
|
puzzle_solver/puzzles/unruly/unruly.py,sha256=sDF0oKT50G-NshyW2DYrvAgD9q9Ku9ANUyNhGSAu7cQ,3827
|
29
|
-
multi_puzzle_solver-0.8.
|
30
|
-
multi_puzzle_solver-0.8.
|
31
|
-
multi_puzzle_solver-0.8.
|
32
|
-
multi_puzzle_solver-0.8.
|
30
|
+
multi_puzzle_solver-0.8.7.dist-info/METADATA,sha256=WEFmdDkVqTMuKK1rDqX5r378UpPvtA8SriZ6ZZcFGJ4,101834
|
31
|
+
multi_puzzle_solver-0.8.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
32
|
+
multi_puzzle_solver-0.8.7.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
|
33
|
+
multi_puzzle_solver-0.8.7.dist-info/RECORD,,
|
puzzle_solver/__init__.py
CHANGED
@@ -1,5 +1,5 @@
|
|
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
3
|
from puzzle_solver.puzzles.dominosa import dominosa as dominosa_solver
|
4
4
|
from puzzle_solver.puzzles.filling import filling as filling_solver
|
5
5
|
from puzzle_solver.puzzles.guess import guess as guess_solver
|
@@ -24,4 +24,4 @@ from puzzle_solver.puzzles.unruly import unruly as unruly_solver
|
|
24
24
|
|
25
25
|
from puzzle_solver.puzzles.inertia.parse_map.parse_map import main as inertia_image_parser
|
26
26
|
|
27
|
-
__version__ = '0.8.
|
27
|
+
__version__ = '0.8.7'
|
@@ -0,0 +1,262 @@
|
|
1
|
+
import json
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import Union
|
4
|
+
from enum import Enum
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
from ortools.sat.python import cp_model
|
8
|
+
|
9
|
+
from puzzle_solver.core.utils import Pos, get_pos, get_all_pos, get_char, set_char, get_row_pos, get_col_pos, Direction, get_next_pos, in_bounds
|
10
|
+
from puzzle_solver.core.utils_ortools import and_constraint, generic_solve_all, or_constraint
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
class PieceType(Enum):
|
15
|
+
KING = 1
|
16
|
+
QUEEN = 2
|
17
|
+
ROOK = 3
|
18
|
+
BISHOP = 4
|
19
|
+
KNIGHT = 5
|
20
|
+
PAWN = 6
|
21
|
+
|
22
|
+
@dataclass(frozen=True)
|
23
|
+
class SingleSolution:
|
24
|
+
assignment: dict[int, tuple[str, Pos, Pos, str]] # every time step a single piece moves from one position to another and eats another piece
|
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 len(algebraic) == 3, 'algebraic notation must be 3 characters'
|
41
|
+
p = {'K': PieceType.KING, 'Q': PieceType.QUEEN, 'R': PieceType.ROOK, 'B': PieceType.BISHOP, 'N': PieceType.KNIGHT, 'P': PieceType.PAWN}
|
42
|
+
assert algebraic[0] in p, 'invalid piece type'
|
43
|
+
assert algebraic[1] in 'abcdefgh', f'invalid file: {algebraic[1]}'
|
44
|
+
assert algebraic[2] in '12345678', f'invalid rank: {algebraic[2]}'
|
45
|
+
piece_type = p[algebraic[0]]
|
46
|
+
file, rank = algebraic[1:]
|
47
|
+
file = ord(file) - ord('a')
|
48
|
+
rank = int(rank) - 1
|
49
|
+
pos = get_pos(x=file, y=rank)
|
50
|
+
return (piece_type, pos)
|
51
|
+
|
52
|
+
def to_algebraic_notation_single_move(piece_type: str, from_pos: Pos, to_pos: Pos, victim_type: str) -> str:
|
53
|
+
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
|
+
from_file_letter = chr(from_pos.x + ord('a'))
|
55
|
+
from_rank_letter = str(from_pos.y + 1)
|
56
|
+
to_file_letter = chr(to_pos.x + ord('a'))
|
57
|
+
to_rank_letter = str(to_pos.y + 1)
|
58
|
+
return f'{letter[piece_type]}{from_file_letter}{from_rank_letter}->{letter[victim_type]}{to_file_letter}{to_rank_letter}'
|
59
|
+
|
60
|
+
def to_algebraic_notation(move_sequence: dict[int, tuple[str, Pos, Pos, str]]) -> list[str]:
|
61
|
+
move_sequence = sorted(move_sequence.items(), key=lambda x: x[0])
|
62
|
+
move_sequence = [x[1] for x in move_sequence]
|
63
|
+
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]
|
64
|
+
|
65
|
+
|
66
|
+
def is_same_row_col(from_pos: Pos, to_pos: Pos) -> bool:
|
67
|
+
return from_pos.x == to_pos.x or from_pos.y == to_pos.y
|
68
|
+
|
69
|
+
def is_diagonal(from_pos: Pos, to_pos: Pos) -> bool:
|
70
|
+
return abs(from_pos.x - to_pos.x) == abs(from_pos.y - to_pos.y)
|
71
|
+
|
72
|
+
def is_move_valid(from_pos: Pos, to_pos: Pos, piece_type: PieceType) -> bool:
|
73
|
+
if piece_type == PieceType.KING:
|
74
|
+
dx = abs(from_pos.x - to_pos.x)
|
75
|
+
dy = abs(from_pos.y - to_pos.y)
|
76
|
+
return dx <= 1 and dy <= 1
|
77
|
+
elif piece_type == PieceType.QUEEN:
|
78
|
+
return is_same_row_col(from_pos, to_pos) or is_diagonal(from_pos, to_pos)
|
79
|
+
elif piece_type == PieceType.ROOK:
|
80
|
+
return is_same_row_col(from_pos, to_pos)
|
81
|
+
elif piece_type == PieceType.BISHOP:
|
82
|
+
return is_diagonal(from_pos, to_pos)
|
83
|
+
elif piece_type == PieceType.KNIGHT:
|
84
|
+
dx = abs(from_pos.x - to_pos.x)
|
85
|
+
dy = abs(from_pos.y - to_pos.y)
|
86
|
+
return (dx == 2 and dy == 1) or (dx == 1 and dy == 2)
|
87
|
+
elif piece_type == PieceType.PAWN: # will always eat because the this is how the puzzle works
|
88
|
+
dx = to_pos.x - from_pos.x
|
89
|
+
dy = to_pos.y - from_pos.y
|
90
|
+
return abs(dx) == 1 and dy == 1
|
91
|
+
|
92
|
+
|
93
|
+
class Board:
|
94
|
+
def __init__(self, pieces: list[str]):
|
95
|
+
self.pieces: dict[int, tuple[PieceType, Pos]] = {i: parse_algebraic_notation(p) for i, p in enumerate(pieces)}
|
96
|
+
self.N = len(self.pieces) # number of pieces
|
97
|
+
self.T = self.N # (N-1) moves + 1 initial state
|
98
|
+
|
99
|
+
self.V = 8 # board size
|
100
|
+
self.H = 8 # board size
|
101
|
+
self.num_positions = self.V * self.H # 8x8 board
|
102
|
+
|
103
|
+
self.model = cp_model.CpModel()
|
104
|
+
# Input numbers: N is number of piece, T is number of time steps (=N here), B is board size (=64 here):
|
105
|
+
# Number of variables
|
106
|
+
# piece_positions: O(NTB)
|
107
|
+
# is_dead: O(NT)
|
108
|
+
# mover: O(NT)
|
109
|
+
# victim: O(NT)
|
110
|
+
# dies_this_timestep: O(NT)
|
111
|
+
# pos_is_p_star: O(NTB)
|
112
|
+
# Total: ~ (2*64)N^2 + 5N^2 = 132N^2
|
113
|
+
|
114
|
+
# (piece_index, time_step, position) -> boolean variable (possible all false if i'm dead)
|
115
|
+
self.piece_positions: dict[tuple[int, int, Pos], cp_model.IntVar] = {}
|
116
|
+
self.is_dead: dict[tuple[int, int], cp_model.IntVar] = {} # Am I currently dead
|
117
|
+
|
118
|
+
# (piece_index, time_step) -> boolean variable indicating if the piece [moved/died]
|
119
|
+
self.mover: dict[tuple[int, int], cp_model.IntVar] = {} # did i move this timestep?
|
120
|
+
self.victim: dict[tuple[int, int], cp_model.IntVar] = {} # did i die this timestep?
|
121
|
+
|
122
|
+
self.create_vars()
|
123
|
+
self.add_all_constraints()
|
124
|
+
|
125
|
+
def create_vars(self):
|
126
|
+
for p in range(self.N):
|
127
|
+
for t in range(self.T):
|
128
|
+
for pos in get_all_pos(self.V, self.H):
|
129
|
+
self.piece_positions[(p, t, pos)] = self.model.NewBoolVar(f'piece_positions[{p},{t},{pos}]')
|
130
|
+
self.is_dead[(p, t)] = self.model.NewBoolVar(f'is_dead[{p},{t}]')
|
131
|
+
for p in range(self.N):
|
132
|
+
for t in range(self.T - 1): # final state does not have a mover or victim
|
133
|
+
self.mover[(p, t)] = self.model.NewIntVar(0, 1, f'mover[{p},{t}]')
|
134
|
+
self.victim[(p, t)] = self.model.NewIntVar(0, 1, f'victim[{p},{t}]')
|
135
|
+
|
136
|
+
def add_all_constraints(self):
|
137
|
+
self.enforce_initial_state()
|
138
|
+
self.enforce_board_state_constraints()
|
139
|
+
self.enforce_mover_victim_constraints()
|
140
|
+
|
141
|
+
def enforce_initial_state(self):
|
142
|
+
# initial state
|
143
|
+
for p, (_, initial_pos) in self.pieces.items():
|
144
|
+
self.model.Add(self.piece_positions[(p, 0, initial_pos)] == 1)
|
145
|
+
# cant be initially dead
|
146
|
+
self.model.Add(self.is_dead[(p, 0)] == 0)
|
147
|
+
# all others are blank
|
148
|
+
for pos in get_all_pos(self.V, self.H):
|
149
|
+
if pos == initial_pos:
|
150
|
+
continue
|
151
|
+
self.model.Add(self.piece_positions[(p, 0, pos)] == 0)
|
152
|
+
|
153
|
+
def enforce_board_state_constraints(self):
|
154
|
+
# at each timestep and each piece, it can only be at exactly one position or dead
|
155
|
+
for p in range(self.N):
|
156
|
+
for t in range(self.T):
|
157
|
+
pos_vars = [self.piece_positions[(p, t, pos)] for pos in get_all_pos(self.V, self.H)]
|
158
|
+
pos_vars.append(self.is_dead[(p, t)])
|
159
|
+
self.model.AddExactlyOne(pos_vars)
|
160
|
+
# if im dead this timestep then im also dead next timestep
|
161
|
+
for p in range(self.N):
|
162
|
+
for t in range(self.T - 1):
|
163
|
+
self.model.Add(self.is_dead[(p, t + 1)] == 1).OnlyEnforceIf(self.is_dead[(p, t)])
|
164
|
+
# every move must be legal chess move
|
165
|
+
for p in range(self.N):
|
166
|
+
for t in range(self.T - 1):
|
167
|
+
for from_pos in get_all_pos(self.V, self.H):
|
168
|
+
for to_pos in get_all_pos(self.V, self.H):
|
169
|
+
if from_pos == to_pos:
|
170
|
+
continue
|
171
|
+
if not is_move_valid(from_pos, to_pos, self.pieces[p][0]):
|
172
|
+
self.model.Add(self.piece_positions[(p, t + 1, to_pos)] == 0).OnlyEnforceIf([self.piece_positions[(p, t, from_pos)]])
|
173
|
+
# if mover is i and victim is j then i HAS to be at the position of j at the next timestep
|
174
|
+
for p_mover in range(self.N):
|
175
|
+
for p_victim in range(self.N):
|
176
|
+
if p_mover == p_victim:
|
177
|
+
continue
|
178
|
+
for t in range(self.T - 1):
|
179
|
+
for pos in get_all_pos(self.V, self.H):
|
180
|
+
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
|
+
|
182
|
+
def enforce_mover_victim_constraints(self):
|
183
|
+
for p in range(self.N):
|
184
|
+
for t in range(self.T - 1):
|
185
|
+
# if i'm dead at time step t then I did not move nor victimized
|
186
|
+
self.model.Add(self.mover[(p, t)] == 0).OnlyEnforceIf(self.is_dead[(p, t)])
|
187
|
+
self.model.Add(self.victim[(p, t)] == 0).OnlyEnforceIf(self.is_dead[(p, t)])
|
188
|
+
# if I was the mover or victim at time step t then I was not dead
|
189
|
+
self.model.Add(self.is_dead[(p, t)] == 0).OnlyEnforceIf(self.mover[(p, t)])
|
190
|
+
self.model.Add(self.is_dead[(p, t)] == 0).OnlyEnforceIf(self.victim[(p, t)])
|
191
|
+
# a victim cannot be the mover and vice versa
|
192
|
+
self.model.Add(self.mover[(p, t)] == 0).OnlyEnforceIf(self.victim[(p, t)])
|
193
|
+
self.model.Add(self.victim[(p, t)] == 0).OnlyEnforceIf(self.mover[(p, t)])
|
194
|
+
|
195
|
+
# if im dead next timestep and i was alive this timestep then im the victim
|
196
|
+
# cant rely on victim var here because the goal it to constrain it
|
197
|
+
dies_this_timestep = self.model.NewBoolVar(f'dies_this_timestep[{p},{t}]')
|
198
|
+
and_constraint(self.model, dies_this_timestep, [self.is_dead[(p, t + 1)], self.is_dead[(p, t)].Not()])
|
199
|
+
self.model.Add(self.victim[(p, t)] == dies_this_timestep)
|
200
|
+
|
201
|
+
# if next timestep im somewhere else then i was the mover
|
202
|
+
# i.e. there exists a position p* s.t. (piece_positions[p, t + 1, p*] AND NOT piece_positions[p, t, p*])
|
203
|
+
pos_is_p_star = []
|
204
|
+
for pos in get_all_pos(self.V, self.H):
|
205
|
+
v = self.model.NewBoolVar(f'pos_is_p_star[{p},{t},{pos}]')
|
206
|
+
self.model.Add(v == 1).OnlyEnforceIf([self.piece_positions[(p, t + 1, pos)], self.piece_positions[(p, t, pos)].Not()])
|
207
|
+
self.model.Add(v == 0).OnlyEnforceIf([self.piece_positions[(p, t + 1, pos)].Not()])
|
208
|
+
self.model.Add(v == 0).OnlyEnforceIf([self.piece_positions[(p, t, pos)]])
|
209
|
+
pos_is_p_star.append(v)
|
210
|
+
or_constraint(self.model, self.mover[(p, t)], pos_is_p_star)
|
211
|
+
|
212
|
+
# at each timestep only one piece can be the mover
|
213
|
+
for t in range(self.T - 1):
|
214
|
+
self.model.AddExactlyOne([self.mover[(p, t)] for p in range(self.N)])
|
215
|
+
# at each timestep only one piece can be victimized
|
216
|
+
for t in range(self.T - 1):
|
217
|
+
self.model.AddExactlyOne([self.victim[(p, t)] for p in range(self.N)])
|
218
|
+
|
219
|
+
|
220
|
+
def solve_and_print(self, verbose: bool = True, max_solutions: int = None):
|
221
|
+
def board_to_solution(board: "Board", solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
222
|
+
pos_assignment: dict[tuple[int, int, Union[Pos, str]], int] = {}
|
223
|
+
for t in range(board.T):
|
224
|
+
for i in range(board.N):
|
225
|
+
for pos in get_all_pos(board.V, board.H):
|
226
|
+
pos_assignment[(i, t, pos)] = solver.Value(board.piece_positions[(i, t, pos)])
|
227
|
+
pos_assignment[(i, t, 'DEAD')] = solver.Value(board.is_dead[(i, t)])
|
228
|
+
mover = {}
|
229
|
+
for t in range(board.T - 1):
|
230
|
+
for i in range(board.N):
|
231
|
+
if solver.Value(board.mover[(i, t)]):
|
232
|
+
mover[t] = (i, board.pieces[i][0].name)
|
233
|
+
victim = {}
|
234
|
+
for t in range(board.T - 1):
|
235
|
+
for i in range(board.N):
|
236
|
+
if solver.Value(board.victim[(i, t)]):
|
237
|
+
victim[t] = (i, board.pieces[i][0].name)
|
238
|
+
|
239
|
+
assignment: dict[int, tuple[int, Pos, Pos]] = {} # final result
|
240
|
+
for t in range(board.T - 1):
|
241
|
+
mover_i = mover[t][0]
|
242
|
+
victim_i = victim[t][0]
|
243
|
+
from_pos = next(pos for pos in get_all_pos(board.V, board.H) if pos_assignment[(mover_i, t, pos)])
|
244
|
+
to_pos = next(pos for pos in get_all_pos(board.V, board.H) if pos_assignment[(mover_i, t + 1, pos)])
|
245
|
+
assignment[t] = (board.pieces[mover_i][0].name, from_pos, to_pos, board.pieces[victim_i][0].name)
|
246
|
+
# return SingleSolution(assignment=assignment, pos_assignment=pos_assignment, mover=mover, victim=victim)
|
247
|
+
return SingleSolution(assignment=assignment)
|
248
|
+
|
249
|
+
def callback(single_res: SingleSolution):
|
250
|
+
print("Solution found")
|
251
|
+
# pieces = sorted(set(i for (i, _, _) in single_res.assignment.keys()))
|
252
|
+
# for piece in pieces:
|
253
|
+
# print(f"Piece {piece} type: {single_res.piece_types[piece]}")
|
254
|
+
# # at each timestep a piece can only be in one position
|
255
|
+
# t_to_pos = {t: pos for (i, t, pos), v in single_res.assignment.items() if i == piece and v == 1}
|
256
|
+
# print(t_to_pos)
|
257
|
+
# print('victims:', single_res.victim)
|
258
|
+
# print('movers:', single_res.mover)
|
259
|
+
# print()
|
260
|
+
move_sequence = to_algebraic_notation(single_res.assignment)
|
261
|
+
print(move_sequence)
|
262
|
+
return generic_solve_all(self, board_to_solution, callback=callback if verbose else None, verbose=verbose, max_solutions=max_solutions)
|
File without changes
|
File without changes
|