DEV Community

Cover image for Creating a chess game with Python, pygame and chess (Pt. 1)

Posted on • Updated on

Creating a chess game with Python, pygame and chess (Pt. 1)

Hey, I'm Prince and I'm going to be walking you through my process of creating a chess game with Python (this is my first project with pygame).
DISCLAIMER: This article is not for beginners but I will make an effort to make it accessible to those with just a little bit of Python knowledge. Some concepts involved here include; OOP and data structures.

The objectives of this program are to create a chess game that can be played with 2 players or against an AI.

Here's a link to the project on Github, feel free to play around with it or contribute.

I relied heavily on the techniques covered in this article

So firstly create a new folder (for the purposes of this article we will call it chess-game) where you want to store the code and in that folder create a virtual environment (if you are not familiar with virtual environments take a look at this ), activate the virtual environment and install the following packages:

  1. chess
  2. pygame

We need the chess module to handle the chess rules and validations and pygame to make the actual game.

Ok, we are going to split this walkthrough into 3 sections:

  • The pieces, squares and the boards
  • Displaying the board and pieces on the pygame window and
  • Creating an AI player

The pieces, squares and the board

We will create a new package in our code, gui_components. To create a package just create a new folder (in this case gui_components) and in that new folder create a new file
We will also create a new folder in our project directory (chess-game) called skins. This is where we will store the images for our pieces. Feel free to copy the skins directory from the repository

The project should have the following structure:

  1. The pieces We will create a file in our gui_components folder. In this file we will create a Piece class. For now the objects of this class will simply be used to display the image and get the value of the piece based on its notation (in chess the different pieces have notations k for King, q for Queen, r for Rook, b for bishop, n for Knight and p for Pawn) and whether or not it has been captured.
import os
import pygame

