multi-puzzle-solver 1.1.8__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-1.1.8.dist-info/METADATA +4326 -0
- multi_puzzle_solver-1.1.8.dist-info/RECORD +106 -0
- multi_puzzle_solver-1.1.8.dist-info/WHEEL +5 -0
- multi_puzzle_solver-1.1.8.dist-info/top_level.txt +1 -0
- puzzle_solver/__init__.py +184 -0
- puzzle_solver/core/utils.py +298 -0
- puzzle_solver/core/utils_ortools.py +333 -0
- puzzle_solver/core/utils_visualizer.py +575 -0
- puzzle_solver/puzzles/abc_view/abc_view.py +75 -0
- puzzle_solver/puzzles/aquarium/aquarium.py +97 -0
- puzzle_solver/puzzles/area_51/area_51.py +159 -0
- puzzle_solver/puzzles/battleships/battleships.py +139 -0
- puzzle_solver/puzzles/binairo/binairo.py +98 -0
- puzzle_solver/puzzles/binairo/binairo_plus.py +7 -0
- puzzle_solver/puzzles/black_box/black_box.py +243 -0
- puzzle_solver/puzzles/branches/branches.py +64 -0
- puzzle_solver/puzzles/bridges/bridges.py +104 -0
- puzzle_solver/puzzles/chess_range/chess_melee.py +6 -0
- puzzle_solver/puzzles/chess_range/chess_range.py +406 -0
- puzzle_solver/puzzles/chess_range/chess_solo.py +9 -0
- puzzle_solver/puzzles/chess_sequence/chess_sequence.py +262 -0
- puzzle_solver/puzzles/circle_9/circle_9.py +44 -0
- puzzle_solver/puzzles/clouds/clouds.py +81 -0
- puzzle_solver/puzzles/connect_the_dots/connect_the_dots.py +50 -0
- puzzle_solver/puzzles/cow_and_cactus/cow_and_cactus.py +66 -0
- puzzle_solver/puzzles/dominosa/dominosa.py +67 -0
- puzzle_solver/puzzles/filling/filling.py +94 -0
- puzzle_solver/puzzles/flip/flip.py +64 -0
- puzzle_solver/puzzles/flood_it/flood_it.py +174 -0
- puzzle_solver/puzzles/flood_it/parse_map/parse_map.py +197 -0
- puzzle_solver/puzzles/galaxies/galaxies.py +110 -0
- puzzle_solver/puzzles/galaxies/parse_map/parse_map.py +216 -0
- puzzle_solver/puzzles/guess/guess.py +232 -0
- puzzle_solver/puzzles/heyawake/heyawake.py +152 -0
- puzzle_solver/puzzles/hidden_stars/hidden_stars.py +52 -0
- puzzle_solver/puzzles/hidoku/hidoku.py +59 -0
- puzzle_solver/puzzles/inertia/inertia.py +121 -0
- puzzle_solver/puzzles/inertia/parse_map/parse_map.py +207 -0
- puzzle_solver/puzzles/inertia/tsp.py +400 -0
- puzzle_solver/puzzles/kakurasu/kakurasu.py +38 -0
- puzzle_solver/puzzles/kakuro/kakuro.py +81 -0
- puzzle_solver/puzzles/kakuro/krypto_kakuro.py +95 -0
- puzzle_solver/puzzles/keen/keen.py +76 -0
- puzzle_solver/puzzles/kropki/kropki.py +94 -0
- puzzle_solver/puzzles/light_up/light_up.py +58 -0
- puzzle_solver/puzzles/linesweeper/linesweeper.py +71 -0
- puzzle_solver/puzzles/link_a_pix/link_a_pix.py +91 -0
- puzzle_solver/puzzles/lits/lits.py +138 -0
- puzzle_solver/puzzles/magnets/magnets.py +96 -0
- puzzle_solver/puzzles/map/map.py +56 -0
- puzzle_solver/puzzles/mathema_grids/mathema_grids.py +119 -0
- puzzle_solver/puzzles/mathrax/mathrax.py +93 -0
- puzzle_solver/puzzles/minesweeper/minesweeper.py +123 -0
- puzzle_solver/puzzles/mosaic/mosaic.py +38 -0
- puzzle_solver/puzzles/n_queens/n_queens.py +71 -0
- puzzle_solver/puzzles/nonograms/nonograms.py +121 -0
- puzzle_solver/puzzles/nonograms/nonograms_colored.py +220 -0
- puzzle_solver/puzzles/norinori/norinori.py +96 -0
- puzzle_solver/puzzles/number_path/number_path.py +76 -0
- puzzle_solver/puzzles/numbermaze/numbermaze.py +97 -0
- puzzle_solver/puzzles/nurikabe/nurikabe.py +130 -0
- puzzle_solver/puzzles/palisade/palisade.py +91 -0
- puzzle_solver/puzzles/pearl/pearl.py +107 -0
- puzzle_solver/puzzles/pipes/pipes.py +82 -0
- puzzle_solver/puzzles/range/range.py +59 -0
- puzzle_solver/puzzles/rectangles/rectangles.py +128 -0
- puzzle_solver/puzzles/ripple_effect/ripple_effect.py +83 -0
- puzzle_solver/puzzles/rooms/rooms.py +75 -0
- puzzle_solver/puzzles/schurs_numbers/schurs_numbers.py +73 -0
- puzzle_solver/puzzles/shakashaka/shakashaka.py +201 -0
- puzzle_solver/puzzles/shingoki/shingoki.py +116 -0
- puzzle_solver/puzzles/signpost/signpost.py +93 -0
- puzzle_solver/puzzles/singles/singles.py +53 -0
- puzzle_solver/puzzles/slant/parse_map/parse_map.py +135 -0
- puzzle_solver/puzzles/slant/slant.py +111 -0
- puzzle_solver/puzzles/slitherlink/slitherlink.py +130 -0
- puzzle_solver/puzzles/snail/snail.py +97 -0
- puzzle_solver/puzzles/split_ends/split_ends.py +93 -0
- puzzle_solver/puzzles/star_battle/star_battle.py +75 -0
- puzzle_solver/puzzles/star_battle/star_battle_shapeless.py +7 -0
- puzzle_solver/puzzles/stitches/parse_map/parse_map.py +267 -0
- puzzle_solver/puzzles/stitches/stitches.py +96 -0
- puzzle_solver/puzzles/sudoku/sudoku.py +267 -0
- puzzle_solver/puzzles/suguru/suguru.py +55 -0
- puzzle_solver/puzzles/suko/suko.py +54 -0
- puzzle_solver/puzzles/tapa/tapa.py +97 -0
- puzzle_solver/puzzles/tatami/tatami.py +64 -0
- puzzle_solver/puzzles/tents/tents.py +80 -0
- puzzle_solver/puzzles/thermometers/thermometers.py +82 -0
- puzzle_solver/puzzles/towers/towers.py +89 -0
- puzzle_solver/puzzles/tracks/tracks.py +88 -0
- puzzle_solver/puzzles/trees_logic/trees_logic.py +48 -0
- puzzle_solver/puzzles/troix/dumplings.py +7 -0
- puzzle_solver/puzzles/troix/troix.py +75 -0
- puzzle_solver/puzzles/twiddle/twiddle.py +112 -0
- puzzle_solver/puzzles/undead/undead.py +130 -0
- puzzle_solver/puzzles/unequal/unequal.py +128 -0
- puzzle_solver/puzzles/unruly/unruly.py +54 -0
- puzzle_solver/puzzles/vectors/vectors.py +94 -0
- puzzle_solver/puzzles/vermicelli/vermicelli.py +74 -0
- puzzle_solver/puzzles/walls/walls.py +52 -0
- puzzle_solver/puzzles/yajilin/yajilin.py +87 -0
- puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py +172 -0
- puzzle_solver/puzzles/yin_yang/yin_yang.py +103 -0
- puzzle_solver/utils/etc/parser/board_color_digit.py +497 -0
- puzzle_solver/utils/visualizer.py +155 -0
|
@@ -0,0 +1,406 @@
|
|
|
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
|
+
class PieceType(Enum):
|
|
13
|
+
KING = 1
|
|
14
|
+
QUEEN = 2
|
|
15
|
+
ROOK = 3
|
|
16
|
+
BISHOP = 4
|
|
17
|
+
KNIGHT = 5
|
|
18
|
+
PAWN = 6
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class SingleSolution:
|
|
22
|
+
assignment: dict[int, tuple[str, Pos, Pos, str]] # every time step a single piece moves from one position to another and eats another piece
|
|
23
|
+
position_occupied: dict[Pos, int]
|
|
24
|
+
# pos_assignment: dict[tuple[int, int, Union[Pos, str]], int]
|
|
25
|
+
# mover: dict[int, tuple[int, PieceType]]
|
|
26
|
+
# victim: dict[int, tuple[int, PieceType]]
|
|
27
|
+
|
|
28
|
+
def get_hashable_solution(self) -> str:
|
|
29
|
+
# only hash assignment
|
|
30
|
+
result = []
|
|
31
|
+
for _, (_, from_pos, to_pos, _) in sorted(self.assignment.items()):
|
|
32
|
+
result.append((from_pos.x, from_pos.y, to_pos.x, to_pos.y))
|
|
33
|
+
# order doesn't matter for uniqueness
|
|
34
|
+
result = sorted(result)
|
|
35
|
+
return json.dumps(result)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_algebraic_notation(algebraic: str) -> tuple[PieceType, Pos]:
|
|
39
|
+
assert isinstance(algebraic, str), f'algebraic notation must be a string, got {type(algebraic)}'
|
|
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
|
+
|
|
53
|
+
def to_algebraic_notation_single_move(piece_type: str, from_pos: Pos, to_pos: Pos, victim_type: str) -> str:
|
|
54
|
+
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'}
|
|
55
|
+
from_file_letter = chr(from_pos.x + ord('a'))
|
|
56
|
+
from_rank_letter = str(from_pos.y + 1)
|
|
57
|
+
to_file_letter = chr(to_pos.x + ord('a'))
|
|
58
|
+
to_rank_letter = str(to_pos.y + 1)
|
|
59
|
+
return f'{letter[piece_type]}{from_file_letter}{from_rank_letter}->{letter[victim_type]}{to_file_letter}{to_rank_letter}'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def to_algebraic_notation(single_solution: SingleSolution) -> list[str]:
|
|
63
|
+
move_sequence = single_solution.assignment
|
|
64
|
+
move_sequence = sorted(move_sequence.items(), key=lambda x: x[0])
|
|
65
|
+
move_sequence = [x[1] for x in move_sequence]
|
|
66
|
+
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]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_same_row_col(from_pos: Pos, to_pos: Pos) -> bool:
|
|
70
|
+
return from_pos.x == to_pos.x or from_pos.y == to_pos.y
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_diagonal(from_pos: Pos, to_pos: Pos) -> bool:
|
|
74
|
+
return abs(from_pos.x - to_pos.x) == abs(from_pos.y - to_pos.y)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def squares_between_rook(from_pos: Pos, to_pos: Pos) -> list[Pos]:
|
|
78
|
+
if not is_same_row_col(from_pos, to_pos):
|
|
79
|
+
return []
|
|
80
|
+
if abs(from_pos.x - to_pos.x) <= 1 and abs(from_pos.y - to_pos.y) <= 1: # within 2x2 thus no intermediate squares
|
|
81
|
+
return []
|
|
82
|
+
squares: list[Pos] = []
|
|
83
|
+
if from_pos.x == to_pos.x:
|
|
84
|
+
x = from_pos.x
|
|
85
|
+
step = 1 if to_pos.y > from_pos.y else -1
|
|
86
|
+
for y in range(from_pos.y + step, to_pos.y, step):
|
|
87
|
+
squares.append(get_pos(x=x, y=y))
|
|
88
|
+
else:
|
|
89
|
+
y = from_pos.y
|
|
90
|
+
step = 1 if to_pos.x > from_pos.x else -1
|
|
91
|
+
for x in range(from_pos.x + step, to_pos.x, step):
|
|
92
|
+
squares.append(get_pos(x=x, y=y))
|
|
93
|
+
return squares
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def squares_between_bishop(from_pos: Pos, to_pos: Pos) -> list[Pos]:
|
|
97
|
+
if not is_diagonal(from_pos, to_pos):
|
|
98
|
+
return []
|
|
99
|
+
if abs(from_pos.x - to_pos.x) <= 1 and abs(from_pos.y - to_pos.y) <= 1: # within 2x2 thus no intermediate squares
|
|
100
|
+
return []
|
|
101
|
+
squares: list[Pos] = []
|
|
102
|
+
step_x = 1 if to_pos.x > from_pos.x else -1
|
|
103
|
+
step_y = 1 if to_pos.y > from_pos.y else -1
|
|
104
|
+
x = from_pos.x + step_x
|
|
105
|
+
y = from_pos.y + step_y
|
|
106
|
+
while x != to_pos.x and y != to_pos.y:
|
|
107
|
+
squares.append(get_pos(x=x, y=y))
|
|
108
|
+
x += step_x
|
|
109
|
+
y += step_y
|
|
110
|
+
return squares
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def is_move_valid(from_pos: Pos, to_pos: Pos, piece_type: PieceType, color=None) -> tuple[bool, list[Pos]]:
|
|
115
|
+
"""Returns: (is_valid, list of positions that must be empty for the move to be valid)
|
|
116
|
+
For Kings, Pawns, and Knights, no positions must be empty for the move to be valid.
|
|
117
|
+
A Queen is equivalent to a Rook and a Bishop.
|
|
118
|
+
A Rook needs all positions directly between the from and to position to be empty for the move to be valid.
|
|
119
|
+
Similarly, a Bishop needs all positions diagonally between the from and to position to be empty for the move to be valid.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
from_pos (Pos): from position
|
|
123
|
+
to_pos (Pos): to position
|
|
124
|
+
piece_type (PieceType): piece type
|
|
125
|
+
color (str, optional): color of the piece (default: None, all pieces are assumed white)
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
tuple[bool, list[Pos]]: (is_valid, list of positions that must be empty for the move to be valid)
|
|
129
|
+
"""
|
|
130
|
+
if piece_type == PieceType.KING:
|
|
131
|
+
dx = abs(from_pos.x - to_pos.x)
|
|
132
|
+
dy = abs(from_pos.y - to_pos.y)
|
|
133
|
+
is_valid = dx <= 1 and dy <= 1
|
|
134
|
+
return is_valid, []
|
|
135
|
+
elif piece_type == PieceType.QUEEN: # rook + bishop
|
|
136
|
+
rook_valid = is_move_valid(from_pos, to_pos, PieceType.ROOK, color)
|
|
137
|
+
if rook_valid[0]:
|
|
138
|
+
return rook_valid
|
|
139
|
+
return is_move_valid(from_pos, to_pos, PieceType.BISHOP, color)
|
|
140
|
+
elif piece_type == PieceType.ROOK:
|
|
141
|
+
return is_same_row_col(from_pos, to_pos), squares_between_rook(from_pos, to_pos)
|
|
142
|
+
elif piece_type == PieceType.BISHOP:
|
|
143
|
+
return is_diagonal(from_pos, to_pos), squares_between_bishop(from_pos, to_pos)
|
|
144
|
+
elif piece_type == PieceType.KNIGHT:
|
|
145
|
+
dx = abs(from_pos.x - to_pos.x)
|
|
146
|
+
dy = abs(from_pos.y - to_pos.y)
|
|
147
|
+
is_valid = (dx == 2 and dy == 1) or (dx == 1 and dy == 2)
|
|
148
|
+
return is_valid, []
|
|
149
|
+
elif piece_type == PieceType.PAWN: # will always eat because the this is how the puzzle works
|
|
150
|
+
dx = to_pos.x - from_pos.x
|
|
151
|
+
dy = to_pos.y - from_pos.y
|
|
152
|
+
is_valid = abs(dx) == 1 and dy == (1 if color != 'B' else -1)
|
|
153
|
+
return is_valid, []
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Board:
|
|
157
|
+
def __init__(self, pieces: list[str], colors: list[str] = None, max_moves_per_piece: int = None, last_piece_alive: Union[PieceType, str] = None):
|
|
158
|
+
"""
|
|
159
|
+
Args:
|
|
160
|
+
pieces: list of algebraic notation of the pieces
|
|
161
|
+
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))
|
|
162
|
+
max_moves_per_piece: maximum number of moves per piece (default: None, no limit)
|
|
163
|
+
last_piece_alive: force the last piece alive to be of this type (default: None, any piece can be last man standing)
|
|
164
|
+
"""
|
|
165
|
+
self.pieces: dict[int, tuple[PieceType, Pos]] = {i: parse_algebraic_notation(p) for i, p in enumerate(pieces)}
|
|
166
|
+
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}'
|
|
167
|
+
self.colors = colors
|
|
168
|
+
self.N = len(self.pieces) # number of pieces
|
|
169
|
+
self.T = self.N # (N-1) moves + 1 initial state
|
|
170
|
+
self.max_moves_per_piece = max_moves_per_piece
|
|
171
|
+
self.last_piece_alive = last_piece_alive
|
|
172
|
+
self.V = 8 # board size
|
|
173
|
+
self.H = 8 # board size
|
|
174
|
+
# the puzzle rules mean the only legal positions are the starting positions of the pieces
|
|
175
|
+
self.all_legal_positions: set[Pos] = {pos for _, pos in self.pieces.values()}
|
|
176
|
+
assert len(self.all_legal_positions) == len(self.pieces), 'positions are not unique'
|
|
177
|
+
|
|
178
|
+
self.model = cp_model.CpModel()
|
|
179
|
+
# 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):
|
|
180
|
+
# Number of variables
|
|
181
|
+
# piece_positions: O(NTB)
|
|
182
|
+
# is_dead: O(NT)
|
|
183
|
+
# mover: O(NT)
|
|
184
|
+
# victim: O(NT)
|
|
185
|
+
# position_occupied: O(TB)
|
|
186
|
+
# dies_this_timestep: O(NT)
|
|
187
|
+
# pos_is_p_star: O(NTB)
|
|
188
|
+
# Total: ~ (2*N)N^2 + 6N^2 = 2N^3 + 6N^2
|
|
189
|
+
|
|
190
|
+
# (piece_index, time_step, position) -> boolean variable (possible all false if i'm dead)
|
|
191
|
+
self.piece_positions: dict[tuple[int, int, Pos], cp_model.IntVar] = {}
|
|
192
|
+
self.is_dead: dict[tuple[int, int], cp_model.IntVar] = {} # Am I currently dead
|
|
193
|
+
|
|
194
|
+
# (piece_index, time_step) -> boolean variable indicating if the piece [moved/died]
|
|
195
|
+
self.mover: dict[tuple[int, int], cp_model.IntVar] = {} # did i move this timestep?
|
|
196
|
+
self.victim: dict[tuple[int, int], cp_model.IntVar] = {} # did i die this timestep?
|
|
197
|
+
|
|
198
|
+
# (time_step, position) -> boolean variable indicating if the position is occupied by any piece at this timestep
|
|
199
|
+
self.position_occupied: dict[tuple[int, Pos], cp_model.IntVar] = {}
|
|
200
|
+
|
|
201
|
+
self.create_vars()
|
|
202
|
+
self.add_all_constraints()
|
|
203
|
+
# total_vars = len(self.piece_positions) + len(self.is_dead) + len(self.mover) + len(self.victim) + len(self.position_occupied)
|
|
204
|
+
# print(f'Total number of variables: {total_vars}')
|
|
205
|
+
# print(f'Total number of constraints: {len(self.model.proto.constraints)}')
|
|
206
|
+
|
|
207
|
+
def can_move(self, p: int, t: int) -> bool:
|
|
208
|
+
c = self.colors[p]
|
|
209
|
+
return (c == 'W' and t % 2 == 0) or (c == 'B' and t % 2 == 1)
|
|
210
|
+
|
|
211
|
+
def can_be_victim(self, p: int, t: int) -> bool:
|
|
212
|
+
c = self.colors[p]
|
|
213
|
+
return (c == 'W' and t % 2 == 1) or (c == 'B' and t % 2 == 0)
|
|
214
|
+
|
|
215
|
+
def create_vars(self):
|
|
216
|
+
for p in range(self.N):
|
|
217
|
+
for t in range(self.T):
|
|
218
|
+
for pos in self.all_legal_positions:
|
|
219
|
+
self.piece_positions[(p, t, pos)] = self.model.NewBoolVar(f'piece_positions[{p},{t},{pos}]')
|
|
220
|
+
self.is_dead[(p, t)] = self.model.NewBoolVar(f'is_dead[{p},{t}]')
|
|
221
|
+
for p in range(self.N):
|
|
222
|
+
for t in range(self.T - 1): # final state does not have a mover or victim
|
|
223
|
+
self.mover[(p, t)] = self.model.NewBoolVar(f'mover[{p},{t}]')
|
|
224
|
+
self.victim[(p, t)] = self.model.NewBoolVar(f'victim[{p},{t}]')
|
|
225
|
+
|
|
226
|
+
for t in range(self.T):
|
|
227
|
+
for pos in self.all_legal_positions:
|
|
228
|
+
self.position_occupied[(t, pos)] = self.model.NewBoolVar(f'position_occupied[{t},{pos}]')
|
|
229
|
+
|
|
230
|
+
def add_all_constraints(self):
|
|
231
|
+
self.enforce_initial_state()
|
|
232
|
+
self.enforce_board_state_constraints()
|
|
233
|
+
self.enforce_mover_victim_constraints()
|
|
234
|
+
self.enforce_position_occupied_constraints()
|
|
235
|
+
if self.colors is not None: # t=0 and even timesteps are white, odd timesteps are black
|
|
236
|
+
self.enforce_colors_constraints()
|
|
237
|
+
|
|
238
|
+
def enforce_initial_state(self):
|
|
239
|
+
# initial state
|
|
240
|
+
for p, (_, initial_pos) in self.pieces.items():
|
|
241
|
+
self.model.Add(self.piece_positions[(p, 0, initial_pos)] == 1)
|
|
242
|
+
# can't be initially dead
|
|
243
|
+
self.model.Add(self.is_dead[(p, 0)] == 0)
|
|
244
|
+
# all others are blank
|
|
245
|
+
for pos in self.all_legal_positions:
|
|
246
|
+
if pos == initial_pos:
|
|
247
|
+
continue
|
|
248
|
+
self.model.Add(self.piece_positions[(p, 0, pos)] == 0)
|
|
249
|
+
|
|
250
|
+
def enforce_board_state_constraints(self):
|
|
251
|
+
# at each timestep and each piece, it can only be at exactly one position or dead
|
|
252
|
+
for p in range(self.N):
|
|
253
|
+
for t in range(self.T):
|
|
254
|
+
pos_vars = [self.piece_positions[(p, t, pos)] for pos in self.all_legal_positions]
|
|
255
|
+
pos_vars.append(self.is_dead[(p, t)])
|
|
256
|
+
self.model.AddExactlyOne(pos_vars)
|
|
257
|
+
# if im dead this timestep then im also dead next timestep
|
|
258
|
+
for p in range(self.N):
|
|
259
|
+
for t in range(self.T - 1):
|
|
260
|
+
self.model.Add(self.is_dead[(p, t + 1)] == 1).OnlyEnforceIf(self.is_dead[(p, t)])
|
|
261
|
+
# every move must be legal chess move
|
|
262
|
+
for p in range(self.N):
|
|
263
|
+
color = self.colors[p] if self.colors is not None else None
|
|
264
|
+
for t in range(self.T - 1):
|
|
265
|
+
for from_pos in self.all_legal_positions:
|
|
266
|
+
for to_pos in self.all_legal_positions:
|
|
267
|
+
if from_pos == to_pos:
|
|
268
|
+
continue
|
|
269
|
+
is_valid, need_to_be_empty = is_move_valid(from_pos, to_pos, self.pieces[p][0], color=color)
|
|
270
|
+
# remove non legal moves
|
|
271
|
+
need_to_be_empty = set(need_to_be_empty) & self.all_legal_positions
|
|
272
|
+
if not is_valid:
|
|
273
|
+
self.model.Add(self.piece_positions[(p, t + 1, to_pos)] == 0).OnlyEnforceIf([self.piece_positions[(p, t, from_pos)]])
|
|
274
|
+
elif len(need_to_be_empty) > 0:
|
|
275
|
+
occupied_between = self.model.NewBoolVar(f'occupied_between[{from_pos},{to_pos},{t},{p}]')
|
|
276
|
+
or_constraint(self.model, occupied_between, [self.position_occupied[(t, pos)] for pos in need_to_be_empty])
|
|
277
|
+
self.model.Add(self.piece_positions[(p, t + 1, to_pos)] == 0).OnlyEnforceIf([self.piece_positions[(p, t, from_pos)], occupied_between])
|
|
278
|
+
|
|
279
|
+
# if mover is i and victim is j then i HAS to be at the position of j at the next timestep
|
|
280
|
+
for p_mover in range(self.N):
|
|
281
|
+
for p_victim in range(self.N):
|
|
282
|
+
if p_mover == p_victim:
|
|
283
|
+
continue
|
|
284
|
+
for t in range(self.T - 1):
|
|
285
|
+
for pos in self.all_legal_positions:
|
|
286
|
+
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)]])
|
|
287
|
+
|
|
288
|
+
# optional parameter to force last piece alive
|
|
289
|
+
if self.last_piece_alive is not None:
|
|
290
|
+
target_ps = [p for p in range(self.N) if self.pieces[p][0] == self.last_piece_alive]
|
|
291
|
+
assert len(target_ps) == 1, f'multiple pieces of type {self.last_piece_alive} found'
|
|
292
|
+
target_p = target_ps[0]
|
|
293
|
+
# target piece is force to be last man standing
|
|
294
|
+
self.model.Add(self.is_dead[(target_p, self.T - 1)] == 0)
|
|
295
|
+
for p in range(self.N):
|
|
296
|
+
if p == target_p:
|
|
297
|
+
continue
|
|
298
|
+
self.model.Add(self.is_dead[(p, self.T - 1)] == 1)
|
|
299
|
+
|
|
300
|
+
def enforce_mover_victim_constraints(self):
|
|
301
|
+
for p in range(self.N):
|
|
302
|
+
for t in range(self.T - 1):
|
|
303
|
+
# if i'm dead at time step t then I did not move nor victimized
|
|
304
|
+
self.model.Add(self.mover[(p, t)] == 0).OnlyEnforceIf(self.is_dead[(p, t)])
|
|
305
|
+
self.model.Add(self.victim[(p, t)] == 0).OnlyEnforceIf(self.is_dead[(p, t)])
|
|
306
|
+
# if I was the mover or victim at time step t then I was not dead
|
|
307
|
+
self.model.Add(self.is_dead[(p, t)] == 0).OnlyEnforceIf(self.mover[(p, t)])
|
|
308
|
+
self.model.Add(self.is_dead[(p, t)] == 0).OnlyEnforceIf(self.victim[(p, t)])
|
|
309
|
+
# a victim cannot be the mover and vice versa
|
|
310
|
+
self.model.Add(self.mover[(p, t)] == 0).OnlyEnforceIf(self.victim[(p, t)])
|
|
311
|
+
self.model.Add(self.victim[(p, t)] == 0).OnlyEnforceIf(self.mover[(p, t)])
|
|
312
|
+
|
|
313
|
+
# if im dead next timestep and i was alive this timestep then im the victim
|
|
314
|
+
# can't rely on victim var here because the goal it to constrain it
|
|
315
|
+
dies_this_timestep = self.model.NewBoolVar(f'dies_this_timestep[{p},{t}]')
|
|
316
|
+
and_constraint(self.model, dies_this_timestep, [self.is_dead[(p, t + 1)], self.is_dead[(p, t)].Not()])
|
|
317
|
+
self.model.Add(self.victim[(p, t)] == dies_this_timestep)
|
|
318
|
+
|
|
319
|
+
# if next timestep im somewhere else then i was the mover
|
|
320
|
+
# i.e. there exists a position p* s.t. (piece_positions[p, t + 1, p*] AND NOT piece_positions[p, t, p*])
|
|
321
|
+
pos_is_p_star = []
|
|
322
|
+
for pos in self.all_legal_positions:
|
|
323
|
+
v = self.model.NewBoolVar(f'pos_is_p_star[{p},{t},{pos}]')
|
|
324
|
+
self.model.Add(v == 1).OnlyEnforceIf([self.piece_positions[(p, t + 1, pos)], self.piece_positions[(p, t, pos)].Not()])
|
|
325
|
+
self.model.Add(v == 0).OnlyEnforceIf([self.piece_positions[(p, t + 1, pos)].Not()])
|
|
326
|
+
self.model.Add(v == 0).OnlyEnforceIf([self.piece_positions[(p, t, pos)]])
|
|
327
|
+
pos_is_p_star.append(v)
|
|
328
|
+
or_constraint(self.model, self.mover[(p, t)], pos_is_p_star)
|
|
329
|
+
|
|
330
|
+
# at each timestep only one piece can be the mover
|
|
331
|
+
for t in range(self.T - 1):
|
|
332
|
+
self.model.AddExactlyOne([self.mover[(p, t)] for p in range(self.N)])
|
|
333
|
+
# at each timestep only one piece can be victimized
|
|
334
|
+
for t in range(self.T - 1):
|
|
335
|
+
self.model.AddExactlyOne([self.victim[(p, t)] for p in range(self.N)])
|
|
336
|
+
|
|
337
|
+
# optional parameter to force
|
|
338
|
+
if self.max_moves_per_piece is not None:
|
|
339
|
+
for p in range(self.N):
|
|
340
|
+
self.model.Add(sum([self.mover[(p, t)] for t in range(self.T - 1)]) <= self.max_moves_per_piece)
|
|
341
|
+
|
|
342
|
+
def enforce_position_occupied_constraints(self):
|
|
343
|
+
for t in range(self.T):
|
|
344
|
+
for pos in self.all_legal_positions:
|
|
345
|
+
self.model.Add(self.position_occupied[(t, pos)] == sum([self.piece_positions[(p, t, pos)] for p in range(self.N)]))
|
|
346
|
+
|
|
347
|
+
def enforce_colors_constraints(self):
|
|
348
|
+
# t=0 and even timesteps are white, odd timesteps are black
|
|
349
|
+
for p in range(self.N):
|
|
350
|
+
for t in range(self.T - 1):
|
|
351
|
+
if self.can_move(p, t):
|
|
352
|
+
continue
|
|
353
|
+
self.model.Add(self.mover[(p, t)] == 0)
|
|
354
|
+
# t=0 and even timesteps only black victims, odd timesteps only white victims
|
|
355
|
+
for p in range(self.N):
|
|
356
|
+
for t in range(self.T - 1):
|
|
357
|
+
if not self.can_be_victim(p, t):
|
|
358
|
+
self.model.Add(self.victim[(p, t)] == 0)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def solve_and_print(self, verbose: bool = True, max_solutions: int = None):
|
|
362
|
+
def board_to_solution(board: "Board", solver: cp_model.CpSolverSolutionCallback) -> SingleSolution:
|
|
363
|
+
pos_assignment: dict[tuple[int, int, Union[Pos, str]], int] = {}
|
|
364
|
+
for t in range(board.T):
|
|
365
|
+
for i in range(board.N):
|
|
366
|
+
for pos in board.all_legal_positions:
|
|
367
|
+
pos_assignment[(i, t, pos)] = solver.Value(board.piece_positions[(i, t, pos)])
|
|
368
|
+
pos_assignment[(i, t, 'DEAD')] = solver.Value(board.is_dead[(i, t)])
|
|
369
|
+
mover = {}
|
|
370
|
+
for t in range(board.T - 1):
|
|
371
|
+
for i in range(board.N):
|
|
372
|
+
if solver.Value(board.mover[(i, t)]):
|
|
373
|
+
mover[t] = (i, board.pieces[i][0].name)
|
|
374
|
+
victim = {}
|
|
375
|
+
for t in range(board.T - 1):
|
|
376
|
+
for i in range(board.N):
|
|
377
|
+
if solver.Value(board.victim[(i, t)]):
|
|
378
|
+
victim[t] = (i, board.pieces[i][0].name)
|
|
379
|
+
|
|
380
|
+
assignment: dict[int, tuple[int, Pos, Pos]] = {} # final result
|
|
381
|
+
for t in range(board.T - 1):
|
|
382
|
+
mover_i = mover[t][0]
|
|
383
|
+
victim_i = victim[t][0]
|
|
384
|
+
from_pos = next(pos for pos in board.all_legal_positions if pos_assignment[(mover_i, t, pos)])
|
|
385
|
+
to_pos = next(pos for pos in board.all_legal_positions if pos_assignment[(mover_i, t + 1, pos)])
|
|
386
|
+
assignment[t] = (board.pieces[mover_i][0].name, from_pos, to_pos, board.pieces[victim_i][0].name)
|
|
387
|
+
# return SingleSolution(assignment=assignment, pos_assignment=pos_assignment, mover=mover, victim=victim)
|
|
388
|
+
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}
|
|
389
|
+
return SingleSolution(assignment=assignment, position_occupied=position_occupied)
|
|
390
|
+
|
|
391
|
+
def callback(single_res: SingleSolution):
|
|
392
|
+
print("Solution found")
|
|
393
|
+
# pieces = sorted(set(i for (i, _, _) in single_res.assignment.keys()))
|
|
394
|
+
# for piece in pieces:
|
|
395
|
+
# print(f"Piece {piece} type: {single_res.piece_types[piece]}")
|
|
396
|
+
# # at each timestep a piece can only be in one position
|
|
397
|
+
# t_to_pos = {t: pos for (i, t, pos), v in single_res.assignment.items() if i == piece and v == 1}
|
|
398
|
+
# print(t_to_pos)
|
|
399
|
+
# print('victims:', single_res.victim)
|
|
400
|
+
# print('movers:', single_res.mover)
|
|
401
|
+
# print()
|
|
402
|
+
# for t in range(self.T):
|
|
403
|
+
# 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])
|
|
404
|
+
move_sequence = to_algebraic_notation(single_res)
|
|
405
|
+
print(move_sequence)
|
|
406
|
+
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, 'exactly one king piece is required'
|
|
8
|
+
super().__init__(pieces, max_moves_per_piece=2, last_piece_alive=PieceType.KING)
|
|
9
|
+
|
|
@@ -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)
|