How to Make a Checkers Game with Pygame in Python

Learn how to build a checkers game from scratch with the help of Pygame library in Python.
  · 21 min read · Updated may 2023 · Game Development

Confused by complex code? Let our AI-powered Code Explainer demystify it for you. Try it out!

Checkers is a classic two-player board game that has been enjoyed by people of all ages for generations. It is a strategy game that requires players to move their pieces across the board, capturing their opponent's pieces and ultimately trying to reach the other end of the board with one of their pieces to become a king.

In this article, we will be making a Checkers game in Python. Our goal is to provide an overview of the game's codebase, breaking it down into several classes that work together to provide the game's functionality. We will cover the installation and setup process, the main class, the game class, the board class, the tile class, the piece class, the pawn class, and the king class.

We also have a tutorial on making a chess game, make sure to check it out if you're interested!

Our focus will be on explaining the code in a way that is easy to understand for new programmers. So, without further ado, let's get started!

Table of Contents:

Setup and Installation

Before we can begin working on our game, we need to make sure we have all the necessary tools installed on our computer. The first thing we need to do is to make sure we have Python installed. You can download Python from the official website.

Also, we need to install the Pygame library. Pygame is a set of Python modules that allow us to create games and multimedia applications. To install Pygame, open up the command prompt/terminal and type the following command:

$ pip install pygame

We can now create the directory for our Checkers game. Open up your file explorer and navigate to the directory where you want to create your game, and create the "Checkers" folder.

Inside the Checkers directory, we need to create several Python files. Namely "Main.py", "Game.py", "Board.py", "Tile.py", "Piece.py", "Pawn.py", and "King.py".

Finally, we need to create a folder named "images" inside the Checkers directory. This folder will contain the images for our game pieces. Inside the folder, place the following image files: black-pawn.png, black-king.png, red-pawn.png, and red-king.png. You can access the pictures here.

The structure of our game should look like this:

The Main class

The Main class is the starting point of our game. It sets up the game window, initializes the game objects, and runs the game loop.

First, we import the necessary modules and classes from the other files in our Checkers game. Then, we initialize Pygame with the pygame.init() function:

# /* Main.py
import pygame
from Board import Board
from Game import Game

pygame.init()

The Checkers class has an __init__() method that takes in a screen parameter which initializes several class attributes, including screen, running, and FPS. screen represents the game window, running is a boolean that determines whether the game is still running, and FPS is a Pygame clock that limits the game to a certain frame rate.

class Checkers:
    def __init__(self, screen):
        self.screen = screen
        self.running = True
        self.FPS = pygame.time.Clock()

Below the __init__() function, add two more functions, and name them, _draw() and main().

The _draw() method is a helper method that takes in a board parameter and draws the game board onto the screen using the board.draw() method. It then updates the display using pygame.display.update().

    def _draw(self, board):
        board.draw(self.screen)
        pygame.display.update()

The main() method is the main game loop. It takes in window_width and window_height parameters, which are the dimensions of the game window. board_size is set to 8, which represents the size of the game board. tile_width and tile_height are calculated based on the size of the window and the size of the board:

    def main(self, window_width, window_height):
        board_size = 8
        tile_width, tile_height = window_width // board_size, window_height // board_size
        board = Board(tile_width, tile_height, board_size)
        game = Game()
        while self.running:
            game.check_jump(board)

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

                if not game.is_game_over(board):
                    if self.event.type == pygame.MOUSEBUTTONDOWN:
                        board.handle_click(self.event.pos)
                else:
                    game.message()
                    self.running = False

            self._draw(board)
            self.FPS.tick(60)

board is then initialized with the Board class, passing in the tile_width, tile_height, and board_size parameters. game is also initialized with the Game class.

The while loop runs as long as self.running is True. Within the loop, we call game.check_jump(board) to check for any available jumps on the board. We then loop through the events in the Pygame event queue using pygame.event.get(). If the event type is pygame.QUIT (when the exit button was clicked), we set self.running to False to exit the game.

If the game is not over, we check if the user has clicked on a tile on the board using board.handle_click(). If the game is over, we display a message using game.message() and exit the game loop by setting self.running to False.

