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.
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