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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-puzzle-solver
3
- Version: 0.8.6
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-sequence-puzzle-type-23"><b>Chess Sequence</b><br><br>
210
- <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_sequence_unsolved.png" alt="Chess sequence" height="120">
209
+ <a href="#chess-range-puzzle-type-23"><b>Chess Range</b><br><br>
210
+ <img src="https://raw.githubusercontent.com/Ar-Kareem/puzzle_solver/master/images/chess_range_unsolved.png" alt="Chess range" height="120">
211
211
  </a>
212
212
  </td>
213
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 Sequence(Puzzle Type #23)](#chess-sequencepuzzle-type-23)
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 Sequence(Puzzle Type #23)
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$ sequence of moves where each move is a legal chess move and captures another piece.
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
- This means that at the end of the $N-1$ moves, there is only one piece left alive on the board.
1950
-
1951
- A move that does not capture another piece is not allowed.
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/chess_sequence_unsolved.png" alt="Chess sequence unsolved" width="500">
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 chess_sequence_solver as solver
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/chess_sequence_solved.png" alt="Chess sequence solved" width="500">
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
- * `chess_sequence` — Chess Sequence (chess moves). ([Puzzle-Chess][23])
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/chess_sequence "puzzle_solver/src/puzzle_solver/puzzles/chess_sequence at master · Ar-Kareem/puzzle_solver · GitHub"
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=uc7Anf6yvOAigUSOX5Rrhq8xnwWlZk9BRJb4EuoL_ew,1664
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.6.dist-info/METADATA,sha256=2dFiJVRUPijxzl7Ie2vpS1S7DXJfQJl7DQpDToFGNVo,101803
30
- multi_puzzle_solver-0.8.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- multi_puzzle_solver-0.8.6.dist-info/top_level.txt,sha256=exwVUQa-anK9vYrpKzBPvH8bX43iElWI4VeNiAyBGJY,14
32
- multi_puzzle_solver-0.8.6.dist-info/RECORD,,
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.chess_sequence import chess_sequence as chess_sequence_solver
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.6'
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)