We then call the _draw() method to draw the game board on the screen and update the display using pygame.display.update(). Finally, we use self.FPS.tick(60) to limit the game to 60 frames per second.

Below the class, let's call the game with the main() function.

if __name__ == "__main__":
    window_size = (640, 640)
    screen = pygame.display.set_mode(window_size)
    pygame.display.set_caption("Checkers")

    checkers = Checkers(screen)
    checkers.main(window_size[0], window_size[1])

We set the window_size to (640, 640), create a Pygame display screen with this size using pygame.display.set_mode(window_size), and set the window caption to "Checkers" using pygame.display.set_caption("Checkers"). Then, we create an instance of the Checkers class called checkers, passing in the screen as a parameter. Then, we call the main() method on checkers, passing in the window_size dimensions. This code sets up the Pygame display screen, creates an instance of the Checkers class, and starts the game loop by calling the main() method.

The Game class

The Game class contains methods for checking if the game is over, checking if there is a jump available, and displaying the winner of the game:

# /* Game.py
class Game:
    def __init__(self):
        self.winner = None

The __init__() method initializes the winner variable to None.

    # checks if both colors still has a piece
    def check_piece(self, board):
        red_piece = 0
        black_piece = 0
        for y in range(board.board_size):
            for x in range(board.board_size):
                tile = board.get_tile_from_pos((x, y))
                if tile.occupying_piece != None:
                    if tile.occupying_piece.color == "red":
                        red_piece += 1
                    else:
                        black_piece += 1
        return red_piece, black_piece

The check_piece() method iterates over each tile on the board and checks if it contains an occupying piece. If it does, it adds to the red_piece count if the piece color is "red", or to the black_piece count if the piece color is "black". It then returns a tuple containing the red_piece and black_piece counts.

    def is_game_over(self, board):
        red_piece, black_piece = self.check_piece(board)
        if red_piece == 0 or black_piece == 0:
            self.winner = "red" if red_piece > black_piece else "black"
            return True
        else:
            return False

The is_game_over() method calls the check_piece() method to get the current piece count for each color. If one color has no pieces left, the other color is declared the winner, and the method returns True. Otherwise, the method returns False.

    def check_jump(self, board):
        piece = None
        for tile in board.tile_list:
            if tile.occupying_piece != None:
                piece = tile.occupying_piece
                if len(piece.valid_jumps()) != 0 and board.turn == piece.color:
                    board.is_jump = True
                    break
                else:
                    board.is_jump = False
        if board.is_jump:
            board.selected_piece = piece
            board.handle_click(piece.pos)
        return board.is_jump

The check_jump() method checks if there is a piece that can make a jump. If there is, it sets the is_jump attribute of the board to True, sets the selected_piece to the first piece that can make a jump, and returns True. If there isn't, it sets the is_jump attribute of the board to False and returns False. Since we're creating the traditional Checkers where jump moves can't be skipped, the check_jump() will force the users to jump.

    def message(self):
        print(f"{self.winner} Wins!!")

The message method simply prints out the winner of the game.

The Board class

The Board class defines the behavior of a checkers game board and how it responds to user input. It has an __init__() method that initializes the board with the specified tile_width, tile_height, and board_size. It also initializes various variables such as selected_piece, turn, and is_jump:

# /* Board.py
import pygame
from Tile import Tile
from Pawn import Pawn

class Board:
    def __init__(self,tile_width, tile_height, board_size):
        self.tile_width = tile_width
        self.tile_height = tile_height
        self.board_size = board_size
        self.selected_piece = None

        self.turn = "black"
        self.is_jump = False

        self.config = [
            ['', 'bp', '', 'bp', '', 'bp', '', 'bp'],
            ['bp', '', 'bp', '', 'bp', '', 'bp', ''],
            ['', 'bp', '', 'bp', '', 'bp', '', 'bp'],
            ['', '', '', '', '', '', '', ''],
            ['', '', '', '', '', '', '', ''],
            ['rp', '', 'rp', '', 'rp', '', 'rp', ''],
            ['', 'rp', '', 'rp', '', 'rp', '', 'rp'],
            ['rp', '', 'rp', '', 'rp', '', 'rp', '']
        ]

        self.tile_list = self._generate_tiles()
        self._setup()

