Home » Tutorials » How to Build a Chess Game with Pygame in Python

How to Build a Chess Game with Pygame in Python

Chess is a game that has captured minds for centuries, blending strategy and skill with every move. Whether you’re a seasoned player or just getting started, building a digital version of this classic game can be a fun and rewarding challenge.

Today, you’ll dive into building your very own chess game using Pygame in Python. We’ll take it step by step, from setting up the game board and loading the chess pieces to implementing both player vs player and AI modes. Along the way, we’ll also tackle key gameplay mechanics like handling checkmate and stalemate scenarios, so by the end, you’ll have a fully functional chess game ready to go!

Table of Contents

Getting Started

Don’t forget to install these libraries to ensure everything runs smoothly!

$ pip install pygame
$ pip install python-chess

First, let’s bring in the tools we need. We’ll start by importing the libraries that make everything tick:

  • Pygame helps us create the graphical interface where the chess game will play out.
  • We use the chess library to handle board representation and move validation—it makes the chess logic much simpler.
  • Finally, random will allow us to make random moves when necessary.
import pygame
import chess
import random

Setting Up Pygame

Now that we have our tools ready, let’s set up Pygame. This step essentially tells Pygame to get everything initialized and ready for use.

# Initialize Pygame
pygame.init()

Next, we need to create the window where the chess game will appear. We’ll define the size of the window (800×500 by default), make it resizable to fit any screen, and give it a distinct title so you know you’re playing “Chess – The Pycodes“.

# Set up display with resizable window
width, height = 800, 500  # Default size
window = pygame.display.set_mode((width, height), pygame.RESIZABLE)
pygame.display.set_caption("Chess - The Pycodes")

Loading and Resizing Chess Piece Images

It’s time to load the images of the chess pieces—after all, we can’t play without them! We’ll create an image dictionary to store all the chess piece images using their shorthand symbols. This way, we can easily reference them later in our game.

# Load images for pieces
images = {}
image_files = {
   'p': 'images/p.png',  # White pawn
   'n': 'images/n.png',  # White knight
   'b': 'images/b.png',  # White bishop
   'r': 'images/r.png',  # White rook
   'q': 'images/q.png',  # White queen
   'k': 'images/k.png',  # White king
   'P1': 'images/P1.png',  # Black pawn
   'N1': 'images/N1.png',  # Black knight
   'B1': 'images/B1.png',  # Black bishop
   'R1': 'images/R1.png',  # Black rook
   'Q1': 'images/Q1.png',  # Black queen
   'K1': 'images/K1.png'  # Black king
}

To make sure these pieces fit perfectly on the chessboard, we’ve added a load_images() function. This function will load the images into our dictionary and resize them to match the square size of the board.

def load_images(square_size):
   """Load chess piece images into the images dictionary, scaled to the square size."""
   for symbol, file in image_files.items():
       try:
           images[symbol] = pygame.transform.scale(pygame.image.load(file), (square_size, square_size))
       except pygame.error as e:
           print(f"Error loading image for {symbol}: {e}")

Creating the Menu and Drawing the Chess Board

With our game framework in place, we can now explore the exciting part of creating the menu screen, where players will choose their game options and set the stage for their chess adventure:

To start, let’s create a handy helper function called draw_text(). This function will help us display text on the screen. It takes in the text we want to show, the font style, the color, and the position where we want it to appear. Pretty straightforward, right?

def draw_text(text, font, color, x, y):
   """Helper function to draw text on the screen."""
   screen_text = font.render(text, True, color)
   window.blit(screen_text, (x, y))

Next up, we have the menu_screen() function. This is where the magic happens! Here, we’re setting up the menu that greets players and presents their options.

  • First, we define the fonts we’ll use for the menu. We also set some initial variables, like selected_mode, selected_difficulty, and selected_side.
