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

How to Build a Checkers Game in Python with Pygame

Welcome to the exciting world of game development! Today, we’re diving into how to build a Checkers game in Python using Pygame. Whether you’re a seasoned coder or just starting out, creating a game is a fantastic way to hone your programming skills and unleash your creativity.

In this tutorial, we’ll explore the fundamental concepts of game design and programming while developing a classic board game that has entertained countless players over the years. You’ll learn how to set up the game board, implement the rules, and even add some interactive elements to make your game enjoyable.

So, roll up your sleeves and get ready to embark on this fun journey where you’ll learn how to create your very own Checkers game in Python!

Table of Contents

Make sure to install the pygame library to keep everything running smoothly!

$ pip install pygame

Setting Up the Stage

Just like all the opening acts we’ve had before, we can’t begin without calling in our trusty tools!

  • First up, we’ve got Pygame, which breathes life into our game. It handles everything from graphics and sound to user input, making it the backbone of our checkers game.
  • Next, we’ll bring in sys, which helps us smoothly exit the game when needed by allowing us to interact with Python itself.
  • Finally, there’s copy, a lifesaver for replicating game states during tactical simulations. It lets us copy objects without messing up the original setup, which is pretty handy when things get complicated.
import pygame
import sys
import copy

Now that we’ve got our libraries and modules in place, it’s time to raise the curtain and kick things off by initializing all the Pygame modules!

# Initialize Pygame
pygame.init()

Structuring the Game Environment: Dimensions, Colors, and Window Setup

Setting Up Screen Dimensions

Alright, folks! With everything set up and ready to go, we’re ready to set the dimensions of the board screen where our Checkers game will unfold. First, we’ll use pygame.display.Info() to grab the current width and height of the screen. This is crucial because we want to make sure everything fits just right!

# Get the screen's width and height
screen_info = pygame.display.Info()
WIDTH, HEIGHT = screen_info.current_w, screen_info.current_h

Next, we need to make sure that our window doesn’t exceed the actual size of the screen. We’ll set a maximum width and height that are friendly to our laptop screens—keeping it to a maximum of 600 pixels. This helps us avoid overwhelming players with an expansive void.

# Ensure the window is not bigger than the actual screen
WIDTH = min(WIDTH, 600)  # Max width 600
HEIGHT = min(HEIGHT, 600)  # Max height 600

Now, let’s talk about the layout of our game. To create the perfect grid for our Checkers board, we need to determine the dimensions for each square. We’ll do this by calculating the size based on the screen’s HEIGHT and WIDTH, making sure it fits snugly within our designated area.

# Screen dimensions
ROWS, COLS = 8, 8
SQUARE_SIZE = min(WIDTH // COLS, HEIGHT // ROWS)

Color Configuration

# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
GREY = (128, 128, 128)
GREEN = (0, 255, 0)

To really set the mood for our players and add some excitement to the game, let’s pick out some fun colors! We’ll go with classic white and black for the squares to create that nice contrast. For the pieces, we’ll use bold red and blue. And to make things even more visually appealing, we’ll throw in some grey and green, especially for those crowned kings on the board. It’ll definitely add some flair to the game!

Setting Up the Game Window

# Initialize screen with resizable option
screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.RESIZABLE)
pygame.display.set_caption('Checkers - The Pycodes')

With our game’s identity set, let’s create the main window where everything will come to life. We use pygame.display.set_mode() to make the window, and we’ve made it resizable with pygame.RESIZABLE, so players can adjust it however they like. Finally, we top it off by adding a title to the window using pygame.display.set_caption().

Defining the Piece Class

Now that we’ve got the board ready, it’s time to create the stars of our game — the pieces. We’ll do that using the Piece class. Here’s how it all comes together:

Giving the Pieces a Polished Look

We want our pieces to look sharp and stand out on the board, so we add two constants: PADDING and BORDER. The padding gives the piece a neat margin, while the border makes sure they have a nice outline.

# Piece class
class Piece:
    PADDING = 15
    BORDER = 2

Defining Each Piece’s Position and Identity

Next, in the __init__ method, we set up each piece’s starting position, its color, and whether it’s a king or not (kings come later). We also calculate its exact position on the screen by calling calc_pos() to ensure each piece is correctly placed on the board grid.

def __init__(self, row, col, color):
    self.row = row
    self.col = col
    self.color = color
    self.king = False
    self.x = 0
    self.y = 0
    self.calc_pos()

Calculating Where Each Piece Should be

The calc_pos() function does the math for us. It takes the row and column of each piece and converts it into pixel coordinates, so every piece is centered perfectly in its square.

def calc_pos(self):
    self.x = SQUARE_SIZE * self.col + SQUARE_SIZE // 2
    self.y = SQUARE_SIZE * self.row + SQUARE_SIZE // 2

Crowning a King

When a piece reaches the opposite side of the board, it becomes a king. The make_king() function handles this by changing its status, giving it the power to move backward.