The self.config contains the starting setup of the board for our game. The tile_list calls the _generate_tiles() method that creates each tile for the board, and the _setup() method sets the starting position of pawns in the game base on config.

    def _generate_tiles(self):
        output = []
        for y in range(self.board_size):
            for x in range(self.board_size):
                output.append(
                    Tile(x,  y, self.tile_width, self.tile_height)
                )
        return output

    def get_tile_from_pos(self, pos):
        for tile in self.tile_list:
            if (tile.x, tile.y) == (pos[0], pos[1]):
                return tile

The _generate_tiles() method generates a list of tiles using the specified tile width, tile height, and board size. The get_tile_from_pos() method returns the tile object at a given position.

    def _setup(self):
        for y_ind, row in enumerate(self.config):
            for x_ind, x in enumerate(row):
                tile = self.get_tile_from_pos((x_ind, y_ind))
                if x != '':
                    if x[-1] == 'p':
                        color = 'red' if x[0] == 'r' else 'black'
                        tile.occupying_piece = Pawn(x_ind, y_ind, color, self)

The _setup() method sets up the initial board configuration by iterating through the self.config list, which represents the starting positions of the pieces, and setting the occupying_piece attribute of the corresponding tile object to a Pawn object.

    def handle_click(self, pos):
        x, y = pos[0], pos[-1]
        if x >= self.board_size or y >= self.board_size:
            x = x // self.tile_width
            y = y // self.tile_height
        clicked_tile = self.get_tile_from_pos((x, y))

        if self.selected_piece is None:
            if clicked_tile.occupying_piece is not None:
                if clicked_tile.occupying_piece.color == self.turn:
                    self.selected_piece = clicked_tile.occupying_piece
        elif self.selected_piece._move(clicked_tile):
            if not self.is_jump:
                self.turn = 'red' if self.turn == 'black' else 'black'
            else:
                if len(clicked_tile.occupying_piece.valid_jumps()) == 0:
                    self.turn = 'red' if self.turn == 'black' else 'black'
        elif clicked_tile.occupying_piece is not None:
            if clicked_tile.occupying_piece.color == self.turn:
                self.selected_piece = clicked_tile.occupying_piece

The handle_click() method takes a position (pos) as an argument, which represents the pixel coordinates of the location on the game board that were clicked by the player.

First, the method extracts the x and y coordinates from the pos argument. If the coordinates are outside the board size, the method calculates which tile was clicked based on the position of the click relative to the size of each tile. Next, the method retrieves the Tile object that was clicked by calling the get_tile_from_pos() method, passing in the (x,y) coordinates of the clicked tile.

If there is no selected_piece currently, the method checks if the clicked tile has a Pawn object on it and whether that Pawn object belongs to the current player's turn. If there is a Pawn object on the clicked tile and it belongs to the current player's turn, the selected_piece attribute of the Board object is set to that Pawn object.

If there is a selected_piece already, the method attempts to move the Pawn object to the clicked tile by calling the _move() method of the Pawn object, passing in the Tile object as an argument. If the move is successful, the turn attribute of the Board object is updated to reflect the next player's turn.

If the move is a jump, the is_jump attribute of the Board object is set to True. The method then checks if there are any more valid jumps available for the same Pawn object, by calling the valid_jumps() method of the Pawn object. If there are no more valid jumps, the turn attribute of the Board object is updated to reflect the next player's turn.

If the clicked tile does have a Pawn object on it and belongs to the current player's turn, the selected_piece attribute of the Board object is set to the Pawn object on the clicked tile.

    def draw(self, display):
        if self.selected_piece is not None:
            self.get_tile_from_pos(self.selected_piece.pos).highlight = True
            if not self.is_jump:
                for tile in self.selected_piece.valid_moves():
                    tile.highlight = True
            else:
                for tile in self.selected_piece.valid_jumps():
                    tile[0].highlight = True

        for tile in self.tile_list:
            tile.draw(display)

