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
class Cell(customtkinter.windows.widgets.ctk_label.CTkLabel):
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.
Cell( frame: customtkinter.windows.widgets.ctk_frame.CTkFrame, figure: piece.Piece | None, position: tuple[int, int], color: str, board)
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.
frame: customtkinter.windows.widgets.ctk_frame.CTkFrame
position: tuple[int, int]
board: Board
figure: None | piece.Piece
frame_around: customtkinter.windows.widgets.ctk_label.CTkLabel | None
cell_size
def on_click(self, event: Any) -> None:
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.
def update(self) -> None:
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)

Updates the asset shown on a cell.

class Board(customtkinter.windows.widgets.ctk_frame.CTkFrame):
 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.
Board(master, moves_record: menus.MovesRecord, size: int)
 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.
master: Any
loading_screen: customtkinter.windows.widgets.ctk_label.CTkLabel | None
font_name: str
font_42
frame_image: customtkinter.windows.widgets.image.ctk_image.CTkImage
def size(self):
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.

turns: Generator[str, NoneType, NoReturn]
current_turn
board_font
board: list[list[Cell]]
highlighted: list[Cell]
clicked_figure: piece.Piece | None
previous_coords: tuple[int, int] | None
notification: None | notifications.Notification
moves_record: menus.MovesRecord
capture: bool
game_over: bool
current_save_name: str | None
def load_sound(self):
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.

@staticmethod
def determine_tile_color(pos: tuple[int, int]) -> str:
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.

def create_outline_l_r_t(self) -> None:
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.

def create_board(self) -> list[list[Cell]]:
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.

def remove_highlights(self) -> None:
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.

def display_message(self, message: str, duration_sec: int) -> None:
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.
def is_game_over(self) -> tuple[bool, bool]:
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.

def handle_clicks(self, figure: piece.Piece, position: tuple[int, int]) -> None:
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.
def hide_clicked_figure_frame(self) -> None:
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.

def handle_chosen_figure_highlight(self, position: tuple[int, int]) -> None:
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_
def check_check(self, move_from: tuple[int, int], move_to: tuple[int, int]) -> bool:
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_

def is_under_attack(self, position: tuple[int, int], color: str) -> bool:
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.

def handle_move(self, position: tuple[int, int]) -> None:
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.
def handle_move_pawn(self, cell: Cell, row: int) -> bool:
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.

def handle_move_castle(self, row: int, col: int, prev_y: int) -> bool:
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.

def turn(self) -> Generator[str, NoneType, NoReturn]:
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.

def play_correct_sound(self, game_over: bool, capture: bool, castle: bool, check: bool) -> None:
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.
def handle_game_over( self, in_check: bool, promotion: bool, capture: bool, check: bool) -> None:
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
def notation_promotion(self, promotion: str) -> None:
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.
def get_king_position(self, color: str) -> tuple[int, int]:
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.

def reset_en_passant_flags(self, current_color: str) -> None:
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.
def restart_game(self) -> None:
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.

def destroy_loading_screen(self) -> None:
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.

def loading_animation(self, i: int) -> None:
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.
def update_board(self) -> None:
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

def load_board_from_file(self, file_info: dict, save_name: str) -> bool:
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.