manim-chess 0.0.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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