def make_king(self):
    self.king = True

Bringing the Pieces to Life

The draw() function takes care of the visual part. It draws a circle for each piece, using the color we assigned, and gives it a border. If the piece is a king, it adds a little extra flair with a green circle to signify royalty.

def draw(self, win):
    radius = SQUARE_SIZE // 2 - self.PADDING
    pygame.draw.circle(win, self.color, (self.x, self.y), radius + self.BORDER)
    pygame.draw.circle(win, GREY, (self.x, self.y), radius)
    if self.king:
        pygame.draw.circle(win, GREEN, (self.x, self.y), radius - 5)

Moving Pieces Across the Board

To wrap it up, the move() function updates the piece’s position on the board. After each move, we recalculate its pixel position using calc_pos() to keep everything aligned.

def move(self, row, col):
    self.row = row
    self.col = col
    self.calc_pos()

Defining the Board Class

With the Piece class complete, let’s move on to the Board class, which manages the game’s entire layout and logic, including drawing the board, handling moves, and evaluating the score.

Board Initialization

The Board class starts by initializing key variables such as the number of red and blue pieces on the board and how many of them are kings. It then creates the board layout using a helper method.

class Board:
    def __init__(self):
        self.board = []
        self.red_left = self.blue_left = 12
        self.red_kings = self.blue_kings = 0
        self.create_board()

Creating the Board

The create_board() method is responsible for setting up the initial pieces on the board. It uses nested loops to alternate between squares and place pieces in the correct positions for the blue and red teams.

def create_board(self):
    for row in range(ROWS):
        self.board.append([])
        for col in range(COLS):
            if (row + col) % 2 == 0:
                self.board[row].append(0)
            else:
                if row < 3:
                    self.board[row].append(Piece(row, col, BLUE))
                elif row > 4:
                    self.board[row].append(Piece(row, col, RED))
                else:
                    self.board[row].append(0)

Drawing the Board

Next, the draw_squares() method fills the board with alternating colors to create the classic checkered pattern, and the draw() method places the pieces on the board in their respective locations.

def draw_squares(self, win):
    win.fill(BLACK)
    for row in range(ROWS):
        for col in range(row % 2, COLS, 2):
            pygame.draw.rect(win, WHITE, (col * SQUARE_SIZE, row * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE))

def draw(self, win):
    self.draw_squares(win)
    for row in range(ROWS):
        for col in range(COLS):
            piece = self.board[row][col]
            if piece != 0:
                piece.draw(win)

Moving Pieces

When a player moves a piece, the move() method swaps the piece’s current position with the target position. If the piece reaches the opposite side of the board, it is crowned as a king.

def move(self, piece, row, col):
    self.board[piece.row][piece.col], self.board[row][col] = self.board[row][col], self.board[piece.row][piece.col]
    piece.move(row, col)

    if row == 0 or row == ROWS - 1:
        piece.make_king()

Fetching a Specific Piece

The get_piece() method simply retrieves the piece at a specific row and column from the board.

def get_piece(self, row, col):
    return self.board[row][col]

Retrieving All Pieces of a Color

The get_all_pieces() method scans the entire board and returns all pieces belonging to a specified color, whether red or blue.

def get_all_pieces(self, color):
    pieces = []
    for row in self.board:
        for piece in row:
            if piece != 0 and piece.color == color:
                pieces.append(piece)
    return pieces

Valid Moves Calculation

The get_valid_moves() method calculates all possible moves a piece can make, based on its position and type (king or regular). It checks for available spaces to move and whether a piece can jump over an opponent.

def get_valid_moves(self, piece):
    moves = {}
    left = piece.col - 1
    right = piece.col + 1
    row = piece.row

    if piece.color == RED or piece.king:
        moves.update(self._traverse_left(row - 1, max(row - 3, -1), -1, piece.color, left))
        moves.update(self._traverse_right(row - 1, max(row - 3, -1), -1, piece.color, right))

    if piece.color == BLUE or piece.king:
        moves.update(self._traverse_left(row + 1, min(row + 3, ROWS), 1, piece.color, left))
        moves.update(self._traverse_right(row + 1, min(row + 3, ROWS), 1, piece.color, right))

    return moves

Traversing Left for Valid Moves

The _traverse_left() method is a helper function that recursively checks for valid moves to the left, including jumps over opponent pieces.

def _traverse_left(self, start, stop, step, color, left, skipped=[]):
    moves = {}
    last = []
    for r in range(start, stop, step):
        if left < 0:
            break

        current = self.board[r][left]
        if current == 0:
            if skipped and not last:
                break
            elif skipped:
                moves[(r, left)] = last + skipped
            else:
                moves[(r, left)] = last

            if last:
                if step == -1:
                    row = max(r - 3, 0)
                else:
                    row = min(r + 3, ROWS)
                moves.update(self._traverse_left(r + step, row, step, color, left - 1, skipped=last))
                moves.update(self._traverse_right(r + step, row, step, color, left + 1, skipped=last))
            break
        elif current.color == color:
            break
        else:
            last = [current]

        left -= 1

    return moves