The draw() method is used to draw the board and the pieces. It is called to update the display with any changes made by the handle_click() method. If a piece is selected, the tile it is on and its valid moves or jumps are highlighted.

The Tile class

The Tile class represents a single tile on the game board.

# /* Tile.py
import pygame

class Tile:
    def __init__(self, x, y, tile_width, tile_height):
        self.x = x
        self.y = y
        self.pos = (x, y)
        self.tile_width = tile_width
        self.tile_height = tile_height
        self.abs_x = x * tile_width
        self.abs_y = y * tile_height
        self.abs_pos = (self.abs_x, self.abs_y)
        self.color = 'light' if (x + y) % 2 == 0 else 'dark'
        self.draw_color = (220, 189, 194) if self.color == 'light' else (53, 53, 53)
        self.highlight_color = (100, 249, 83) if self.color == 'light' else (0, 228, 10)
        self.occupying_piece = None
        self.coord = self.get_coord()
        self.highlight = False
        self.rect = pygame.Rect(
            self.abs_x,
            self.abs_y,
            self.tile_width,
            self.tile_height
        )

The __init__() method initializes the object's properties, including the x and y coordinates of the tile, its width and height, its absolute position (abs_x and abs_y) and position tuple (abs_pos).

The color property is determined by whether the sum of the tile's x and y coordinates are even or odd. If it is even, the tile is a light color, otherwise, it is dark. The draw_color and highlight_color properties are tuples representing RGB values for the tile's fill color and highlight color, respectively.

The occupying_piece property is set to None by default but can be assigned a Piece object if there is a piece occupying the tile.

Let's also have the get_coord() and draw() methods:

    def get_coord(self):
        columns = 'abcdefgh'
        return columns[self.x] + str(self.y + 1)

    def draw(self, display):
        if self.highlight:
            pygame.draw.rect(display, self.highlight_color, self.rect)
        else:
            pygame.draw.rect(display, self.draw_color, self.rect)

        if self.occupying_piece != None:
            centering_rect = self.occupying_piece.img.get_rect()
            centering_rect.center = self.rect.center
            display.blit(self.occupying_piece.img, centering_rect.topleft)

The get_coord() method returns a string representing the tile's coordinate in standard chess notation, using the lettered columns and numbered rows.

The draw() method draws the tile on the screen using pygame.draw.rect, using the draw_color property as the fill color. If the highlight property is set to True, the tile is drawn with the highlight_color instead. If there is an occupying_piece, the piece's image is blitted onto the center of the tile using pygame.Surface.blit(). The image is first centered using the get_rect() and center properties of the piece's image.

The Piece class

The Piece class is a parent class for all checker pieces in the game. It contains common attributes and methods that are shared among all pieces such as the position of the piece on the board, its color, and the ability to move to certain tiles on the board based on the game rules.

# /* Piece.py
import pygame

class Piece:
    def __init__(self, x, y, color, board):
        self.x = x
        self.y = y
        self.pos = (x, y)
        self.board = board
        self.color = color

For the Pawn and King classes specifically, they override some of the methods of the Piece class to account for the specific rules that apply to these pieces in the game of checkers. For example, the valid_moves() method of the Pawn class returns the valid moves that a pawn can make on the board based on the game rules for a pawn (can move or jump forward/against their starting side only). Similarly, the valid_moves() method of the King class returns the valid moves that a king can make on the board based on the game rules for a king (can move or jump forward or backward).

In addition, the Pawn class includes a specific implementation for pawn promotion, which occurs when a pawn reaches the opposite end of the board. The King class does not require a specific implementation for promotion since kings are already the highest-ranking pieces in the game.

