How to Make a Chess Game with Pygame in Python

Learn how you can build a chess game from scratch with Python classes and pygame library in Python.
  · 23 min read · Updated may 2023 · Game Development

Turn your code into any language with our Code Converter. It's the ultimate tool for multi-language programming. Start converting now!

The chess game is a pretty cool project idea for intermediate Python programmers. It's good practice for making apps with GUIs while getting good at using classes. In this tutorial, you learn about:

  • Using the basics of pygame.
  • Learn how to code a chess game with Python classes.

Related: How to Make a Hangman Game in Python.

Installation and Setting Up

Before we start coding, let's first install the pygame module in the terminal:

$ pip install pygame

Once we installed the pygame, let's move into setting up our environment by making the py files and folder we're using in this order:

> python-chess
    > data
        > classes
            > pieces
                /* Bishop.py
                /* King.py
                /* Knight.py
                /* Pawn.py
                /* Queen.py
                /* Rook.py
            /* Board.py
            /* Piece.py
            /* Square.py
        > imgs
    /* main.py

Move the images of the chess icons you'll use in the python/data/imgs/ directory. Make sure your image files are named [color's 1st letter]_[piece name].png just like this:

If you don't have chess icons, you can use mine here.

Coding the Game

And now we're done setting up; we can start coding now. Our chess game has two main code parts; creating the board and creating the pieces. The board will mainly focus on square positions and game rules, while the pieces focus on the piece they represent and the moves it has.

Making the Board

Let's start by making the Square class. The Square class creates, colors, position, and draw each chess tile inside our game window:

# /* Square.py
import pygame

# Tile creator
class Square:
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.abs_x = x * width
        self.abs_y = y * height
        self.abs_pos = (self.abs_x, self.abs_y)
        self.pos = (x, y)
        self.color = 'light' if (x + y) % 2 == 0 else 'dark'
        self.draw_color = (220, 208, 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.width,
            self.height
        )

    # get the formal notation of the tile
    def get_coord(self):
        columns = 'abcdefgh'
        return columns[self.x] + str(self.y + 1)

    def draw(self, display):
        # configures if tile should be light or dark or highlighted tile
        if self.highlight:
            pygame.draw.rect(display, self.highlight_color, self.rect)
        else:
            pygame.draw.rect(display, self.draw_color, self.rect)
        # adds the chess piece icons
        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 first thing we're gonna do is to make a class for making chess Square. Let's start by adding the __init__() function to get the square's width, height, x for a row, and y for the column.

With this basic information, we can fulfill other variables using them. As you see above, we have self.x and self.y while we also have self.abs_x and self.abs_y. self.abs_x and self.abs_y dictates where the chess tile is assigned to be drawn inside the window, and we compile them both in self.abs_pos.

The self.color tells the square tile should be light colored if it is divisible by 2 or instead dark if not, while the self.draw_color tells the color configuration for light and dark. We also have self.highlight_color which we use to highlight the tiles with the possible movement of a piece if it was selected. The self.rect configures the width, height, and location (using self.abs_x and self.abs_y) of a square or tile.

The get_coord() returns the name of the tile depending on its x and y based on the real board. Letters symbolize rows, and the number symbolizes columns. Like "a1", it is the bottom leftmost tile in a chess board.

The draw(), executes the configurations we did by drawing the tile in the canvas, in the color it was assigned. The second if statement tells that if the square has a piece in this position, you should access its icon and place it inside the tile.

Now we have a class for making a square. Let's make another class for handling tiles and the whole board.

# /* Board.py

import pygame
from data.classes.Square import Square
from data.classes.pieces.Rook import Rook
from data.classes.pieces.Bishop import Bishop
from data.classes.pieces.Knight import Knight
from data.classes.pieces.Queen import Queen
from data.classes.pieces.King import King
from data.classes.pieces.Pawn import Pawn

# Game state checker
class Board:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.tile_width = width // 8
        self.tile_height = height // 8
        self.selected_piece = None
        self.turn = 'white'
        self.config = [
            ['bR', 'bN', 'bB', 'bQ', 'bK', 'bB', 'bN', 'bR'],
            ['bP', 'bP', 'bP', 'bP', 'bP', 'bP', 'bP', 'bP'],
            ['','','','','','','',''],
            ['','','','','','','',''],
            ['','','','','','','',''],
            ['','','','','','','',''],
            ['wP', 'wP', 'wP', 'wP', 'wP', 'wP', 'wP', 'wP'],
            ['wR', 'wN', 'wB', 'wQ', 'wK', 'wB', 'wN', 'wR'],
        ]
        self.squares = self.generate_squares()
        self.setup_board()

    def generate_squares(self):
        output = []
        for y in range(8):
            for x in range(8):
                output.append(
                    Square(x,  y, self.tile_width, self.tile_height)
                )
        return output

    def get_square_from_pos(self, pos):
        for square in self.squares:
            if (square.x, square.y) == (pos[0], pos[1]):
                return square

    def get_piece_from_pos(self, pos):
        return self.get_square_from_pos(pos).occupying_piece

In making the whole chess board, it's important to know first what is the width and height of the game window so we can divide it into 8 rows with 8 columns to identify our tiles' exact size.

The self.config represents the chessboard configuration with a 2D list having our pieces with their default position. Below it, we configure the self.squares with a value calling our self.generate_squares() for making chess tiles and putting them all in a list.

Now let's create the other parts of the Board, including the self.setup_board() we called above.

    def setup_board(self):
        for y, row in enumerate(self.config):
            for x, piece in enumerate(row):
                if piece != '':
                    square = self.get_square_from_pos((x, y))
                    # looking inside contents, what piece does it have
                    if piece[1] == 'R':
                        square.occupying_piece = Rook(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self
                        )
                    # as you notice above, we put `self` as argument, or means our class Board
                    elif piece[1] == 'N':
                        square.occupying_piece = Knight(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self
                        )
                    elif piece[1] == 'B':
                        square.occupying_piece = Bishop(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self
                        )
                    elif piece[1] == 'Q':
                        square.occupying_piece = Queen(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self
                        )
                    elif piece[1] == 'K':
                        square.occupying_piece = King(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self
                        )
                    elif piece[1] == 'P':
                        square.occupying_piece = Pawn(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self
                        )

The setup_board() creates each piece, and puts them in their respective place by mapping the self.config with the whole board. If the current value of piece in self.config is an empty string or '', then the tile must be empty, and if not, it will access its respective tile position through the current value of x and y. Each piece from self.config will be identified according to the capital letter it has, except the Knight.

If we got 'N', then it's a Knight if 'P', then it's a Pawn. If 'R', then it's a Rook, 'B' for Bishop, and so on. After we configure the letters, we'll overwrite the current square.occupying_piece with a value of their piece class with the color depending on the first value of the piece string. As you noticed here and in the other statement:

                    if piece[1] == 'R':
                        square.occupying_piece = Rook(
                            (x, y), 'white' if piece[0] == 'w' else 'black', self

We put a self as an argument for the Rook class. That means we put our current class, Board, as an argument.

We need a function that detects each click in our game. So let's make handle_click() in our Board class:

    def handle_click(self, mx, my):
        x = mx // self.tile_width
        y = my // self.tile_height
        clicked_square = self.get_square_from_pos((x, y))
        if self.selected_piece is None:
            if clicked_square.occupying_piece is not None:
                if clicked_square.occupying_piece.color == self.turn:
                    self.selected_piece = clicked_square.occupying_piece
        elif self.selected_piece.move(self, clicked_square):
            self.turn = 'white' if self.turn == 'black' else 'black'
        elif clicked_square.occupying_piece is not None:
            if clicked_square.occupying_piece.color == self.turn:
                self.selected_piece = clicked_square.occupying_piece

The handle_click() accepts the x (mx) and y (my) coordinates of where you click inside the game window as an argument. The x and y variables inside this function compute what row and column you clicked, then we pass its outcomes to clicked_square to get the square or the tile.

This configuration can now receive our every click inside the game window. The following if/else statements process our click if we're making a move or just clicking around.

It all works once you clicked somewhere inside the game window so let's assume that you're playing the game using a white piece and you've already clicked. If we haven't selected any piece yet, it will look as if the tile you clicked has a piece, and if it's your colors turn, and if yes, it will be your self.selected_piece.

With the help of other classes, your piece's possible move will be highlighted in the game. After selecting a piece's move, it will convert the self.turn into the next player's piece color.

Now that we have a selected piece and chosen a move, it will make the move. I'll explain the other moving later as we make the classes for that piece.

Let's add another feature for the Board class; we are adding functions that check if a player is in check or checkmate.

    # check state checker
    def is_in_check(self, color, board_change=None): # board_change = [(x1, y1), (x2, y2)]
        output = False
        king_pos = None
        changing_piece = None
        old_square = None
        new_square = None
        new_square_old_piece = None
        if board_change is not None:
            for square in self.squares:
                if square.pos == board_change[0]:
                    changing_piece = square.occupying_piece
                    old_square = square
                    old_square.occupying_piece = None
            for square in self.squares:
                if square.pos == board_change[1]:
                    new_square = square
                    new_square_old_piece = new_square.occupying_piece
                    new_square.occupying_piece = changing_piece
        pieces = [
            i.occupying_piece for i in self.squares if i.occupying_piece is not None
        ]
        if changing_piece is not None:
            if changing_piece.notation == 'K':
                king_pos = new_square.pos
        if king_pos == None:
            for piece in pieces:
                if piece.notation == 'K' and piece.color == color:
                        king_pos = piece.pos
        for piece in pieces:
            if piece.color != color:
                for square in piece.attacking_squares(self):
                    if square.pos == king_pos:
                        output = True
        if board_change is not None:
            old_square.occupying_piece = changing_piece
            new_square.occupying_piece = new_square_old_piece
        return output

For every move we make, the is_in_check() function will be called, whenever the board_change is not empty.

In the first iteration, it locates the position of the old tile, passes its current piece in changing_piece, and empty that tile while in the second iteration, it catches the new tile position and passes its current piece to new_square_old_piece and give it a new piece from changing_piece.

Once our changing_piece is not empty, it'll try to identify if it's a King by getting its self.notation. If so, it'll override the king_pos and give it the value of new_square.pos.

Note: The self.notation is a variable from the pieces' class, that serves as an identification containing their letter symbols.

The next thing we'll try to identify is what the enemy piece does to do the check to our player's King, where we check starting by if piece.color != color.

        for piece in pieces:
            if piece.color != color:
                for square in piece.attacking_squares(self):
                    if square.pos == king_pos:
                        output = True

With the following code above, we can iterate through enemy pieces and check their attacking_squares, which gets all the possible moves of a piece. If a piece has a position in attacking_squares the same value as king_pos, which means one of the players is checked, so we set the output to True. The output tells if a King is in check or not, so we have to return it.

Now let's make the is_in_checkmate() function for identifying if we have a winner yet:

    # checkmate state checker
    def is_in_checkmate(self, color):
        output = False
        for piece in [i.occupying_piece for i in self.squares]:
            if piece != None:
                if piece.notation == 'K' and piece.color == color:
                    king = piece
        if king.get_valid_moves(self) == []:
            if self.is_in_check(color):
                output = True
        return output

Once we get the King the same color as the argument we passed, it'll try to see if it has any moves left. If none, then it'll check if the player is in check. If that's the case, then it will return the value of the output which is True, which means the side of the color we passed is checkmate.

Now we have all the board configurations; it's time to add the final function for the Board class which is the draw() function:

    def draw(self, display):
        if self.selected_piece is not None:
            self.get_square_from_pos(self.selected_piece.pos).highlight = True
            for square in self.selected_piece.get_valid_moves(self):
                square.highlight = True
        for square in self.squares:
            square.draw(display)

This function highlights all the possible moves of a piece once selected while it is its color's turn.

Learn also: How to Make a Tetris Game using PyGame in Python.

Making the Pieces

Now we're finished with Board class, let's make another class for pieces in Piece.py.

Let's start by adding a function that gets all the available moves and a checker if the next player got checked by the previous player:

# /* Piece.py

import pygame

class Piece:
    def __init__(self, pos, color, board):
        self.pos = pos
        self.x = pos[0]
        self.y = pos[1]
        self.color = color
        self.has_moved = False

    def get_moves(self, board):
        output = []
        for direction in self.get_possible_moves(board):
            for square in direction:
                if square.occupying_piece is not None:
                    if square.occupying_piece.color == self.color:
                        break
                    else:
                        output.append(square)
                        break
                else:
                    output.append(square)
        return output

The get_moves() gets all the available moves of the current player, including attacking the enemy piece. If an opponent piece is in range of a piece's move, the piece can capture it where its range will limit on these opponent's piece tile position through output.append(square) then break unless the piece is Knight which can move in an 'L-shape'.

    def get_valid_moves(self, board):
        output = []
        for square in self.get_moves(board):
            if not board.is_in_check(self.color, board_change=[self.pos, square.pos]):
                output.append(square)
        return output

Before proceeding for our current player in making a move, the get_valid_moves() checks first if the last player does a move that checked our current player. And if not, then it will return the available moves.

To make the pieces work, we're adding a move() function that handles every move we make on the board:

    def move(self, board, square, force=False):
        for i in board.squares:
            i.highlight = False
        if square in self.get_valid_moves(board) or force:
            prev_square = board.get_square_from_pos(self.pos)
            self.pos, self.x, self.y = square.pos, square.x, square.y
            prev_square.occupying_piece = None
            square.occupying_piece = self
            board.selected_piece = None
            self.has_moved = True
            # Pawn promotion
            if self.notation == ' ':
                if self.y == 0 or self.y == 7:
                    from data.classes.pieces.Queen import Queen
                    square.occupying_piece = Queen(
                        (self.x, self.y),
                        self.color,
                        board
                    )
            # Move rook if king castles
            if self.notation == 'K':
                if prev_square.x - self.x == 2:
                    rook = board.get_piece_from_pos((0, self.y))
                    rook.move(board, board.get_square_from_pos((3, self.y)), force=True)
                elif prev_square.x - self.x == -2:
                    rook = board.get_piece_from_pos((7, self.y))
                    rook.move(board, board.get_square_from_pos((5, self.y)), force=True)
            return True
        else:
            board.selected_piece = None
            return False

    # True for all pieces except pawn
    def attacking_squares(self, board):
        return self.get_moves(board)

It takes board and square as arguments. If the tile we select to move our chosen piece is in self.get_valid_moves(), the move is valid to execute. To make it happen, the move() function will get the current square using board.get_square_from_pos(self.pos) and save it in prev_square and get its positions square.pos, square.x, square.y and save it in self.pos, self.x, and self.y for further use.

Then the function will empty the prev_square, and the piece (self - the current chess piece class) will be moved to the square.occupying_piece.

Chess has cool features; some of them are castling and pawn promotion, and that's what we do next.

If the notation of the piece we've just moved in is ' ', which is a pawn, and it reaches row 0 (for white pawns) or row 7 (for black pawns), the pawn will be replaced by another queen of the same color.

And if the piece's notation is 'K' and then moved 2 tiles to the left or right, it means the player's move is casting.

Making a Class for Each Piece

Now that we've finished the Square, Board, and Piece classes, it's time to create different classes for every piece type. Each piece will have the main Piece class as its parent class:

# /* Pawn.py

import pygame

from data.classes.Piece import Piece

class Pawn(Piece):
    def __init__(self, pos, color, board):
        super().__init__(pos, color, board)
        img_path = 'data/imgs/' + color[0] + '_pawn.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width - 35, board.tile_height - 35))
        self.notation = ' '

    def get_possible_moves(self, board):
        output = []
        moves = []
        # move forward
        if self.color == 'white':
            moves.append((0, -1))
            if not self.has_moved:
                moves.append((0, -2))
        elif self.color == 'black':
            moves.append((0, 1))
            if not self.has_moved:
                moves.append((0, 2))
        for move in moves:
            new_pos = (self.x, self.y + move[1])
            if new_pos[1] < 8 and new_pos[1] >= 0:
                output.append(
                    board.get_square_from_pos(new_pos)
                )
        return output

    def get_moves(self, board):
        output = []
        for square in self.get_possible_moves(board):
            if square.occupying_piece != None:
                break
            else:
                output.append(square)
        if self.color == 'white':
            if self.x + 1 < 8 and self.y - 1 >= 0:
                square = board.get_square_from_pos(
                    (self.x + 1, self.y - 1)
                )
                if square.occupying_piece != None:
                    if square.occupying_piece.color != self.color:
                        output.append(square)
            if self.x - 1 >= 0 and self.y - 1 >= 0:
                square = board.get_square_from_pos(
                    (self.x - 1, self.y - 1)
                )
                if square.occupying_piece != None:
                    if square.occupying_piece.color != self.color:
                        output.append(square)
        elif self.color == 'black':
            if self.x + 1 < 8 and self.y + 1 < 8:
                square = board.get_square_from_pos(
                    (self.x + 1, self.y + 1)
                )
                if square.occupying_piece != None:
                    if square.occupying_piece.color != self.color:
                        output.append(square)
            if self.x - 1 >= 0 and self.y + 1 < 8:
                square = board.get_square_from_pos(
                    (self.x - 1, self.y + 1)
                )
                if square.occupying_piece != None:
                    if square.occupying_piece.color != self.color:
                        output.append(square)
        return output

    def attacking_squares(self, board):
        moves = self.get_moves(board)
        # return the diagonal moves 
        return [i for i in moves if i.x != self.x]

Here is our code for the Pawn pieces, whether it is black or white. As you notice, we have get_moves() and attacking_square() functions here in the Pawn class, just like the functions we have in the Piece class but given with a different script. It is because pawn pieces are basically allowed to move 1 step at a time away from their team position. A pawn also has 3 possible moves; the pawn can move up to 2 tiles from its starting position only, can move 1 step forward at a time, and can capture a piece 1 diagonally step at a time.

As we noticed, we have another function which is the get_possible_moves(). As of its name, it gets all the possible moves of a piece base on the current state of the board.

Now let's move to do the other codes for other pieces.

Code for Knight.py:

# /* Kinght.py

import pygame
from data.classes.Piece import Piece

class Knight(Piece):
    def __init__(self, pos, color, board):
        super().__init__(pos, color, board)
        img_path = 'data/imgs/' + color[0] + '_knight.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width - 20, board.tile_height - 20))
        self.notation = 'N'

    def get_possible_moves(self, board):
        output = []
        moves = [
            (1, -2),
            (2, -1),
            (2, 1),
            (1, 2),
            (-1, 2),
            (-2, 1),
            (-2, -1),
            (-1, -2)
        ]
        for move in moves:
            new_pos = (self.x + move[0], self.y + move[1])
            if (
                new_pos[0] < 8 and
                new_pos[0] >= 0 and 
                new_pos[1] < 8 and 
                new_pos[1] >= 0
            ):
                output.append([
                    board.get_square_from_pos(
                        new_pos
                    )
                ])
        return output

Code for Bishop.py:

# /* Bishop.py

import pygame
from data.classes.Piece import Piece

class Bishop(Piece):
    def __init__(self, pos, color, board):
        super().__init__(pos, color, board)
        img_path = 'data/imgs/' + color[0] + '_bishop.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width - 20, board.tile_height - 20))
        self.notation = 'B'

    def get_possible_moves(self, board):
        output = []
        moves_ne = []
        for i in range(1, 8):
            if self.x + i > 7 or self.y - i < 0:
                break
            moves_ne.append(board.get_square_from_pos(
                (self.x + i, self.y - i)
            ))
        output.append(moves_ne)
        moves_se = []
        for i in range(1, 8):
            if self.x + i > 7 or self.y + i > 7:
                break
            moves_se.append(board.get_square_from_pos(
                (self.x + i, self.y + i)
            ))
        output.append(moves_se)
        moves_sw = []
        for i in range(1, 8):
            if self.x - i < 0 or self.y + i > 7:
                break
            moves_sw.append(board.get_square_from_pos(
                (self.x - i, self.y + i)
            ))
        output.append(moves_sw)
        moves_nw = []
        for i in range(1, 8):
            if self.x - i < 0 or self.y - i < 0:
                break
            moves_nw.append(board.get_square_from_pos(
                (self.x - i, self.y - i)
            ))
        output.append(moves_nw)
        return output

Code for Rook.py:

# /* Rook.py

import pygame

from data.classes.Piece import Piece

class Rook(Piece):
    def __init__(self, pos, color, board):
        super().__init__(pos, color, board)
        img_path = 'data/imgs/' + color[0] + '_rook.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width - 20, board.tile_height - 20))
        self.notation = 'R'

    def get_possible_moves(self, board):
        output = []
        moves_north = []
        for y in range(self.y)[::-1]:
            moves_north.append(board.get_square_from_pos(
                (self.x, y)
            ))
        output.append(moves_north)
        moves_east = []
        for x in range(self.x + 1, 8):
            moves_east.append(board.get_square_from_pos(
                (x, self.y)
            ))
        output.append(moves_east)
        moves_south = []
        for y in range(self.y + 1, 8):
            moves_south.append(board.get_square_from_pos(
                (self.x, y)
            ))
        output.append(moves_south)
        moves_west = []
        for x in range(self.x)[::-1]:
            moves_west.append(board.get_square_from_pos(
                (x, self.y)
            ))
        output.append(moves_west)
        return output

Code for Queen.py:

# /* Queen.py

import pygame
from data.classes.Piece import Piece

class Queen(Piece):
    def __init__(self, pos, color, board):
        super().__init__(pos, color, board)
        img_path = 'data/imgs/' + color[0] + '_queen.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width - 20, board.tile_height - 20))
        self.notation = 'Q'

    def get_possible_moves(self, board):
        output = []
        moves_north = []
        for y in range(self.y)[::-1]:
            moves_north.append(board.get_square_from_pos(
                (self.x, y)
            ))
        output.append(moves_north)
        moves_ne = []
        for i in range(1, 8):
            if self.x + i > 7 or self.y - i < 0:
                break
            moves_ne.append(board.get_square_from_pos(
                (self.x + i, self.y - i)
            ))
        output.append(moves_ne)
        moves_east = []
        for x in range(self.x + 1, 8):
            moves_east.append(board.get_square_from_pos(
                (x, self.y)
            ))
        output.append(moves_east)
        moves_se = []
        for i in range(1, 8):
            if self.x + i > 7 or self.y + i > 7:
                break
            moves_se.append(board.get_square_from_pos(
                (self.x + i, self.y + i)
            ))
        output.append(moves_se)
        moves_south = []
        for y in range(self.y + 1, 8):
            moves_south.append(board.get_square_from_pos(
                (self.x, y)
            ))
        output.append(moves_south)
        moves_sw = []
        for i in range(1, 8):
            if self.x - i < 0 or self.y + i > 7:
                break
            moves_sw.append(board.get_square_from_pos(
                (self.x - i, self.y + i)
            ))
        output.append(moves_sw)
        moves_west = []
        for x in range(self.x)[::-1]:
            moves_west.append(board.get_square_from_pos(
                (x, self.y)
            ))
        output.append(moves_west)
        moves_nw = []
        for i in range(1, 8):
            if self.x - i < 0 or self.y - i < 0:
                break
            moves_nw.append(board.get_square_from_pos(
                (self.x - i, self.y - i)
            ))
        output.append(moves_nw)
        return output

Code for King.py:

# /* King.py

import pygame
from data.classes.Piece import Piece

class King(Piece):
    def __init__(self, pos, color, board):
        super().__init__(pos, color, board)
        img_path = 'data/imgs/' + color[0] + '_king.png'
        self.img = pygame.image.load(img_path)
        self.img = pygame.transform.scale(self.img, (board.tile_width - 20, board.tile_height - 20))
        self.notation = 'K'

    def get_possible_moves(self, board):
        output = []
        moves = [
            (0,-1), # north
            (1, -1), # ne
            (1, 0), # east
            (1, 1), # se
            (0, 1), # south
            (-1, 1), # sw
            (-1, 0), # west
            (-1, -1), # nw
        ]
        for move in moves:
            new_pos = (self.x + move[0], self.y + move[1])
            if (
                new_pos[0] < 8 and
                new_pos[0] >= 0 and 
                new_pos[1] < 8 and 
                new_pos[1] >= 0
            ):
                output.append([
                    board.get_square_from_pos(
                        new_pos
                    )
                ])
        return output

    def can_castle(self, board):
        if not self.has_moved:
            if self.color == 'white':
                queenside_rook = board.get_piece_from_pos((0, 7))
                kingside_rook = board.get_piece_from_pos((7, 7))
                if queenside_rook != None:
                    if not queenside_rook.has_moved:
                        if [
                            board.get_piece_from_pos((i, 7)) for i in range(1, 4)
                        ] == [None, None, None]:
                            return 'queenside'
                if kingside_rook != None:
                    if not kingside_rook.has_moved:
                        if [
                            board.get_piece_from_pos((i, 7)) for i in range(5, 7)
                        ] == [None, None]:
                            return 'kingside'
            elif self.color == 'black':
                queenside_rook = board.get_piece_from_pos((0, 0))
                kingside_rook = board.get_piece_from_pos((7, 0))
                if queenside_rook != None:
                    if not queenside_rook.has_moved:
                        if [
                            board.get_piece_from_pos((i, 0)) for i in range(1, 4)
                        ] == [None, None, None]:
                            return 'queenside'
                if kingside_rook != None:
                    if not kingside_rook.has_moved:
                        if [
                            board.get_piece_from_pos((i, 0)) for i in range(5, 7)
                        ] == [None, None]:
                            return 'kingside'

    def get_valid_moves(self, board):
        output = []
        for square in self.get_moves(board):
            if not board.is_in_check(self.color, board_change=[self.pos, square.pos]):
                output.append(square)
        if self.can_castle(board) == 'queenside':
            output.append(
                board.get_square_from_pos((self.x - 2, self.y))
            )
        if self.can_castle(board) == 'kingside':
            output.append(
                board.get_square_from_pos((self.x + 2, self.y))
            )
        return output

Let's finish the game by adding code in main.py that runs our whole game:

import pygame

from data.classes.Board import Board

pygame.init()

WINDOW_SIZE = (600, 600)
screen = pygame.display.set_mode(WINDOW_SIZE)

board = Board(WINDOW_SIZE[0], WINDOW_SIZE[1])

def draw(display):
	display.fill('white')
	board.draw(display)
	pygame.display.update()


if __name__ == '__main__':
	running = True
	while running:
		mx, my = pygame.mouse.get_pos()
		for event in pygame.event.get():
			# Quit the game if the user presses the close button
			if event.type == pygame.QUIT:
				running = False
			elif event.type == pygame.MOUSEBUTTONDOWN: 
       			# If the mouse is clicked
				if event.button == 1:
					board.handle_click(mx, my)
		if board.is_in_checkmate('black'): # If black is in checkmate
			print('White wins!')
			running = False
		elif board.is_in_checkmate('white'): # If white is in checkmate
			print('Black wins!')
			running = False
		# Draw the board
		draw(screen)

As you see above, we had screen and board variable, which has pretty similar arguments but not really.

The screen handles the rendering of the chess board on the screen so we can see what's happening in the board. The code pygame.display.set_mode(WINDOW_SIZE) creates the game window.

While we use the board for making and handling tiles, tile positions, and what piece a chess square has. As you remember, in the Board class code, we give it two arguments: the game window's length and width.

To keep the game running, we give it a while loop that runs as long as the value of running is True.

The mx, my = pygame.mouse.get_pos() locates the current position of your mouse as long as it's inside the game window. If you add print(mx, my) below this code, you'll see the current mouse position, and its value changes every time you hover it inside the window.

The event.type == pygame.MOUSEBUTTONDOWN catches every click you make. To identify if a player is making a move, every time it catches a player doing a click, the current position of the mouse we get from pygame.mouse.get_pos() will be sent in Board.handle_click(), and process your click back there.

Ok, now let's try this game. If it's working so in your terminal, move to the directory where our Main.py file was saved, then run the Main.py. Once you run the file, the game will start immediately:

Start clicking on the pieces that can move, and you'll see available moves:

Conclusion

To simplify it, always remember that the chess game has two main parts, the board, and the pieces.

The board is in charge of every tile's name & position and the rules of the game, while the piece classes take care of moves and attacks for every piece.

To make the board, you should have a Square class that creates the handling chess tiles that also mind the piece it contains, and another class called Board, which contains the game rules. We also need to do class for every chess piece, from the Pawn to the King. And that's how you make a chess game with Python using only classes and pygame!

You can check the complete code here.

Here is a list of other pygame tutorials:

Happy coding ♥

Just finished the article? Why not take your Python skills a notch higher with our Python Code Assistant? Check it out!

View Full Code Build My Python 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!