DEV Community

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

Posted on • Edited 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 __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

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) 
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 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 
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 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 
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 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 ) 
Enter fullscreen mode Exit fullscreen mode

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() 
Enter fullscreen mode Exit fullscreen mode

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

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)