Creating a chess game with Python, pygame and chess (Pt. 1)
Tags: python, gamedev, ai
Published: 2022-11-21 10:45:39

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](https://github.com/Nguh-Prince/Chess-Pygame) on Github, feel free to play around with it or contribute.

I relied heavily on the techniques covered in this [article](https://www.freecodecamp.org/news/simple-chess-ai-step-by-step-1d55a9266977/)

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](https://www.freecodecamp.org/news/how-to-setup-virtual-environments-in-python/) ), 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 `__init__.py`)
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](https://github.com/Nguh-Prince/Chess-Pygame)

The project should have the following structure: 
chess-game/
---|gui_components/
---|skins/
1. The pieces
We will create a `pieces.py` 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:
        self.name = name
        self.__notation = notation
        self.color = color
        self.skin_directory = skin_directory
        self.set_is_captured(is_captured)

        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")
        else:
            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)
        else:
            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")
        self.__set_notation(notation)
            
```

2. 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 `boards.py`. 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
```

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()}'
```

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
        self.top = 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):
        pass

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:
        super().__init__(
            8, 8, left, top, width, height, 
            horizontal_padding, vertical_padding, **kwargs
        )
        self.__set_square_size()
        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')
        print(self.board)
        self.rect = pygame.Rect(left, top, width, height)

        self.create_squares()
        
        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

    @property
    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), self.top + (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.top, 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}")

                else:
                    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 ):
                self.squares[i][j].empty()
                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()}"
                    else:
                        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'
            else:
                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(_square.center) ]

        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.make_move(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.make_move(move)
            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.make_move(move)
            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)
        
        else:
            print("None of the conditions were fulfilled. No move is currently being made")
        
        self.place_pieces()

        print('The current board is')
        print(self.board)

    def make_move(self, move):
        """
        Makes a move either with an str object or a chess.Move object
        """
        if isinstance(move, str):
            self.board.push_san(move)
        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:
                    piece.set_is_captured(True)
                    color = piece.color

                    self.captured_pieces[color].append(piece)

            self.board.push(move)

    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

```

###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 `components.py`. 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
        )

```

Now in our root directory (chess-game), create a new file `main.py`. 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

mixer.init()

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

from ai import players as ai_players

pygame.init()

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")

SOURCE_POSITION = None
DESTINATION_POSITION = None
PREVIOUSLY_CLICKED_POSITION = None
POSSIBLE_MOVES = []
TURN = True
IS_FIRST_MOVE = 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
    pygame.draw.rect( 
        screen, board_bordered_rectangle.border_color, board_bordered_rectangle.inner_rectangle, 
        width=1
    )

    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
                )
            else:
                _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()
            text_rect.center = _rect.center

            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
                )
            else:
                _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()
            text_rect.center = _rect.center

            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 )
            else:
                pygame.draw.rect( screen, square.background_color, square )
            
            if square.piece:
                # draw the piece on the square
                try:
                    image = square.piece.get_image()
                    image_rect = image.get_rect()
                    image_rect.center = square.center

                    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
                pygame.draw.circle( 
                    screen, (50, 50, 50), 
                    square.center,
                    board.square_size*0.25
                )

def play_sound(board):
    """
    Play sound after move based on move type
    """
    if board.is_checkmate():
        mixer.Sound.play(checkmate_sound)
    
    elif board.is_check():
        mixer.Sound.play(check_sound)
    
    elif board.is_stalemate():
        pass
    
    else:
        mixer.Sound.play(move_sound)

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
        player.make_move(chess_board)
        play_sound(board)
        
        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)
    else:
        if source_coordinates and destination_coordinates:
            # user to play
            print("User is making move")
            chess_board.play(source_coordinates, destination_coordinates)
            play_sound(board)
            TURN = not TURN

    if IS_FIRST_MOVE:
        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
    """
    global SOURCE_POSITION, POSSIBLE_MOVES, TURN

    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
            else:
                # 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
                else:
                    destination_square = destination_square[0]
                    print(f"In main.py, about to play, the source and destination are {SOURCE_POSITION} and {position} respectively")
                    chess_board.get_possible_moves(SOURCE_POSITION, remove_hints=True)
                    
                    # chess_board.play( 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()
            click_handler(MOUSE_CLICKED_POSITION)

    screen.fill( (255, 255, 255) )
    
    draw_chessboard(chess_board, True)

    pygame.display.flip()

pygame.quit()
```


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

![A new game GUI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y7dtub2dx7olkm0e09qo.png)

Here's a gif of a game between two users 

![User v User](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zgc1qgz83lg9u87koxq0.gif)

In the [next](https://dev.to/nguhprince/creating-a-chess-game-with-python-pygame-and-chess-pt-2-539b) article, we are going to look at the creation of an AI player and how to integrate that with our existing code.