class Piece:
    colors_notations_and_values = {
        "w": {
            "p": 1,
            "n": 3,
            "b": 3,
            "r": 5,
            "q": 9,
            "k": 90
        "b": {
            "p": -1,
            "n": -3,
            "b": -3,
            "r": -5,
            "q": -9,
            "k": -90

    def __init__(self, name, notation, color, skin_directory="skins/default", is_captured=False) -> None: = name
        self.__notation = notation
        self.color = color
        self.skin_directory = skin_directory

        self.value = self.get_piece_value()

    def get_piece_value(self):
        return Piece.colors_notations_and_values[self.color][self.__notation.lower()]

    def get_piece_color_based_on_notation(notation) -> str:
        The chess module displays black pieces' notations in lowercase and white in uppercase, so we can get the color based on this
        return "w" if notation.isupper() else "b"

    def get_value_from_notation(notation: str, color: str) -> int:
        A class method that gets the corresponding value for a particular notation and color
        return Piece.colors_notations_and_values[color][notation.lower()]

    def set_is_captured(self, is_captured: bool):
        self.__is_captured = bool(is_captured)

    def get_image_path(self):
        Gets the path to the image of the piece based on its notation and 
        whether or not it has been captured
        if not self.__is_captured:
            path = os.path.join(self.skin_directory, self.color, f"{self.__notation.lower()}.png")
            path = os.path.join(self.skin_directory, self.color, "captured", f"{self.__notation.lower()}.png")

        return path

    def get_image(self):
        Returns a pygame image object from the piece's corresponding image path
        image_path = self.get_image_path()

        if os.path.exists(image_path):
            return pygame.image.load(image_path)
            raise FileNotFoundError(f"The image was not found in the {image_path}")

    def __str__(self):
        return f"{self.__notation} {self.color}"

    def get_notation(self) -> str:
        Returns the notation of the piece, (pawns' notations are empty strings)
        if self.__notation != 'p':
            return self.__notation.upper()

        return ''

    def __set_notation(self, notation):
        self.__notation = notation

    def promote(self, notation: str):
        Promotes this piece to a piece with the notation notation.
        It is important to note that promotion does not increase the piece's value, 
        just its capabilities
        if self.__notation.lower() != "p":
            raise ValueError("Cannot promote a piece other than a pawn")

        if notation not in ["q", "r", "n", "b"]:
            raise ValueError("Can only promote to queen, rook, bishop or knight pieces")

Enter fullscreen mode Exit fullscreen mode
  1. The squares and board When creating this game I thought about being able to have a checkers game with it, so the classes in this section kind of reflect that vision. First and foremost, create a new file In this file create a Square class (a generic class for squares checkers or chess)
import chess

import pygame

from gui_components.pieces import Piece

class Square(pygame.Rect):
    def __init__(self, left: float, top: float, width: float, height: float, background_color: str, border_color: str, piece: Piece = None) -> None:
        super().__init__(left, top, width, height)
        self.background_color = background_color
        self.border_color = border_color
        self.piece = piece
        self.is_possible_move = False

    def toggle_is_possible_move(self):
        self.is_possible_move = not self.is_possible_move
        return self

    def empty(self):
        self.piece = None

        return self

    def set_is_possible_move(self, value: bool):
        self.is_possible_move = bool(value)
        return self
Enter fullscreen mode Exit fullscreen mode

Now a square for chess pieces

class ChessSquare(Square):
    def __init__(self, left: float, top: float, width: float, height: float, background_color: str, border_color: str, file_number, rank_number, piece: Piece = None) -> None:
        super().__init__(left, top, width, height, background_color, border_color, piece)
        self.file_number = file_number
        self.rank_number = rank_number
        self.ranks = list( str(i) for i in range(1, 9) )
        self.files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']

    def get_chess_square(self) -> chess.Square:
        Returns a chess.Square object that corresponds to this one
        return chess.square(self.file_number, self.rank_number)

    def is_identical_to_chess_square(self, square: chess.Square) -> bool:
        Checks if this object corresponds to a chess.Square object
        return (
            self.file_number == chess.square_file(square) and 
            self.rank_number == chess.square_rank(square)

    def get_rank(self) -> str:
        Gets the rank of the object. Ranks are the rows of the board and they range from 1 to 8
        return self.ranks[ self.rank_number ]

    def get_file(self) -> str:
        Gets the file of the object. Files are the columns of the board and range from A to H
        return self.files[ self.file_number ]

    def get_notation(self) -> str:
        Gets the notation of the square object. A squares notation is simply its file and rank
        return f'{self.get_file()}{self.get_rank()}'
Enter fullscreen mode Exit fullscreen mode

Now for the board. Same as the square we will create 2 board classes although the parent board class doesn't do much for now. This class will help us keep track of the pieces on our squares, highlight a move made, display the possible moves, get a square that corresponds to particular coordinates and make a move.

class Board(pygame.sprite.Sprite):
    RANKS = [ i+1 for i in range(0, 8) ]
    FILES = [ chr(i) for i in range(65, 65+9) ]

    def __init__(self, number_of_rows, number_of_columns, left, top, width, height, horizontal_padding, vertical_padding, **kwargs) -> None:
        self.left = left = top
        self.number_of_rows = number_of_rows
        self.number_of_columns = number_of_columns
        self.width = width
        self.height = height
        self.horizontal_padding = horizontal_padding
        self.vertical_padding = vertical_padding
        self.squares = []

    def create_squares(self):

class ChessBoard(Board):
    def __init__(
        self, left, top, width, height, 
        horizontal_padding=None, vertical_padding=None, 
        light_square_color: str=(245, 245, 245), dark_square_color: str=(100, 100, 100), 
        previous_square_highlight_color=(186, 202, 43),
        current_square_highlight_color=(246, 246, 105),
        board: chess.Board=None, move_hints=True, **kwargs
    ) -> None:
            8, 8, left, top, width, height, 
            horizontal_padding, vertical_padding, **kwargs
        self.light_square_color = light_square_color
        self.dark_square_color = dark_square_color
        self.board = board
        self.move_hints = move_hints
        print('The current board is')
        self.rect = pygame.Rect(left, top, width, height)


        self.captured_pieces = {
            "w": [],
            "b": []

        # the square the piece that made the latest move came from
        self.previous_move_square = None 
        self.current_move_square = None 

        self.previous_square_highlight_color = previous_square_highlight_color
        self.current_square_highlight_color = current_square_highlight_color

        self.is_flipped = bool(kwargs["flipped"]) if "flipped" in kwargs else False

        # set to True if a pawn has the right to promote and has to choose which piece it wants to promote to
        self.awaiting_promotion = False

        # self.flip()

    def __set_square_size(self):
        self.__square_size = self.height // 8

    def square_size(self) -> int:
        return self.__square_size

    def get_piece_from_notation(self, notation):
        Returns a piece object based on a particular notation
        if notation != '.':
            piece_color = "b" if notation.islower() else "w"
            notation = notation.lower()
            piece = Piece(name=notation, notation=notation, color=piece_color)

            return piece

        return None

    def get_square_from_chess_square(self, square: chess.Square) -> ChessSquare:
        Returns a Square object that corresponds to a particular chess.Square object
        square_file = chess.square_file(square)
        square_rank = chess.square_rank(square)

        rank = self.squares[ 7 - square_rank ]

        return rank[ square_file ]

    def create_squares(self):
        Creates the squares oon the board and places pieces on them based on the state of the chess.Board object
        string = self.board.__str__()
        ranks_inverted = string.split('\n')#[::-1]

        for i in range(self.number_of_rows):
            self.squares.append( [] )

            rank = ranks_inverted[i].split(' ')

            for j in range(self.number_of_columns):
                square = rank[j]

                piece = self.get_piece_from_notation(square)

                color = self.light_square_color if (i+j) % 2 == 0 else self.dark_square_color

                board_square = ChessSquare( 
                    self.left + (j*self.square_size), + (i*self.square_size), self.square_size, 
                    self.square_size, color, self.dark_square_color, j, 7 - i, piece=piece

                self.squares[i].append( board_square )

    def flip(self):
        Changes the coordinates of the squares in essence flipping them
        board_rect = pygame.Rect(self.left,, self.width, self.height)

        for (i, rank) in enumerate(self.squares):
            print(f"Flipping the squares on rank: {8 - i}")
            for (j, square) in enumerate(rank):
                square: ChessSquare = square
                _old = square.__repr__()

                square.x += (7 - j) * self.square_size
                square.y += (7 - i) * self.square_size

                if not square.colliderect(board_rect):
                    print("Square is out of bounds of the board")
                    print(f"The board rectangle is: {board_rect}. The square rectangle is: {square}")

                    print(f"Square was flipped successfully. Old coordinates: {_old}, new: {square}")

        self.is_flipped = not self.is_flipped

    def place_pieces(self):
        places pieces on the board based on the progress of the board attribute 
        different from create_squares in that it doesn't create squares it instead 
        clears all the squares of existing pieces and positions the pieces on the board
        string = self.board.__str__()
        ranks_inverted = string.split('\n')#[::-1]

        for i in range( self.number_of_rows ):
            rank = ranks_inverted[i].split(' ')

            for j in range( self.number_of_columns ):
                board_square = rank[j]

                piece = self.get_piece_from_notation(board_square)

                self.squares[i][j].piece = piece

    def get_possible_moves(self, source_coordinates, remove_hints=False):
        Gets the possible moves from some coordinates and marks the squares as possible moves if move_hints are enabled
        # source_square = [ square.get_chess_square() for square in self.iter_squares() if square.collidepoint(source_coordinates) ]
        source_square = self.get_square_from_coordinates(source_coordinates)

        if source_square:
            destination_chess_squares = [ move.to_square for move in self.board.legal_moves if move.from_square == source_square ]
            destination_squares = [ square.set_is_possible_move(not remove_hints) for square in self.iter_squares() if square.get_chess_square() in destination_chess_squares ]

            return destination_squares

        return []

    def get_possible_moves_without_hint(self, source_coordinates):
        Gets the possible moves from some coordinates
        source_square = self.get_square_from_coordinates(source_coordinates)

        if source_square:
            destination_chess_squares = [ move.to_square for move in self.board.legal_moves if move.from_square == source_square ]
            destination_squares = [ square for square in self.iter_squares() if square.get_chess_square() in destination_chess_squares ]

            return destination_squares

        return []

    def hide_hints(self):
        Hides the hints on the squares
        [square.set_is_possible_move(False) for square in self.iter_squares()]

    def get_square_from_coordinates(self, coordinates, return_chess_square=True) -> ChessSquare:
        Returns a square that corresponds to the coordinates passed
        square = [ (square.get_chess_square() if return_chess_square else square) for square in self.iter_squares() if square.collidepoint(coordinates) ]

        if len(square) > 0:
            square = square[0]

            return square
        print(f"There is no square at the {coordinates} coordinates")
        return None

    def get_move_notation(self, source_square: ChessSquare, destination_square: ChessSquare):
        Gets the notation for a particular move made from source_square to destination_square
        move = ''

        if source_square.piece:
            other_pieces_of_the_same_type_that_can_make_move = self.get_pieces_that_can_make_move( [source_square.piece.get_notation()], source_square.piece.color, destination_square, [source_square] )
            same_rank = False
            same_file = False

            if source_square.piece.get_notation() != '':
                for square in other_pieces_of_the_same_type_that_can_make_move:
                    if square.rank_number == source_square.rank_number:
                        same_rank = True
                    if square.file_number == source_square.file_number:
                        same_file = True

                move = move + source_square.piece.get_notation()

                if same_file or same_rank:
                    if not same_file:
                        move = move + f"{source_square.get_file()}"
                    elif same_file and not same_rank:
                        move = move + f"{source_square.get_rank()}"
                        move = move + f"{source_square.get_notation()}"

        if destination_square.piece:
            move = move + 'x'

            if source_square.piece and source_square.piece.get_notation() == '':
                move = source_square.get_file() + move

        move = move + f'{destination_square.get_notation()}'

        if source_square.piece.get_notation() == 'K' and source_square.get_file() == 'e' and destination_square.get_file() in [ 'c', 'g' ]:
            # castling
            if destination_square.get_file() == 'c':
                return '0-0-0'
                return '0-0'

        move = chess.Move(
            from_square=source_square.get_chess_square(), to_square=destination_square.get_chess_square()

        return move

    def get_pieces_that_can_make_move(self, piece_notations: list, color, square: ChessSquare, squares_to_exclude: list):
        Returns the pieces with notations in <piece_notations> list and of color <color> that can make a move the <square> square 
        while excluding the pieces on the <squares_to_exclude> list
        squares_with_pieces_of_specified_types = [ _square for _square in self.iter_squares() if _square.piece and _square.piece.get_notation() in piece_notations and _square.piece.color == color and _square not in squares_to_exclude ]
        squares_that_can_make_move = [ _square for _square in squares_with_pieces_of_specified_types if square in self.get_possible_moves_without_hint( ]

        return squares_that_can_make_move

    def play(self, source_coordinates, destination_coordinates):
        Makes a move from source_coordinates to destination_coordinates
        source_square = self.get_square_from_coordinates(source_coordinates, return_chess_square=False)
        destination_square = self.get_square_from_coordinates(destination_coordinates, return_chess_square=False)

        self._play(source_square, destination_square)

    def _play(self, source_square: ChessSquare=None, destination_square: ChessSquare=None, 
        source_chess_square: chess.Square=None, destination_chess_square: chess.Square=None,
        move: chess.Move=None
        Makes a move based on the arguments.
        if move:
            self.previous_move_square = self.get_square_from_chess_square(move.from_square)
            self.current_move_square = self.get_square_from_chess_square(move.to_square)

        elif source_square and destination_square:
            move = self.get_move_notation(source_square, destination_square)
            self.previous_move_square = source_square
            self.current_move_square = destination_square

        elif source_chess_square and destination_chess_square:
            move = chess.Move(from_square=source_chess_square, to_square=destination_chess_square)
            self.previous_move_square = self.get_square_from_chess_square(source_chess_square)
            self.current_move_square = self.get_square_from_chess_square(destination_chess_square)

            print("None of the conditions were fulfilled. No move is currently being made")


        print('The current board is')

    def make_move(self, move):
        Makes a move either with an str object or a chess.Move object
        if isinstance(move, str):
        elif isinstance(move, chess.Move):

            if self.board.is_capture(move):
                destination_square: ChessSquare = self.get_square_from_chess_square(move.to_square)
                piece: Piece = destination_square.piece

                print("The move was a capture")

                if piece is not None:
                    color = piece.color



    def iter_squares(self):
        A generator that returns the different squares on the board
        for rank in self.squares:
            for square in rank:
                yield square

Enter fullscreen mode Exit fullscreen mode

Displaying the board in a pygame window

Before we move forward with this, let's firstly create some classes we will use in this file. In our gui_components folder we will create a new file Put this code inside that file

import pygame

class BorderedRectangle():
    An object that contains 2 pygame.Rect object, one put inside the other
    def __init__(
        self, left: float, top: float, width: float, height: float, 
        background_color: str, border_color: str, border_width: int,
        outer_rectangle_border_width=2, inner_rectangle_border_width=2
    ) -> None:
        self.background_color = background_color
        self.border_color = border_color
        self.is_possible_move = False
        self.outer_rectangle_border_width = outer_rectangle_border_width
        self.inner_rectangle_border_width = inner_rectangle_border_width

        self.outer_rectangle = pygame.Rect(left, top, width, height)

        self.inner_rectangle = pygame.Rect(
            left+(border_width / 2), top+(border_width/2), 
            width - border_width, height - border_width

Enter fullscreen mode Exit fullscreen mode

Now in our root directory (chess-game), create a new file In this file we will write the code to display our board in a pygame window and even to play the game without AI and board flips.

import chess

import pygame

from pygame import mixer


from gui_components.board import ChessBoard
from gui_components.components import BorderedRectangle

from ai import players as ai_players


screen = pygame.display.set_mode([500, 500])

board = chess.Board()

# A dictionary of the different players in the game. True corresponds to white and 
# False to black
players = { 
    True: "user",
    False: "user"

turns_taken = {
    True: False, # set to True if white has already started playing 
    False: False # set to True if black has already started playing

# the different sounds for the moves
move_sound = mixer.Sound("sound_effects/piece_move.mp3")
check_sound = mixer.Sound("sound_effects/check.mp3")
checkmate_sound = mixer.Sound("sound_effects/checkmate.mp3")

TURN = True

running = True

LIGHT_COLOR = (245, 245, 245) # color of the light squares
DARK_COLOR = ( 100, 100, 100 ) # color of the dark squares
WHITE_COLOR = (255, 255, 255) # white
BLACK_COLOR = (0, 0, 0) # black

chess_board = ChessBoard(  # creating a new ChessBoard object
    50, 50, 400, 400, 0, 0, board=board

def draw_bordered_rectangle(rectangle: BorderedRectangle, screen):
    pygame.draw.rect( screen, rectangle.border_color, rectangle.outer_rectangle, width=rectangle.outer_rectangle_border_width )
    pygame.draw.rect( screen, rectangle.background_color, rectangle.inner_rectangle, width=rectangle.inner_rectangle_border_width )

def draw_chessboard(board: ChessBoard):
    Draw the chess board on the pygame window
    ranks = board.squares # get the rows of the board

    # a rectangle enclosing the board and the files and ranks labels
    board_bordered_rectangle = BorderedRectangle(25, 25, 450, 450, WHITE_COLOR, DARK_COLOR, 48)
    draw_bordered_rectangle(board_bordered_rectangle, screen)

    # draw the inner rectangle of the bordered rectangle with the same color 
    # as that of the dark squares
        screen, board_bordered_rectangle.border_color, board_bordered_rectangle.inner_rectangle, 

    board_top_left = board.rect.topleft
    board_top_right = board.rect.topright
    board_bottom_left = board.rect.bottomleft

    for i, rank in enumerate(ranks):
        rank_number = ChessBoard.RANKS[ 7 - i ]
        file_letter = ChessBoard.RANKS[i]

        font_size = 15 # font size for the ranks and files

        # add the text rectangle on the left and right of the board
        font = pygame.font.SysFont('helvetica', font_size)

        # render the ranks (1-8)
        for _i in range(1):
            if _i == 0:
                _rect = pygame.Rect(
                    board_top_left[0] - font_size, board_top_left[1] + (i*board.square_size), 
                    font_size, board.square_size
                _rect = pygame.Rect(
                    board_top_right[0], board_top_right[1] + (i*board.square_size),
                    font_size, board.square_size

            text = font.render(f"{rank_number}", True, DARK_COLOR)
            text_rect = text.get_rect()

            screen.blit(text, text_rect)

        # render the files A-H
        for _i in range(1):
            if _i == 0:
                _rect = pygame.Rect(
                    board_top_left[0] + (i*board.square_size), board_top_left[1] - font_size, 
                    board.square_size, font_size
                _rect = pygame.Rect(
                    board_top_left[0] + (i*board.square_size), board_bottom_left[1], 
                    board.square_size, font_size

            text = font.render(f"{file_letter}", True, DARK_COLOR)
            text_rect = text.get_rect()

            screen.blit(text, text_rect)

        for j, square in enumerate(rank):
            if square is board.previous_move_square:
                # highlight source square of the latest move
                pygame.draw.rect( screen, board.previous_square_highlight_color, square )
            elif square is board.current_move_square:
                # highlight the destination square of the latest move
                pygame.draw.rect( screen, board.current_square_highlight_color, square )
                pygame.draw.rect( screen, square.background_color, square )

            if square.piece:
                # draw the piece on the square
                    image = square.piece.get_image()
                    image_rect = image.get_rect()

                    screen.blit( image, image_rect )
                except TypeError as e:
                    raise e
                except FileNotFoundError as e:
                    print(f"Error on the square on the {i}th rank and the {j}th rank")
                    raise e

            if square.is_possible_move and board.move_hints:
                # draw a circle in the center of the square to highlight is as a possible move
                    screen, (50, 50, 50), 

def play_sound(board):
    Play sound after move based on move type
    if board.is_checkmate():

    elif board.is_check():

    elif board.is_stalemate():


def play(source_coordinates: tuple=None, destination_coordinates: tuple=None):
    Make a move on the board based on the source and destination coordinates if a user is playing
    global board, TURN, IS_FIRST_MOVE, chess_board

    turn = board.turn

    player = players[turn]
    turns_taken[turn] = not turns_taken[turn]
    print(f"Setting {turns_taken[turn]} to {not turns_taken[turn]}")

    if not isinstance(player, str):
        # AI model to play

        TURN = not TURN

        if isinstance(players[TURN], ai_players.AIPlayer):
            # if the next player is an AI, automatically play
            print("Next player is AI, making a move for them automaically")
            # sleep(5)
        if source_coordinates and destination_coordinates:
            # user to play
            print("User is making move")
  , destination_coordinates)
            TURN = not TURN

        IS_FIRST_MOVE = False

    turns_taken[turn] = not turns_taken[turn]
    print(f"Setting {turns_taken[turn]} to {not turns_taken[turn]}")

def click_handler(position):
    Handle the click events of the game

    if chess_board.rect.collidepoint(position): # if position is in the board
        current_player = players[TURN]

        if isinstance(current_player, str):
            if SOURCE_POSITION is None:
                POSSIBLE_MOVES = chess_board.get_possible_moves(position)
                SOURCE_POSITION = position if POSSIBLE_MOVES else None
                # getting the squares in the possible destinations that correspond to the clicked point
                destination_square = [ square for square in POSSIBLE_MOVES if square.collidepoint(position) ]

                if not destination_square:
                    chess_board.get_possible_moves(SOURCE_POSITION, remove_hints=True)
                    SOURCE_POSITION = None
                    destination_square = destination_square[0]
                    print(f"In, about to play, the source and destination are {SOURCE_POSITION} and {position} respectively")
                    chess_board.get_possible_moves(SOURCE_POSITION, remove_hints=True)

                    # SOURCE_POSITION, position )
                    play(SOURCE_POSITION, position)
                    SOURCE_POSITION = None

                    current_player = players[TURN]

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

        if event.type == pygame.MOUSEBUTTONDOWN:
            MOUSE_CLICKED_POSITION = pygame.mouse.get_pos()

    screen.fill( (255, 255, 255) )

    draw_chessboard(chess_board, True)


Enter fullscreen mode Exit fullscreen mode

Now if you activate your virtual environment and run the file python a GUI chess game should be displayed:

A new game GUI

Here's a gif of a game between two users

User v User

In the next article, we are going to look at the creation of an AI player and how to integrate that with our existing code.

Top comments (0)