Traversing Right for Valid Moves

Similar to the left traversal, the _traverse_right() method checks for valid moves on the right side.

def _traverse_right(self, start, stop, step, color, right, skipped=[]):
    moves = {}
    last = []
    for r in range(start, stop, step):
        if right >= COLS:
            break

        current = self.board[r][right]
        if current == 0:
            if skipped and not last:
                break
            elif skipped:
                moves[(r, right)] = last + skipped
            else:
                moves[(r, right)] = last

            if last:
                if step == -1:
                    row = max(r - 3, 0)
                else:
                    row = min(r + 3, ROWS)
                moves.update(self._traverse_left(r + step, row, step, color, right - 1, skipped=last))
                moves.update(self._traverse_right(r + step, row, step, color, right + 1, skipped=last))
            break
        elif current.color == color:
            break
        else:
            last = [current]

        right += 1

    return moves

Removing Pieces from the Board

When a piece is captured, the remove() method eliminates it from the board and updates the count of remaining pieces for each player.

def remove(self, pieces):
    for piece in pieces:
        self.board[piece.row][piece.col] = 0
        if piece != 0:
            if piece.color == RED:
                self.red_left -= 1
            else:
                self.blue_left -= 1

Evaluating the Board

To determine the current game state, the evaluate() method assigns a score based on the number of pieces each player has left and whether any of them are kings.

def evaluate(self):
    score = 0
    for piece in self.get_all_pieces(BLUE):
        score += 10  # Higher score for BLUE pieces
        if piece.king:
            score += 5  # Additional score for kings
    for piece in self.get_all_pieces(RED):
        score -= 10  # Lower score for RED pieces
        if piece.king:
            score -= 5  # Additional penalty for opponent's kings
    return score

Determining the Winner

Lastly, the winner() method checks if either player has run out of pieces, declaring the remaining player as the winner.

def winner(self):
    if self.red_left <= 0:
        return "BLUE"
    elif self.blue_left <= 0:
        return "RED"
    return None

This wraps up the Board class, which handles the game’s main mechanics from moves and captures to evaluating the game state.

Defining the Game Class

Now it is time for the game class that not only provides a clean structure for managing the Checkers game, but also handles both the player and computer turns. It updates the board and game states dynamically through a collection of functions.

  • The first one being the __init__() method, which initializes a new game board instance by calling the Board() class the moment a new game is created. It also ensures that red always starts first, while no piece is selected initially. Additionally, it sets up a valid_moves dictionary for the selected pieces and determines whether you’re playing against the computer or not.
class Game:
    def __init__(self, is_computer=False):
        self.board = Board()
        self.turn = RED
        self.selected_piece = None
        self.valid_moves = {}
        self.is_computer = is_computer  # True if playing against computer
  • Next up, we have the reset() function. When the game is over and you want to play again, the reset function is there to reset all game states, ensuring a fresh start. It simply calls the __init__() method again, keeping the is_computer setting intact.
def reset(self):
    self.__init__(self.is_computer)
  • Once the game is in progress, you can’t play without selecting a piece first. That’s where the select() function comes into play. This method checks if a piece has been selected or not. If it has, it tries to move it to the new square. If the move is invalid, the piece is deselected, and you can try selecting another one. If no piece has been selected yet, it retrieves a piece from the board and checks if it belongs to the current player. If it does, that piece is selected, and its valid moves are calculated.
def select(self, row, col):
    if self.selected_piece:
        result = self._move(row, col)
        if not result:
            self.selected_piece = None
            self.select(row, col)

    piece = self.board.get_piece(row, col)
    if piece != 0 and piece.color == self.turn:
        self.selected_piece = piece
        self.valid_moves = self.board.get_valid_moves(piece)
        return True
    return False
  • After selecting a piece, the next step is moving it, and that’s where the _move() function comes in. This function checks if the selected move is valid. If it is, the piece is moved to the new location. If the move involves a capture (skipping over another piece), the skipped piece is removed from the board using the remove() function. After the move, the turn is switched.
def _move(self, row, col):
    piece = self.selected_piece
    if piece and (row, col) in self.valid_moves:
        self.board.move(piece, row, col)
        skipped = self.valid_moves[(row, col)]
        if skipped:
            self.board.remove(skipped)
        self.change_turn()
    else:
        return False
    return True
  • Now that you’ve made your move, it’s time for the opponent to play. The change_turn() function handles this by resetting the valid moves and switching turns to the other player. If you’re playing against the computer, it calls the computer_turn() function when it’s the computer’s turn.