def menu_screen():
   """Menu screen to choose game options."""
   font = pygame.font.Font(None, 60)
   small_font = pygame.font.Font(None, 40)
   selected_mode = None
   selected_difficulty = 'medium'
   selected_side = 'w'
  • Inside a loop, we fill the window with a black background. Then, we call our draw_text() function to show the title “Chess Game” right at the top.
   running = True
   while running:
       window.fill(pygame.Color("black"))
       draw_text("Chess Game", font, pygame.Color("white"), 280, 50)
  • Now, let’s get into the options! Here, we give players the choice between “Player vs Player” or “Player vs Computer” by drawing more text on the screen.
       # Game mode options
       draw_text("1. Player vs Player", small_font, pygame.Color("white"), 250, 150)
       draw_text("2. Player vs Computer", small_font, pygame.Color("white"), 250, 200)
  • If a player selects “Player vs Computer”, we show the current difficulty level and let them know how to change it. They can cycle through easy, medium, and hard by pressing a “D” key.
       # Difficulty options (Only if Player vs Computer is selected)
       if selected_mode == 'computer':
           draw_text(f"Difficulty: {selected_difficulty.capitalize()}", small_font, pygame.Color("white"), 250, 250)
           draw_text("Press D to toggle difficulty", small_font, pygame.Color("white"), 250, 300)
  • Next, we allow players to choose their side. This means they can pick whether they want to play as White or Black pieces. The screen will show their current selection, and they can switch sides by pressing another “S” key.
       # Side selection
       draw_text(f"Your side: {'White' if selected_side == 'w' else 'Black'}", small_font, pygame.Color("white"), 250, 350)
       draw_text("Press S to toggle side", small_font, pygame.Color("white"), 250, 400)
  • Lastly, we let players know how to start the game by pressing the “ENTER” key. This keeps the excitement building!
       draw_text("Press ENTER to start", small_font, pygame.Color("white"), 250, 500)
  • Now, let’s listen for any player input. If they decide to quit, we’ll close the game. Otherwise, we handle key presses to set the selected mode, toggle difficulty, switch sides, and eventually exit the menu when they’re ready to play.
       for event in pygame.event.get():
           if event.type == pygame.QUIT:
               running = False
               pygame.quit()
               return None, None, None  # Exit the game

           if event.type == pygame.KEYDOWN:
               if event.key == pygame.K_1:
                   selected_mode = 'player'
               elif event.key == pygame.K_2:
                   selected_mode = 'computer'
               elif event.key == pygame.K_d and selected_mode == 'computer':
                   # Toggle difficulty
                   if selected_difficulty == 'easy':
                       selected_difficulty = 'medium'
                   elif selected_difficulty == 'medium':
                       selected_difficulty = 'hard'
                   else:
                       selected_difficulty = 'easy'
               elif event.key == pygame.K_s:
                   # Toggle player side
                   selected_side = 'b' if selected_side == 'w' else 'w'
               elif event.key == pygame.K_RETURN and selected_mode is not None:
                   running = False  # Exit the menu
  • Once we’ve wrapped up the menu, we refresh the display to show any updates and return the selected options. This ensures that everything flows seamlessly as players embark on their chess adventure!
       pygame.display.flip()

   return selected_mode, selected_difficulty, selected_side

Awesome! Now that you’ve chosen all your options from the menu, the game smoothly takes you to the main board display. This is where all the action happens! The draw_board() function steps in, bringing the chessboard to life with a beautiful mix of white and gray squares. It makes sure each chess piece is placed just right, thanks to the chess library.

And here’s where it gets even cooler: the highlight_square() function helps you see the last move made and the square you’ve selected. Plus, the get_custom_piece_symbol() function grabs the right symbol for each piece (like ‘n’ for knight) and puts it exactly where it should go on the board. Everything is lined up for an exciting game ahead!

def draw_board(board, square_size, selected_square=None, last_move=None):
   """Draw the chess board and pieces, adjusting to the current square size."""
   colors = [pygame.Color("white"), pygame.Color("gray")]
   for row in range(8):
       for col in range(8):
           color = colors[(row + col) % 2]
           pygame.draw.rect(window, color, pygame.Rect(col * square_size, row * square_size, square_size, square_size))


           # Draw pieces on the board
           piece = board.piece_at(chess.square(col, 7 - row))
           if piece:
               custom_symbol = get_custom_piece_symbol(piece)
               piece_image = images.get(custom_symbol)
               if piece_image:
                   window.blit(piece_image, (col * square_size, row * square_size))


   # Highlight the last move (from and to squares)
   if last_move:
       highlight_square(last_move.from_square, square_size, color="blue")
       highlight_square(last_move.to_square, square_size, color="blue")


   # Highlight the selected square
   if selected_square is not None:
       highlight_square(selected_square, square_size)

Once the menu and chess board are established, we can now focus on how we handle highlights and legal moves to elevate the gameplay experience.

So, to give players feedback on their actions in the chess game, nothing beats some good highlights! To achieve this, we’ve created a few functions, each serving a specific purpose:

  • The first one is highlight_square(), which highlights the selected square. It retrieves the square coordinates of the pressed piece using chess.square_file() and chess.square_rank(). Once it has that information, it draws a bright yellow rectangle on that square with a border width of 3 using pygame.draw.rect().
def highlight_square(square, square_size, color="yellow"):
   """Highlight the selected square."""
   col, row = chess.square_file(square), 7 - chess.square_rank(square)
   pygame.draw.rect(window, pygame.Color(color), pygame.Rect(col * square_size, row * square_size, square_size, square_size), 3)
  • Next up is our second function, highlight_check(). This one adds a bit of drama and a sense of danger to the game. It keeps a close eye on the king’s square while also checking whose turn it is to play. If it finds out that the king is in check, thanks to the board.is_check() function, it calls the highlight_square() function to highlight that square in a bold red.
def highlight_check(board, square_size):
   """Highlight the king if in check."""
   if board.is_check():
       king_square = board.king(board.turn)
       highlight_square(king_square, square_size, color="red")
  • Then we have the third function, highlight_legal_moves(), which is pretty self-explanatory by its name! Essentially, it uses board.legal_moves to gather all the possible moves for the current player. It iterates through these moves to find those associated with the selected square and highlights them with a blue circle at the center of each square using pygame.draw.circle().