Here in Piece class, we also define the move() method for both pieces.

    def _move(self, tile):
        for i in self.board.tile_list:
            i.highlight = False
        # ordinary move/s
        if tile in self.valid_moves() and not self.board.is_jump:
            prev_tile = self.board.get_tile_from_pos(self.pos)
            self.pos, self.x, self.y = tile.pos, tile.x, tile.y
            prev_tile.occupying_piece = None
            tile.occupying_piece = self
            self.board.selected_piece = None
            self.has_moved = True
            # Pawn promotion
            if self.notation == 'p':
                if self.y == 0 or self.y == 7:
                    from King import King
                    tile.occupying_piece = King(
                        self.x, self.y, self.color, self.board
                    )
            return True
        # jump move/s
        elif self.board.is_jump:
            for move in self.valid_jumps():
                if tile in move:
                    prev_tile = self.board.get_tile_from_pos(self.pos)
                    jumped_piece = move[-1]
                    self.pos, self.x, self.y = tile.pos, tile.x, tile.y
                    prev_tile.occupying_piece = None
                    jumped_piece.occupying_piece = None
                    tile.occupying_piece = self
                    self.board.selected_piece = None
                    self.has_moved = True
                    # Pawn promotion
                    if self.notation == 'p':
                        if self.y == 0 or self.y == 7:
                            from King import King
                            tile.occupying_piece = King(
                                self.x, self.y, self.color, self.board
                            )
                    return True
        else:
            self.board.selected_piece = None
            return False

The _move() method takes in a tile object representing the new position for the piece. It first checks if the new position is a valid move for the piece by calling the valid_moves() method. If the move is valid and no jump is occurring, the method updates the position of the piece and the occupying_piece attribute of the relevant tiles to reflect the new position of the piece. If the piece is a pawn and reaches the last row, it will be promoted to a king.

