Board
File containing implementation for Board and Cells on the board.
1"""File containing implementation for Board and Cells on the board. 2""" 3 4import customtkinter as ctk 5from typing import Any 6from properties import COLOR, SYSTEM 7if SYSTEM == 'Windows': 8 import pywinstyles 9from PIL import Image 10import os 11import threading 12import soundfile 13from typing import cast, Generator, NoReturn 14import re 15 16from notifications import Notification 17from menus import MovesRecord 18from tools import get_from_config, resource_path, play_sound, update_error_log 19import piece 20 21class Cell(ctk.CTkLabel): 22 """Class handling actions inside specific cell and linking figure to the position on the board. 23 24 Args: 25 ctk.CTkLabel : Inheritance from customtkinter CTkLabel widget. 26 """ 27 def __init__(self, frame: ctk.CTkFrame, figure: piece.Piece | None, position: tuple[int, int], color: str, board) -> None: 28 """Constructor: 29 - binds left button to on_click function. 30 - displays itself on the screen. 31 32 Args: 33 frame (ctk.CTkFrame): Parent Frame on which cell will be represented. 34 figure (piece.Piece | None): Figure on a cell. 35 position (tuple[int, int]): Position on a board. 36 color (str): Color of the cell white or black. 37 board (Board): Parent class handling cell placement. 38 """ 39 self.frame: ctk.CTkFrame = frame 40 self.position: tuple[int, int] = position 41 self.board: Board = board 42 self.figure: None | piece.Piece = figure 43 self.frame_around: ctk.CTkLabel | None = None 44 figure_asset: ctk.CTkImage | None = self.figure.image if self.figure else None 45 self.cell_size = get_from_config('size') 46 super().__init__( 47 master = frame, 48 image = figure_asset, 49 text = '', 50 fg_color = color, 51 width = self.cell_size, 52 height = self.cell_size, 53 bg_color = COLOR.BACKGROUND 54 ) 55 self.bind('<Button-1>', self.on_click) 56 self.pack(side=ctk.LEFT, padx=2, pady=2) 57 58 def on_click(self, event: Any) -> None: 59 """Handles clicks by calling board functions handling game logic. If user clicks wrong cell the illegal move sound effect 60 will play. To avoid calling functions more than necessary it checks if the previously clicked figure isn't the same as 61 currently clicked one and if the color of current player turn is the same as figure which is being clicked. 62 63 Args: 64 event (Any): Event type. Doesn't matter but is required parameter by customtkinter. 65 """ 66 if self.board.clicked_figure and self.board.clicked_figure.color: 67 if self.board.board[self.position[0]][self.position[1]] not in self.board.highlighted and self.board.clicked_figure != self.figure: 68 if self.figure: 69 if self.figure.color != self.board.clicked_figure.color: 70 threading.Thread(target=play_sound, args=(self.board.illegal_sound,)).start() 71 else: 72 threading.Thread(target=play_sound, args=(self.board.illegal_sound,)).start() 73 self.board.handle_move(self.position) 74 if self.figure and not self.board.clicked_figure and self.board.current_turn == self.figure.color: 75 self.board.handle_clicks(self.figure, self.position) 76 else: 77 self.board.handle_move(self.position) 78 79 def update(self) -> None: 80 """Updates the asset shown on a cell. 81 """ 82 figure_asset = self.figure.image if self.figure else b'' 83 self.configure(image=figure_asset, require_redraw=True) 84 85class Board(ctk.CTkFrame): 86 """Class handling all cells and move related logic. 87 88 Args: 89 ctk.CTkFrame : Inheritance from customtkinter CTkLabel widget. 90 """ 91 def __init__(self, master, moves_record: MovesRecord, size: int) -> None: 92 """Constructor: 93 - setups all important variables 94 - loads all sound files 95 - loads font with different sizes 96 - creates board with default figure placement 97 - calls loading_animation for better user experience 98 99 Args: 100 master (Any): Parent widget. 101 moves_record (MovesRecord): class handling move records. 102 size (int): Size of the tiles and fonts. 103 """ 104 super().__init__(master, fg_color=COLOR.DARK_TEXT, corner_radius=0) 105 self.master: Any = master 106 self.loading_screen: ctk.CTkLabel | None = None 107 self.font_name: str = str(get_from_config('font_name')) 108 self.font_42 = ctk.CTkFont(self.font_name, 42) 109 self.master.after(1, lambda: self.loading_animation(0)) 110 self.pack(side=ctk.RIGHT, padx=10, pady=10, expand=True, ipadx=5, ipady=5, anchor=ctk.CENTER) 111 thread = threading.Thread(target=self.load_sound) 112 self.frame_image: ctk.CTkImage = ctk.CTkImage(Image.open(resource_path(os.path.join('assets', 'menu', 'frame.png'))).convert('RGBA'), size=(80, 80)) 113 thread.start() 114 self.size: int = size 115 self.turns: Generator[str, None, NoReturn] = self.turn() 116 self.current_turn = next(self.turns) 117 self.board_font = ctk.CTkFont(self.font_name, self.size // 3) 118 self.board: list[list[Cell]] = self.create_board() 119 self.highlighted: list[Cell] = [] 120 self.clicked_figure: piece.Piece | None = None 121 self.previous_coords: tuple[int, int] | None = None 122 self.notification: None | Notification = None 123 self.moves_record: MovesRecord = moves_record 124 self.capture: bool = False 125 self.game_over: bool = False 126 self.current_save_name: str | None = None 127 thread.join() 128 self.destroy_loading_screen() 129 130 def load_sound(self): 131 """Function loading sound on thread to speed up the process if possible. Constructor will have to wait with destroying loading screen if process could take too long. 132 """ 133 self.move_sound = soundfile.read(resource_path(os.path.join('sounds', 'move-self.wav')), dtype='float32')[0] 134 self.capture_sound = soundfile.read(resource_path(os.path.join('sounds', 'capture.wav')), dtype='float32')[0] 135 self.move_check_sound = soundfile.read(resource_path(os.path.join('sounds', 'capture.wav')), dtype='float32')[0] 136 self.castle_sound = soundfile.read(resource_path(os.path.join('sounds', 'capture.wav')), dtype='float32')[0] 137 self.end_game_sound = soundfile.read(resource_path(os.path.join('sounds', 'game-end.wav')), dtype='float32')[0] 138 self.illegal_sound = soundfile.read(resource_path(os.path.join('sounds', 'illegal.wav')), dtype='float32')[0] 139 140 @staticmethod 141 def determine_tile_color(pos: tuple[int, int]) -> str: 142 """Static method to determine color of the tile on the board. 143 144 Args: 145 pos (tuple[int, int]): Position of the cell on the board. 146 147 Returns: 148 str: Color of the cell. 149 """ 150 return COLOR.TILE_1 if (pos[0] % 2) == (pos[1] % 2) else COLOR.TILE_2 151 152 def create_outline_l_r_t(self) -> None: 153 """Creates outline of the board with coordinates. 154 """ 155 ctk.CTkLabel( 156 master = self, 157 text =f' ', 158 font =self.board_font, 159 text_color =COLOR.DARK_TEXT 160 ).pack(padx=10, pady=1) 161 new_frame = ctk.CTkFrame( 162 master = self, 163 fg_color = COLOR.DARK_TEXT, 164 corner_radius = 0 165 ) 166 new_frame.pack(side=ctk.LEFT, padx=3, pady=0, fill=ctk.Y) 167 for i in range(8, 0, -1): 168 ctk.CTkLabel( 169 master = new_frame, 170 text = f' {i}', 171 font = self.board_font, 172 fg_color = COLOR.DARK_TEXT, 173 anchor = ctk.W 174 ).pack(side=ctk.TOP, padx=10, pady=1, expand=True) 175 ctk.CTkLabel( 176 master = new_frame, 177 text = ' ', 178 font = ctk.CTkFont(self.font_name, int(int(get_from_config('size')) * 0.4)) 179 ).pack(side=ctk.BOTTOM, padx=0, pady=0) 180 new_frame = ctk.CTkFrame( 181 master = self, 182 fg_color = COLOR.DARK_TEXT, 183 corner_radius=0 184 ) 185 new_frame.pack(side=ctk.RIGHT, padx=1, pady=0, fill=ctk.Y) 186 ctk.CTkLabel( 187 master = new_frame, 188 text = '', 189 font = self.board_font, 190 text_color = COLOR.DARK_TEXT, 191 fg_color = COLOR.DARK_TEXT, 192 width = int(int(get_from_config('size')) * 0.4) 193 ).pack(padx=10, pady=1) 194 195 def create_board(self) -> list[list[Cell]]: 196 """Creates a board filled with colored tiles and figures. Uses prepared dictionary with correct figures positions to place the Figures. 197 198 Returns: 199 list[list[Cell]]: 2D representation of the board with Cell linking figures to correct positions. 200 """ 201 self.create_outline_l_r_t() 202 board: list[list[Cell]] = cast(list[list[Cell]], [[None] * 8] * 8) 203 board_frame = ctk.CTkFrame( 204 master = self, 205 corner_radius = 0, 206 fg_color = COLOR.DARK_TEXT 207 ) 208 board_frame.pack(side=ctk.TOP, padx=0, pady=0) 209 piece_positions = { 210 (0, 0): piece.Rook('b', self, (0, 0)), # Black rook 211 (0, 7): piece.Rook('b', self, (0, 7)), # Black rook 212 (7, 0): piece.Rook('w', self, (7, 0)), # White rook 213 (7, 7): piece.Rook('w', self, (7, 7)), # White rook 214 (0, 1): piece.Knight('b', self, (0, 1)), # Black knight 215 (0, 6): piece.Knight('b', self, (0, 6)), # Black knight 216 (7, 1): piece.Knight('w', self, (7, 1)), # White knight 217 (7, 6): piece.Knight('w', self, (7, 6)), # White knight 218 (0, 2): piece.Bishop('b', self, (0, 2)), # Black bishop 219 (0, 5): piece.Bishop('b', self, (0, 5)), # Black bishop 220 (7, 2): piece.Bishop('w', self, (7, 2)), # White bishop 221 (7, 5): piece.Bishop('w', self, (7, 5)), # White bishop 222 (0, 3): piece.Queen('b', self, (0, 3)), # Black Queen 223 (7, 3): piece.Queen('w', self, (7, 3)), # White Queen 224 (0, 4): piece.King('b', self, (0, 4)), # Black King 225 (7, 4): piece.King('w', self, (7, 4)) # White King 226 } 227 for i in range(8): 228 row: list[Cell] = cast(list[Cell], [None] * 8) 229 new_frame: ctk.CTkFrame = ctk.CTkFrame( 230 master = board_frame, 231 fg_color = COLOR.DARK_TEXT, 232 corner_radius = 0 233 ) 234 new_frame.pack(padx=0, pady=0) 235 for j in range(8): 236 color: str = self.determine_tile_color((i, j)) 237 figure: piece.Piece | None = piece_positions.get((i, j)) if (i, j) in piece_positions else ( 238 piece.Pawn('b' if i == 1 else 'w', self, (i, j), self.notation_promotion) if i in [1, 6] else None 239 ) 240 cell = Cell(new_frame, figure, (i, j), color, self) 241 row[j] = cell 242 board[i] = row 243 new_frame = ctk.CTkFrame( 244 master = self, 245 fg_color = COLOR.DARK_TEXT, 246 corner_radius = 0 247 ) 248 new_frame.pack(padx=0, pady=1, fill=ctk.X) 249 for letter in ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'): 250 ctk.CTkLabel( 251 master = new_frame, 252 text = letter, 253 font = self.board_font, 254 fg_color = COLOR.DARK_TEXT 255 ).pack(side=ctk.LEFT, padx=2, pady=0, expand=True) 256 return board 257 258 def remove_highlights(self) -> None: 259 """Removes highlights from the cells. Ensures proper move handling and enhances user experience. 260 If user doesn't want the help they can change the color of the highlighted tile to the same as not highlighted in customization menu. 261 """ 262 for cell in self.highlighted: 263 cell.configure(fg_color=self.determine_tile_color(cell.position)) 264 self.highlighted.clear() 265 266 def display_message(self, message: str, duration_sec: int) -> None: 267 """Displays message on the screen using Notification module. 268 269 Args: 270 message (str): Desired message to display. 271 duration_sec (int): Amount of seconds before hiding the notification. 272 """ 273 if self.notification: 274 self.notification.destroy() 275 self.notification = Notification(self, message=message, duration_sec=duration_sec) 276 277 def is_game_over(self) -> tuple[bool, bool]: 278 """Checks if checkmate or stalemate occurred. Is also used to check if the king is in check in handle_move method. 279 Function scans all combinations of moving the King, if found at leas one, loop breaks to check if king is in check. 280 281 Returns: 282 tuple[bool, bool]: 1st tuple element is game_over and 2nd is in check both True or False. 283 """ 284 in_check = False 285 has_legal_moves = False 286 for row in self.board: 287 for cell in row: 288 if cell.figure and cell.figure.color == self.current_turn: 289 possible_moves = cell.figure.check_possible_moves(self.current_turn) 290 for move in possible_moves: 291 if not self.check_check(cell.figure.position, move): 292 has_legal_moves = True 293 break 294 if has_legal_moves: 295 break 296 if has_legal_moves: 297 break 298 king_position = self.get_king_position(self.current_turn) 299 in_check = self.is_under_attack(king_position, self.current_turn) 300 return (not has_legal_moves, in_check) 301 302 def handle_clicks(self, figure: piece.Piece, position: tuple[int, int]) -> None: 303 """Handles actions after clicking on a specific cell. Function filter from all possible moves for the figure to only these which are legal. 304 After filtering the moves to only legal ones, proper tiles are being highlighted. 305 306 Args: 307 figure (piece.Piece): Chosen figure. 308 position (tuple[int, int]): Position of that figure. 309 """ 310 if self.game_over: 311 return 312 self.handle_chosen_figure_highlight(position) 313 possible_moves = figure.check_possible_moves(self.current_turn) 314 if not possible_moves and self.board[position[0]][position[1]].figure: 315 self.previous_coords = position 316 return 317 self.clicked_figure = figure if figure else None 318 self.previous_coords = position 319 if self.board and possible_moves: 320 valid_moves = [] 321 for coords in possible_moves: 322 if not self.check_check(position, coords): 323 valid_moves.append(coords) 324 for coords in valid_moves: 325 x_ = coords[0] 326 y_ = coords[1] 327 color = self.board[x_][y_].cget('fg_color') 328 new_color = COLOR.HIGH_TILE_1 if color == COLOR.TILE_1 else COLOR.HIGH_TILE_2 329 self.board[x_][y_].configure(fg_color=new_color) 330 self.highlighted.append(self.board[x_][y_]) 331 332 def hide_clicked_figure_frame(self) -> None: 333 """Hides the frame or highlight around the chosen figure. 334 """ 335 if self.previous_coords: 336 previous_x = self.previous_coords[0] 337 previous_y = self.previous_coords[1] 338 if SYSTEM == 'Windows': 339 if frame := self.board[previous_x][previous_y].frame_around: 340 frame.destroy() 341 else: 342 self.board[previous_x][previous_y].configure(fg_color=self.determine_tile_color(self.previous_coords)) 343 344 def handle_chosen_figure_highlight(self, position: tuple[int, int]) -> None: 345 """Highlights with frame or color under the tile of the figure for better visibility and improved user experience. 346 347 Args: 348 position (tuple[int, int]): _description_ 349 """ 350 self.hide_clicked_figure_frame() 351 x: int = position[0] 352 y: int = position[1] 353 if SYSTEM == 'Windows': 354 image_test: ctk.CTkLabel = ctk.CTkLabel( 355 fg_color = COLOR.TRANSPARENT_MASK, 356 master = self.board[x][y], 357 text = '', 358 image = self.frame_image 359 ) 360 pywinstyles.set_opacity(image_test, value=1, color=COLOR.TRANSPARENT_MASK) 361 image_test.place(relx=0.5, rely=0.5, anchor='center') 362 self.board[x][y].frame_around = image_test 363 else: 364 self.board[x][y].configure(fg_color=COLOR.TEXT) 365 366 def check_check(self, move_from: tuple[int, int], move_to: tuple[int, int]) -> bool: 367 """Checks if King is in a check. Checks if any of the opponents figure are causing the check. If any found loop breaks to improve performance. 368 369 Args: 370 move_from (tuple[int, int]): Starting position. 371 move_to (tuple[int, int]): Desired position. 372 373 Returns: 374 bool: _description_ 375 """ 376 move_to_x: int = move_to[0] 377 move_to_y: int = move_to[1] 378 move_from_x: int = move_from[0] 379 move_from_y: int = move_from[1] 380 original_from_figure: piece.Piece | None = self.board[move_from_x][move_from_y].figure 381 original_to_figure: piece.Piece | None = self.board[move_to_x][move_to_y].figure 382 self.board[move_to_x][move_to_y].figure = original_from_figure 383 self.board[move_from_x][move_from_y].figure = None 384 king_position = None 385 if isinstance(original_from_figure, piece.King): 386 king_position = move_to 387 else: 388 for row in self.board: 389 for cell in row: 390 if isinstance(cell.figure, piece.King) and cell.figure.color == self.current_turn: 391 king_position = cell.figure.position 392 break 393 if king_position: 394 break 395 is_in_check = False 396 for row in self.board: 397 for cell in row: 398 if cell.figure and cell.figure.color != self.current_turn: 399 possible_moves = cell.figure.check_possible_moves(cell.figure.color) 400 if king_position in possible_moves: 401 is_in_check = True 402 break 403 if is_in_check: 404 break 405 self.board[move_from_x][move_from_y].figure = original_from_figure 406 self.board[move_to_x][move_to_y].figure = original_to_figure 407 return is_in_check 408 409 def is_under_attack(self, position: tuple[int, int], color: str) -> bool: 410 """Checks if king is under attack. 411 412 Args: 413 position (tuple[int, int]): Position of the king. 414 color (str): Color of the king. 415 416 Returns: 417 bool: Returns True if is under attack, False otherwise. 418 """ 419 for row in self.board: 420 for cell in row: 421 if cell.figure and cell.figure.color != color: 422 if position in cell.figure.check_possible_moves(cell.figure.color, checking=True): 423 return True 424 return False 425 426 def handle_move(self, position: tuple[int, int]) -> None: 427 """Function handles moving pieces on the board. Calls notations functions, plays sounds appropriately to the move and removes previous highlights. 428 Function handles all possible moves including pawn promotion, castle, en_passant and moving pawn by two squares at first move. Function has to check if move is 429 for sure legal and is not causing check for the current player taking turn. Ensures proper notation in every case. 430 431 Args: 432 position (tuple[int, int]): Position of the figure. 433 """ 434 if self.clicked_figure and self.clicked_figure.position == position: 435 return 436 if self.previous_coords: 437 prev_x: int = self.previous_coords[0] 438 prev_y: int = self.previous_coords[1] 439 if SYSTEM == 'Windows': 440 if x := self.board[prev_x][prev_y].frame_around: 441 x.destroy() 442 else: 443 self.board[prev_x][prev_y].configure(fg_color=self.determine_tile_color(self.previous_coords)) 444 if self.clicked_figure and self.previous_coords: 445 row, col = position 446 cell = self.board[row][col] 447 self.capture = bool(cell.figure) 448 promotion: bool = False 449 if cell in self.highlighted: 450 castle: bool = False 451 if not self.check_check(self.previous_coords, position): 452 if isinstance(self.clicked_figure, piece.Pawn) and self.clicked_figure.can_en_passant and col != prev_y and not cell.figure: 453 self.board[row - self.clicked_figure.move][col].figure = None 454 self.board[row - self.clicked_figure.move][col].update() 455 self.capture = True 456 if isinstance(self.clicked_figure, piece.King): 457 castle = self.handle_move_castle(row, col, prev_y) 458 cell.figure = self.clicked_figure 459 cell.figure.position = position 460 cell.update() 461 self.board[prev_x][prev_y].figure = None 462 self.board[prev_x][prev_y].update() 463 promotion = self.handle_move_pawn(cell, row) 464 if cell.figure.first_move: 465 cell.figure.first_move = False 466 self.current_turn = next(self.turns) 467 game_over, in_check = self.is_game_over() 468 if game_over: 469 self.handle_game_over(in_check, promotion, self.capture, in_check) 470 elif not castle and not promotion: 471 self.moves_record.record_move( 472 moved_piece = self.clicked_figure, 473 capture = self.capture, 474 previous_coords = self.previous_coords, 475 check = in_check, 476 checkmate = game_over and in_check 477 ) 478 threading.Thread(target=lambda: self.play_correct_sound(game_over, self.capture, castle, in_check)).start() 479 if not promotion: 480 self.clicked_figure = None 481 self.previous_coords = None 482 if self.highlighted: 483 self.remove_highlights() 484 485 def handle_move_pawn(self, cell: Cell, row: int) -> bool: 486 """Helper function handling pawn special case - promotion. Ensures all flags are properly reset after each move. 487 488 Args: 489 cell (Cell): Cell object linking figure to position. 490 row (int): Row in which pawn is being moved. 491 492 Returns: 493 bool: True if pawn promoted, False otherwise. 494 """ 495 if not cell.figure or not self.previous_coords: 496 return False 497 promotion = False 498 if isinstance(cell.figure, piece.Pawn): 499 if cell.figure.first_move and abs(self.previous_coords[0] - row) == 2: 500 cell.figure.moved_by_two = True 501 else: 502 cell.figure.moved_by_two = False 503 if cell.figure.promote(): 504 promotion = True 505 self.reset_en_passant_flags(cell.figure.color) 506 return promotion 507 508 def handle_move_castle(self, row: int, col: int, prev_y: int) -> bool: 509 """Helper function handling castle special move. Ensures notation is proper and all figures moves properly. 510 511 Args: 512 row (int): Row of the king in which castle will be performed. 513 col (int): Column of the king in which king figure is. 514 prev_y (int): Previous y coordinate of the king. 515 516 Returns: 517 bool: Returns True if castle occurred, False otherwise. 518 """ 519 if not self.clicked_figure: 520 return False 521 castle = False 522 if abs(col - prev_y) == 2: 523 if col == 6: 524 self.board[row][5].figure = self.board[row][7].figure 525 self.board[row][7].figure = None 526 self.board[row][5].figure.position = (row, 5) # type: ignore # isinstance already checks it but mypy don't understand it 527 self.board[row][5].update() 528 self.board[row][7].update() 529 self.moves_record.record_move(self.clicked_figure, castle="kingside") 530 castle = True 531 elif col == 2: 532 self.board[row][3].figure = self.board[row][0].figure 533 self.board[row][0].figure = None 534 self.board[row][3].figure.position = (row, 3) # type: ignore # isinstance already checks it but mypy don't understand it 535 self.board[row][3].update() 536 self.board[row][0].update() 537 self.moves_record.record_move(self.clicked_figure, castle="queenside") 538 castle =True 539 return castle 540 541 def turn(self) -> Generator[str, None, NoReturn]: 542 """Simple infinite yielding function for easy turn changing. 543 544 Yields: 545 Generator[str, None, NoReturn]: Current turn color representation. 546 """ 547 while True: 548 yield 'w' 549 yield 'b' 550 551 def play_correct_sound(self, game_over: bool, capture: bool, castle: bool, check: bool) -> None: 552 """Plays sound according to the users move. 553 554 Args: 555 game_over (bool): Flag corresponding to game over. 556 capture (bool): Flag corresponding to capturing other piece. 557 castle (bool): Flag corresponding to castle move. 558 check (bool): Flag corresponding to check. 559 """ 560 if game_over: 561 play_sound(self.end_game_sound) 562 elif capture: 563 play_sound(self.capture_sound) 564 elif castle: 565 play_sound(self.castle_sound) 566 elif check: 567 play_sound(self.move_check_sound) 568 else: 569 play_sound(self.move_sound) 570 571 def handle_game_over(self, in_check: bool, promotion: bool, capture: bool, check: bool) -> None: 572 """Handles displaying notification of who won or if it was a stalemate. Sets game_over flag to True, and notates the end of the game. 573 574 Args: 575 in_check (bool): Flag corresponding to check. 576 promotion (bool): Flag corresponding to pawn promotion. 577 capture (bool): Flag corresponding to capturing other piece. 578 check (bool): Flag corresponding to check 579 """ 580 self.game_over = True 581 if in_check: 582 self.display_message(f'Checkmate {"White wins!" if self.current_turn == "b" else "Black wins!"}', 9) 583 if not promotion and self.clicked_figure: 584 self.moves_record.record_move(self.clicked_figure, capture=capture, previous_coords=self.previous_coords, check=True, checkmate=check and in_check) 585 else: 586 self.display_message('Stalemate', 9) 587 if not promotion and self.clicked_figure: 588 self.moves_record.record_move(self.clicked_figure, capture=capture, previous_coords=self.previous_coords, check=False, checkmate=check and in_check) 589 590 def notation_promotion(self, promotion:str) -> None: 591 """Helper function to note the promotion of the pawn. Calls pawn functions corresponding to choosing figure to promote to. Notates the promotion 592 593 Args: 594 promotion (str): Figure representation to which pawn was promoted. 595 """ 596 check = self.is_under_attack(self.get_king_position(self.current_turn), self.current_turn) 597 game_over, in_check = self.is_game_over() 598 if game_over: 599 if in_check: 600 self.display_message(f'Checkmate {"White wins!" if self.current_turn == "b" else "Black wins!"}', 9) 601 if self.clicked_figure: 602 self.moves_record.record_move(self.clicked_figure, capture=self.capture, previous_coords=self.previous_coords, check=check, checkmate=game_over and in_check, promotion=promotion[0]) 603 else: 604 self.display_message('Stalemate', 9) 605 if self.clicked_figure: 606 self.moves_record.record_move(self.clicked_figure, capture=self.capture, previous_coords=self.previous_coords, check=check, checkmate=game_over and in_check, promotion=promotion[0]) 607 else: 608 if self.clicked_figure: 609 self.moves_record.record_move(self.clicked_figure, capture=self.capture, previous_coords=self.previous_coords, check=check, checkmate=game_over and in_check, promotion=promotion[0]) 610 self.clicked_figure = None 611 self.previous_coords = None 612 613 def get_king_position(self, color: str) -> tuple[int, int]: 614 """Function returning king position on the board. Loop searching for king just breaks after finding king with correct color. 615 616 Args: 617 color (str): Color of the king. 618 619 Returns: 620 tuple[int, int]: Position of the king. 621 """ 622 for row in self.board: 623 for cell in row: 624 if isinstance(cell.figure, piece.King) and cell.figure.color == color: 625 return cell.figure.position 626 update_error_log(Exception('Not enough kings on the board, check the save file')) 627 Notification(self.master, 'No king on the board, check save file', 2, 'top') 628 self.game_over = True 629 self.master.after(2001, self.restart_game) 630 raise Exception('Not enough kings on the board, check the save file') 631 632 def reset_en_passant_flags(self, current_color: str) -> None: 633 """Helper function to reset en passant and first move flag. 634 635 Args: 636 current_color (str): Color of the current player. 637 """ 638 for row in self.board: 639 for cell in row: 640 if isinstance(cell.figure, piece.Pawn) and cell.figure.color != current_color: 641 cell.figure.moved_by_two = False 642 cell.figure.can_en_passant = False 643 644 def restart_game(self) -> None: 645 """Function restarting the game with all necessary flags and variables. 646 """ 647 self.loading_animation(0) 648 for child in self.winfo_children(): 649 self.master.after(1, child.destroy) 650 self.highlighted.clear() 651 self.clicked_figure = None 652 self.previous_coords = None 653 self.turns = self.turn() 654 self.current_turn = next(self.turns) 655 self.notification = None 656 self.game_over = False 657 self.board = self.create_board() 658 self.current_save_name = None 659 660 def destroy_loading_screen(self) -> None: 661 """Destroys loading screen widget. 662 """ 663 def update_opacity(i: int) -> None: 664 if i >= 0 and self.loading_screen: 665 pywinstyles.set_opacity(self.loading_screen, value=i*0.005, color='#000001') 666 self.master.after(1, lambda: update_opacity(i - 1)) 667 else: 668 if self.loading_screen: 669 self.loading_screen.destroy() 670 self.loading_screen = None 671 if self.loading_screen: 672 update_opacity(200) 673 674 def loading_animation(self, i: int) -> None: 675 """Function to animate loading screen. 676 677 Args: 678 i (int, optional): Iteration value passed by recursive formula. Defaults to 0. 679 """ 680 if not self.loading_screen: 681 self.loading_screen = ctk.CTkFrame( 682 master = self.master, 683 fg_color = COLOR.BACKGROUND 684 ) 685 self.loading_screen.place(relx=0, rely=0, relwidth=1, relheight=1) 686 self.loading_text = ctk.CTkLabel( 687 master = self.loading_screen, 688 text = 'Loading ', 689 font = self.font_42, 690 text_color = COLOR.TEXT, 691 ) 692 self.loading_text.pack(side=ctk.TOP, expand=True) 693 self.master.after(90, lambda: self.loading_animation(0)) 694 else: 695 self.loading_text.configure(text=f'Loading{'.' * i}{' ' * (3 - i)}') 696 if i <= 2: 697 i += 1 698 self.master.after(90, lambda: self.loading_animation(i)) 699 else: 700 self.master.after(90, self.destroy_loading_screen) 701 702 def update_board(self) -> None: 703 """Updates all cells on the board 704 """ 705 for row in self.board: 706 for cell in row: 707 cell.update() 708 709 def load_board_from_file(self, file_info: dict, save_name: str) -> bool: 710 """Updates board to match the state from the save file. Ensures all save information are in the file in correct format. 711 712 Args: 713 file_info (dict): All needed information to load save. 714 715 Returns: 716 bool: Returns True if load was successful, False otherwise. 717 """ 718 self.master.after(1, lambda: self.loading_animation(0)) 719 save_keys: set[str] = {'current_turn', 'board_state', 'white_moves', 'black_moves', 'game_over'} 720 if not all(key in file_info for key in save_keys): 721 update_error_log(KeyError('Save file doesn\'t contain all necessary information')) 722 return False 723 king_w: int = 0 724 king_b: int = 0 725 for row in self.board: 726 for cell in row: 727 if cell.figure: 728 cell.figure = None 729 cell.update() 730 for key, value in file_info['board_state'].items(): 731 if not bool(re.match(r'[0-9]{1},[0-9]{1}', key)): 732 self.restart_game() 733 update_error_log(KeyError('Save file doesn\'t contain all necessary information')) 734 return False 735 try: 736 value[1] 737 value[2] 738 except: 739 self.restart_game() 740 update_error_log(KeyError('Save file doesn\'t contain all necessary information')) 741 return False 742 if not bool(re.match(r'[wb]{1}', value[1])): 743 self.restart_game() 744 update_error_log(KeyError('Save file doesn\'t contain all necessary information')) 745 return False 746 coord: tuple[int, ...] = tuple(map(int, key.split(','))) 747 x: int = coord[0] 748 y: int = coord[1] 749 match value[0]: 750 case 'Pawn': 751 pawn = piece.Pawn(value[1], self, (x, y), self.notation_promotion) 752 if not value[2]: 753 pawn.first_move = False 754 self.board[x][y].figure = pawn 755 case 'Knight': 756 self.board[x][y].figure = piece.Knight(value[1], self, (x, y)) 757 case 'Bishop': 758 self.board[x][y].figure = piece.Bishop(value[1], self, (x, y)) 759 case 'Rook': 760 rook = piece.Rook(value[1], self, (x, y)) 761 self.board[x][y].figure = rook 762 if not value[2]: 763 rook.first_move = False 764 case 'Queen': 765 self.board[x][y].figure = piece.Queen(value[1], self, (x, y)) 766 case 'King': 767 king = piece.King(value[1], self, (x, y)) 768 self.board[x][y].figure = king 769 if not value[2]: 770 king.first_move = False 771 king_w += 1 if value[1] == 'w' else 0 772 king_b += 1 if value[1] == 'b' else 0 773 self.master.after(1, self.board[x][y].update) 774 if king_w != 1 or king_b != 1: 775 update_error_log(KeyError('Save file doesn\'t contain proper amount of kings')) 776 return False 777 current_turn = str(file_info['current_turn']) 778 self.turns = self.turn() 779 if current_turn == 'b': 780 self.current_turn = next(self.turns) 781 self.current_turn = next(self.turns) 782 else: 783 self.current_turn = next(self.turns) 784 self.master.after(21, lambda: self.moves_record.load_notation_from_save(file_info['white_moves'], file_info['black_moves'])) 785 self.master.after(21, self.hide_clicked_figure_frame) 786 self.master.after(21, self.remove_highlights) 787 self.game_over = file_info['game_over'] 788 self.clicked_figure = None 789 self.current_save_name = save_name 790 return True
22class Cell(ctk.CTkLabel): 23 """Class handling actions inside specific cell and linking figure to the position on the board. 24 25 Args: 26 ctk.CTkLabel : Inheritance from customtkinter CTkLabel widget. 27 """ 28 def __init__(self, frame: ctk.CTkFrame, figure: piece.Piece | None, position: tuple[int, int], color: str, board) -> None: 29 """Constructor: 30 - binds left button to on_click function. 31 - displays itself on the screen. 32 33 Args: 34 frame (ctk.CTkFrame): Parent Frame on which cell will be represented. 35 figure (piece.Piece | None): Figure on a cell. 36 position (tuple[int, int]): Position on a board. 37 color (str): Color of the cell white or black. 38 board (Board): Parent class handling cell placement. 39 """ 40 self.frame: ctk.CTkFrame = frame 41 self.position: tuple[int, int] = position 42 self.board: Board = board 43 self.figure: None | piece.Piece = figure 44 self.frame_around: ctk.CTkLabel | None = None 45 figure_asset: ctk.CTkImage | None = self.figure.image if self.figure else None 46 self.cell_size = get_from_config('size') 47 super().__init__( 48 master = frame, 49 image = figure_asset, 50 text = '', 51 fg_color = color, 52 width = self.cell_size, 53 height = self.cell_size, 54 bg_color = COLOR.BACKGROUND 55 ) 56 self.bind('<Button-1>', self.on_click) 57 self.pack(side=ctk.LEFT, padx=2, pady=2) 58 59 def on_click(self, event: Any) -> None: 60 """Handles clicks by calling board functions handling game logic. If user clicks wrong cell the illegal move sound effect 61 will play. To avoid calling functions more than necessary it checks if the previously clicked figure isn't the same as 62 currently clicked one and if the color of current player turn is the same as figure which is being clicked. 63 64 Args: 65 event (Any): Event type. Doesn't matter but is required parameter by customtkinter. 66 """ 67 if self.board.clicked_figure and self.board.clicked_figure.color: 68 if self.board.board[self.position[0]][self.position[1]] not in self.board.highlighted and self.board.clicked_figure != self.figure: 69 if self.figure: 70 if self.figure.color != self.board.clicked_figure.color: 71 threading.Thread(target=play_sound, args=(self.board.illegal_sound,)).start() 72 else: 73 threading.Thread(target=play_sound, args=(self.board.illegal_sound,)).start() 74 self.board.handle_move(self.position) 75 if self.figure and not self.board.clicked_figure and self.board.current_turn == self.figure.color: 76 self.board.handle_clicks(self.figure, self.position) 77 else: 78 self.board.handle_move(self.position) 79 80 def update(self) -> None: 81 """Updates the asset shown on a cell. 82 """ 83 figure_asset = self.figure.image if self.figure else b'' 84 self.configure(image=figure_asset, require_redraw=True)
Class handling actions inside specific cell and linking figure to the position on the board.
Arguments:
- ctk.CTkLabel : Inheritance from customtkinter CTkLabel widget.
28 def __init__(self, frame: ctk.CTkFrame, figure: piece.Piece | None, position: tuple[int, int], color: str, board) -> None: 29 """Constructor: 30 - binds left button to on_click function. 31 - displays itself on the screen. 32 33 Args: 34 frame (ctk.CTkFrame): Parent Frame on which cell will be represented. 35 figure (piece.Piece | None): Figure on a cell. 36 position (tuple[int, int]): Position on a board. 37 color (str): Color of the cell white or black. 38 board (Board): Parent class handling cell placement. 39 """ 40 self.frame: ctk.CTkFrame = frame 41 self.position: tuple[int, int] = position 42 self.board: Board = board 43 self.figure: None | piece.Piece = figure 44 self.frame_around: ctk.CTkLabel | None = None 45 figure_asset: ctk.CTkImage | None = self.figure.image if self.figure else None 46 self.cell_size = get_from_config('size') 47 super().__init__( 48 master = frame, 49 image = figure_asset, 50 text = '', 51 fg_color = color, 52 width = self.cell_size, 53 height = self.cell_size, 54 bg_color = COLOR.BACKGROUND 55 ) 56 self.bind('<Button-1>', self.on_click) 57 self.pack(side=ctk.LEFT, padx=2, pady=2)
Constructor:
- binds left button to on_click function.
- displays itself on the screen.
Arguments:
- frame (ctk.CTkFrame): Parent Frame on which cell will be represented.
- figure (piece.Piece | None): Figure on a cell.
- position (tuple[int, int]): Position on a board.
- color (str): Color of the cell white or black.
- board (Board): Parent class handling cell placement.
59 def on_click(self, event: Any) -> None: 60 """Handles clicks by calling board functions handling game logic. If user clicks wrong cell the illegal move sound effect 61 will play. To avoid calling functions more than necessary it checks if the previously clicked figure isn't the same as 62 currently clicked one and if the color of current player turn is the same as figure which is being clicked. 63 64 Args: 65 event (Any): Event type. Doesn't matter but is required parameter by customtkinter. 66 """ 67 if self.board.clicked_figure and self.board.clicked_figure.color: 68 if self.board.board[self.position[0]][self.position[1]] not in self.board.highlighted and self.board.clicked_figure != self.figure: 69 if self.figure: 70 if self.figure.color != self.board.clicked_figure.color: 71 threading.Thread(target=play_sound, args=(self.board.illegal_sound,)).start() 72 else: 73 threading.Thread(target=play_sound, args=(self.board.illegal_sound,)).start() 74 self.board.handle_move(self.position) 75 if self.figure and not self.board.clicked_figure and self.board.current_turn == self.figure.color: 76 self.board.handle_clicks(self.figure, self.position) 77 else: 78 self.board.handle_move(self.position)
Handles clicks by calling board functions handling game logic. If user clicks wrong cell the illegal move sound effect will play. To avoid calling functions more than necessary it checks if the previously clicked figure isn't the same as currently clicked one and if the color of current player turn is the same as figure which is being clicked.
Arguments:
- event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
86class Board(ctk.CTkFrame): 87 """Class handling all cells and move related logic. 88 89 Args: 90 ctk.CTkFrame : Inheritance from customtkinter CTkLabel widget. 91 """ 92 def __init__(self, master, moves_record: MovesRecord, size: int) -> None: 93 """Constructor: 94 - setups all important variables 95 - loads all sound files 96 - loads font with different sizes 97 - creates board with default figure placement 98 - calls loading_animation for better user experience 99 100 Args: 101 master (Any): Parent widget. 102 moves_record (MovesRecord): class handling move records. 103 size (int): Size of the tiles and fonts. 104 """ 105 super().__init__(master, fg_color=COLOR.DARK_TEXT, corner_radius=0) 106 self.master: Any = master 107 self.loading_screen: ctk.CTkLabel | None = None 108 self.font_name: str = str(get_from_config('font_name')) 109 self.font_42 = ctk.CTkFont(self.font_name, 42) 110 self.master.after(1, lambda: self.loading_animation(0)) 111 self.pack(side=ctk.RIGHT, padx=10, pady=10, expand=True, ipadx=5, ipady=5, anchor=ctk.CENTER) 112 thread = threading.Thread(target=self.load_sound) 113 self.frame_image: ctk.CTkImage = ctk.CTkImage(Image.open(resource_path(os.path.join('assets', 'menu', 'frame.png'))).convert('RGBA'), size=(80, 80)) 114 thread.start() 115 self.size: int = size 116 self.turns: Generator[str, None, NoReturn] = self.turn() 117 self.current_turn = next(self.turns) 118 self.board_font = ctk.CTkFont(self.font_name, self.size // 3) 119 self.board: list[list[Cell]] = self.create_board() 120 self.highlighted: list[Cell] = [] 121 self.clicked_figure: piece.Piece | None = None 122 self.previous_coords: tuple[int, int] | None = None 123 self.notification: None | Notification = None 124 self.moves_record: MovesRecord = moves_record 125 self.capture: bool = False 126 self.game_over: bool = False 127 self.current_save_name: str | None = None 128 thread.join() 129 self.destroy_loading_screen() 130 131 def load_sound(self): 132 """Function loading sound on thread to speed up the process if possible. Constructor will have to wait with destroying loading screen if process could take too long. 133 """ 134 self.move_sound = soundfile.read(resource_path(os.path.join('sounds', 'move-self.wav')), dtype='float32')[0] 135 self.capture_sound = soundfile.read(resource_path(os.path.join('sounds', 'capture.wav')), dtype='float32')[0] 136 self.move_check_sound = soundfile.read(resource_path(os.path.join('sounds', 'capture.wav')), dtype='float32')[0] 137 self.castle_sound = soundfile.read(resource_path(os.path.join('sounds', 'capture.wav')), dtype='float32')[0] 138 self.end_game_sound = soundfile.read(resource_path(os.path.join('sounds', 'game-end.wav')), dtype='float32')[0] 139 self.illegal_sound = soundfile.read(resource_path(os.path.join('sounds', 'illegal.wav')), dtype='float32')[0] 140 141 @staticmethod 142 def determine_tile_color(pos: tuple[int, int]) -> str: 143 """Static method to determine color of the tile on the board. 144 145 Args: 146 pos (tuple[int, int]): Position of the cell on the board. 147 148 Returns: 149 str: Color of the cell. 150 """ 151 return COLOR.TILE_1 if (pos[0] % 2) == (pos[1] % 2) else COLOR.TILE_2 152 153 def create_outline_l_r_t(self) -> None: 154 """Creates outline of the board with coordinates. 155 """ 156 ctk.CTkLabel( 157 master = self, 158 text =f' ', 159 font =self.board_font, 160 text_color =COLOR.DARK_TEXT 161 ).pack(padx=10, pady=1) 162 new_frame = ctk.CTkFrame( 163 master = self, 164 fg_color = COLOR.DARK_TEXT, 165 corner_radius = 0 166 ) 167 new_frame.pack(side=ctk.LEFT, padx=3, pady=0, fill=ctk.Y) 168 for i in range(8, 0, -1): 169 ctk.CTkLabel( 170 master = new_frame, 171 text = f' {i}', 172 font = self.board_font, 173 fg_color = COLOR.DARK_TEXT, 174 anchor = ctk.W 175 ).pack(side=ctk.TOP, padx=10, pady=1, expand=True) 176 ctk.CTkLabel( 177 master = new_frame, 178 text = ' ', 179 font = ctk.CTkFont(self.font_name, int(int(get_from_config('size')) * 0.4)) 180 ).pack(side=ctk.BOTTOM, padx=0, pady=0) 181 new_frame = ctk.CTkFrame( 182 master = self, 183 fg_color = COLOR.DARK_TEXT, 184 corner_radius=0 185 ) 186 new_frame.pack(side=ctk.RIGHT, padx=1, pady=0, fill=ctk.Y) 187 ctk.CTkLabel( 188 master = new_frame, 189 text = '', 190 font = self.board_font, 191 text_color = COLOR.DARK_TEXT, 192 fg_color = COLOR.DARK_TEXT, 193 width = int(int(get_from_config('size')) * 0.4) 194 ).pack(padx=10, pady=1) 195 196 def create_board(self) -> list[list[Cell]]: 197 """Creates a board filled with colored tiles and figures. Uses prepared dictionary with correct figures positions to place the Figures. 198 199 Returns: 200 list[list[Cell]]: 2D representation of the board with Cell linking figures to correct positions. 201 """ 202 self.create_outline_l_r_t() 203 board: list[list[Cell]] = cast(list[list[Cell]], [[None] * 8] * 8) 204 board_frame = ctk.CTkFrame( 205 master = self, 206 corner_radius = 0, 207 fg_color = COLOR.DARK_TEXT 208 ) 209 board_frame.pack(side=ctk.TOP, padx=0, pady=0) 210 piece_positions = { 211 (0, 0): piece.Rook('b', self, (0, 0)), # Black rook 212 (0, 7): piece.Rook('b', self, (0, 7)), # Black rook 213 (7, 0): piece.Rook('w', self, (7, 0)), # White rook 214 (7, 7): piece.Rook('w', self, (7, 7)), # White rook 215 (0, 1): piece.Knight('b', self, (0, 1)), # Black knight 216 (0, 6): piece.Knight('b', self, (0, 6)), # Black knight 217 (7, 1): piece.Knight('w', self, (7, 1)), # White knight 218 (7, 6): piece.Knight('w', self, (7, 6)), # White knight 219 (0, 2): piece.Bishop('b', self, (0, 2)), # Black bishop 220 (0, 5): piece.Bishop('b', self, (0, 5)), # Black bishop 221 (7, 2): piece.Bishop('w', self, (7, 2)), # White bishop 222 (7, 5): piece.Bishop('w', self, (7, 5)), # White bishop 223 (0, 3): piece.Queen('b', self, (0, 3)), # Black Queen 224 (7, 3): piece.Queen('w', self, (7, 3)), # White Queen 225 (0, 4): piece.King('b', self, (0, 4)), # Black King 226 (7, 4): piece.King('w', self, (7, 4)) # White King 227 } 228 for i in range(8): 229 row: list[Cell] = cast(list[Cell], [None] * 8) 230 new_frame: ctk.CTkFrame = ctk.CTkFrame( 231 master = board_frame, 232 fg_color = COLOR.DARK_TEXT, 233 corner_radius = 0 234 ) 235 new_frame.pack(padx=0, pady=0) 236 for j in range(8): 237 color: str = self.determine_tile_color((i, j)) 238 figure: piece.Piece | None = piece_positions.get((i, j)) if (i, j) in piece_positions else ( 239 piece.Pawn('b' if i == 1 else 'w', self, (i, j), self.notation_promotion) if i in [1, 6] else None 240 ) 241 cell = Cell(new_frame, figure, (i, j), color, self) 242 row[j] = cell 243 board[i] = row 244 new_frame = ctk.CTkFrame( 245 master = self, 246 fg_color = COLOR.DARK_TEXT, 247 corner_radius = 0 248 ) 249 new_frame.pack(padx=0, pady=1, fill=ctk.X) 250 for letter in ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'): 251 ctk.CTkLabel( 252 master = new_frame, 253 text = letter, 254 font = self.board_font, 255 fg_color = COLOR.DARK_TEXT 256 ).pack(side=ctk.LEFT, padx=2, pady=0, expand=True) 257 return board 258 259 def remove_highlights(self) -> None: 260 """Removes highlights from the cells. Ensures proper move handling and enhances user experience. 261 If user doesn't want the help they can change the color of the highlighted tile to the same as not highlighted in customization menu. 262 """ 263 for cell in self.highlighted: 264 cell.configure(fg_color=self.determine_tile_color(cell.position)) 265 self.highlighted.clear() 266 267 def display_message(self, message: str, duration_sec: int) -> None: 268 """Displays message on the screen using Notification module. 269 270 Args: 271 message (str): Desired message to display. 272 duration_sec (int): Amount of seconds before hiding the notification. 273 """ 274 if self.notification: 275 self.notification.destroy() 276 self.notification = Notification(self, message=message, duration_sec=duration_sec) 277 278 def is_game_over(self) -> tuple[bool, bool]: 279 """Checks if checkmate or stalemate occurred. Is also used to check if the king is in check in handle_move method. 280 Function scans all combinations of moving the King, if found at leas one, loop breaks to check if king is in check. 281 282 Returns: 283 tuple[bool, bool]: 1st tuple element is game_over and 2nd is in check both True or False. 284 """ 285 in_check = False 286 has_legal_moves = False 287 for row in self.board: 288 for cell in row: 289 if cell.figure and cell.figure.color == self.current_turn: 290 possible_moves = cell.figure.check_possible_moves(self.current_turn) 291 for move in possible_moves: 292 if not self.check_check(cell.figure.position, move): 293 has_legal_moves = True 294 break 295 if has_legal_moves: 296 break 297 if has_legal_moves: 298 break 299 king_position = self.get_king_position(self.current_turn) 300 in_check = self.is_under_attack(king_position, self.current_turn) 301 return (not has_legal_moves, in_check) 302 303 def handle_clicks(self, figure: piece.Piece, position: tuple[int, int]) -> None: 304 """Handles actions after clicking on a specific cell. Function filter from all possible moves for the figure to only these which are legal. 305 After filtering the moves to only legal ones, proper tiles are being highlighted. 306 307 Args: 308 figure (piece.Piece): Chosen figure. 309 position (tuple[int, int]): Position of that figure. 310 """ 311 if self.game_over: 312 return 313 self.handle_chosen_figure_highlight(position) 314 possible_moves = figure.check_possible_moves(self.current_turn) 315 if not possible_moves and self.board[position[0]][position[1]].figure: 316 self.previous_coords = position 317 return 318 self.clicked_figure = figure if figure else None 319 self.previous_coords = position 320 if self.board and possible_moves: 321 valid_moves = [] 322 for coords in possible_moves: 323 if not self.check_check(position, coords): 324 valid_moves.append(coords) 325 for coords in valid_moves: 326 x_ = coords[0] 327 y_ = coords[1] 328 color = self.board[x_][y_].cget('fg_color') 329 new_color = COLOR.HIGH_TILE_1 if color == COLOR.TILE_1 else COLOR.HIGH_TILE_2 330 self.board[x_][y_].configure(fg_color=new_color) 331 self.highlighted.append(self.board[x_][y_]) 332 333 def hide_clicked_figure_frame(self) -> None: 334 """Hides the frame or highlight around the chosen figure. 335 """ 336 if self.previous_coords: 337 previous_x = self.previous_coords[0] 338 previous_y = self.previous_coords[1] 339 if SYSTEM == 'Windows': 340 if frame := self.board[previous_x][previous_y].frame_around: 341 frame.destroy() 342 else: 343 self.board[previous_x][previous_y].configure(fg_color=self.determine_tile_color(self.previous_coords)) 344 345 def handle_chosen_figure_highlight(self, position: tuple[int, int]) -> None: 346 """Highlights with frame or color under the tile of the figure for better visibility and improved user experience. 347 348 Args: 349 position (tuple[int, int]): _description_ 350 """ 351 self.hide_clicked_figure_frame() 352 x: int = position[0] 353 y: int = position[1] 354 if SYSTEM == 'Windows': 355 image_test: ctk.CTkLabel = ctk.CTkLabel( 356 fg_color = COLOR.TRANSPARENT_MASK, 357 master = self.board[x][y], 358 text = '', 359 image = self.frame_image 360 ) 361 pywinstyles.set_opacity(image_test, value=1, color=COLOR.TRANSPARENT_MASK) 362 image_test.place(relx=0.5, rely=0.5, anchor='center') 363 self.board[x][y].frame_around = image_test 364 else: 365 self.board[x][y].configure(fg_color=COLOR.TEXT) 366 367 def check_check(self, move_from: tuple[int, int], move_to: tuple[int, int]) -> bool: 368 """Checks if King is in a check. Checks if any of the opponents figure are causing the check. If any found loop breaks to improve performance. 369 370 Args: 371 move_from (tuple[int, int]): Starting position. 372 move_to (tuple[int, int]): Desired position. 373 374 Returns: 375 bool: _description_ 376 """ 377 move_to_x: int = move_to[0] 378 move_to_y: int = move_to[1] 379 move_from_x: int = move_from[0] 380 move_from_y: int = move_from[1] 381 original_from_figure: piece.Piece | None = self.board[move_from_x][move_from_y].figure 382 original_to_figure: piece.Piece | None = self.board[move_to_x][move_to_y].figure 383 self.board[move_to_x][move_to_y].figure = original_from_figure 384 self.board[move_from_x][move_from_y].figure = None 385 king_position = None 386 if isinstance(original_from_figure, piece.King): 387 king_position = move_to 388 else: 389 for row in self.board: 390 for cell in row: 391 if isinstance(cell.figure, piece.King) and cell.figure.color == self.current_turn: 392 king_position = cell.figure.position 393 break 394 if king_position: 395 break 396 is_in_check = False 397 for row in self.board: 398 for cell in row: 399 if cell.figure and cell.figure.color != self.current_turn: 400 possible_moves = cell.figure.check_possible_moves(cell.figure.color) 401 if king_position in possible_moves: 402 is_in_check = True 403 break 404 if is_in_check: 405 break 406 self.board[move_from_x][move_from_y].figure = original_from_figure 407 self.board[move_to_x][move_to_y].figure = original_to_figure 408 return is_in_check 409 410 def is_under_attack(self, position: tuple[int, int], color: str) -> bool: 411 """Checks if king is under attack. 412 413 Args: 414 position (tuple[int, int]): Position of the king. 415 color (str): Color of the king. 416 417 Returns: 418 bool: Returns True if is under attack, False otherwise. 419 """ 420 for row in self.board: 421 for cell in row: 422 if cell.figure and cell.figure.color != color: 423 if position in cell.figure.check_possible_moves(cell.figure.color, checking=True): 424 return True 425 return False 426 427 def handle_move(self, position: tuple[int, int]) -> None: 428 """Function handles moving pieces on the board. Calls notations functions, plays sounds appropriately to the move and removes previous highlights. 429 Function handles all possible moves including pawn promotion, castle, en_passant and moving pawn by two squares at first move. Function has to check if move is 430 for sure legal and is not causing check for the current player taking turn. Ensures proper notation in every case. 431 432 Args: 433 position (tuple[int, int]): Position of the figure. 434 """ 435 if self.clicked_figure and self.clicked_figure.position == position: 436 return 437 if self.previous_coords: 438 prev_x: int = self.previous_coords[0] 439 prev_y: int = self.previous_coords[1] 440 if SYSTEM == 'Windows': 441 if x := self.board[prev_x][prev_y].frame_around: 442 x.destroy() 443 else: 444 self.board[prev_x][prev_y].configure(fg_color=self.determine_tile_color(self.previous_coords)) 445 if self.clicked_figure and self.previous_coords: 446 row, col = position 447 cell = self.board[row][col] 448 self.capture = bool(cell.figure) 449 promotion: bool = False 450 if cell in self.highlighted: 451 castle: bool = False 452 if not self.check_check(self.previous_coords, position): 453 if isinstance(self.clicked_figure, piece.Pawn) and self.clicked_figure.can_en_passant and col != prev_y and not cell.figure: 454 self.board[row - self.clicked_figure.move][col].figure = None 455 self.board[row - self.clicked_figure.move][col].update() 456 self.capture = True 457 if isinstance(self.clicked_figure, piece.King): 458 castle = self.handle_move_castle(row, col, prev_y) 459 cell.figure = self.clicked_figure 460 cell.figure.position = position 461 cell.update() 462 self.board[prev_x][prev_y].figure = None 463 self.board[prev_x][prev_y].update() 464 promotion = self.handle_move_pawn(cell, row) 465 if cell.figure.first_move: 466 cell.figure.first_move = False 467 self.current_turn = next(self.turns) 468 game_over, in_check = self.is_game_over() 469 if game_over: 470 self.handle_game_over(in_check, promotion, self.capture, in_check) 471 elif not castle and not promotion: 472 self.moves_record.record_move( 473 moved_piece = self.clicked_figure, 474 capture = self.capture, 475 previous_coords = self.previous_coords, 476 check = in_check, 477 checkmate = game_over and in_check 478 ) 479 threading.Thread(target=lambda: self.play_correct_sound(game_over, self.capture, castle, in_check)).start() 480 if not promotion: 481 self.clicked_figure = None 482 self.previous_coords = None 483 if self.highlighted: 484 self.remove_highlights() 485 486 def handle_move_pawn(self, cell: Cell, row: int) -> bool: 487 """Helper function handling pawn special case - promotion. Ensures all flags are properly reset after each move. 488 489 Args: 490 cell (Cell): Cell object linking figure to position. 491 row (int): Row in which pawn is being moved. 492 493 Returns: 494 bool: True if pawn promoted, False otherwise. 495 """ 496 if not cell.figure or not self.previous_coords: 497 return False 498 promotion = False 499 if isinstance(cell.figure, piece.Pawn): 500 if cell.figure.first_move and abs(self.previous_coords[0] - row) == 2: 501 cell.figure.moved_by_two = True 502 else: 503 cell.figure.moved_by_two = False 504 if cell.figure.promote(): 505 promotion = True 506 self.reset_en_passant_flags(cell.figure.color) 507 return promotion 508 509 def handle_move_castle(self, row: int, col: int, prev_y: int) -> bool: 510 """Helper function handling castle special move. Ensures notation is proper and all figures moves properly. 511 512 Args: 513 row (int): Row of the king in which castle will be performed. 514 col (int): Column of the king in which king figure is. 515 prev_y (int): Previous y coordinate of the king. 516 517 Returns: 518 bool: Returns True if castle occurred, False otherwise. 519 """ 520 if not self.clicked_figure: 521 return False 522 castle = False 523 if abs(col - prev_y) == 2: 524 if col == 6: 525 self.board[row][5].figure = self.board[row][7].figure 526 self.board[row][7].figure = None 527 self.board[row][5].figure.position = (row, 5) # type: ignore # isinstance already checks it but mypy don't understand it 528 self.board[row][5].update() 529 self.board[row][7].update() 530 self.moves_record.record_move(self.clicked_figure, castle="kingside") 531 castle = True 532 elif col == 2: 533 self.board[row][3].figure = self.board[row][0].figure 534 self.board[row][0].figure = None 535 self.board[row][3].figure.position = (row, 3) # type: ignore # isinstance already checks it but mypy don't understand it 536 self.board[row][3].update() 537 self.board[row][0].update() 538 self.moves_record.record_move(self.clicked_figure, castle="queenside") 539 castle =True 540 return castle 541 542 def turn(self) -> Generator[str, None, NoReturn]: 543 """Simple infinite yielding function for easy turn changing. 544 545 Yields: 546 Generator[str, None, NoReturn]: Current turn color representation. 547 """ 548 while True: 549 yield 'w' 550 yield 'b' 551 552 def play_correct_sound(self, game_over: bool, capture: bool, castle: bool, check: bool) -> None: 553 """Plays sound according to the users move. 554 555 Args: 556 game_over (bool): Flag corresponding to game over. 557 capture (bool): Flag corresponding to capturing other piece. 558 castle (bool): Flag corresponding to castle move. 559 check (bool): Flag corresponding to check. 560 """ 561 if game_over: 562 play_sound(self.end_game_sound) 563 elif capture: 564 play_sound(self.capture_sound) 565 elif castle: 566 play_sound(self.castle_sound) 567 elif check: 568 play_sound(self.move_check_sound) 569 else: 570 play_sound(self.move_sound) 571 572 def handle_game_over(self, in_check: bool, promotion: bool, capture: bool, check: bool) -> None: 573 """Handles displaying notification of who won or if it was a stalemate. Sets game_over flag to True, and notates the end of the game. 574 575 Args: 576 in_check (bool): Flag corresponding to check. 577 promotion (bool): Flag corresponding to pawn promotion. 578 capture (bool): Flag corresponding to capturing other piece. 579 check (bool): Flag corresponding to check 580 """ 581 self.game_over = True 582 if in_check: 583 self.display_message(f'Checkmate {"White wins!" if self.current_turn == "b" else "Black wins!"}', 9) 584 if not promotion and self.clicked_figure: 585 self.moves_record.record_move(self.clicked_figure, capture=capture, previous_coords=self.previous_coords, check=True, checkmate=check and in_check) 586 else: 587 self.display_message('Stalemate', 9) 588 if not promotion and self.clicked_figure: 589 self.moves_record.record_move(self.clicked_figure, capture=capture, previous_coords=self.previous_coords, check=False, checkmate=check and in_check) 590 591 def notation_promotion(self, promotion:str) -> None: 592 """Helper function to note the promotion of the pawn. Calls pawn functions corresponding to choosing figure to promote to. Notates the promotion 593 594 Args: 595 promotion (str): Figure representation to which pawn was promoted. 596 """ 597 check = self.is_under_attack(self.get_king_position(self.current_turn), self.current_turn) 598 game_over, in_check = self.is_game_over() 599 if game_over: 600 if in_check: 601 self.display_message(f'Checkmate {"White wins!" if self.current_turn == "b" else "Black wins!"}', 9) 602 if self.clicked_figure: 603 self.moves_record.record_move(self.clicked_figure, capture=self.capture, previous_coords=self.previous_coords, check=check, checkmate=game_over and in_check, promotion=promotion[0]) 604 else: 605 self.display_message('Stalemate', 9) 606 if self.clicked_figure: 607 self.moves_record.record_move(self.clicked_figure, capture=self.capture, previous_coords=self.previous_coords, check=check, checkmate=game_over and in_check, promotion=promotion[0]) 608 else: 609 if self.clicked_figure: 610 self.moves_record.record_move(self.clicked_figure, capture=self.capture, previous_coords=self.previous_coords, check=check, checkmate=game_over and in_check, promotion=promotion[0]) 611 self.clicked_figure = None 612 self.previous_coords = None 613 614 def get_king_position(self, color: str) -> tuple[int, int]: 615 """Function returning king position on the board. Loop searching for king just breaks after finding king with correct color. 616 617 Args: 618 color (str): Color of the king. 619 620 Returns: 621 tuple[int, int]: Position of the king. 622 """ 623 for row in self.board: 624 for cell in row: 625 if isinstance(cell.figure, piece.King) and cell.figure.color == color: 626 return cell.figure.position 627 update_error_log(Exception('Not enough kings on the board, check the save file')) 628 Notification(self.master, 'No king on the board, check save file', 2, 'top') 629 self.game_over = True 630 self.master.after(2001, self.restart_game) 631 raise Exception('Not enough kings on the board, check the save file') 632 633 def reset_en_passant_flags(self, current_color: str) -> None: 634 """Helper function to reset en passant and first move flag. 635 636 Args: 637 current_color (str): Color of the current player. 638 """ 639 for row in self.board: 640 for cell in row: 641 if isinstance(cell.figure, piece.Pawn) and cell.figure.color != current_color: 642 cell.figure.moved_by_two = False 643 cell.figure.can_en_passant = False 644 645 def restart_game(self) -> None: 646 """Function restarting the game with all necessary flags and variables. 647 """ 648 self.loading_animation(0) 649 for child in self.winfo_children(): 650 self.master.after(1, child.destroy) 651 self.highlighted.clear() 652 self.clicked_figure = None 653 self.previous_coords = None 654 self.turns = self.turn() 655 self.current_turn = next(self.turns) 656 self.notification = None 657 self.game_over = False 658 self.board = self.create_board() 659 self.current_save_name = None 660 661 def destroy_loading_screen(self) -> None: 662 """Destroys loading screen widget. 663 """ 664 def update_opacity(i: int) -> None: 665 if i >= 0 and self.loading_screen: 666 pywinstyles.set_opacity(self.loading_screen, value=i*0.005, color='#000001') 667 self.master.after(1, lambda: update_opacity(i - 1)) 668 else: 669 if self.loading_screen: 670 self.loading_screen.destroy() 671 self.loading_screen = None 672 if self.loading_screen: 673 update_opacity(200) 674 675 def loading_animation(self, i: int) -> None: 676 """Function to animate loading screen. 677 678 Args: 679 i (int, optional): Iteration value passed by recursive formula. Defaults to 0. 680 """ 681 if not self.loading_screen: 682 self.loading_screen = ctk.CTkFrame( 683 master = self.master, 684 fg_color = COLOR.BACKGROUND 685 ) 686 self.loading_screen.place(relx=0, rely=0, relwidth=1, relheight=1) 687 self.loading_text = ctk.CTkLabel( 688 master = self.loading_screen, 689 text = 'Loading ', 690 font = self.font_42, 691 text_color = COLOR.TEXT, 692 ) 693 self.loading_text.pack(side=ctk.TOP, expand=True) 694 self.master.after(90, lambda: self.loading_animation(0)) 695 else: 696 self.loading_text.configure(text=f'Loading{'.' * i}{' ' * (3 - i)}') 697 if i <= 2: 698 i += 1 699 self.master.after(90, lambda: self.loading_animation(i)) 700 else: 701 self.master.after(90, self.destroy_loading_screen) 702 703 def update_board(self) -> None: 704 """Updates all cells on the board 705 """ 706 for row in self.board: 707 for cell in row: 708 cell.update() 709 710 def load_board_from_file(self, file_info: dict, save_name: str) -> bool: 711 """Updates board to match the state from the save file. Ensures all save information are in the file in correct format. 712 713 Args: 714 file_info (dict): All needed information to load save. 715 716 Returns: 717 bool: Returns True if load was successful, False otherwise. 718 """ 719 self.master.after(1, lambda: self.loading_animation(0)) 720 save_keys: set[str] = {'current_turn', 'board_state', 'white_moves', 'black_moves', 'game_over'} 721 if not all(key in file_info for key in save_keys): 722 update_error_log(KeyError('Save file doesn\'t contain all necessary information')) 723 return False 724 king_w: int = 0 725 king_b: int = 0 726 for row in self.board: 727 for cell in row: 728 if cell.figure: 729 cell.figure = None 730 cell.update() 731 for key, value in file_info['board_state'].items(): 732 if not bool(re.match(r'[0-9]{1},[0-9]{1}', key)): 733 self.restart_game() 734 update_error_log(KeyError('Save file doesn\'t contain all necessary information')) 735 return False 736 try: 737 value[1] 738 value[2] 739 except: 740 self.restart_game() 741 update_error_log(KeyError('Save file doesn\'t contain all necessary information')) 742 return False 743 if not bool(re.match(r'[wb]{1}', value[1])): 744 self.restart_game() 745 update_error_log(KeyError('Save file doesn\'t contain all necessary information')) 746 return False 747 coord: tuple[int, ...] = tuple(map(int, key.split(','))) 748 x: int = coord[0] 749 y: int = coord[1] 750 match value[0]: 751 case 'Pawn': 752 pawn = piece.Pawn(value[1], self, (x, y), self.notation_promotion) 753 if not value[2]: 754 pawn.first_move = False 755 self.board[x][y].figure = pawn 756 case 'Knight': 757 self.board[x][y].figure = piece.Knight(value[1], self, (x, y)) 758 case 'Bishop': 759 self.board[x][y].figure = piece.Bishop(value[1], self, (x, y)) 760 case 'Rook': 761 rook = piece.Rook(value[1], self, (x, y)) 762 self.board[x][y].figure = rook 763 if not value[2]: 764 rook.first_move = False 765 case 'Queen': 766 self.board[x][y].figure = piece.Queen(value[1], self, (x, y)) 767 case 'King': 768 king = piece.King(value[1], self, (x, y)) 769 self.board[x][y].figure = king 770 if not value[2]: 771 king.first_move = False 772 king_w += 1 if value[1] == 'w' else 0 773 king_b += 1 if value[1] == 'b' else 0 774 self.master.after(1, self.board[x][y].update) 775 if king_w != 1 or king_b != 1: 776 update_error_log(KeyError('Save file doesn\'t contain proper amount of kings')) 777 return False 778 current_turn = str(file_info['current_turn']) 779 self.turns = self.turn() 780 if current_turn == 'b': 781 self.current_turn = next(self.turns) 782 self.current_turn = next(self.turns) 783 else: 784 self.current_turn = next(self.turns) 785 self.master.after(21, lambda: self.moves_record.load_notation_from_save(file_info['white_moves'], file_info['black_moves'])) 786 self.master.after(21, self.hide_clicked_figure_frame) 787 self.master.after(21, self.remove_highlights) 788 self.game_over = file_info['game_over'] 789 self.clicked_figure = None 790 self.current_save_name = save_name 791 return True
Class handling all cells and move related logic.
Arguments:
- ctk.CTkFrame : Inheritance from customtkinter CTkLabel widget.
92 def __init__(self, master, moves_record: MovesRecord, size: int) -> None: 93 """Constructor: 94 - setups all important variables 95 - loads all sound files 96 - loads font with different sizes 97 - creates board with default figure placement 98 - calls loading_animation for better user experience 99 100 Args: 101 master (Any): Parent widget. 102 moves_record (MovesRecord): class handling move records. 103 size (int): Size of the tiles and fonts. 104 """ 105 super().__init__(master, fg_color=COLOR.DARK_TEXT, corner_radius=0) 106 self.master: Any = master 107 self.loading_screen: ctk.CTkLabel | None = None 108 self.font_name: str = str(get_from_config('font_name')) 109 self.font_42 = ctk.CTkFont(self.font_name, 42) 110 self.master.after(1, lambda: self.loading_animation(0)) 111 self.pack(side=ctk.RIGHT, padx=10, pady=10, expand=True, ipadx=5, ipady=5, anchor=ctk.CENTER) 112 thread = threading.Thread(target=self.load_sound) 113 self.frame_image: ctk.CTkImage = ctk.CTkImage(Image.open(resource_path(os.path.join('assets', 'menu', 'frame.png'))).convert('RGBA'), size=(80, 80)) 114 thread.start() 115 self.size: int = size 116 self.turns: Generator[str, None, NoReturn] = self.turn() 117 self.current_turn = next(self.turns) 118 self.board_font = ctk.CTkFont(self.font_name, self.size // 3) 119 self.board: list[list[Cell]] = self.create_board() 120 self.highlighted: list[Cell] = [] 121 self.clicked_figure: piece.Piece | None = None 122 self.previous_coords: tuple[int, int] | None = None 123 self.notification: None | Notification = None 124 self.moves_record: MovesRecord = moves_record 125 self.capture: bool = False 126 self.game_over: bool = False 127 self.current_save_name: str | None = None 128 thread.join() 129 self.destroy_loading_screen()
Constructor:
- setups all important variables
- loads all sound files
- loads font with different sizes
- creates board with default figure placement
- calls loading_animation for better user experience
Arguments:
- master (Any): Parent widget.
- moves_record (MovesRecord): class handling move records.
- size (int): Size of the tiles and fonts.
1893 def grid_size(self): 1894 """Return a tuple of the number of column and rows in the grid.""" 1895 return self._getints( 1896 self.tk.call('grid', 'size', self._w)) or None
Return a tuple of the number of column and rows in the grid.
131 def load_sound(self): 132 """Function loading sound on thread to speed up the process if possible. Constructor will have to wait with destroying loading screen if process could take too long. 133 """ 134 self.move_sound = soundfile.read(resource_path(os.path.join('sounds', 'move-self.wav')), dtype='float32')[0] 135 self.capture_sound = soundfile.read(resource_path(os.path.join('sounds', 'capture.wav')), dtype='float32')[0] 136 self.move_check_sound = soundfile.read(resource_path(os.path.join('sounds', 'capture.wav')), dtype='float32')[0] 137 self.castle_sound = soundfile.read(resource_path(os.path.join('sounds', 'capture.wav')), dtype='float32')[0] 138 self.end_game_sound = soundfile.read(resource_path(os.path.join('sounds', 'game-end.wav')), dtype='float32')[0] 139 self.illegal_sound = soundfile.read(resource_path(os.path.join('sounds', 'illegal.wav')), dtype='float32')[0]
Function loading sound on thread to speed up the process if possible. Constructor will have to wait with destroying loading screen if process could take too long.
141 @staticmethod 142 def determine_tile_color(pos: tuple[int, int]) -> str: 143 """Static method to determine color of the tile on the board. 144 145 Args: 146 pos (tuple[int, int]): Position of the cell on the board. 147 148 Returns: 149 str: Color of the cell. 150 """ 151 return COLOR.TILE_1 if (pos[0] % 2) == (pos[1] % 2) else COLOR.TILE_2
Static method to determine color of the tile on the board.
Arguments:
- pos (tuple[int, int]): Position of the cell on the board.
Returns:
str: Color of the cell.
153 def create_outline_l_r_t(self) -> None: 154 """Creates outline of the board with coordinates. 155 """ 156 ctk.CTkLabel( 157 master = self, 158 text =f' ', 159 font =self.board_font, 160 text_color =COLOR.DARK_TEXT 161 ).pack(padx=10, pady=1) 162 new_frame = ctk.CTkFrame( 163 master = self, 164 fg_color = COLOR.DARK_TEXT, 165 corner_radius = 0 166 ) 167 new_frame.pack(side=ctk.LEFT, padx=3, pady=0, fill=ctk.Y) 168 for i in range(8, 0, -1): 169 ctk.CTkLabel( 170 master = new_frame, 171 text = f' {i}', 172 font = self.board_font, 173 fg_color = COLOR.DARK_TEXT, 174 anchor = ctk.W 175 ).pack(side=ctk.TOP, padx=10, pady=1, expand=True) 176 ctk.CTkLabel( 177 master = new_frame, 178 text = ' ', 179 font = ctk.CTkFont(self.font_name, int(int(get_from_config('size')) * 0.4)) 180 ).pack(side=ctk.BOTTOM, padx=0, pady=0) 181 new_frame = ctk.CTkFrame( 182 master = self, 183 fg_color = COLOR.DARK_TEXT, 184 corner_radius=0 185 ) 186 new_frame.pack(side=ctk.RIGHT, padx=1, pady=0, fill=ctk.Y) 187 ctk.CTkLabel( 188 master = new_frame, 189 text = '', 190 font = self.board_font, 191 text_color = COLOR.DARK_TEXT, 192 fg_color = COLOR.DARK_TEXT, 193 width = int(int(get_from_config('size')) * 0.4) 194 ).pack(padx=10, pady=1)
Creates outline of the board with coordinates.
196 def create_board(self) -> list[list[Cell]]: 197 """Creates a board filled with colored tiles and figures. Uses prepared dictionary with correct figures positions to place the Figures. 198 199 Returns: 200 list[list[Cell]]: 2D representation of the board with Cell linking figures to correct positions. 201 """ 202 self.create_outline_l_r_t() 203 board: list[list[Cell]] = cast(list[list[Cell]], [[None] * 8] * 8) 204 board_frame = ctk.CTkFrame( 205 master = self, 206 corner_radius = 0, 207 fg_color = COLOR.DARK_TEXT 208 ) 209 board_frame.pack(side=ctk.TOP, padx=0, pady=0) 210 piece_positions = { 211 (0, 0): piece.Rook('b', self, (0, 0)), # Black rook 212 (0, 7): piece.Rook('b', self, (0, 7)), # Black rook 213 (7, 0): piece.Rook('w', self, (7, 0)), # White rook 214 (7, 7): piece.Rook('w', self, (7, 7)), # White rook 215 (0, 1): piece.Knight('b', self, (0, 1)), # Black knight 216 (0, 6): piece.Knight('b', self, (0, 6)), # Black knight 217 (7, 1): piece.Knight('w', self, (7, 1)), # White knight 218 (7, 6): piece.Knight('w', self, (7, 6)), # White knight 219 (0, 2): piece.Bishop('b', self, (0, 2)), # Black bishop 220 (0, 5): piece.Bishop('b', self, (0, 5)), # Black bishop 221 (7, 2): piece.Bishop('w', self, (7, 2)), # White bishop 222 (7, 5): piece.Bishop('w', self, (7, 5)), # White bishop 223 (0, 3): piece.Queen('b', self, (0, 3)), # Black Queen 224 (7, 3): piece.Queen('w', self, (7, 3)), # White Queen 225 (0, 4): piece.King('b', self, (0, 4)), # Black King 226 (7, 4): piece.King('w', self, (7, 4)) # White King 227 } 228 for i in range(8): 229 row: list[Cell] = cast(list[Cell], [None] * 8) 230 new_frame: ctk.CTkFrame = ctk.CTkFrame( 231 master = board_frame, 232 fg_color = COLOR.DARK_TEXT, 233 corner_radius = 0 234 ) 235 new_frame.pack(padx=0, pady=0) 236 for j in range(8): 237 color: str = self.determine_tile_color((i, j)) 238 figure: piece.Piece | None = piece_positions.get((i, j)) if (i, j) in piece_positions else ( 239 piece.Pawn('b' if i == 1 else 'w', self, (i, j), self.notation_promotion) if i in [1, 6] else None 240 ) 241 cell = Cell(new_frame, figure, (i, j), color, self) 242 row[j] = cell 243 board[i] = row 244 new_frame = ctk.CTkFrame( 245 master = self, 246 fg_color = COLOR.DARK_TEXT, 247 corner_radius = 0 248 ) 249 new_frame.pack(padx=0, pady=1, fill=ctk.X) 250 for letter in ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'): 251 ctk.CTkLabel( 252 master = new_frame, 253 text = letter, 254 font = self.board_font, 255 fg_color = COLOR.DARK_TEXT 256 ).pack(side=ctk.LEFT, padx=2, pady=0, expand=True) 257 return board
Creates a board filled with colored tiles and figures. Uses prepared dictionary with correct figures positions to place the Figures.
Returns:
list[list[Cell]]: 2D representation of the board with Cell linking figures to correct positions.
259 def remove_highlights(self) -> None: 260 """Removes highlights from the cells. Ensures proper move handling and enhances user experience. 261 If user doesn't want the help they can change the color of the highlighted tile to the same as not highlighted in customization menu. 262 """ 263 for cell in self.highlighted: 264 cell.configure(fg_color=self.determine_tile_color(cell.position)) 265 self.highlighted.clear()
Removes highlights from the cells. Ensures proper move handling and enhances user experience. If user doesn't want the help they can change the color of the highlighted tile to the same as not highlighted in customization menu.
267 def display_message(self, message: str, duration_sec: int) -> None: 268 """Displays message on the screen using Notification module. 269 270 Args: 271 message (str): Desired message to display. 272 duration_sec (int): Amount of seconds before hiding the notification. 273 """ 274 if self.notification: 275 self.notification.destroy() 276 self.notification = Notification(self, message=message, duration_sec=duration_sec)
Displays message on the screen using Notification module.
Arguments:
- message (str): Desired message to display.
- duration_sec (int): Amount of seconds before hiding the notification.
278 def is_game_over(self) -> tuple[bool, bool]: 279 """Checks if checkmate or stalemate occurred. Is also used to check if the king is in check in handle_move method. 280 Function scans all combinations of moving the King, if found at leas one, loop breaks to check if king is in check. 281 282 Returns: 283 tuple[bool, bool]: 1st tuple element is game_over and 2nd is in check both True or False. 284 """ 285 in_check = False 286 has_legal_moves = False 287 for row in self.board: 288 for cell in row: 289 if cell.figure and cell.figure.color == self.current_turn: 290 possible_moves = cell.figure.check_possible_moves(self.current_turn) 291 for move in possible_moves: 292 if not self.check_check(cell.figure.position, move): 293 has_legal_moves = True 294 break 295 if has_legal_moves: 296 break 297 if has_legal_moves: 298 break 299 king_position = self.get_king_position(self.current_turn) 300 in_check = self.is_under_attack(king_position, self.current_turn) 301 return (not has_legal_moves, in_check)
Checks if checkmate or stalemate occurred. Is also used to check if the king is in check in handle_move method. Function scans all combinations of moving the King, if found at leas one, loop breaks to check if king is in check.
Returns:
tuple[bool, bool]: 1st tuple element is game_over and 2nd is in check both True or False.
303 def handle_clicks(self, figure: piece.Piece, position: tuple[int, int]) -> None: 304 """Handles actions after clicking on a specific cell. Function filter from all possible moves for the figure to only these which are legal. 305 After filtering the moves to only legal ones, proper tiles are being highlighted. 306 307 Args: 308 figure (piece.Piece): Chosen figure. 309 position (tuple[int, int]): Position of that figure. 310 """ 311 if self.game_over: 312 return 313 self.handle_chosen_figure_highlight(position) 314 possible_moves = figure.check_possible_moves(self.current_turn) 315 if not possible_moves and self.board[position[0]][position[1]].figure: 316 self.previous_coords = position 317 return 318 self.clicked_figure = figure if figure else None 319 self.previous_coords = position 320 if self.board and possible_moves: 321 valid_moves = [] 322 for coords in possible_moves: 323 if not self.check_check(position, coords): 324 valid_moves.append(coords) 325 for coords in valid_moves: 326 x_ = coords[0] 327 y_ = coords[1] 328 color = self.board[x_][y_].cget('fg_color') 329 new_color = COLOR.HIGH_TILE_1 if color == COLOR.TILE_1 else COLOR.HIGH_TILE_2 330 self.board[x_][y_].configure(fg_color=new_color) 331 self.highlighted.append(self.board[x_][y_])
Handles actions after clicking on a specific cell. Function filter from all possible moves for the figure to only these which are legal. After filtering the moves to only legal ones, proper tiles are being highlighted.
Arguments:
- figure (piece.Piece): Chosen figure.
- position (tuple[int, int]): Position of that figure.
333 def hide_clicked_figure_frame(self) -> None: 334 """Hides the frame or highlight around the chosen figure. 335 """ 336 if self.previous_coords: 337 previous_x = self.previous_coords[0] 338 previous_y = self.previous_coords[1] 339 if SYSTEM == 'Windows': 340 if frame := self.board[previous_x][previous_y].frame_around: 341 frame.destroy() 342 else: 343 self.board[previous_x][previous_y].configure(fg_color=self.determine_tile_color(self.previous_coords))
Hides the frame or highlight around the chosen figure.
345 def handle_chosen_figure_highlight(self, position: tuple[int, int]) -> None: 346 """Highlights with frame or color under the tile of the figure for better visibility and improved user experience. 347 348 Args: 349 position (tuple[int, int]): _description_ 350 """ 351 self.hide_clicked_figure_frame() 352 x: int = position[0] 353 y: int = position[1] 354 if SYSTEM == 'Windows': 355 image_test: ctk.CTkLabel = ctk.CTkLabel( 356 fg_color = COLOR.TRANSPARENT_MASK, 357 master = self.board[x][y], 358 text = '', 359 image = self.frame_image 360 ) 361 pywinstyles.set_opacity(image_test, value=1, color=COLOR.TRANSPARENT_MASK) 362 image_test.place(relx=0.5, rely=0.5, anchor='center') 363 self.board[x][y].frame_around = image_test 364 else: 365 self.board[x][y].configure(fg_color=COLOR.TEXT)
Highlights with frame or color under the tile of the figure for better visibility and improved user experience.
Arguments:
- position (tuple[int, int]): _description_
367 def check_check(self, move_from: tuple[int, int], move_to: tuple[int, int]) -> bool: 368 """Checks if King is in a check. Checks if any of the opponents figure are causing the check. If any found loop breaks to improve performance. 369 370 Args: 371 move_from (tuple[int, int]): Starting position. 372 move_to (tuple[int, int]): Desired position. 373 374 Returns: 375 bool: _description_ 376 """ 377 move_to_x: int = move_to[0] 378 move_to_y: int = move_to[1] 379 move_from_x: int = move_from[0] 380 move_from_y: int = move_from[1] 381 original_from_figure: piece.Piece | None = self.board[move_from_x][move_from_y].figure 382 original_to_figure: piece.Piece | None = self.board[move_to_x][move_to_y].figure 383 self.board[move_to_x][move_to_y].figure = original_from_figure 384 self.board[move_from_x][move_from_y].figure = None 385 king_position = None 386 if isinstance(original_from_figure, piece.King): 387 king_position = move_to 388 else: 389 for row in self.board: 390 for cell in row: 391 if isinstance(cell.figure, piece.King) and cell.figure.color == self.current_turn: 392 king_position = cell.figure.position 393 break 394 if king_position: 395 break 396 is_in_check = False 397 for row in self.board: 398 for cell in row: 399 if cell.figure and cell.figure.color != self.current_turn: 400 possible_moves = cell.figure.check_possible_moves(cell.figure.color) 401 if king_position in possible_moves: 402 is_in_check = True 403 break 404 if is_in_check: 405 break 406 self.board[move_from_x][move_from_y].figure = original_from_figure 407 self.board[move_to_x][move_to_y].figure = original_to_figure 408 return is_in_check
Checks if King is in a check. Checks if any of the opponents figure are causing the check. If any found loop breaks to improve performance.
Arguments:
- move_from (tuple[int, int]): Starting position.
- move_to (tuple[int, int]): Desired position.
Returns:
bool: _description_
410 def is_under_attack(self, position: tuple[int, int], color: str) -> bool: 411 """Checks if king is under attack. 412 413 Args: 414 position (tuple[int, int]): Position of the king. 415 color (str): Color of the king. 416 417 Returns: 418 bool: Returns True if is under attack, False otherwise. 419 """ 420 for row in self.board: 421 for cell in row: 422 if cell.figure and cell.figure.color != color: 423 if position in cell.figure.check_possible_moves(cell.figure.color, checking=True): 424 return True 425 return False
Checks if king is under attack.
Arguments:
- position (tuple[int, int]): Position of the king.
- color (str): Color of the king.
Returns:
bool: Returns True if is under attack, False otherwise.
427 def handle_move(self, position: tuple[int, int]) -> None: 428 """Function handles moving pieces on the board. Calls notations functions, plays sounds appropriately to the move and removes previous highlights. 429 Function handles all possible moves including pawn promotion, castle, en_passant and moving pawn by two squares at first move. Function has to check if move is 430 for sure legal and is not causing check for the current player taking turn. Ensures proper notation in every case. 431 432 Args: 433 position (tuple[int, int]): Position of the figure. 434 """ 435 if self.clicked_figure and self.clicked_figure.position == position: 436 return 437 if self.previous_coords: 438 prev_x: int = self.previous_coords[0] 439 prev_y: int = self.previous_coords[1] 440 if SYSTEM == 'Windows': 441 if x := self.board[prev_x][prev_y].frame_around: 442 x.destroy() 443 else: 444 self.board[prev_x][prev_y].configure(fg_color=self.determine_tile_color(self.previous_coords)) 445 if self.clicked_figure and self.previous_coords: 446 row, col = position 447 cell = self.board[row][col] 448 self.capture = bool(cell.figure) 449 promotion: bool = False 450 if cell in self.highlighted: 451 castle: bool = False 452 if not self.check_check(self.previous_coords, position): 453 if isinstance(self.clicked_figure, piece.Pawn) and self.clicked_figure.can_en_passant and col != prev_y and not cell.figure: 454 self.board[row - self.clicked_figure.move][col].figure = None 455 self.board[row - self.clicked_figure.move][col].update() 456 self.capture = True 457 if isinstance(self.clicked_figure, piece.King): 458 castle = self.handle_move_castle(row, col, prev_y) 459 cell.figure = self.clicked_figure 460 cell.figure.position = position 461 cell.update() 462 self.board[prev_x][prev_y].figure = None 463 self.board[prev_x][prev_y].update() 464 promotion = self.handle_move_pawn(cell, row) 465 if cell.figure.first_move: 466 cell.figure.first_move = False 467 self.current_turn = next(self.turns) 468 game_over, in_check = self.is_game_over() 469 if game_over: 470 self.handle_game_over(in_check, promotion, self.capture, in_check) 471 elif not castle and not promotion: 472 self.moves_record.record_move( 473 moved_piece = self.clicked_figure, 474 capture = self.capture, 475 previous_coords = self.previous_coords, 476 check = in_check, 477 checkmate = game_over and in_check 478 ) 479 threading.Thread(target=lambda: self.play_correct_sound(game_over, self.capture, castle, in_check)).start() 480 if not promotion: 481 self.clicked_figure = None 482 self.previous_coords = None 483 if self.highlighted: 484 self.remove_highlights()
Function handles moving pieces on the board. Calls notations functions, plays sounds appropriately to the move and removes previous highlights. Function handles all possible moves including pawn promotion, castle, en_passant and moving pawn by two squares at first move. Function has to check if move is for sure legal and is not causing check for the current player taking turn. Ensures proper notation in every case.
Arguments:
- position (tuple[int, int]): Position of the figure.
486 def handle_move_pawn(self, cell: Cell, row: int) -> bool: 487 """Helper function handling pawn special case - promotion. Ensures all flags are properly reset after each move. 488 489 Args: 490 cell (Cell): Cell object linking figure to position. 491 row (int): Row in which pawn is being moved. 492 493 Returns: 494 bool: True if pawn promoted, False otherwise. 495 """ 496 if not cell.figure or not self.previous_coords: 497 return False 498 promotion = False 499 if isinstance(cell.figure, piece.Pawn): 500 if cell.figure.first_move and abs(self.previous_coords[0] - row) == 2: 501 cell.figure.moved_by_two = True 502 else: 503 cell.figure.moved_by_two = False 504 if cell.figure.promote(): 505 promotion = True 506 self.reset_en_passant_flags(cell.figure.color) 507 return promotion
Helper function handling pawn special case - promotion. Ensures all flags are properly reset after each move.
Arguments:
- cell (Cell): Cell object linking figure to position.
- row (int): Row in which pawn is being moved.
Returns:
bool: True if pawn promoted, False otherwise.
509 def handle_move_castle(self, row: int, col: int, prev_y: int) -> bool: 510 """Helper function handling castle special move. Ensures notation is proper and all figures moves properly. 511 512 Args: 513 row (int): Row of the king in which castle will be performed. 514 col (int): Column of the king in which king figure is. 515 prev_y (int): Previous y coordinate of the king. 516 517 Returns: 518 bool: Returns True if castle occurred, False otherwise. 519 """ 520 if not self.clicked_figure: 521 return False 522 castle = False 523 if abs(col - prev_y) == 2: 524 if col == 6: 525 self.board[row][5].figure = self.board[row][7].figure 526 self.board[row][7].figure = None 527 self.board[row][5].figure.position = (row, 5) # type: ignore # isinstance already checks it but mypy don't understand it 528 self.board[row][5].update() 529 self.board[row][7].update() 530 self.moves_record.record_move(self.clicked_figure, castle="kingside") 531 castle = True 532 elif col == 2: 533 self.board[row][3].figure = self.board[row][0].figure 534 self.board[row][0].figure = None 535 self.board[row][3].figure.position = (row, 3) # type: ignore # isinstance already checks it but mypy don't understand it 536 self.board[row][3].update() 537 self.board[row][0].update() 538 self.moves_record.record_move(self.clicked_figure, castle="queenside") 539 castle =True 540 return castle
Helper function handling castle special move. Ensures notation is proper and all figures moves properly.
Arguments:
- row (int): Row of the king in which castle will be performed.
- col (int): Column of the king in which king figure is.
- prev_y (int): Previous y coordinate of the king.
Returns:
bool: Returns True if castle occurred, False otherwise.
542 def turn(self) -> Generator[str, None, NoReturn]: 543 """Simple infinite yielding function for easy turn changing. 544 545 Yields: 546 Generator[str, None, NoReturn]: Current turn color representation. 547 """ 548 while True: 549 yield 'w' 550 yield 'b'
Simple infinite yielding function for easy turn changing.
Yields:
Generator[str, None, NoReturn]: Current turn color representation.
552 def play_correct_sound(self, game_over: bool, capture: bool, castle: bool, check: bool) -> None: 553 """Plays sound according to the users move. 554 555 Args: 556 game_over (bool): Flag corresponding to game over. 557 capture (bool): Flag corresponding to capturing other piece. 558 castle (bool): Flag corresponding to castle move. 559 check (bool): Flag corresponding to check. 560 """ 561 if game_over: 562 play_sound(self.end_game_sound) 563 elif capture: 564 play_sound(self.capture_sound) 565 elif castle: 566 play_sound(self.castle_sound) 567 elif check: 568 play_sound(self.move_check_sound) 569 else: 570 play_sound(self.move_sound)
Plays sound according to the users move.
Arguments:
- game_over (bool): Flag corresponding to game over.
- capture (bool): Flag corresponding to capturing other piece.
- castle (bool): Flag corresponding to castle move.
- check (bool): Flag corresponding to check.
572 def handle_game_over(self, in_check: bool, promotion: bool, capture: bool, check: bool) -> None: 573 """Handles displaying notification of who won or if it was a stalemate. Sets game_over flag to True, and notates the end of the game. 574 575 Args: 576 in_check (bool): Flag corresponding to check. 577 promotion (bool): Flag corresponding to pawn promotion. 578 capture (bool): Flag corresponding to capturing other piece. 579 check (bool): Flag corresponding to check 580 """ 581 self.game_over = True 582 if in_check: 583 self.display_message(f'Checkmate {"White wins!" if self.current_turn == "b" else "Black wins!"}', 9) 584 if not promotion and self.clicked_figure: 585 self.moves_record.record_move(self.clicked_figure, capture=capture, previous_coords=self.previous_coords, check=True, checkmate=check and in_check) 586 else: 587 self.display_message('Stalemate', 9) 588 if not promotion and self.clicked_figure: 589 self.moves_record.record_move(self.clicked_figure, capture=capture, previous_coords=self.previous_coords, check=False, checkmate=check and in_check)
Handles displaying notification of who won or if it was a stalemate. Sets game_over flag to True, and notates the end of the game.
Arguments:
- in_check (bool): Flag corresponding to check.
- promotion (bool): Flag corresponding to pawn promotion.
- capture (bool): Flag corresponding to capturing other piece.
- check (bool): Flag corresponding to check
591 def notation_promotion(self, promotion:str) -> None: 592 """Helper function to note the promotion of the pawn. Calls pawn functions corresponding to choosing figure to promote to. Notates the promotion 593 594 Args: 595 promotion (str): Figure representation to which pawn was promoted. 596 """ 597 check = self.is_under_attack(self.get_king_position(self.current_turn), self.current_turn) 598 game_over, in_check = self.is_game_over() 599 if game_over: 600 if in_check: 601 self.display_message(f'Checkmate {"White wins!" if self.current_turn == "b" else "Black wins!"}', 9) 602 if self.clicked_figure: 603 self.moves_record.record_move(self.clicked_figure, capture=self.capture, previous_coords=self.previous_coords, check=check, checkmate=game_over and in_check, promotion=promotion[0]) 604 else: 605 self.display_message('Stalemate', 9) 606 if self.clicked_figure: 607 self.moves_record.record_move(self.clicked_figure, capture=self.capture, previous_coords=self.previous_coords, check=check, checkmate=game_over and in_check, promotion=promotion[0]) 608 else: 609 if self.clicked_figure: 610 self.moves_record.record_move(self.clicked_figure, capture=self.capture, previous_coords=self.previous_coords, check=check, checkmate=game_over and in_check, promotion=promotion[0]) 611 self.clicked_figure = None 612 self.previous_coords = None
Helper function to note the promotion of the pawn. Calls pawn functions corresponding to choosing figure to promote to. Notates the promotion
Arguments:
- promotion (str): Figure representation to which pawn was promoted.
614 def get_king_position(self, color: str) -> tuple[int, int]: 615 """Function returning king position on the board. Loop searching for king just breaks after finding king with correct color. 616 617 Args: 618 color (str): Color of the king. 619 620 Returns: 621 tuple[int, int]: Position of the king. 622 """ 623 for row in self.board: 624 for cell in row: 625 if isinstance(cell.figure, piece.King) and cell.figure.color == color: 626 return cell.figure.position 627 update_error_log(Exception('Not enough kings on the board, check the save file')) 628 Notification(self.master, 'No king on the board, check save file', 2, 'top') 629 self.game_over = True 630 self.master.after(2001, self.restart_game) 631 raise Exception('Not enough kings on the board, check the save file')
Function returning king position on the board. Loop searching for king just breaks after finding king with correct color.
Arguments:
- color (str): Color of the king.
Returns:
tuple[int, int]: Position of the king.
633 def reset_en_passant_flags(self, current_color: str) -> None: 634 """Helper function to reset en passant and first move flag. 635 636 Args: 637 current_color (str): Color of the current player. 638 """ 639 for row in self.board: 640 for cell in row: 641 if isinstance(cell.figure, piece.Pawn) and cell.figure.color != current_color: 642 cell.figure.moved_by_two = False 643 cell.figure.can_en_passant = False
Helper function to reset en passant and first move flag.
Arguments:
- current_color (str): Color of the current player.
645 def restart_game(self) -> None: 646 """Function restarting the game with all necessary flags and variables. 647 """ 648 self.loading_animation(0) 649 for child in self.winfo_children(): 650 self.master.after(1, child.destroy) 651 self.highlighted.clear() 652 self.clicked_figure = None 653 self.previous_coords = None 654 self.turns = self.turn() 655 self.current_turn = next(self.turns) 656 self.notification = None 657 self.game_over = False 658 self.board = self.create_board() 659 self.current_save_name = None
Function restarting the game with all necessary flags and variables.
661 def destroy_loading_screen(self) -> None: 662 """Destroys loading screen widget. 663 """ 664 def update_opacity(i: int) -> None: 665 if i >= 0 and self.loading_screen: 666 pywinstyles.set_opacity(self.loading_screen, value=i*0.005, color='#000001') 667 self.master.after(1, lambda: update_opacity(i - 1)) 668 else: 669 if self.loading_screen: 670 self.loading_screen.destroy() 671 self.loading_screen = None 672 if self.loading_screen: 673 update_opacity(200)
Destroys loading screen widget.
675 def loading_animation(self, i: int) -> None: 676 """Function to animate loading screen. 677 678 Args: 679 i (int, optional): Iteration value passed by recursive formula. Defaults to 0. 680 """ 681 if not self.loading_screen: 682 self.loading_screen = ctk.CTkFrame( 683 master = self.master, 684 fg_color = COLOR.BACKGROUND 685 ) 686 self.loading_screen.place(relx=0, rely=0, relwidth=1, relheight=1) 687 self.loading_text = ctk.CTkLabel( 688 master = self.loading_screen, 689 text = 'Loading ', 690 font = self.font_42, 691 text_color = COLOR.TEXT, 692 ) 693 self.loading_text.pack(side=ctk.TOP, expand=True) 694 self.master.after(90, lambda: self.loading_animation(0)) 695 else: 696 self.loading_text.configure(text=f'Loading{'.' * i}{' ' * (3 - i)}') 697 if i <= 2: 698 i += 1 699 self.master.after(90, lambda: self.loading_animation(i)) 700 else: 701 self.master.after(90, self.destroy_loading_screen)
Function to animate loading screen.
Arguments:
- i (int, optional): Iteration value passed by recursive formula. Defaults to 0.
703 def update_board(self) -> None: 704 """Updates all cells on the board 705 """ 706 for row in self.board: 707 for cell in row: 708 cell.update()
Updates all cells on the board
710 def load_board_from_file(self, file_info: dict, save_name: str) -> bool: 711 """Updates board to match the state from the save file. Ensures all save information are in the file in correct format. 712 713 Args: 714 file_info (dict): All needed information to load save. 715 716 Returns: 717 bool: Returns True if load was successful, False otherwise. 718 """ 719 self.master.after(1, lambda: self.loading_animation(0)) 720 save_keys: set[str] = {'current_turn', 'board_state', 'white_moves', 'black_moves', 'game_over'} 721 if not all(key in file_info for key in save_keys): 722 update_error_log(KeyError('Save file doesn\'t contain all necessary information')) 723 return False 724 king_w: int = 0 725 king_b: int = 0 726 for row in self.board: 727 for cell in row: 728 if cell.figure: 729 cell.figure = None 730 cell.update() 731 for key, value in file_info['board_state'].items(): 732 if not bool(re.match(r'[0-9]{1},[0-9]{1}', key)): 733 self.restart_game() 734 update_error_log(KeyError('Save file doesn\'t contain all necessary information')) 735 return False 736 try: 737 value[1] 738 value[2] 739 except: 740 self.restart_game() 741 update_error_log(KeyError('Save file doesn\'t contain all necessary information')) 742 return False 743 if not bool(re.match(r'[wb]{1}', value[1])): 744 self.restart_game() 745 update_error_log(KeyError('Save file doesn\'t contain all necessary information')) 746 return False 747 coord: tuple[int, ...] = tuple(map(int, key.split(','))) 748 x: int = coord[0] 749 y: int = coord[1] 750 match value[0]: 751 case 'Pawn': 752 pawn = piece.Pawn(value[1], self, (x, y), self.notation_promotion) 753 if not value[2]: 754 pawn.first_move = False 755 self.board[x][y].figure = pawn 756 case 'Knight': 757 self.board[x][y].figure = piece.Knight(value[1], self, (x, y)) 758 case 'Bishop': 759 self.board[x][y].figure = piece.Bishop(value[1], self, (x, y)) 760 case 'Rook': 761 rook = piece.Rook(value[1], self, (x, y)) 762 self.board[x][y].figure = rook 763 if not value[2]: 764 rook.first_move = False 765 case 'Queen': 766 self.board[x][y].figure = piece.Queen(value[1], self, (x, y)) 767 case 'King': 768 king = piece.King(value[1], self, (x, y)) 769 self.board[x][y].figure = king 770 if not value[2]: 771 king.first_move = False 772 king_w += 1 if value[1] == 'w' else 0 773 king_b += 1 if value[1] == 'b' else 0 774 self.master.after(1, self.board[x][y].update) 775 if king_w != 1 or king_b != 1: 776 update_error_log(KeyError('Save file doesn\'t contain proper amount of kings')) 777 return False 778 current_turn = str(file_info['current_turn']) 779 self.turns = self.turn() 780 if current_turn == 'b': 781 self.current_turn = next(self.turns) 782 self.current_turn = next(self.turns) 783 else: 784 self.current_turn = next(self.turns) 785 self.master.after(21, lambda: self.moves_record.load_notation_from_save(file_info['white_moves'], file_info['black_moves'])) 786 self.master.after(21, self.hide_clicked_figure_frame) 787 self.master.after(21, self.remove_highlights) 788 self.game_over = file_info['game_over'] 789 self.clicked_figure = None 790 self.current_save_name = save_name 791 return True
Updates board to match the state from the save file. Ensures all save information are in the file in correct format.
Arguments:
- file_info (dict): All needed information to load save.
Returns:
bool: Returns True if load was successful, False otherwise.