manim-chess 0.0.1__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.
- manim_chess-0.0.1.dist-info/METADATA +212 -0
- manim_chess-0.0.1.dist-info/RECORD +9 -0
- manim_chess-0.0.1.dist-info/WHEEL +5 -0
- manim_chess-0.0.1.dist-info/top_level.txt +1 -0
- src/__init__.py +11 -0
- src/board.py +532 -0
- src/evaluation_bar.py +84 -0
- src/game_player.py +711 -0
- src/pieces.py +246 -0
src/game_player.py
ADDED
@@ -0,0 +1,711 @@
|
|
1
|
+
from .board import *
|
2
|
+
from .evaluation_bar import *
|
3
|
+
|
4
|
+
import re
|
5
|
+
from typing import Tuple
|
6
|
+
|
7
|
+
DEFAULT_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
|
8
|
+
|
9
|
+
def play_game(scene, board: Board, moves: list[Tuple[str, str, str]], eval_bar: EvaluationBar = None, evals: list[float] = None) -> None:
|
10
|
+
"""
|
11
|
+
Executes a series of chess moves on a given board and updates the evaluation bar if provided.
|
12
|
+
|
13
|
+
Parameters:
|
14
|
+
----------
|
15
|
+
scene : Scene
|
16
|
+
The Manim scene where the game is being played.
|
17
|
+
board : Board
|
18
|
+
The chess board object on which the moves are executed.
|
19
|
+
moves : list of Tuple[str, str]
|
20
|
+
A list of moves, where each move is a tuple containing the starting and ending positions,
|
21
|
+
and optionally a promotion piece.
|
22
|
+
eval_bar : EvaluationBar, optional
|
23
|
+
An evaluation bar object to visualize the evaluation of the board state (default is None).
|
24
|
+
evals : list of float, optional
|
25
|
+
A list of evaluation scores corresponding to each move (default is None).
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
-------
|
29
|
+
None
|
30
|
+
"""
|
31
|
+
# Resize the evals array if not enough
|
32
|
+
if not evals:
|
33
|
+
evals = []
|
34
|
+
while len(evals) < len(moves):
|
35
|
+
evals.append(0)
|
36
|
+
|
37
|
+
for move, evaluation in zip(moves, evals):
|
38
|
+
|
39
|
+
# Check for en passant, if True then remove the captured piece
|
40
|
+
if __check_for_en_passant(board, move):
|
41
|
+
direction = int(move[1][1]) - int(move[0][1])
|
42
|
+
if direction == 1:
|
43
|
+
board.remove_piece(f'{move[1][0]}{int(move[1][1])-1}')
|
44
|
+
else:
|
45
|
+
board.remove_piece(f'{move[1][0]}{int(move[1][1])+1}')
|
46
|
+
|
47
|
+
# Check for castling, if True move the rook next to the king
|
48
|
+
if __check_for_castle(board, move):
|
49
|
+
letters_in_order = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8}
|
50
|
+
direction = letters_in_order[move[1][0]] - letters_in_order[move[0][0]]
|
51
|
+
if direction > 1:
|
52
|
+
board.move_piece(f'h{move[0][1]}', f'f{move[0][1]}')
|
53
|
+
else:
|
54
|
+
board.move_piece(f'a{move[0][1]}', f'd{move[0][1]}')
|
55
|
+
|
56
|
+
board.move_piece(move[0], move[1])
|
57
|
+
if move[2]:
|
58
|
+
board.promote_piece(move[1], move[2])
|
59
|
+
|
60
|
+
if eval_bar:
|
61
|
+
scene.play(eval_bar.set_evaluation(evaluation))
|
62
|
+
|
63
|
+
scene.wait()
|
64
|
+
|
65
|
+
def __check_for_en_passant(board: Board, move: Tuple[str, str, str]) -> bool:
|
66
|
+
"""
|
67
|
+
Checks if a given move is an en passant capture.
|
68
|
+
|
69
|
+
Parameters:
|
70
|
+
----------
|
71
|
+
board : Board
|
72
|
+
The chess board object.
|
73
|
+
move : Tuple[str, str]
|
74
|
+
A tuple representing the starting and ending positions of the move.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
-------
|
78
|
+
bool
|
79
|
+
True if the move is an en passant capture, False otherwise.
|
80
|
+
"""
|
81
|
+
starting_square = move[0]
|
82
|
+
ending_square = move[1]
|
83
|
+
if type(board.get_piece_at_square(starting_square)).__name__ == "Pawn": # Check if the moving piece is a pawn
|
84
|
+
if not board.get_piece_at_square(ending_square): # Check if the ending square is empty
|
85
|
+
if starting_square[0] != ending_square[0]: # Check if the pawn did not move straight
|
86
|
+
return True
|
87
|
+
return False
|
88
|
+
|
89
|
+
def __check_for_castle(board: Board, move: Tuple[str, str, str]) -> bool:
|
90
|
+
"""
|
91
|
+
Checks if a given move is a castling move.
|
92
|
+
|
93
|
+
Parameters:
|
94
|
+
----------
|
95
|
+
board : Board
|
96
|
+
The chess board object.
|
97
|
+
move : Tuple[str, str]
|
98
|
+
A tuple representing the starting and ending positions of the move.
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
-------
|
102
|
+
bool
|
103
|
+
True if the move is a castling move, False otherwise.
|
104
|
+
"""
|
105
|
+
letters_in_order = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8}
|
106
|
+
starting_square = move[0]
|
107
|
+
ending_square = move[1]
|
108
|
+
if type(board.get_piece_at_square(starting_square)).__name__ == "King": # Check if the moving piece is a king
|
109
|
+
# Check if the king moved more than 1 square left or right
|
110
|
+
distance = abs(letters_in_order[ending_square[0]] - letters_in_order[starting_square[0]])
|
111
|
+
if distance > 1:
|
112
|
+
return True
|
113
|
+
return False
|
114
|
+
|
115
|
+
def __get_coordinate_from_index(index: int) -> str:
|
116
|
+
"""
|
117
|
+
Converts a linear index to a board coordinate.
|
118
|
+
|
119
|
+
Parameters:
|
120
|
+
----------
|
121
|
+
index : int
|
122
|
+
The linear index of a square.
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
-------
|
126
|
+
str
|
127
|
+
The board coordinate corresponding to the index.
|
128
|
+
"""
|
129
|
+
number_to_letter = {
|
130
|
+
0: 'a',
|
131
|
+
1: 'b',
|
132
|
+
2: 'c',
|
133
|
+
3: 'd',
|
134
|
+
4: 'e',
|
135
|
+
5: 'f',
|
136
|
+
6: 'g',
|
137
|
+
7: 'h'
|
138
|
+
}
|
139
|
+
coordinate = f'{number_to_letter[index % 8]}{8 - math.floor(index / 8)}'
|
140
|
+
return coordinate
|
141
|
+
|
142
|
+
def __get_index_from_FEN(FEN: str, position_in_FEN: int) -> int:
|
143
|
+
piece_info = FEN.split()[0][:position_in_FEN]
|
144
|
+
current_index = 0
|
145
|
+
for char in piece_info:
|
146
|
+
if char in {'1', '2', '3', '4', '5', '6', '7', '8'}:
|
147
|
+
current_index += int(char)
|
148
|
+
elif char == '/':
|
149
|
+
pass
|
150
|
+
else:
|
151
|
+
current_index += 1
|
152
|
+
return current_index
|
153
|
+
|
154
|
+
def __find_all_pieces(FEN: str) -> list[str]:
|
155
|
+
"""
|
156
|
+
Returns all coordiantes that contain a piece.
|
157
|
+
|
158
|
+
Parameters:
|
159
|
+
----------
|
160
|
+
FEN: str
|
161
|
+
The FEN string of the current board position.
|
162
|
+
"""
|
163
|
+
coordinates = []
|
164
|
+
for i, char in enumerate(FEN.split()[0]):
|
165
|
+
if char not in {'/', '1', '2', '3', '4', '5', '6', '7', '8', '9'}:
|
166
|
+
index = __get_index_from_FEN(FEN, i)
|
167
|
+
coordinate = __get_coordinate_from_index(index)
|
168
|
+
coordinates.append(coordinate)
|
169
|
+
return coordinates
|
170
|
+
|
171
|
+
def __find_piece(FEN: str, piece_type: str) -> list[str]:
|
172
|
+
"""
|
173
|
+
Returns all coordiantes the piece type was found at.
|
174
|
+
|
175
|
+
Parameters:
|
176
|
+
----------
|
177
|
+
FEN: str
|
178
|
+
The FEN string of the current board position.
|
179
|
+
piece_type: str
|
180
|
+
The type of piece you would like to find. Case sensitive, lowercase for white
|
181
|
+
upper case for black.
|
182
|
+
|
183
|
+
"""
|
184
|
+
coordinates = []
|
185
|
+
for i, char in enumerate(FEN.split()[0]):
|
186
|
+
if char == piece_type:
|
187
|
+
index = __get_index_from_FEN(FEN, i)
|
188
|
+
coordinate = __get_coordinate_from_index(index)
|
189
|
+
coordinates.append(coordinate)
|
190
|
+
return coordinates
|
191
|
+
|
192
|
+
def __castling_notation(algebraic_notation, FEN) -> Tuple[str, str, str]:
|
193
|
+
turn = FEN.split()[1] # w or b
|
194
|
+
castling_king_side = True if algebraic_notation == 'O-O' else False
|
195
|
+
|
196
|
+
if turn == 'w': # If player is white
|
197
|
+
move = ('e1', 'g1', "") if castling_king_side else ('e1', 'c1', "") # King side castling or queen side castling
|
198
|
+
else: # If player is black
|
199
|
+
move = ('e8', 'g8', "") if castling_king_side else ('e8', 'c8', "") # King side castling or queen side castling
|
200
|
+
return move
|
201
|
+
|
202
|
+
def pawn_algebraic_notation(algebraic_notation, FEN) -> Tuple[str, str, str]:
|
203
|
+
# If the piece is a pawn than the starting square can be determined by seeing which pawn can go to the ending square
|
204
|
+
#
|
205
|
+
# NOTE ascending means +1 rank and descending means -1 rank
|
206
|
+
#
|
207
|
+
# This can be done by:
|
208
|
+
# 1. If not capturing, check the square descending from the ending square (or ascending if black) if a pawn
|
209
|
+
# is there than that is the starting square, else it is the square descending from that square (or ascending if black)
|
210
|
+
# 2. If capturing than the file is specified as first char on algebraic notation. Then the only needed information is the rank
|
211
|
+
# which can be determined by the turn (w or b) since white can only move ascending with pawns and black can only move descending
|
212
|
+
# with pawns.
|
213
|
+
# 3. If promoting than promotion piece is specified, make sure to check if the checking or checkmating since that moves the promotion
|
214
|
+
# piece to the second to last char
|
215
|
+
turn = FEN.split()[1]
|
216
|
+
capturing = True if 'x' in algebraic_notation else False
|
217
|
+
promoting = True if '=' in algebraic_notation else False
|
218
|
+
checkmate = True if '#' in algebraic_notation else False
|
219
|
+
check = True if '+' in algebraic_notation else False
|
220
|
+
|
221
|
+
ending_square_index = promoting * 2 + checkmate or check # this shifts the index over depending on if it has the + # or =piece_type
|
222
|
+
|
223
|
+
ending_square_file = algebraic_notation[-(2+ending_square_index)]
|
224
|
+
ending_square_rank = int(algebraic_notation[-(1+ending_square_index)])
|
225
|
+
|
226
|
+
all_piece_coordiantes = __find_all_pieces(FEN)
|
227
|
+
|
228
|
+
if not capturing:
|
229
|
+
starting_square_file = ending_square_file
|
230
|
+
if turn == 'w':
|
231
|
+
if f'{ending_square_file}{ending_square_rank-1}' in all_piece_coordiantes:
|
232
|
+
starting_square_rank = ending_square_rank-1
|
233
|
+
else:
|
234
|
+
starting_square_rank = ending_square_rank-2
|
235
|
+
else:
|
236
|
+
if f'{ending_square_file}{ending_square_rank+1}' in all_piece_coordiantes:
|
237
|
+
starting_square_rank = ending_square_rank+1
|
238
|
+
else:
|
239
|
+
starting_square_rank = ending_square_rank+2
|
240
|
+
|
241
|
+
else:
|
242
|
+
starting_square_file = algebraic_notation[0]
|
243
|
+
starting_square_rank = f'{ending_square_rank-1}' if turn == 'w' else f'{ending_square_rank+1}'
|
244
|
+
|
245
|
+
if promoting:
|
246
|
+
promotion_piece = algebraic_notation[-2] if check or checkmate else algebraic_notation[-1]
|
247
|
+
move = (f'{starting_square_file}{starting_square_rank}', f'{ending_square_file}{ending_square_rank}', promotion_piece)
|
248
|
+
else:
|
249
|
+
move = (f'{starting_square_file}{starting_square_rank}', f'{ending_square_file}{ending_square_rank}', '')
|
250
|
+
|
251
|
+
return move
|
252
|
+
|
253
|
+
def knight_algebraic_notation(algebraic_notation, FEN) -> Tuple[str, str, str]:
|
254
|
+
# If the piece is a knight than the starting square can be determined by seeing which knight can go to the ending square,
|
255
|
+
# if the case where 2+ knights can go to the starting square the algebraic notation will give enough information to determine the
|
256
|
+
# correct knight
|
257
|
+
#
|
258
|
+
# This can be done by:
|
259
|
+
# 1. Check all squares around the ending square with a knight's movement, if the correct color knight is found (based on turn)
|
260
|
+
# and it is the only knight that is found, the position that knight is at is the starting square.
|
261
|
+
# 2. If ambiguous check third char of algebraic notation if it is a letter (x or file specification) than the correct knight
|
262
|
+
# has the second char in it's coordinate
|
263
|
+
# 3. If ambiguous and the third char is not a letter than the correct knight is given by the coordinate secondchar + thirdchar
|
264
|
+
# of the algebraic notation
|
265
|
+
turn = FEN.split()[1]
|
266
|
+
knight_movements = [(-2, -1), (-1, -2), (2, 1), (1, 2), (-2, 1), (1, -2), (2, -1), (-1, 2)] # (x, y)
|
267
|
+
|
268
|
+
checkmate = True if '#' in algebraic_notation else False
|
269
|
+
check = True if '+' in algebraic_notation else False
|
270
|
+
|
271
|
+
ending_square_index = checkmate or check # this shifts the index over depending on if it has the + #
|
272
|
+
|
273
|
+
ending_square_file = algebraic_notation[-(2+ending_square_index)]
|
274
|
+
ending_square_rank = int(algebraic_notation[-(1+ending_square_index)])
|
275
|
+
|
276
|
+
piece_type = 'N' if turn == 'w' else 'n'
|
277
|
+
knight_coordinates = __find_piece(FEN, piece_type)
|
278
|
+
|
279
|
+
coordinates_that_passed = [] # A list of all the knight coordinates that could have moved to the ending square
|
280
|
+
|
281
|
+
# Gives the letters that are to the right and to the left with the order of [current_postion, right_one, right_two, left_two, left_one]
|
282
|
+
x_coordinate_to_letter = {
|
283
|
+
'a': ['a', 'b', 'c', None, None],
|
284
|
+
'b': ['b', 'c', 'd', None, 'a' ],
|
285
|
+
'c': ['c', 'd', 'e', 'a', 'b' ],
|
286
|
+
'd': ['d', 'e', 'f', 'b', 'c' ],
|
287
|
+
'e': ['e', 'f', 'g', 'c', 'd' ],
|
288
|
+
'f': ['f', 'g', 'h', 'd', 'e' ],
|
289
|
+
'g': ['g', 'h', None, 'e', 'f' ],
|
290
|
+
'h': ['h', None, None, 'f', 'g']
|
291
|
+
}
|
292
|
+
for movement in knight_movements:
|
293
|
+
file_to_check = x_coordinate_to_letter[ending_square_file][movement[0]]
|
294
|
+
rank_to_check = int(ending_square_rank)+movement[1]
|
295
|
+
if f'{file_to_check}{rank_to_check}' in knight_coordinates:
|
296
|
+
coordinates_that_passed.append(f'{file_to_check}{rank_to_check}')
|
297
|
+
|
298
|
+
if len(coordinates_that_passed) == 1:
|
299
|
+
return (coordinates_that_passed[0], f'{ending_square_file}{ending_square_rank}', '')
|
300
|
+
|
301
|
+
else: # Ambiguous
|
302
|
+
if algebraic_notation[2] in {'x', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}:
|
303
|
+
for knight_coordinate in coordinates_that_passed:
|
304
|
+
if algebraic_notation[1] in knight_coordinate:
|
305
|
+
return (knight_coordinate, f'{ending_square_file}{ending_square_rank}', '')
|
306
|
+
else:
|
307
|
+
return (algebraic_notation[1:3], f'{ending_square_file}{ending_square_rank}', '')
|
308
|
+
|
309
|
+
def bishop_algebraic_notation(algebraic_notation, FEN) -> Tuple[str, str, str]:
|
310
|
+
# If the piece is a bishop than the starting square can be determined by seeing which bishop can go to the ending square,
|
311
|
+
# if the case where 2+ bishops can go to the starting square the algebraic notation will give enough information to determine the
|
312
|
+
# correct bishop
|
313
|
+
#
|
314
|
+
# This can be done by:
|
315
|
+
# 1. Check each square diagnol one square at a time. Like this:
|
316
|
+
# 2###2
|
317
|
+
# #1#1#
|
318
|
+
# ##O##
|
319
|
+
# #1#1#
|
320
|
+
# 2###2
|
321
|
+
# If the search runs into a piece and it's a bishop goto 2.) if not ambiguous than return the coordinate of that bishop
|
322
|
+
# If the search runs into a piece that is not a bishop than stop searching in that direction
|
323
|
+
# 2. Check if ambiguous by seeing the length of the algebraic notation.
|
324
|
+
# a.) If checkmate or check, length can be +1
|
325
|
+
# b.) If capturing, length can be +1
|
326
|
+
# c.) Check if length is 2 greater than 3+(1 if check or checkmate)+(1 if capturing) if so it is ambiguous and the starting coordinate
|
327
|
+
# is algebraic_notation[1:2]
|
328
|
+
# d.) Check if length is 1 greater than 3+(1 if check or checkmate)+(1 if capturing) if so it is ambiguous and the specifier is
|
329
|
+
# algebraic_notation[1], so if the bishop that was found has that specifier in the coordinate than it is the correct bishop
|
330
|
+
# e.) else not ambiguous
|
331
|
+
turn = FEN.split()[1]
|
332
|
+
bishop_direction = [(-1, -1), (-1, 1), (1, -1), (1, 1)]
|
333
|
+
|
334
|
+
checkmate = True if '#' in algebraic_notation else False
|
335
|
+
check = True if '+' in algebraic_notation else False
|
336
|
+
|
337
|
+
capturing = True if 'x' in algebraic_notation else False
|
338
|
+
|
339
|
+
ending_square_index = checkmate or check # this shifts the index over depending on if it has the + #
|
340
|
+
|
341
|
+
ending_square_file = algebraic_notation[-(2+ending_square_index)]
|
342
|
+
ending_square_rank = int(algebraic_notation[-(1+ending_square_index)])
|
343
|
+
|
344
|
+
piece_type = 'B' if turn == 'w' else 'b'
|
345
|
+
bishop_coordiantes = __find_piece(FEN, piece_type)
|
346
|
+
all_pieces = __find_all_pieces(FEN)
|
347
|
+
|
348
|
+
# Gives the letters that are to the right and to the left with the order of [current_positon, right_one, left_one]
|
349
|
+
x_coordinate_to_letter = {
|
350
|
+
'a': ['a', 'b', None],
|
351
|
+
'b': ['b', 'c', 'a' ],
|
352
|
+
'c': ['c', 'd', 'b' ],
|
353
|
+
'd': ['d', 'e', 'c' ],
|
354
|
+
'e': ['e', 'f', 'd' ],
|
355
|
+
'f': ['f', 'g', 'e' ],
|
356
|
+
'g': ['g', 'h', 'f' ],
|
357
|
+
'h': ['h', None, 'g']
|
358
|
+
}
|
359
|
+
for direction in bishop_direction:
|
360
|
+
current_search_coordinate = f'{ending_square_file}{ending_square_rank}'
|
361
|
+
hit_piece = False
|
362
|
+
new_file = x_coordinate_to_letter[current_search_coordinate[0]][direction[0]]
|
363
|
+
new_rank = int(current_search_coordinate[1]) + direction[1]
|
364
|
+
while not hit_piece:
|
365
|
+
if not new_file or new_rank < 1 or new_rank > 8:
|
366
|
+
hit_piece = True
|
367
|
+
else:
|
368
|
+
if f'{new_file}{new_rank}' in bishop_coordiantes:
|
369
|
+
non_ambiguous_length = 3
|
370
|
+
if checkmate or check:
|
371
|
+
non_ambiguous_length += 1
|
372
|
+
if capturing:
|
373
|
+
non_ambiguous_length += 1
|
374
|
+
if len(algebraic_notation) == 2 + non_ambiguous_length:
|
375
|
+
return (algebraic_notation[1:2], f'{ending_square_file}{ending_square_rank}', '')
|
376
|
+
if len(algebraic_notation) == 1 + non_ambiguous_length:
|
377
|
+
if algebraic_notation[1] in f'{new_file}{new_rank}':
|
378
|
+
return (f'{new_file}{new_rank}', f'{ending_square_file}{ending_square_rank}', '')
|
379
|
+
else:
|
380
|
+
hit_piece = True
|
381
|
+
if len(algebraic_notation) == non_ambiguous_length:
|
382
|
+
return (f'{new_file}{new_rank}', f'{ending_square_file}{ending_square_rank}', '')
|
383
|
+
elif f'{new_file}{new_rank}' in all_pieces:
|
384
|
+
hit_piece = True
|
385
|
+
else:
|
386
|
+
new_file = x_coordinate_to_letter[new_file][direction[0]]
|
387
|
+
new_rank = int(new_rank) + direction[1]
|
388
|
+
|
389
|
+
|
390
|
+
def rook_algebraic_notation(algebraic_notation, FEN) -> Tuple[str, str, str]:
|
391
|
+
# Works almost the same as bishop just with horizontal and vertical movement
|
392
|
+
turn = FEN.split()[1]
|
393
|
+
rook_direction = [(-1, 0), (0, 1), (0, -1), (1, 0)]
|
394
|
+
|
395
|
+
checkmate = True if '#' in algebraic_notation else False
|
396
|
+
check = True if '+' in algebraic_notation else False
|
397
|
+
|
398
|
+
capturing = True if 'x' in algebraic_notation else False
|
399
|
+
|
400
|
+
ending_square_index = checkmate or check # this shifts the index over depending on if it has the + #
|
401
|
+
|
402
|
+
ending_square_file = algebraic_notation[-(2+ending_square_index)]
|
403
|
+
ending_square_rank = int(algebraic_notation[-(1+ending_square_index)])
|
404
|
+
|
405
|
+
piece_type = 'R' if turn == 'w' else 'r'
|
406
|
+
rook_coordiantes = __find_piece(FEN, piece_type)
|
407
|
+
all_pieces = __find_all_pieces(FEN)
|
408
|
+
|
409
|
+
# Gives the letters that are to the right and to the left with the order of [current_position, right_one, left_one]
|
410
|
+
x_coordinate_to_letter = {
|
411
|
+
'a': ['a', 'b', None],
|
412
|
+
'b': ['b', 'c', 'a' ],
|
413
|
+
'c': ['c', 'd', 'b' ],
|
414
|
+
'd': ['d', 'e', 'c' ],
|
415
|
+
'e': ['e', 'f', 'd' ],
|
416
|
+
'f': ['f', 'g', 'e' ],
|
417
|
+
'g': ['g', 'h', 'f' ],
|
418
|
+
'h': ['h', None, 'g']
|
419
|
+
}
|
420
|
+
for direction in rook_direction:
|
421
|
+
current_search_coordinate = f'{ending_square_file}{ending_square_rank}'
|
422
|
+
hit_piece = False
|
423
|
+
new_file = x_coordinate_to_letter[current_search_coordinate[0]][direction[0]]
|
424
|
+
new_rank = int(current_search_coordinate[1]) + direction[1]
|
425
|
+
while not hit_piece:
|
426
|
+
if not new_file or new_rank < 1 or new_rank > 8:
|
427
|
+
hit_piece = True
|
428
|
+
else:
|
429
|
+
if f'{new_file}{new_rank}' in rook_coordiantes:
|
430
|
+
non_ambiguous_length = 3
|
431
|
+
if checkmate or check:
|
432
|
+
non_ambiguous_length += 1
|
433
|
+
if capturing:
|
434
|
+
non_ambiguous_length += 1
|
435
|
+
if len(algebraic_notation) == 2 + non_ambiguous_length:
|
436
|
+
return (algebraic_notation[1:2], f'{ending_square_file}{ending_square_rank}', '')
|
437
|
+
if len(algebraic_notation) == 1 + non_ambiguous_length:
|
438
|
+
if algebraic_notation[1] in f'{new_file}{new_rank}':
|
439
|
+
return (f'{new_file}{new_rank}', f'{ending_square_file}{ending_square_rank}', '')
|
440
|
+
else:
|
441
|
+
hit_piece = True
|
442
|
+
if len(algebraic_notation) == non_ambiguous_length:
|
443
|
+
return (f'{new_file}{new_rank}', f'{ending_square_file}{ending_square_rank}', '')
|
444
|
+
elif f'{new_file}{new_rank}' in all_pieces:
|
445
|
+
hit_piece = True
|
446
|
+
else:
|
447
|
+
new_file = x_coordinate_to_letter[new_file][direction[0]]
|
448
|
+
new_rank = int(new_rank) + direction[1]
|
449
|
+
|
450
|
+
def queen_algebraic_notation(algebraic_notation, FEN) -> Tuple[str, str, str]:
|
451
|
+
# Works like bishop + rook
|
452
|
+
turn = FEN.split()[1]
|
453
|
+
queen_direction = [(-1, 0), (0, 1), (0, -1), (1, 0), (-1, -1), (-1, 1), (1, -1), (1, 1)]
|
454
|
+
|
455
|
+
checkmate = True if '#' in algebraic_notation else False
|
456
|
+
check = True if '+' in algebraic_notation else False
|
457
|
+
|
458
|
+
capturing = True if 'x' in algebraic_notation else False
|
459
|
+
|
460
|
+
ending_square_index = checkmate or check # this shifts the index over depending on if it has the + #
|
461
|
+
|
462
|
+
ending_square_file = algebraic_notation[-(2+ending_square_index)]
|
463
|
+
ending_square_rank = int(algebraic_notation[-(1+ending_square_index)])
|
464
|
+
|
465
|
+
piece_type = 'Q' if turn == 'w' else 'q'
|
466
|
+
queen_coordiantes = __find_piece(FEN, piece_type)
|
467
|
+
all_pieces = __find_all_pieces(FEN)
|
468
|
+
|
469
|
+
# Gives the letters that are to the right and to the left with the order of [current_position, right_one, left_one]
|
470
|
+
x_coordinate_to_letter = {
|
471
|
+
'a': ['a', 'b', None],
|
472
|
+
'b': ['b', 'c', 'a' ],
|
473
|
+
'c': ['c', 'd', 'b' ],
|
474
|
+
'd': ['d', 'e', 'c' ],
|
475
|
+
'e': ['e', 'f', 'd' ],
|
476
|
+
'f': ['f', 'g', 'e' ],
|
477
|
+
'g': ['g', 'h', 'f' ],
|
478
|
+
'h': ['h', None, 'g']
|
479
|
+
}
|
480
|
+
for direction in queen_direction:
|
481
|
+
current_search_coordinate = f'{ending_square_file}{ending_square_rank}'
|
482
|
+
hit_piece = False
|
483
|
+
new_file = x_coordinate_to_letter[current_search_coordinate[0]][direction[0]]
|
484
|
+
new_rank = int(current_search_coordinate[1]) + direction[1]
|
485
|
+
while not hit_piece:
|
486
|
+
if not new_file or new_rank < 1 or new_rank > 8:
|
487
|
+
hit_piece = True
|
488
|
+
else:
|
489
|
+
if f'{new_file}{new_rank}' in queen_coordiantes:
|
490
|
+
non_ambiguous_length = 3
|
491
|
+
if checkmate or check:
|
492
|
+
non_ambiguous_length += 1
|
493
|
+
if capturing:
|
494
|
+
non_ambiguous_length += 1
|
495
|
+
if len(algebraic_notation) == 2 + non_ambiguous_length:
|
496
|
+
return (algebraic_notation[1:2], f'{ending_square_file}{ending_square_rank}', '')
|
497
|
+
if len(algebraic_notation) == 1 + non_ambiguous_length:
|
498
|
+
if algebraic_notation[1] in f'{new_file}{new_rank}':
|
499
|
+
return (f'{new_file}{new_rank}', f'{ending_square_file}{ending_square_rank}', '')
|
500
|
+
else:
|
501
|
+
hit_piece = True
|
502
|
+
if len(algebraic_notation) == non_ambiguous_length:
|
503
|
+
return (f'{new_file}{new_rank}', f'{ending_square_file}{ending_square_rank}', '')
|
504
|
+
elif f'{new_file}{new_rank}' in all_pieces:
|
505
|
+
hit_piece = True
|
506
|
+
else:
|
507
|
+
new_file = x_coordinate_to_letter[new_file][direction[0]]
|
508
|
+
new_rank = int(new_rank) + direction[1]
|
509
|
+
|
510
|
+
def king_algebraic_notation(algebraic_notation, FEN) -> Tuple[str, str, str]:
|
511
|
+
# If the piece is a king than the starting square can be determined by seeing where the king is
|
512
|
+
turn = FEN.split()[1]
|
513
|
+
|
514
|
+
checkmate = True if '#' in algebraic_notation else False
|
515
|
+
check = True if '+' in algebraic_notation else False
|
516
|
+
|
517
|
+
ending_square_index = checkmate or check # this shifts the index over depending on if it has the + #
|
518
|
+
|
519
|
+
ending_square_file = algebraic_notation[-(2+ending_square_index)]
|
520
|
+
ending_square_rank = int(algebraic_notation[-(1+ending_square_index)])
|
521
|
+
|
522
|
+
piece_type = 'K' if turn == 'w' else 'k'
|
523
|
+
king_coordinate = __find_piece(FEN, piece_type)[0]
|
524
|
+
|
525
|
+
return (king_coordinate, f'{ending_square_file}{ending_square_rank}', '')
|
526
|
+
|
527
|
+
def convert_from_algebraic_notation(algebraic_notation: str, FEN: str) -> Tuple[str, str, str]:
|
528
|
+
"""
|
529
|
+
Converts a move from algebraic notation to a tuple representing the starting and ending squares. Use this for
|
530
|
+
single moves.
|
531
|
+
|
532
|
+
Parameters:
|
533
|
+
----------
|
534
|
+
algebraic_notation : str
|
535
|
+
The move in algebraic notation, e.g., 'e2e4', 'Nf3', 'O-O', etc.
|
536
|
+
board : Board
|
537
|
+
The chess board object to interpret the move in context.
|
538
|
+
|
539
|
+
Returns:
|
540
|
+
-------
|
541
|
+
Tuple[str, str]
|
542
|
+
A tuple representing the starting and ending positions of the move in the format (starting_square, ending_square).
|
543
|
+
"""
|
544
|
+
board = FEN.split()[0] # ex: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR
|
545
|
+
|
546
|
+
castling = True if 'O' in algebraic_notation else False
|
547
|
+
if castling: # Castling
|
548
|
+
return __castling_notation(algebraic_notation, FEN)
|
549
|
+
|
550
|
+
else: # Not castling
|
551
|
+
piece_being_moved = algebraic_notation[0] if algebraic_notation[0] in {'K', 'Q', 'R', 'N', 'B'} else 'P'
|
552
|
+
|
553
|
+
match piece_being_moved:
|
554
|
+
case 'K':
|
555
|
+
return king_algebraic_notation(algebraic_notation, FEN)
|
556
|
+
case 'Q':
|
557
|
+
return queen_algebraic_notation(algebraic_notation, FEN)
|
558
|
+
case 'R':
|
559
|
+
return rook_algebraic_notation(algebraic_notation, FEN)
|
560
|
+
case 'N':
|
561
|
+
return knight_algebraic_notation(algebraic_notation, FEN)
|
562
|
+
case 'B':
|
563
|
+
return bishop_algebraic_notation(algebraic_notation, FEN)
|
564
|
+
case 'P':
|
565
|
+
return pawn_algebraic_notation(algebraic_notation, FEN)
|
566
|
+
|
567
|
+
def __apply_move_to_FEN(move: str, fen: str) -> str:
|
568
|
+
# Parse the FEN components
|
569
|
+
board, turn = fen.split()[:2]
|
570
|
+
|
571
|
+
# Initialize the board (8x8 grid)
|
572
|
+
rows = board.split('/')
|
573
|
+
board_matrix = []
|
574
|
+
|
575
|
+
# Convert each row in FEN into a list of characters, replacing numbers with '1'
|
576
|
+
for row in rows:
|
577
|
+
board_row = []
|
578
|
+
for char in row:
|
579
|
+
if char.isdigit(): # If the character is a number, replace it with '1's
|
580
|
+
board_row.extend(['1'] * int(char))
|
581
|
+
else:
|
582
|
+
board_row.append(char)
|
583
|
+
board_matrix.append(board_row)
|
584
|
+
|
585
|
+
# Extract the move components
|
586
|
+
starting_square, ending_square, promotion_piece = move
|
587
|
+
|
588
|
+
# Convert squares from algebraic notation to matrix indices
|
589
|
+
start_row, start_col = 8 - int(starting_square[1]), ord(starting_square[0]) - ord('a')
|
590
|
+
end_row, end_col = 8 - int(ending_square[1]), ord(ending_square[0]) - ord('a')
|
591
|
+
|
592
|
+
# Get the piece being moved
|
593
|
+
piece = board_matrix[start_row][start_col]
|
594
|
+
|
595
|
+
# Check if this move is castling
|
596
|
+
if piece.lower() == 'k' and abs(start_col - end_col) == 2:
|
597
|
+
# Castling logic for the King (moving two squares)
|
598
|
+
# Determine the rook's position
|
599
|
+
if end_col > start_col: # Kingside castling
|
600
|
+
rook_col = 7
|
601
|
+
new_rook_col = 5
|
602
|
+
else: # Queenside castling
|
603
|
+
rook_col = 0
|
604
|
+
new_rook_col = 3
|
605
|
+
|
606
|
+
# Get the rook piece
|
607
|
+
rook_piece = board_matrix[start_row][rook_col]
|
608
|
+
|
609
|
+
# Move the king
|
610
|
+
board_matrix[end_row][end_col] = 'k' if piece.islower() else 'K'
|
611
|
+
board_matrix[start_row][start_col] = '1' # Empty the starting square
|
612
|
+
|
613
|
+
# Move the rook
|
614
|
+
board_matrix[start_row][new_rook_col] = rook_piece
|
615
|
+
board_matrix[start_row][rook_col] = '1' # Empty the rook's original position
|
616
|
+
else:
|
617
|
+
# Regular move (non-castling)
|
618
|
+
# Update the board matrix: move the piece
|
619
|
+
board_matrix[end_row][end_col] = piece
|
620
|
+
board_matrix[start_row][start_col] = '1' # Empty the starting square
|
621
|
+
|
622
|
+
# Handle promotion
|
623
|
+
if promotion_piece:
|
624
|
+
board_matrix[end_row][end_col] = promotion_piece.lower() # Use lowercase for black pieces
|
625
|
+
|
626
|
+
# Convert the board back into FEN format
|
627
|
+
updated_board = []
|
628
|
+
for row in board_matrix:
|
629
|
+
# Join the row and collapse consecutive '1's into numbers
|
630
|
+
row_str = ''.join(row)
|
631
|
+
collapsed_row = re.sub(r'1+', lambda m: str(len(m.group(0))), row_str)
|
632
|
+
updated_board.append(collapsed_row)
|
633
|
+
|
634
|
+
# Join all rows with '/'
|
635
|
+
updated_board_str = '/'.join(updated_board)
|
636
|
+
|
637
|
+
# Rebuild the FEN string
|
638
|
+
updated_fen = f"{updated_board_str} {' w' if turn == 'b' else ' b'}"
|
639
|
+
|
640
|
+
return updated_fen
|
641
|
+
|
642
|
+
def process_move(move: str, FEN: str) -> Tuple[Tuple[str, str, str], str]:
|
643
|
+
"""
|
644
|
+
Processes a single move in algebraic notation, converting it to coordinate notation and updating the FEN string.
|
645
|
+
|
646
|
+
Parameters:
|
647
|
+
----------
|
648
|
+
move : str
|
649
|
+
The move in algebraic notation.
|
650
|
+
FEN : str
|
651
|
+
The current FEN string representing the board state.
|
652
|
+
|
653
|
+
Returns:
|
654
|
+
-------
|
655
|
+
Tuple[Tuple[str, str, str], str]
|
656
|
+
A tuple containing the move in coordinate notation and the updated FEN string.
|
657
|
+
If the move is invalid, returns (None, FEN).
|
658
|
+
"""
|
659
|
+
coordinates = convert_from_algebraic_notation(move, FEN)
|
660
|
+
if coordinates is None:
|
661
|
+
print("Invalid notation/ impossible move")
|
662
|
+
return None, FEN
|
663
|
+
FEN = __apply_move_to_FEN(coordinates, FEN)
|
664
|
+
return coordinates, FEN
|
665
|
+
|
666
|
+
def convert_from_PGN(PGN: list[str], FEN: str = DEFAULT_FEN) -> list[Tuple[str, str]]:
|
667
|
+
"""
|
668
|
+
Converts a list of moves in PGN (Portable Game Notation) format to a list of tuples representing the starting and ending squares.
|
669
|
+
Use this for entire game.
|
670
|
+
|
671
|
+
Parameters:
|
672
|
+
----------
|
673
|
+
PGN : list[str]
|
674
|
+
A list of moves in PGN format, e.g., ['e4', 'Nf3', 'O-O', etc.].
|
675
|
+
board : Board
|
676
|
+
The chess board object to interpret the moves in context.
|
677
|
+
|
678
|
+
Returns:
|
679
|
+
-------
|
680
|
+
list[Tuple[str, str]]
|
681
|
+
A list of tuples, each representing the starting and ending positions of the moves in the format (starting_square, ending_square).
|
682
|
+
"""
|
683
|
+
|
684
|
+
# I will find the start of the game by reversing the PGN string, using find() to find the first instance of ']' and then that is the start.
|
685
|
+
# However this will give the index of the reverse string, I can get the actual index with len(FEN) - index_of_reverse.
|
686
|
+
start_index = len(PGN) - PGN[::-1].find(']')
|
687
|
+
|
688
|
+
movetext = PGN[start_index:].split()
|
689
|
+
|
690
|
+
# this removes the 1. and other nonmoves NOTE all lowercase so do .lower
|
691
|
+
allowed_start_of_moves = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'o', 'k', 'n', 'q', 'r'}
|
692
|
+
|
693
|
+
filtered_movetext = []
|
694
|
+
in_alternate_line = False
|
695
|
+
for string in movetext:
|
696
|
+
if string[0] == '(':
|
697
|
+
in_alternate_line = True
|
698
|
+
elif string[-1] == ')': # removes the end of the ( ) for other lines looked at in pgn
|
699
|
+
in_alternate_line = False
|
700
|
+
elif string[0].lower() in allowed_start_of_moves and not in_alternate_line:
|
701
|
+
filtered_movetext.append(string)
|
702
|
+
|
703
|
+
game_in_coordinate_notation = []
|
704
|
+
print(filtered_movetext)
|
705
|
+
for move in filtered_movetext:
|
706
|
+
coordinates, FEN = process_move(move, FEN)
|
707
|
+
if coordinates == None:
|
708
|
+
break
|
709
|
+
game_in_coordinate_notation.append(coordinates)
|
710
|
+
|
711
|
+
return game_in_coordinate_notation
|