def highlight_legal_moves(board, selected_square, square_size):
   """Highlight legal moves for the selected piece."""
   legal_moves = board.legal_moves
   for move in legal_moves:
       if move.from_square == selected_square:
           row = 7 - chess.square_rank(move.to_square)
           col = chess.square_file(move.to_square)
           pygame.draw.circle(window, pygame.Color("blue"), (col * square_size + square_size // 2, row * square_size + square_size // 2), square_size // 6)

By combining these three functions, we’re able to make this chess game not just playable but also an immersive experience!

Evaluating Moves, Piece Values, and Pawn Promotion

Custom Piece Symbols and Assigning Piece Values

In a regular chess game, notation is typically based on lowercase letters like ‘n’ or ‘p’ for white pieces, and uppercase letters like ‘Q’ or ‘K’ for black pieces. But here, we decided to add a unique twist to the black pieces’ symbols by appending ‘1’ to their notation. So, for example, ‘Q’ becomes ‘Q1’, adding a bit of flair. We do this using the get_custom_piece_symbol() function. Because we can, why not?

def get_custom_piece_symbol(piece):
    """Map python-chess piece symbols to custom notation."""
    symbol = piece.symbol()
    if piece.color == chess.WHITE:
        return symbol.lower()
    else:
        return symbol.upper() + '1'

Now that the symbols are customized, the next task is to assign values to each piece. In chess, pieces like the Queen or Rook are more valuable than pawns. We map each piece to its value using the piece_value() function. This dictionary will help the computer understand how important each piece is when evaluating the board.

def piece_value(piece):
    """Assign a value to each piece type."""
    piece_values = {
        chess.PAWN: 1,
        chess.KNIGHT: 3,
        chess.BISHOP: 3,
        chess.ROOK: 5,
        chess.QUEEN: 9,
        chess.KING: 1000  # King has an artificially high value to avoid danger
    }
    return piece_values.get(piece.piece_type, 0)

Board Evaluation and Threat Detection

A crucial aspect of chess is evaluating the material on the board—this helps in determining which side has the upper hand. We created the evaluate_board() function to assess the current state of the board. It calculates the total evaluation score by summing up the values of the white pieces and subtracting the values of the black pieces.

If the score is positive, it means white is leading; if it’s negative, black has the advantage. The king has a high value (1000) since losing it results in a loss of the game.

def evaluate_board(board):
    """A simple evaluation function that sums up the values of pieces on the board."""
    piece_values = {
        chess.PAWN: 1,
        chess.KNIGHT: 3,
        chess.BISHOP: 3,
        chess.ROOK: 5,
        chess.QUEEN: 9,
        chess.KING: 1000
    }
    score = 0
    for square in chess.SQUARES:
        piece = board.piece_at(square)
        if piece:
            value = piece_values.get(piece.piece_type, 0)
            if piece.color == chess.WHITE:
                score += value  # Add value for White pieces
            else:
                score -= value  # Subtract value for Black pieces
    return score

Besides material evaluation, protecting pieces from being threatened is essential. The is_piece_threatened() function helps in checking whether a piece on a particular square is under attack. It uses board.attackers() from the python-chess library to detect threats.

def is_piece_threatened(board, square):
    """Check if a piece on a given square is attacked by the opponent."""
    attackers = board.attackers(not board.piece_at(square).color, square)
    return bool(attackers)

Move Evaluation

The next step is to determine whether a move is good or bad, as this can turn the tide of the game. The evaluate_move() function evaluates a move by simulating it on the board with board.push(), and then calling evaluate_board() to assess the new board state.

If a valuable opponent’s piece is captured, the score increases. On the other hand, if the move exposes your piece to danger, the score decreases. After this analysis, the function uses board.pop() to undo the move, leaving the game state unchanged.

def evaluate_move(board, move):
    """Evaluate the value of a move based on material gain/loss and board state."""
    board.push(move)

    # Basic evaluation by counting material (more complex evaluation can be added)
    score = evaluate_board(board)

    # Adjust score for different piece values
    piece_values = {
        chess.PAWN: 1,
        chess.KNIGHT: 3,
        chess.BISHOP: 3,
        chess.ROOK: 5,
        chess.QUEEN: 9,
        chess.KING: 1000  # King is invaluable
    }

    # Prioritize capturing opponent pieces
    captured_piece = board.piece_at(move.to_square)
    if captured_piece:
        score += piece_values.get(captured_piece.piece_type, 0)  # Gain value from capture

    # Penalize if moving a piece to a threatened square
    moving_piece = board.piece_at(move.from_square)
    if moving_piece and board.is_attacked_by(not board.turn, move.to_square):
        score -= piece_values.get(moving_piece.piece_type, 0)

    board.pop()  # Undo the move
    return score

Generating the Best Move

The heart of our chess AI is the computer_move() function, which generates the best possible move for the computer. It starts by retrieving all legal moves, checks if the game is over, and then evaluates each move using evaluate_move().

If the computer plays white, it tries to maximize the score; if black, it minimizes the score. The function ultimately selects the best move or falls back on a random legal move if no move stands out.

def computer_move(board, difficulty):
    """Generate the best move for the computer, ensuring proper evaluation for both White and Black."""
    legal_moves = list(board.legal_moves)

    if board.is_game_over():
        return None  # No move if the game is over

    best_move = None
    if board.turn == chess.WHITE:
        best_value = -float('inf')  # White aims to maximize
    else:
        best_value = float('inf')  # Black aims to minimize

    for move in legal_moves:
        move_value = evaluate_move(board, move)

        if board.turn == chess.WHITE:
            if move_value > best_value:
                best_value = move_value
                best_move = move
        else:
            if move_value < best_value:
                best_value = move_value
                best_move = move

    # Fallback to a random move if no best move found
    if not best_move:
        best_move = random.choice(legal_moves)

    return best_move

Pawn Promotion

Pawns, while undervalued, can become game-changers when they reach the end of the board. Our AI handles pawn promotion with the promote_pawn() function. When a pawn reaches rank 7 (for white) or rank 0 (for black), it calls choose_promotion(), allowing the player to promote the pawn to any piece they choose.

def promote_pawn(board, move):
    """Promote a pawn if it reaches the opposite end of the board."""
    piece = board.piece_at(move.from_square)

    # Check if the move is a pawn reaching the promotion rank
    if piece and piece.piece_type == chess.PAWN:
        promotion_rank_white = chess.square_rank(move.to_square) == 7
        promotion_rank_black = chess.square_rank(move.to_square) == 0

        if (piece.color == chess.WHITE and promotion_rank_white) or (
                piece.color == chess.BLACK and promotion_rank_black):
            return choose_promotion()  # Prompt player for promotion choice
    return None

The choose_promotion() function provides the player with a choice of which piece they’d like to promote their pawn to, with a default promotion to Queen if no choice is made.

def choose_promotion():
    """Prompt the player to choose the promotion piece."""
    font = pygame.font.Font(None, 40)
    prompt = True
    promotion_choice = None

    while prompt:
        window.fill(pygame.Color("black"))
        draw_text("Choose Promotion Piece", font, pygame.Color("white"), 200, 200)
        draw_text("Q: Queen, R: Rook, B: Bishop, N: Knight", font, pygame.Color("white"), 150, 300)
        pygame.display.flip()

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                return chess.QUEEN  # Default promotion to Queen if the game is quit

            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_q:
                    promotion_choice = chess.QUEEN
                elif event.key == pygame.K_r:
                    promotion_choice = chess.ROOK
                elif event.key == pygame.K_b:
                    promotion_choice = chess.BISHOP
                elif event.key == pygame.K_n:
                    promotion_choice = chess.KNIGHT

        if promotion_choice:
            prompt = False

    return promotion_choice

What Happens When the Game Ends?

When it’s time to bring the game to a close, we need to do it smoothly. That’s where the checkmate_screen() function comes in. It takes a winner argument so it knows how to display the game result. Two fonts are used here: a large one for showing who won or if it’s a draw, and a smaller one for instructions. Once the game reaches either a checkmate or stalemate, this function loops until the player decides what to do next.

def checkmate_screen(winner):
    """Display a screen when checkmate or stalemate occurs, showing the winner."""
    font = pygame.font.Font(None, 80)  # Larger font for results
    small_font = pygame.font.Font(None, 40)  # Smaller font for instructions
    running = True

Next, the game window gets a black background, and depending on the game outcome, it displays the appropriate message. If it’s a stalemate, the message will say “It’s a Draw!” but if one player won by checkmate, the message will announce the winner. At the same time, the screen will also show instructions on how to restart or quit the game.

    while running:
        window.fill(pygame.Color("black"))  # Set background to black

        if winner == 'draw':
            draw_text("Stalemate! It's a Draw!", font, pygame.Color("white"), 130, 200)
        else:
            draw_text(f"Checkmate! {winner} Wins!", font, pygame.Color("white"), 130, 200)

        draw_text("Press R to Restart", small_font, pygame.Color("white"), 150, 400)
        draw_text("Press Q to Quit", small_font, pygame.Color("white"), 150, 440)

The loop continues to listen for player actions. If the player presses the R key, the function will return True, signaling that the game should restart. If they press the Q key, it returns False, meaning the game should quit. If the player clicks the close button, the game will also exit.

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
                pygame.quit()

            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_r:
                    return True  # Restart game
                elif event.key == pygame.K_q:
                    return False  # Quit game

In the end, pygame.display.flip() is used to make sure everything on the screen stays updated while waiting for player input. Once the player makes a choice, the loop ends, and the function returns False, breaking out of the game loop.

        pygame.display.flip()

    return False  # End the game if no action

Main Function

We have finally reached the core of our chess game, the one that manages everything the main() function, let’s break it down :

  • First, we kick things off by creating the main function, which is the heart of our chess game. It handles setting up the game window, managing the game loop, responding to player input, and checking for the endgame conditions like checkmate or stalemate.
def main():

Setting Up the Game Window

We start by setting up the main game window, defining its initial size (800×800 pixels), and allowing it to be resized. This is where the game will be displayed, and it’s made resizable for flexibility.

   DEFAULT_WIDTH, DEFAULT_HEIGHT = 800, 800  # Default size
   window = pygame.display.set_mode((DEFAULT_WIDTH, DEFAULT_HEIGHT), pygame.RESIZABLE)
   pygame.display.set_caption("Chess - The Pycodes")
   width, height = window.get_size()
   square_size = width // 8

Loading the Chess Piece Images

Next, we load the chess pieces. We adjust their size to fit perfectly on the board, with each square’s size calculated by dividing the window width by 8.

   load_images(square_size)

Choosing Game Mode and Setup

Here, we call the menu_screen() function, which allows the player to choose the game mode (like Player vs. Computer) and set difficulty or which side to play. If the player exits from the menu, the game will quit before proceeding.

   game_mode, difficulty, player_side = menu_screen()
   board = chess.Board()
   selected_square = None
   last_move = None
   running = True

   if game_mode is None:  # If the game is exited from the menu
       return

Managing the Game Loop

Once everything is set up, we enter the main game loop, which controls the flow of the game. We adjust the square size dynamically in case the window is resized and ensure the board always fits neatly within the window.

   clock = pygame.time.Clock()

   while running:
       width, height = window.get_size()
       square_size = min(width, height) // 8  # Ensure the board fits in the window

Drawing the Board and Highlighting Moves

In every iteration of the loop, we call the draw_board() function to render the board and pieces. We also highlight checks and any legal moves for the currently selected piece.

       draw_board(board, square_size, selected_square, last_move)
       highlight_check(board, square_size)

       if selected_square:
           highlight_legal_moves(board, selected_square, square_size)
       pygame.display.flip()

Handling Player Input

Next, we handle the player’s input using pygame.event.get(). If the player clicks on a square, the game checks if a piece is selected or if a move is attempted. We also check for window resizing or the player trying to quit the game.

       for event in pygame.event.get():
           if event.type == pygame.QUIT:
               running = False
           elif event.type == pygame.VIDEORESIZE:  # Handle window resizing
               window = pygame.display.set_mode((event.w, event.h), pygame.RESIZABLE)
           elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:  # Left mouse click
               col, row = event.pos[0] // square_size, event.pos[1] // square_size
               clicked_square = chess.square(col, 7 - row)

Making a Move

Once the player selects a square, the game checks if a move is legal using board.legal_moves. If the move is valid, it gets executed. If it’s invalid, the selection is cleared.

               if selected_square is None:
                   if board.piece_at(clicked_square) and board.piece_at(clicked_square).color == board.turn:
                       selected_square = clicked_square
                       highlight_legal_moves(board, selected_square, square_size)
               else:
                   move = chess.Move(selected_square, clicked_square, promotion=promote_pawn(board, chess.Move(selected_square, clicked_square)))
                   if move in board.legal_moves:
                       last_move = move
                       board.push(move)
                       selected_square = None

Checking for Endgame Conditions

Once a move is made, we check for game-ending conditions like checkmate or stalemate using board.is_checkmate() and board.is_stalemate(). If either occurs, the checkmate_screen() function is triggered to display the result, and the player can choose to restart or quit.

                       if board.is_checkmate():
                           winner = "White" if board.turn == chess.BLACK else "Black"
                           if not checkmate_screen(winner):
                               running = False
                           else:
                               return main()  # Restart game

                       elif board.is_stalemate():
                           if not checkmate_screen("draw"):
                               running = False
                           else:
                               return main()  # Restart game

Computer’s Turn (For Player vs Computer Mode)

If the player is facing the computer, and it’s the computer’s turn, we simulate thinking with a short delay and let the computer make its move.

       if game_mode == 'computer' and board.turn != (player_side == 'w'):
           pygame.time.delay(1000)  # Add delay to simulate thinking
           move = computer_move(board, difficulty)
           if move:
               last_move = move
               board.push(move)

Capping the Frame Rate

We cap the frame rate at 60 frames per second to ensure smooth gameplay.

       clock.tick(60)

Quitting the Game

Finally, if the player quits the game, we call pygame.quit() to close everything properly.

   pygame.quit()

Main Block

if __name__ == "__main__":
   main()

This part ensures that the code only runs when executed directly, and not when it’s imported as a module.

Example

You can find the chess pieces available for download on GitHub.

Conclusion

While this chess game code is relatively simple and not overly complex, it forms the backbone of any chess engine. Once you have these basic mechanics in place, you can add more advanced features like deeper AI logic, enhanced move evaluations, and more polished visuals or interactions. Overall, it serves as a great stepping stone.

Full Code

import pygame
import chess
import random


# Initialize Pygame
pygame.init()


# Set up display with resizable window
width, height = 800, 500  # Default size
window = pygame.display.set_mode((width, height), pygame.RESIZABLE)
pygame.display.set_caption("Chess - The Pycodes")


# Load images for pieces
images = {}
image_files = {
   'p': 'images/p.png',  # White pawn
   'n': 'images/n.png',  # White knight
   'b': 'images/b.png',  # White bishop
   'r': 'images/r.png',  # White rook
   'q': 'images/q.png',  # White queen
   'k': 'images/k.png',  # White king
   'P1': 'images/P1.png',  # Black pawn
   'N1': 'images/N1.png',  # Black knight
   'B1': 'images/B1.png',  # Black bishop
   'R1': 'images/R1.png',  # Black rook
   'Q1': 'images/Q1.png',  # Black queen
   'K1': 'images/K1.png'  # Black king
}


def load_images(square_size):
   """Load chess piece images into the images dictionary, scaled to the square size."""
   for symbol, file in image_files.items():
       try:
           images[symbol] = pygame.transform.scale(pygame.image.load(file), (square_size, square_size))
       except pygame.error as e:
           print(f"Error loading image for {symbol}: {e}")


def draw_text(text, font, color, x, y):
   """Helper function to draw text on the screen."""
   screen_text = font.render(text, True, color)
   window.blit(screen_text, (x, y))


def menu_screen():
   """Menu screen to choose game options."""
   font = pygame.font.Font(None, 60)
   small_font = pygame.font.Font(None, 40)
   selected_mode = None
   selected_difficulty = 'medium'
   selected_side = 'w'


   running = True
   while running:
       window.fill(pygame.Color("black"))


       draw_text("Chess Game", font, pygame.Color("white"), 280, 50)


       # Game mode options
       draw_text("1. Player vs Player", small_font, pygame.Color("white"), 250, 150)
       draw_text("2. Player vs Computer", small_font, pygame.Color("white"), 250, 200)


       # Difficulty options (Only if Player vs Computer is selected)
       if selected_mode == 'computer':
           draw_text(f"Difficulty: {selected_difficulty.capitalize()}", small_font, pygame.Color("white"), 250, 250)
           draw_text("Press D to toggle difficulty", small_font, pygame.Color("white"), 250, 300)


       # Side selection
       draw_text(f"Your side: {'White' if selected_side == 'w' else 'Black'}", small_font, pygame.Color("white"), 250, 350)
       draw_text("Press S to toggle side", small_font, pygame.Color("white"), 250, 400)


       draw_text("Press ENTER to start", small_font, pygame.Color("white"), 250, 500)


       for event in pygame.event.get():
           if event.type == pygame.QUIT:
               running = False
               pygame.quit()
               return None, None, None  # Exit the game


           if event.type == pygame.KEYDOWN:
               if event.key == pygame.K_1:
                   selected_mode = 'player'
               elif event.key == pygame.K_2:
                   selected_mode = 'computer'
               elif event.key == pygame.K_d and selected_mode == 'computer':
                   # Toggle difficulty
                   if selected_difficulty == 'easy':
                       selected_difficulty = 'medium'
                   elif selected_difficulty == 'medium':
                       selected_difficulty = 'hard'
                   else:
                       selected_difficulty = 'easy'
               elif event.key == pygame.K_s:
                   # Toggle player side
                   selected_side = 'b' if selected_side == 'w' else 'w'
               elif event.key == pygame.K_RETURN and selected_mode is not None:
                   running = False  # Exit the menu


       pygame.display.flip()


   return selected_mode, selected_difficulty, selected_side


def draw_board(board, square_size, selected_square=None, last_move=None):
   """Draw the chess board and pieces, adjusting to the current square size."""
   colors = [pygame.Color("white"), pygame.Color("gray")]
   for row in range(8):
       for col in range(8):
           color = colors[(row + col) % 2]
           pygame.draw.rect(window, color, pygame.Rect(col * square_size, row * square_size, square_size, square_size))


           # Draw pieces on the board
           piece = board.piece_at(chess.square(col, 7 - row))
           if piece:
               custom_symbol = get_custom_piece_symbol(piece)
               piece_image = images.get(custom_symbol)
               if piece_image:
                   window.blit(piece_image, (col * square_size, row * square_size))


   # Highlight the last move (from and to squares)
   if last_move:
       highlight_square(last_move.from_square, square_size, color="blue")
       highlight_square(last_move.to_square, square_size, color="blue")


   # Highlight the selected square
   if selected_square is not None:
       highlight_square(selected_square, square_size)


def highlight_square(square, square_size, color="yellow"):
   """Highlight the selected square."""
   col, row = chess.square_file(square), 7 - chess.square_rank(square)
   pygame.draw.rect(window, pygame.Color(color), pygame.Rect(col * square_size, row * square_size, square_size, square_size), 3)


def highlight_check(board, square_size):
   """Highlight the king if in check."""
   if board.is_check():
       king_square = board.king(board.turn)
       highlight_square(king_square, square_size, color="red")


def highlight_legal_moves(board, selected_square, square_size):
   """Highlight legal moves for the selected piece."""
   legal_moves = board.legal_moves
   for move in legal_moves:
       if move.from_square == selected_square:
           row = 7 - chess.square_rank(move.to_square)
           col = chess.square_file(move.to_square)
           pygame.draw.circle(window, pygame.Color("blue"), (col * square_size + square_size // 2, row * square_size + square_size // 2), square_size // 6)




def get_custom_piece_symbol(piece):
   """Map python-chess piece symbols to custom notation."""
   symbol = piece.symbol()
   if piece.color == chess.WHITE:
       return symbol.lower()
   else:
       return symbol.upper() + '1'


def piece_value(piece):
   """Assign a value to each piece type."""
   piece_values = {
       chess.PAWN: 1,
       chess.KNIGHT: 3,
       chess.BISHOP: 3,
       chess.ROOK: 5,
       chess.QUEEN: 9,
       chess.KING: 1000  # King has an artificially high value to avoid danger
   }
   return piece_values.get(piece.piece_type, 0)


def evaluate_board(board):
   """A simple evaluation function that sums up the values of pieces on the board."""
   piece_values = {
       chess.PAWN: 1,
       chess.KNIGHT: 3,
       chess.BISHOP: 3,
       chess.ROOK: 5,
       chess.QUEEN: 9,
       chess.KING: 1000
   }
   score = 0
   for square in chess.SQUARES:
       piece = board.piece_at(square)
       if piece:
           value = piece_values.get(piece.piece_type, 0)
           if piece.color == chess.WHITE:
               score += value  # Add value for White pieces
           else:
               score -= value  # Subtract value for Black pieces
   return score


def is_piece_threatened(board, square):
   """Check if a piece on a given square is attacked by the opponent."""
   attackers = board.attackers(not board.piece_at(square).color, square)
   return bool(attackers)




def evaluate_move(board, move):
   """Evaluate the value of a move based on material gain/loss and board state."""
   board.push(move)


   # Basic evaluation by counting material (more complex evaluation can be added)
   score = evaluate_board(board)


   # Adjust score for different piece values
   piece_values = {
       chess.PAWN: 1,
       chess.KNIGHT: 3,
       chess.BISHOP: 3,
       chess.ROOK: 5,
       chess.QUEEN: 9,
       chess.KING: 1000  # King is invaluable
   }


   # Prioritize capturing opponent pieces
   captured_piece = board.piece_at(move.to_square)
   if captured_piece:
       score += piece_values.get(captured_piece.piece_type, 0)  # Gain value from capture


   # Penalize if moving a piece to a threatened square, but first ensure the piece exists
   moving_piece = board.piece_at(move.from_square)
   if moving_piece and board.is_attacked_by(not board.turn, move.to_square):
       score -= piece_values.get(moving_piece.piece_type, 0)


   board.pop()  # Undo the move
   return score


def computer_move(board, difficulty):
   """Generate the best move for the computer, ensuring proper evaluation for both White and Black."""
   legal_moves = list(board.legal_moves)


   if board.is_game_over():
       return None  # No move if the game is over


   best_move = None
   if board.turn == chess.WHITE:
       best_value = -float('inf')  # White aims to maximize
   else:
       best_value = float('inf')  # Black aims to minimize


   for move in legal_moves:
       move_value = evaluate_move(board, move)


       if board.turn == chess.WHITE:
           if move_value > best_value:
               best_value = move_value
               best_move = move
       else:
           if move_value < best_value:
               best_value = move_value
               best_move = move


   # Fallback to a random move if no best move found
   if not best_move:
       best_move = random.choice(legal_moves)


   return best_move


def promote_pawn(board, move):
  """Promote a pawn if it reaches the opposite end of the board."""
  piece = board.piece_at(move.from_square)



  # Check if the move is a pawn reaching the promotion rank
  if piece and piece.piece_type == chess.PAWN:
      promotion_rank_white = chess.square_rank(move.to_square) == 7
      promotion_rank_black = chess.square_rank(move.to_square) == 0




      if (piece.color == chess.WHITE and promotion_rank_white) or (
              piece.color == chess.BLACK and promotion_rank_black):
          return choose_promotion()  # Prompt player for promotion choice
  return None


def choose_promotion():
  """Prompt the player to choose the promotion piece."""
  font = pygame.font.Font(None, 40)
  prompt = True
  promotion_choice = None




  while prompt:
      window.fill(pygame.Color("black"))
      draw_text("Choose Promotion Piece", font, pygame.Color("white"), 200, 200)
      draw_text("Q: Queen, R: Rook, B: Bishop, N: Knight", font, pygame.Color("white"), 150, 300)
      pygame.display.flip()




      for event in pygame.event.get():
          if event.type == pygame.QUIT:
              pygame.quit()
              return chess.QUEEN  # Default promotion to Queen if the game is quit




          if event.type == pygame.KEYDOWN:
              if event.key == pygame.K_q:
                  promotion_choice = chess.QUEEN
              elif event.key == pygame.K_r:
                  promotion_choice = chess.ROOK
              elif event.key == pygame.K_b:
                  promotion_choice = chess.BISHOP
              elif event.key == pygame.K_n:
                  promotion_choice = chess.KNIGHT




      if promotion_choice:
          prompt = False




  return promotion_choice


def checkmate_screen(winner):
   """Display a screen when checkmate or stalemate occurs, showing the winner."""
   font = pygame.font.Font(None, 80)
   small_font = pygame.font.Font(None, 40)
   running = True


   while running:
       window.fill(pygame.Color("black"))


       if winner == 'draw':
           draw_text("Stalemate! It's a Draw!", font, pygame.Color("white"), 130, 200)
       else:
           draw_text(f"Checkmate! {winner} Wins!", font, pygame.Color("white"), 130, 200)


       draw_text("Press R to Restart", small_font, pygame.Color("white"), 150, 400)
       draw_text("Press Q to Quit", small_font, pygame.Color("white"), 150, 440)


       for event in pygame.event.get():
           if event.type == pygame.QUIT:
               running = False
               pygame.quit()


           if event.type == pygame.KEYDOWN:
               if event.key == pygame.K_r:
                   return True  # Restart game
               elif event.key == pygame.K_q:
                   return False  # Quit game


       pygame.display.flip()


   return False




def main():


   # Get initial window size
   # Initialize the window
   DEFAULT_WIDTH, DEFAULT_HEIGHT = 800, 800  # Default size
   window = pygame.display.set_mode((DEFAULT_WIDTH, DEFAULT_HEIGHT), pygame.RESIZABLE)
   pygame.display.set_caption("Chess - The Pycodes")
   width, height = window.get_size()
   square_size = width // 8


   # Load images based on square size
   load_images(square_size)


   # Show menu screen to choose game options
   game_mode, difficulty, player_side = menu_screen()


   board = chess.Board()
   selected_square = None
   last_move = None
   running = True


   if game_mode is None:  # If the game is exited from the menu
       return


   # Set up the clock to manage frame rate
   clock = pygame.time.Clock()


   while running:
       # Adjust square size dynamically based on current window dimensions
       width, height = window.get_size()
       square_size = min(width, height) // 8  # Ensure the board fits in the window


       # Draw the board and update highlights
       draw_board(board, square_size, selected_square, last_move)
       highlight_check(board, square_size)


       if selected_square:
           highlight_legal_moves(board, selected_square, square_size)


       pygame.display.flip()


       # Event handling
       for event in pygame.event.get():
           if event.type == pygame.QUIT:
               running = False
           elif event.type == pygame.VIDEORESIZE:  # Handle window resizing
               window = pygame.display.set_mode((event.w, event.h), pygame.RESIZABLE)
           elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:  # Left mouse click
               col, row = event.pos[0] // square_size, event.pos[1] // square_size
               clicked_square = chess.square(col, 7 - row)


               if selected_square is None:
                   # If no square is selected, select this square
                   if board.piece_at(clicked_square) and board.piece_at(clicked_square).color == board.turn:
                       selected_square = clicked_square
                       highlight_legal_moves(board, selected_square, square_size)
               else:
                   # Try to move to the clicked square
                   move = chess.Move(selected_square, clicked_square, promotion=promote_pawn(board, chess.Move(selected_square, clicked_square)))
                   if move in board.legal_moves:
                       last_move = move
                       board.push(move)
                       selected_square = None


                       # Check for game end (checkmate or stalemate)
                       if board.is_checkmate():
                           winner = "White" if board.turn == chess.BLACK else "Black"
                           if not checkmate_screen(winner):  # If player quits, exit loop
                               running = False
                           else:
                               return main()  # Restart game


                       elif board.is_stalemate():
                           if not checkmate_screen("draw"):  # If player quits, exit loop
                               running = False
                           else:
                               return main()  # Restart game
                   else:
                       # If move is illegal, deselect the square
                       selected_square = None


       # Computer move (for Player vs Computer mode)
       if game_mode == 'computer' and board.turn != (player_side == 'w'):
           pygame.time.delay(1000)  # Add delay to simulate thinking
           move = computer_move(board, difficulty)
           if move:
               last_move = move
               board.push(move)


               # Check for game end (checkmate or stalemate)
               if board.is_checkmate():
                   winner = "White" if board.turn == chess.BLACK else "Black"
                   if not checkmate_screen(winner):  # If player quits, exit loop
                       running = False
                   else:
                       return main()  # Restart game


               elif board.is_stalemate():
                   if not checkmate_screen("draw"):  # If player quits, exit loop
                       running = False
                   else:
                       return main()  # Restart game


       # Cap frame rate at 60 FPS
       clock.tick(60)


   pygame.quit()


if __name__ == "__main__":
   main()

Happy Coding!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top