If a jump is occurring, the _move() method checks if the new position is a valid jump move by calling the valid_jumps() method. If the move is valid, the method updates the position of the piece, the occupying_piece attribute of the relevant tiles, and removes the jumped_piece (opponent's piece that caused the jump). The method checks if the pawn has reached the last row and promotes it to a king if necessary.

If the move is not valid, the method simply returns False.

The Pawn class

The Pawn class is a subclass of the Piece class, pawns are the pieces used from the start of the game. The class has an __init__() method that calls the parent's __init__() method to initialize the object's x and y position, color, and board. It also loads an image for the pawn and assigns it a notation of 'p':

# /* Pawn.py
import pygame
from Piece import Piece

class Pawn(Piece):
    def __init__(self, x, y, color, board):
        super().__init__(x, y, color, board)
        img_path = f'images/{color}-pawn.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width, board.tile_height))
        self.notation = 'p'

Let's add three more methods; the _possible_moves(), the valid_moves(), and the valid_jumps() methods:

    def _possible_moves(self):
        # (x, y) move for left and right
        if self.color == "red":
            possible_moves = ((-1, -1), (+1, -1)) 
        else:
            possible_moves = ((-1, +1), (+1, +1))
        return possible_moves

The _possible_moves() method returns a tuple of possible moves that a pawn can make in the form of (x, y) coordinates for left and right directions. The possible moves depend on the color of the pawn. For example, a red pawn can move left and right in the (-1, -1) and (+1, -1) directions respectively.

    def valid_moves(self):
        tile_moves = []
        moves = self._possible_moves()
        for move in moves:
            tile_pos = (self.x + move[0], self.y + move[-1])
            if tile_pos[0] < 0 or tile_pos[0] > 7 or tile_pos[-1] < 0 or tile_pos[-1] > 7:
                pass
            else:
                tile = self.board.get_tile_from_pos(tile_pos)
                if tile.occupying_piece == None:
                    tile_moves.append(tile)
        return tile_moves

The valid_moves() method checks all possible moves for a pawn on the board and returns a list of valid moves. It does so by iterating through the possible moves and checking if each move results in a valid tile position on the board that does not contain any occupying piece. If the tile position is valid and empty, it appends the tile object to the list of valid moves.

    def valid_jumps(self):
        tile_jumps = []
        moves = self._possible_moves()
        for move in moves:
            tile_pos = (self.x + move[0], self.y + move[-1])
            if tile_pos[0] < 0 or tile_pos[0] > 7 or tile_pos[-1] < 0 or tile_pos[-1] > 7:
                pass
            else:
                tile = self.board.get_tile_from_pos(tile_pos)
                if self.board.turn == self.color:
                    if tile.occupying_piece != None and tile.occupying_piece.color != self.color:
                        next_pos = (tile_pos[0] + move[0], tile_pos[-1] + move[-1])
                        next_tile = self.board.get_tile_from_pos(next_pos)		
                        if next_pos[0] < 0 or next_pos[0] > 7 or next_pos[-1] < 0 or next_pos[-1] > 7:
                            pass
                        else:
                            if next_tile.occupying_piece == None:
                                tile_jumps.append((next_tile, tile))
        return tile_jumps

The valid_jumps() method returns a list of tuples that represents a valid jump move for the pawn. It checks for jumps in a similar way to valid_moves() but also checks for a tile position with an opposing piece. If such a tile position exists, it checks the next position in the direction of the jump to ensure that it is empty. If the next position is empty, it appends a tuple containing the current tile and the tile that contains the opponent's piece to the list of valid jumps.

The King class

The King class is very similar to the Pawn class in terms of its structure and methods. However, the _possible_moves() method now returns all the diagonal directions, as a king can move diagonally in any direction.

The valid_moves() method returns a list of tiles that the king can move to, which are tiles that are empty and within the boundaries of the board. The valid_jumps() method returns a list of tuples, where each tuple contains two tiles, representing a jump that the king can make over an opposing piece. The implementation of valid_jumps() is very similar to that of the Pawn class, with the only difference being that the King can jump over opposing pieces in any diagonal direction:

# /* King.py
import pygame
from Piece import Piece

class King(Piece):
    def __init__(self, x, y, color, board):
        super().__init__(x, y, color, board)
        img_path = f'images/{color}-king.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width, board.tile_height))
        self.notation = 'k'

    def _possible_moves(self):
        possible_moves = ((-1, -1), (+1, -1), (-1, +1), (+1, +1))
        return possible_moves

    def valid_moves(self):
        tile_moves = []
        moves = self._possible_moves()
        for move in moves:
            tile_pos = (self.x + move[0], self.y + move[-1])
            if tile_pos[0] < 0 or tile_pos[0] > 7 or tile_pos[-1] < 0 or tile_pos[-1] > 7:
                pass
            else:
                tile = self.board.get_tile_from_pos(tile_pos)
                if tile.occupying_piece == None:
                    tile_moves.append(tile)
        return tile_moves

    def valid_jumps(self):
        tile_jumps = []
        moves = self._possible_moves()
        for move in moves:
            tile_pos = (self.x + move[0], self.y + move[-1])
            if tile_pos[0] < 0 or tile_pos[0] > 7 or tile_pos[-1] < 0 or tile_pos[-1] > 7:
                pass
            else:
                tile = self.board.get_tile_from_pos(tile_pos)
                if self.board.turn == self.color:
                    if tile.occupying_piece != None and tile.occupying_piece.color != self.color:
                        next_pos = (tile_pos[0] + move[0], tile_pos[-1] + move[-1])
                        next_tile = self.board.get_tile_from_pos(next_pos)		
                        if next_pos[0] < 0 or next_pos[0] > 7 or next_pos[-1] < 0 or next_pos[-1] > 7:
                            pass
                        else:
                            if next_tile.occupying_piece == None:
                                tile_jumps.append((next_tile, tile))
        return tile_jumps

And now the game is done! You can try the game by running the Main.py on your terminal:

$ python Main.py

Here are some of the game snapshots:

Starting the game:

Pawn's move:

Pawn's Jump:

Pawn Promotion:

King's moves:

King's Jump:

Conclusion

In this tutorial, we've explored how to implement a checkers game in Python using the Pygame library. We've covered how to create the game board, pieces, and their movements, as well as the basic rules of the game.

Playing checkers is an enjoyable way to pass the time and test your strategy and critical thinking skills. We hope this article has been helpful in understanding the basics of checkers programming in Python. See you on the next one!

We also have a chess game tutorial, if you want to build one!

Get the complete code here.

Here is a list of other pygame tutorials:

Happy coding ♥

Let our Code Converter simplify your multi-language projects. It's like having a coding translator at your fingertips. Don't miss out!

View Full Code Convert My Code
Sharing is caring!



Read Also



Comment panel

    Got a coding query or need some guidance before you comment? Check out this Python Code Assistant for expert advice and handy tips. It's like having a coding tutor right in your fingertips!