def change_turn(self):
    self.valid_moves = {}
    self.turn = BLUE if self.turn == RED else RED

    if self.is_computer and self.turn == BLUE:
        self.computer_turn()
  • To give you a visual aid, the draw_valid_moves() function highlights the valid moves that your selected piece can make. It does this by drawing green circles on the board to indicate the positions where the piece can move.
def draw_valid_moves(self, moves):
    for move in moves:
        row, col = move
        pygame.draw.circle(screen, GREEN,
                           (col * SQUARE_SIZE + SQUARE_SIZE // 2, row * SQUARE_SIZE + SQUARE_SIZE // 2), 15)
  • As you play, the update() function ensures that everything is displayed properly on the screen. It draws the board, the pieces, and the valid moves, and refreshes the display to reflect any changes during the game.
def update(self):
    self.board.draw(screen)
    if self.selected_piece:
        self.draw_valid_moves(self.valid_moves)
    pygame.display.update()
  • Once we’ve handled the visual feedback and ensured the game keeps refreshing, the next step is determining the winner. The winner() function checks whether one side has lost all their pieces or if there are no valid moves left for that player. If that’s the case, it declares the other player as the winner.
def winner(self):
    red_moves = any(self.board.get_valid_moves(piece) for piece in self.board.get_all_pieces(RED))
    blue_moves = any(self.board.get_valid_moves(piece) for piece in self.board.get_all_pieces(BLUE))

    if self.board.red_left <= 0 or not red_moves:
        return "BLUE"
    elif self.board.blue_left <= 0 or not blue_moves:
        return "RED"

    return None
  • Now, let’s dive into how the AI takes its turn. When you’re up against the computer, the computer_turn() function calls the minimax algorithm to choose the best possible move. First, the AI checks all available moves for its pieces (BLUE). If there aren’t any valid moves, the AI has to skip its turn, but before that, it checks if there’s a winner. If no winner is declared, control shifts back to you, the player.
  • However, when there are valid moves, the minimax algorithm kicks in. Here’s where the depth comes into play. The depth is set to 3, meaning the AI looks ahead three moves into the future, considering all possible actions for both itself and the player. After this strategic evaluation, the AI makes its best move and hands the turn back to you.
def computer_turn(self):
    # Get all possible moves for BLUE (AI)
    all_moves = get_all_moves(self.board, BLUE)

    # If no valid moves, skip turn or handle it appropriately
    if not all_moves:
        winner = self.winner()  # Check if there's a winner
        if winner:
            return  # The game is over, no need to continue.

        self.change_turn()  # AI skips its turn due to no valid moves
        return

    # If valid moves exist, proceed with minimax
    _, new_board = minimax(self.board, 3, True)  # depth of 3
    self.board = new_board
    self.change_turn()

By breaking down the Game class in this way, you can see how it efficiently manages the flow of the Checkers game while providing a clean and structured approach to handling player and computer interactions.

Minimax Algorithm for AI Moves

So, how does the AI know what to do next? This is where the minimax() algorithm steps in. Imagine it as the AI’s way of thinking ahead—analyzing not only its own moves but also predicting the player’s responses. The function takes in three things: the current position of the board, the depth, which is how many moves ahead the AI can look, and max_player, which tells whether it’s the AI’s turn or not.

Here’s how it works: if the maximum depth is reached or if there’s already a winner, the function stops and evaluates the current board. If it’s the AI’s turn (max_player is True), it looks for the best move by setting max_eval to negative infinity. Then, it tries every possible move using get_all_moves(position, BLUE) (since BLUE is the AI). Each time it finds a better move, it updates max_eval. For every move it tries, it also calls minimax() again, but now pretending it’s the player’s turn, reducing the depth by one. This way, the AI anticipates the player’s moves, trying to stay one step ahead. At the end, the AI picks the move that maximizes its advantage.

When it’s the player’s turn (max_player is False), the process is similar, but this time, it looks for the worst possible scenario for the AI by minimizing the evaluation (min_eval).

Here’s the code:

# Minimax algorithm
def minimax(position, depth, max_player):
    if depth == 0 or position.winner() is not None:
        return position.evaluate(), position

    if max_player:
        max_eval = float('-inf')
        best_move = None
        for move in get_all_moves(position, BLUE):
            evaluation = minimax(move, depth - 1, False)[0]
            max_eval = max(max_eval, evaluation)
            if max_eval == evaluation:
                best_move = move
        return max_eval, best_move
    else:
        min_eval = float('inf')
        best_move = None
        for move in get_all_moves(position, RED):
            evaluation = minimax(move, depth - 1, True)[0]
            min_eval = min(min_eval, evaluation)
            if min_eval == evaluation:
                best_move = move
        return min_eval, best_move

Getting All the Moves

Now, to make this strategy work, the AI needs to know all possible moves it can make. That’s where the get_all_moves() function comes in. This function looks at the current board and lists out every possible move for either RED or BLUE.

First, it creates an empty list called moves where it stores all the options. Then, for each piece of the selected color, it calls the get_valid_moves() function to find out where that piece can move and if it can capture any opponent’s pieces. Using Python’s copy.deepcopy(), a copy of the current board is made so we can simulate these moves without messing with the actual game board. If the move captures an opponent’s piece, the remove() function is called to take that piece off the board.

Finally, it adds the new, simulated board to the list of moves and returns it so the AI can evaluate all possible outcomes with the minimax algorithm.

Here’s the code:

def get_all_moves(board, color):
    moves = []
    for piece in board.get_all_pieces(color):
        valid_moves = board.get_valid_moves(piece)
        for move, skipped in valid_moves.items():
            temp_board = copy.deepcopy(board)
            temp_piece = temp_board.get_piece(piece.row, piece.col)
            temp_board.move(temp_piece, move[0], move[1])

            if skipped:
                temp_board.remove(skipped)  # Capture the skipped pieces

            moves.append(temp_board)

    return moves

I know I’ve mentioned this before, but creating user-friendly programs is our top priority, and this game is no exception. To make the experience enjoyable, we designed the main_menu() function, which displays the game title on a black background. Right at the center of the screen, you’ll find two buttons: “Player vs Player” and “Player vs AI”.

When the user exits the window, pygame.quit() is called to ensure everything closes down smoothly. We also use the MOUSEBUTTONDOWN event to track the mouse position. If the user clicks on the “Player vs Player” button, they can challenge another player. On the other hand, clicking “Player vs AI” means they want to play against the computer.

# Main menu function
def main_menu():
 screen.fill(BLACK)
 font = pygame.font.Font(None, 60)
 text = font.render("Checkers - The Pycodes", True, WHITE)
 screen.blit(text, (WIDTH // 2 - text.get_width() // 2, HEIGHT // 2 - 150))
 
 play_pvp_button = pygame.Rect(WIDTH // 2 - 100, HEIGHT // 2 - 50, 200, 50)
 play_ai_button = pygame.Rect(WIDTH // 2 - 100, HEIGHT // 2 + 50, 200, 50)
 
 pygame.draw.rect(screen, RED, play_pvp_button)
 pygame.draw.rect(screen, BLUE, play_ai_button)
 
 pvp_text = font.render("Player vs Player", True, WHITE)
 ai_text = font.render("Player vs AI", True, WHITE)
 
 screen.blit(pvp_text, (play_pvp_button.x + 10, play_pvp_button.y + 5))
 screen.blit(ai_text, (play_ai_button.x + 20, play_ai_button.y + 5))
 
 pygame.display.update()
 
 while True:
     for event in pygame.event.get():
         if event.type == pygame.QUIT:
             pygame.quit()
             sys.exit()
         if event.type == pygame.MOUSEBUTTONDOWN:
             if play_pvp_button.collidepoint(event.pos):
                 return False  # Player vs Player mode
             if play_ai_button.collidepoint(event.pos):
                 return True  # Player vs Computer mode

Displaying the Winner Screen

Once the game reaches its conclusion, the display_winner_screen() function takes over. It fills the screen with a black background, announcing the winner in a bold way. Along with this announcement, there are two buttons: one that takes you back to the main menu and another that lets you quit the game.

def display_winner_screen(winner):
 screen.fill(BLACK)
 font = pygame.font.Font(None, 60)
 text = font.render(f"The winner is {winner}", True, WHITE)
 screen.blit(text, (WIDTH // 2 - text.get_width() // 2, HEIGHT // 2 - 150))
 
 # Create buttons
 main_menu_button = pygame.Rect(WIDTH // 2 - 100, HEIGHT // 2 - 50, 200, 50)
 quit_button = pygame.Rect(WIDTH // 2 - 100, HEIGHT // 2 + 50, 200, 50)
 
 # Draw buttons
 pygame.draw.rect(screen, BLUE, main_menu_button)
 pygame.draw.rect(screen, RED, quit_button)
 
 # Button text
 menu_text = font.render("Main Menu", True, WHITE)
 quit_text = font.render("Quit", True, WHITE)
 
 screen.blit(menu_text, (main_menu_button.x + 10, main_menu_button.y + 5))
 screen.blit(quit_text, (quit_button.x + 60, quit_button.y + 5))
 
 pygame.display.update()
 
 while True:
     for event in pygame.event.get():
         if event.type == pygame.QUIT:
             pygame.quit()
             sys.exit()
 
         if event.type == pygame.MOUSEBUTTONDOWN:
             if main_menu_button.collidepoint(event.pos):
                 return "main_menu"
             if quit_button.collidepoint(event.pos):
                 pygame.quit()
                 sys.exit()

Main Function

With this, we’ve reached the function that manages the entire game flow. It starts with an infinite loop that begins by calling the main_menu() function, allowing the user to choose who they want to play against. This choice is passed to is_computer from the game class, which determines if the opponent is another player or the computer.

# Main function
def main():
    while True:
        is_computer = main_menu()  # Returns True if AI is chosen
        game = Game(is_computer)

In each iteration, the code calls game.update() to display any changes on the screen. After each frame, it uses game.winner() to check if there’s a winner. If a winner is found, the display_winner_screen() function is called to announce the victor.

The program then waits for the player to make a choice: they can either return to the main menu by calling main_menu() again or quit the game using pygame.quit().

        clock = pygame.time.Clock()
    
        while True:
            clock.tick(60)
            game.update()

            winner = game.winner()
            if winner:
                result = display_winner_screen(winner)
                if result == "main_menu":
                    break  # Return to the main menu loop

Additionally, the loop continuously retrieves the coordinates of the mouse with pygame.mouse.get_pos() to check if any buttons have been clicked.

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()

                if event.type == pygame.MOUSEBUTTONDOWN:
                    pos = pygame.mouse.get_pos()
                    row, col = pos[1] // SQUARE_SIZE, pos[0] // SQUARE_SIZE
                    game.select(row, col)

Lastly, we ensure that this code can only be executed directly and not imported as a module.

if __name__ == '__main__':
 main()

Example

Full Code

import pygame
import sys
import copy

# Initialize Pygame
pygame.init()

# Get the screen's width and height
screen_info = pygame.display.Info()
WIDTH, HEIGHT = screen_info.current_w, screen_info.current_h

# Ensure the window is not bigger than the actual screen
WIDTH = min(WIDTH, 600)  # Max width 600
HEIGHT = min(HEIGHT, 600)  # Max height 600

# Screen dimensions
ROWS, COLS = 8, 8
SQUARE_SIZE = min(WIDTH // COLS, HEIGHT // ROWS)

# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
GREY = (128, 128, 128)
GREEN = (0, 255, 0)

# Initialize screen with resizable option
screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.RESIZABLE)
pygame.display.set_caption('Checkers - The Pycodes')

# Piece class
class Piece:
 PADDING = 15
 BORDER = 2

 def __init__(self, row, col, color):
     self.row = row
     self.col = col
     self.color = color
     self.king = False
     self.x = 0
     self.y = 0
     self.calc_pos()

 def calc_pos(self):
     self.x = SQUARE_SIZE * self.col + SQUARE_SIZE // 2
     self.y = SQUARE_SIZE * self.row + SQUARE_SIZE // 2

 def make_king(self):
     self.king = True

 def draw(self, win):
     radius = SQUARE_SIZE // 2 - self.PADDING
     pygame.draw.circle(win, self.color, (self.x, self.y), radius + self.BORDER)
     pygame.draw.circle(win, GREY, (self.x, self.y), radius)
     if self.king:
         pygame.draw.circle(win, GREEN, (self.x, self.y), radius - 5)

 def move(self, row, col):
     self.row = row
     self.col = col
     self.calc_pos()

# Board class
class Board:
 def __init__(self):
     self.board = []
     self.red_left = self.blue_left = 12
     self.red_kings = self.blue_kings = 0
     self.create_board()

 def create_board(self):
     for row in range(ROWS):
         self.board.append([])
         for col in range(COLS):
             if (row + col) % 2 == 0:
                 self.board[row].append(0)
             else:
                 if row < 3:
                     self.board[row].append(Piece(row, col, BLUE))
                 elif row > 4:
                     self.board[row].append(Piece(row, col, RED))
                 else:
                     self.board[row].append(0)

 def draw_squares(self, win):
     win.fill(BLACK)
     for row in range(ROWS):
         for col in range(row % 2, COLS, 2):
             pygame.draw.rect(win, WHITE, (col * SQUARE_SIZE, row * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE))

 def draw(self, win):
     self.draw_squares(win)
     for row in range(ROWS):
         for col in range(COLS):
             piece = self.board[row][col]
             if piece != 0:
                 piece.draw(win)

 def move(self, piece, row, col):
     self.board[piece.row][piece.col], self.board[row][col] = self.board[row][col], self.board[piece.row][piece.col]
     piece.move(row, col)


     if row == 0 or row == ROWS - 1:
         piece.make_king()

 def get_piece(self, row, col):
     return self.board[row][col]

 def get_all_pieces(self, color):
     pieces = []
     for row in self.board:
         for piece in row:
             if piece != 0 and piece.color == color:
                 pieces.append(piece)
     return pieces

 def get_valid_moves(self, piece):
     moves = {}
     left = piece.col - 1
     right = piece.col + 1
     row = piece.row

     if piece.color == RED or piece.king:
         moves.update(self._traverse_left(row - 1, max(row - 3, -1), -1, piece.color, left))
         moves.update(self._traverse_right(row - 1, max(row - 3, -1), -1, piece.color, right))

     if piece.color == BLUE or piece.king:
         moves.update(self._traverse_left(row + 1, min(row + 3, ROWS), 1, piece.color, left))
         moves.update(self._traverse_right(row + 1, min(row + 3, ROWS), 1, piece.color, right))

     return moves

 def _traverse_left(self, start, stop, step, color, left, skipped=[]):
     moves = {}
     last = []
     for r in range(start, stop, step):
         if left < 0:
             break

         current = self.board[r][left]
         if current == 0:
             if skipped and not last:
                 break
             elif skipped:
                 moves[(r, left)] = last + skipped
             else:
                 moves[(r, left)] = last

             if last:
                 if step == -1:
                     row = max(r - 3, 0)
                 else:
                     row = min(r + 3, ROWS)
                 moves.update(self._traverse_left(r + step, row, step, color, left - 1, skipped=last))
                 moves.update(self._traverse_right(r + step, row, step, color, left + 1, skipped=last))
             break
         elif current.color == color:
             break
         else:
             last = [current]

         left -= 1

     return moves

 def _traverse_right(self, start, stop, step, color, right, skipped=[]):
     moves = {}
     last = []
     for r in range(start, stop, step):
         if right >= COLS:
             break

         current = self.board[r][right]
         if current == 0:
             if skipped and not last:
                 break
             elif skipped:
                 moves[(r, right)] = last + skipped
             else:
                 moves[(r, right)] = last

             if last:
                 if step == -1:
                     row = max(r - 3, 0)
                 else:
                     row = min(r + 3, ROWS)
                 moves.update(self._traverse_left(r + step, row, step, color, right - 1, skipped=last))
                 moves.update(self._traverse_right(r + step, row, step, color, right + 1, skipped=last))
             break
         elif current.color == color:
             break
         else:
             last = [current]

         right += 1

     return moves

 def remove(self, pieces):
     for piece in pieces:
         self.board[piece.row][piece.col] = 0
         if piece != 0:
             if piece.color == RED:
                 self.red_left -= 1
             else:
                 self.blue_left -= 1

 def evaluate(self):
     score = 0
     for piece in self.get_all_pieces(BLUE):
         score += 10  # Higher score for BLUE pieces
         if piece.king:
             score += 5  # Additional score for kings
     for piece in self.get_all_pieces(RED):
         score -= 10  # Lower score for RED pieces
         if piece.king:
             score -= 5  # Additional penalty for opponent's kings
     return score

 def winner(self):
     if self.red_left <= 0:
         return "BLUE"
     elif self.blue_left <= 0:
         return "RED"
     return None

# Game class
class Game:
 def __init__(self, is_computer=False):
     self.board = Board()
     self.turn = RED
     self.selected_piece = None
     self.valid_moves = {}
     self.is_computer = is_computer  # True if playing against computer

 def reset(self):
     self.__init__(self.is_computer)

 def select(self, row, col):
     if self.selected_piece:
         result = self._move(row, col)
         if not result:
             self.selected_piece = None
             self.select(row, col)

     piece = self.board.get_piece(row, col)
     if piece != 0 and piece.color == self.turn:
         self.selected_piece = piece
         self.valid_moves = self.board.get_valid_moves(piece)
         return True
     return False

 def _move(self, row, col):
     piece = self.selected_piece
     if piece and (row, col) in self.valid_moves:
         self.board.move(piece, row, col)
         skipped = self.valid_moves[(row, col)]
         if skipped:
             self.board.remove(skipped)
         self.change_turn()
     else:
         return False
     return True

 def change_turn(self):
     self.valid_moves = {}
     self.turn = BLUE if self.turn == RED else RED

     if self.is_computer and self.turn == BLUE:
         self.computer_turn()

 def draw_valid_moves(self, moves):
     for move in moves:
         row, col = move
         pygame.draw.circle(screen, GREEN,
                            (col * SQUARE_SIZE + SQUARE_SIZE // 2, row * SQUARE_SIZE + SQUARE_SIZE // 2), 15)

 def update(self):
     self.board.draw(screen)
     if self.selected_piece:
         self.draw_valid_moves(self.valid_moves)
     pygame.display.update()

 def winner(self):
     red_moves = any(self.board.get_valid_moves(piece) for piece in self.board.get_all_pieces(RED))
     blue_moves = any(self.board.get_valid_moves(piece) for piece in self.board.get_all_pieces(BLUE))

     if self.board.red_left <= 0 or not red_moves:
         return "BLUE"
     elif self.board.blue_left <= 0 or not blue_moves:
         return "RED"

     return None

 def computer_turn(self):
     # Get all possible moves for BLUE (AI)
     all_moves = get_all_moves(self.board, BLUE)

     # If no valid moves, skip turn or handle it appropriately
     if not all_moves:
         winner = self.winner()  # Check if there's a winner
         if winner:
             return  # The game is over, no need to continue.

         self.change_turn()  # AI skips its turn due to no valid moves
         return

     # If valid moves exist, proceed with minimax
     _, new_board = minimax(self.board, 3, True)  # depth of 3
     self.board = new_board
     self.change_turn()


# Minimax algorithm
def minimax(position, depth, max_player):
 if depth == 0 or position.winner() is not None:
     return position.evaluate(), position

 if max_player:
     max_eval = float('-inf')
     best_move = None
     for move in get_all_moves(position, BLUE):
         evaluation = minimax(move, depth - 1, False)[0]
         max_eval = max(max_eval, evaluation)
         if max_eval == evaluation:
             best_move = move
     return max_eval, best_move
 else:
     min_eval = float('inf')
     best_move = None
     for move in get_all_moves(position, RED):
         evaluation = minimax(move, depth - 1, True)[0]
         min_eval = min(min_eval, evaluation)
         if min_eval == evaluation:
             best_move = move
     return min_eval, best_move

def get_all_moves(board, color):
 moves = []
 for piece in board.get_all_pieces(color):
     valid_moves = board.get_valid_moves(piece)
     for move, skipped in valid_moves.items():
         temp_board = copy.deepcopy(board)
         temp_piece = temp_board.get_piece(piece.row, piece.col)
         temp_board.move(temp_piece, move[0], move[1])

         if skipped:
             temp_board.remove(skipped)  # Capture the skipped pieces

         moves.append(temp_board)

 return moves

# Main menu function
def main_menu():
 screen.fill(BLACK)
 font = pygame.font.Font(None, 60)
 text = font.render("Checkers - The Pycodes", True, WHITE)
 screen.blit(text, (WIDTH // 2 - text.get_width() // 2, HEIGHT // 2 - 150))

 play_pvp_button = pygame.Rect(WIDTH // 2 - 100, HEIGHT // 2 - 50, 200, 50)
 play_ai_button = pygame.Rect(WIDTH // 2 - 100, HEIGHT // 2 + 50, 200, 50)

 pygame.draw.rect(screen, RED, play_pvp_button)
 pygame.draw.rect(screen, BLUE, play_ai_button)

 pvp_text = font.render("Player vs Player", True, WHITE)
 ai_text = font.render("Player vs AI", True, WHITE)

 screen.blit(pvp_text, (play_pvp_button.x + 10, play_pvp_button.y + 5))
 screen.blit(ai_text, (play_ai_button.x + 20, play_ai_button.y + 5))

 pygame.display.update()

 while True:
     for event in pygame.event.get():
         if event.type == pygame.QUIT:
             pygame.quit()
             sys.exit()
         if event.type == pygame.MOUSEBUTTONDOWN:
             if play_pvp_button.collidepoint(event.pos):
                 return False  # Player vs Player mode
             if play_ai_button.collidepoint(event.pos):
                 return True  # Player vs Computer mode

def display_winner_screen(winner):
 screen.fill(BLACK)
 font = pygame.font.Font(None, 60)
 text = font.render(f"The winner is {winner}", True, WHITE)
 screen.blit(text, (WIDTH // 2 - text.get_width() // 2, HEIGHT // 2 - 150))

 # Create buttons
 main_menu_button = pygame.Rect(WIDTH // 2 - 100, HEIGHT // 2 - 50, 200, 50)
 quit_button = pygame.Rect(WIDTH // 2 - 100, HEIGHT // 2 + 50, 200, 50)

 # Draw buttons
 pygame.draw.rect(screen, BLUE, main_menu_button)
 pygame.draw.rect(screen, RED, quit_button)

 # Button text
 menu_text = font.render("Main Menu", True, WHITE)
 quit_text = font.render("Quit", True, WHITE)

 screen.blit(menu_text, (main_menu_button.x + 10, main_menu_button.y + 5))
 screen.blit(quit_text, (quit_button.x + 60, quit_button.y + 5))

 pygame.display.update()

 while True:
     for event in pygame.event.get():
         if event.type == pygame.QUIT:
             pygame.quit()
             sys.exit()

         if event.type == pygame.MOUSEBUTTONDOWN:
             if main_menu_button.collidepoint(event.pos):
                 return "main_menu"
             if quit_button.collidepoint(event.pos):
                 pygame.quit()
                 sys.exit()

# Main function
def main():
 while True:
     is_computer = main_menu()  # Returns True if AI is chosen
     game = Game(is_computer)
     clock = pygame.time.Clock()

     while True:
         clock.tick(60)
         game.update()

         winner = game.winner()
         if winner:
             result = display_winner_screen(winner)
             if result == "main_menu":
                 break  # Return to the main menu loop

         for event in pygame.event.get():
             if event.type == pygame.QUIT:
                 pygame.quit()
                 sys.exit()

             if event.type == pygame.MOUSEBUTTONDOWN:
                 pos = pygame.mouse.get_pos()
                 row, col = pos[1] // SQUARE_SIZE, pos[0] // SQUARE_SIZE
                 game.select(row, col)

if __name__ == '__main__':
 main()

Happy Coding!

Leave a Comment

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

Scroll to Top