diff --git a/components/image_button.py b/components/image_button.py index 02a4eff..31da780 100644 --- a/components/image_button.py +++ b/components/image_button.py @@ -44,13 +44,16 @@ def __init__(self): class RestartButton(ImageButton): def __init__(self): super().__init__(resource_path("assets/icons/restart_button_icon.png"), 30, 30) + + # Cache both enabled and disabled images + self.enabled_image = self.image + disabled_img = pygame.image.load(resource_path("assets/icons/restart_button_icon_disabled.png")).convert_alpha() + self.disabled_image = pygame.transform.smoothscale(disabled_img, (30, 30)) def disable(self): - self.image = pygame.image.load(resource_path("assets/icons/restart_button_icon_disabled.png")).convert_alpha() - self.image = pygame.transform.smoothscale(self.image, (30, 30)) + self.image = self.disabled_image self.disabled = True def enable(self): - self.image = pygame.image.load(resource_path("assets/icons/restart_button_icon.png")).convert_alpha() - self.image = pygame.transform.smoothscale(self.image, (30, 30)) + self.image = self.enabled_image self.disabled = False \ No newline at end of file diff --git a/components/popup.py b/components/popup.py index 4f23da4..51e0685 100644 --- a/components/popup.py +++ b/components/popup.py @@ -21,6 +21,10 @@ def __init__(self, screen, message, button_type="ok", size=(300, 200), callbacks self.callbacks = callbacks if callbacks else {} self.buttons = self.create_buttons() self.visible = False + + # Cache the overlay surface to avoid creating it every frame + self.overlay = pygame.Surface(self.screen.get_size(), pygame.SRCALPHA) + self.overlay.fill((0, 0, 0, 180)) # Black with alpha for transparency @@ -54,10 +58,14 @@ def draw(self): if not self.visible: return - overlay = pygame.Surface(self.screen.get_size(), pygame.SRCALPHA) - overlay.fill((0, 0, 0, 180)) # Black with alpha for transparency + # Check if screen size has changed and recreate overlay if needed + current_size = self.screen.get_size() + if self.overlay.get_size() != current_size: + self.overlay = pygame.Surface(current_size, pygame.SRCALPHA) + self.overlay.fill((0, 0, 0, 180)) - self.screen.blit(overlay, (0, 0)) + # Use the cached overlay surface + self.screen.blit(self.overlay, (0, 0)) self.screen.blit(self.text, self.text_rect.move(self.popup_rect.topleft)) for text, button, _ in self.buttons: diff --git a/utils/board_pieces_manager.py b/utils/board_pieces_manager.py index 2260202..ff286f5 100644 --- a/utils/board_pieces_manager.py +++ b/utils/board_pieces_manager.py @@ -12,6 +12,9 @@ from components.popup import Popup from constants.fonts import CHECK_MATETEXT_MAIN +# Global cache for circle surfaces used in movement indicators +_circle_surface_cache = {} + def get_possible_positions(piece, color, board, x, y, king_moved, rook1_moved, rook2_moved): # Adjust positions based on player perspective @@ -31,17 +34,31 @@ def get_possible_positions(piece, color, board, x, y, king_moved, rook1_moved, r moves = [] # If the king is under check, filter moves to only include those that prevent check + # Optimize by storing original piece and using in-place modifications valid_moves = [] + opponent_color = "white" if color == "black" else "black" + original_piece = board[y-1][x-1] + + piece_code = f"{color[0]}{piece.piece_type.name[0]}" + if piece.piece_type == PieceType.KNIGHT: + piece_code = f"{color[0]}N" + piece_code = piece_code.upper() + for move in moves: - new_board = [row[:] for row in board] # Create a copy of the board - new_board[y-1][x-1] = "" # Remove the piece from the original position - piece_code = f"{color[0]}{piece.piece_type.name[0]}" - if piece.piece_type == PieceType.KNIGHT: - piece_code = f"{color[0]}N" - new_board[move[1]-1][move[0]-1] = piece_code.upper() # Place the piece in the new position - if not is_check(new_board, "white" if color == "black" else "black")[0]: - + # Store the target square's original piece + target_piece = board[move[1]-1][move[0]-1] + + # Make the move temporarily on the actual board + board[y-1][x-1] = "" + board[move[1]-1][move[0]-1] = piece_code + + # Check if this move prevents check + if not is_check(board, opponent_color)[0]: valid_moves.append(move) + + # Restore the board state + board[y-1][x-1] = original_piece + board[move[1]-1][move[0]-1] = target_piece moves = valid_moves @@ -122,11 +139,13 @@ def _draw_rectangle(self, x, y, color=(105, 176, 50)): pygame.draw.rect(self.screen, color, (x, y, self.square_size, self.square_size), 4) def _no_move_left(self): - if not self.is_under_check: return + if not self.is_under_check: + return False for piece in self.pieces: if piece[0].piece_color.name.lower() == self.turn: moves = get_possible_positions(piece[0], piece[0].piece_color.name.lower(), self.layout, piece[1], piece[2], False, False, False) - if moves: return False + if moves: + return False return True @@ -138,15 +157,19 @@ def _draw_circle(self, x, y): x_center = (x - 1) * self.square_size + self.square_size // 2 y_center = (y - 1) * self.square_size + self.square_size // 2 + self.board_top_bar_height - # Create a higher resolution surface (4 times the original size) - high_res_size = self.square_size * 4 - high_res_surface = pygame.Surface((high_res_size, high_res_size), pygame.SRCALPHA) + # Check if we have a cached circle surface for this size + if self.square_size not in _circle_surface_cache: + # Create a higher resolution surface (4 times the original size) + high_res_size = self.square_size * 4 + high_res_surface = pygame.Surface((high_res_size, high_res_size), pygame.SRCALPHA) + + # Draw the circle on the high resolution surface + pygame.draw.circle(high_res_surface, (105, 176, 50), (high_res_size // 2, high_res_size // 2), high_res_size // 6) - # Draw the circle on the high resolution surface - pygame.draw.circle(high_res_surface, (105, 176, 50), (high_res_size // 2, high_res_size // 2), high_res_size // 6) + # Scale the high resolution surface down to the original size and cache it + _circle_surface_cache[self.square_size] = pygame.transform.smoothscale(high_res_surface, (self.square_size, self.square_size)) - # Scale the high resolution surface down to the original size - scaled_surface = pygame.transform.smoothscale(high_res_surface, (self.square_size, self.square_size)) + scaled_surface = _circle_surface_cache[self.square_size] # Blit the scaled surface onto the main screen self.screen.blit(scaled_surface, (x_center - self.square_size // 2, y_center - self.square_size // 2)) @@ -311,9 +334,6 @@ def display(self): if self.is_check_mate: self._draw_checkmate_popup() self.reset_popup.draw() - - # Update the display once after all drawing operations - pygame.display.flip() # Handle the event if self.event: diff --git a/utils/movements/king.py b/utils/movements/king.py index b9835fa..28e1291 100644 --- a/utils/movements/king.py +++ b/utils/movements/king.py @@ -82,56 +82,36 @@ def king_moves(board, color, x, y, king_moved, rook1_moved, rook2_moved): # Castling logic if not king_moved: if color == 'white': + # Check if king is currently under attack (common check for both castling types) + king_not_attacked = not is_square_attacked(board, 5, 8, 'black') + # Kingside castling - if not rook2_moved and board[7][5] == "" and board[7][6] == "" and \ - not is_square_attacked(board, 5 + 1, 8, 'black') and \ - not is_square_attacked(board, 6 + 1, 8, 'black') and \ - not is_square_attacked(board, 4 + 1, 8, 'black'): # Check current square + if king_not_attacked and not rook2_moved and board[7][5] == "" and board[7][6] == "" and \ + not is_square_attacked(board, 6, 8, 'black') and \ + not is_square_attacked(board, 7, 8, 'black'): moves.append((7, 8)) # Queenside castling - if not rook1_moved and board[7][1] == "" and board[7][2] == "" and board[7][3] == "" and \ - not is_square_attacked(board, 3 + 1, 8, 'black') and \ - not is_square_attacked(board, 4 + 1, 8, 'black') and \ - not is_square_attacked(board, 4 + 1, 8, 'black'): # Check current square + if king_not_attacked and not rook1_moved and board[7][1] == "" and board[7][2] == "" and board[7][3] == "" and \ + not is_square_attacked(board, 4, 8, 'black') and \ + not is_square_attacked(board, 3, 8, 'black'): moves.append((3, 8)) else: + # Check if king is currently under attack (common check for both castling types) + king_not_attacked = not is_square_attacked(board, 5, 1, 'white') + # Kingside castling - if not rook2_moved and board[0][5] == "" and board[0][6] == "" and \ - not is_square_attacked(board, 5 + 1, 1, 'white') and \ - not is_square_attacked(board, 6 + 1, 1, 'white') and \ - not is_square_attacked(board, 4 + 1, 1, 'white'): # Check current square + if king_not_attacked and not rook2_moved and board[0][5] == "" and board[0][6] == "" and \ + not is_square_attacked(board, 6, 1, 'white') and \ + not is_square_attacked(board, 7, 1, 'white'): moves.append((7, 1)) # Queenside castling - if not rook1_moved and board[0][1] == "" and board[0][2] == "" and board[0][3] == "" and \ - not is_square_attacked(board, 3 + 1, 1, 'white') and \ - not is_square_attacked(board, 4 + 1, 1, 'white') and \ - not is_square_attacked(board, 4 + 1, 1, 'white'): # Check current square + if king_not_attacked and not rook1_moved and board[0][1] == "" and board[0][2] == "" and board[0][3] == "" and \ + not is_square_attacked(board, 4, 1, 'white') and \ + not is_square_attacked(board, 3, 1, 'white'): moves.append((3, 1)) return moves - - -def flatten(nested_list): - """ - Flattens a nested list into a single list, safely handling deeply nested structures. - - Args: - nested_list (list): The list to flatten. - - Returns: - list: A single flat list with all elements. - """ - result = [] - stack = [nested_list] - while stack: - current = stack.pop() - if isinstance(current, list): - stack.extend(reversed(current)) # Add elements in reverse order to process correctly - else: - result.append(current) - return result - def is_check(board, color): """ Checks if the given color is in check. @@ -141,51 +121,50 @@ def is_check(board, color): color (str): The color to check ('white' or 'black'). Returns: - bool: True if the color is in check, False otherwise. + tuple: (bool, tuple) - True if the color is in check and the king's position. """ - king_pos = None color = "white" if color == "black" else "black" + # Find king position for y, row in enumerate(board): for x, piece in enumerate(row): if piece.lower() == f"{color[0]}k": - king_pos = (x + 1, y + 1) - - # for row in board: - # for piece in row: - # elm = " " - # if piece != "": - # elm = piece - # print(elm, end=" ") - # print() - # print("\n","#"*50,"\n") + break + if king_pos: + break opponent_color = 'white' if color == 'black' else 'black' - opponents_possible_moves = [] - i = -1 - for p in flatten(board): - i+=1 - if p and p[0].lower() == opponent_color[0]: - piece_type = p[1].lower() - - x = i%8 + 1 - y = i//8 + 1 + + # Directly iterate over board without flattening + for y, row in enumerate(board): + for x, piece in enumerate(row): + if piece and piece[0].lower() == opponent_color[0]: + piece_type = piece[1].lower() + # Convert to 1-indexed coordinates + pos_x = x + 1 + pos_y = y + 1 - if piece_type == 'p': - opponents_possible_moves.extend(pawn_moves(board, opponent_color, x, y)) - elif piece_type == 'r': - opponents_possible_moves.extend(rook_moves(board, opponent_color, x, y)) - elif piece_type == 'n': - opponents_possible_moves.extend(knight_moves(board, opponent_color, x, y)) - elif piece_type == 'b': - opponents_possible_moves.extend(bishop_moves(board, opponent_color, x, y)) - elif piece_type == 'q': - opponents_possible_moves.extend(queen_moves(board, opponent_color, x, y)) - elif piece_type == 'k': - opponents_possible_moves.extend(king_moves(board, opponent_color, x, y, False, False, False)) + # Get moves for this piece + moves = [] + if piece_type == 'p': + moves = pawn_moves(board, opponent_color, pos_x, pos_y) + elif piece_type == 'r': + moves = rook_moves(board, opponent_color, pos_x, pos_y) + elif piece_type == 'n': + moves = knight_moves(board, opponent_color, pos_x, pos_y) + elif piece_type == 'b': + moves = bishop_moves(board, opponent_color, pos_x, pos_y) + elif piece_type == 'q': + moves = queen_moves(board, opponent_color, pos_x, pos_y) + elif piece_type == 'k': + moves = king_moves(board, opponent_color, pos_x, pos_y, False, False, False) + + # Early return if king is in the attack range of this piece + if king_pos in moves: + return True, king_pos - return (king_pos in opponents_possible_moves), king_pos + return False, king_pos \ No newline at end of file diff --git a/utils/pieces.py b/utils/pieces.py index 0f46e9c..ab8bf5c 100644 --- a/utils/pieces.py +++ b/utils/pieces.py @@ -14,8 +14,16 @@ class PieceColor(Enum): BLACK = 0 WHITE = 1 +# Global cache for the chess pieces image to avoid repeated loading +_piece_image_cache = None + +# Global cache for pre-scaled piece surfaces +_scaled_piece_cache = {} + class Piece: def __init__(self, screen: pygame.Surface, square_size: int, player: str, piece_type: PieceType, piece_color: PieceColor): + global _piece_image_cache + self.screen = screen self.square_size = square_size self.player = player @@ -24,7 +32,11 @@ def __init__(self, screen: pygame.Surface, square_size: int, player: str, piece_ self.piece_width = 128 # Assuming each piece is 128x128 pixels self.piece_height = 128 - self.image = pygame.image.load(resource_path("assets/chess_pieces_edited.png")).convert_alpha() + + # Load the image only once and cache it + if _piece_image_cache is None: + _piece_image_cache = pygame.image.load(resource_path("assets/chess_pieces_edited.png")).convert_alpha() + self.image = _piece_image_cache def _extract_piece(self): """ @@ -63,13 +75,16 @@ def display(self, x, y, board_top_bar_height: int, absolute_coordinates: bool = x = (x - 1) * self.square_size y = (y - 1) * self.square_size + board_top_bar_height - piece = self._extract_piece() - - # Calculate the new size for the piece - new_size = (self.square_size, self.square_size) - - # Resize the piece using smoothscale for better quality - resized_piece = pygame.transform.smoothscale(piece, new_size) + # Create a cache key based on piece type, color, and size + cache_key = (self.piece_type.value, self.piece_color.value, self.square_size) + + # Check if this piece at this size is already cached + if cache_key not in _scaled_piece_cache: + piece = self._extract_piece() + new_size = (self.square_size, self.square_size) + _scaled_piece_cache[cache_key] = pygame.transform.smoothscale(piece, new_size) + + resized_piece = _scaled_piece_cache[cache_key] # Display the resized piece on the screen self.screen.blit(resized_piece, (x, y)) \ No newline at end of file