Confused by complex code? Let our AI-powered Code Explainer demystify it for you. Try it out!
Checkers is a classic two-player board game that has been enjoyed by people of all ages for generations. It is a strategy game that requires players to move their pieces across the board, capturing their opponent's pieces and ultimately trying to reach the other end of the board with one of their pieces to become a king.
In this article, we will be making a Checkers game in Python. Our goal is to provide an overview of the game's codebase, breaking it down into several classes that work together to provide the game's functionality. We will cover the installation and setup process, the main class, the game class, the board class, the tile class, the piece class, the pawn class, and the king class.
We also have a tutorial on making a chess game, make sure to check it out if you're interested!
Our focus will be on explaining the code in a way that is easy to understand for new programmers. So, without further ado, let's get started!
Table of Contents:
Main
classGame
classBoard
classTile
classPiece
classPawn
classKing
classBefore we can begin working on our game, we need to make sure we have all the necessary tools installed on our computer. The first thing we need to do is to make sure we have Python installed. You can download Python from the official website.
Also, we need to install the Pygame library. Pygame is a set of Python modules that allow us to create games and multimedia applications. To install Pygame, open up the command prompt/terminal and type the following command:
$ pip install pygame
We can now create the directory for our Checkers game. Open up your file explorer and navigate to the directory where you want to create your game, and create the "Checkers"
folder.
Inside the Checkers
directory, we need to create several Python files. Namely "Main.py"
, "Game.py
", "Board.py
", "Tile.py
", "Piece.py
", "Pawn.py
", and "King.py
".
Finally, we need to create a folder named "images"
inside the Checkers
directory. This folder will contain the images for our game pieces. Inside the folder, place the following image files: black-pawn.png
, black-king.png
, red-pawn.png
, and red-king.png
. You can access the pictures here.
The structure of our game should look like this:
Main
classThe Main
class is the starting point of our game. It sets up the game window, initializes the game objects, and runs the game loop.
First, we import the necessary modules and classes from the other files in our Checkers game. Then, we initialize Pygame with the pygame.init()
function:
# /* Main.py
import pygame
from Board import Board
from Game import Game
pygame.init()
The Checkers
class has an __init__()
method that takes in a screen
parameter which initializes several class attributes, including screen
, running
, and FPS
. screen
represents the game window, running
is a boolean that determines whether the game is still running, and FPS
is a Pygame clock that limits the game to a certain frame rate.
class Checkers:
def __init__(self, screen):
self.screen = screen
self.running = True
self.FPS = pygame.time.Clock()
Below the __init__()
function, add two more functions, and name them, _draw()
and main()
.
The _draw()
method is a helper method that takes in a board
parameter and draws the game board onto the screen using the board.draw()
method. It then updates the display using pygame.display.update()
.
def _draw(self, board):
board.draw(self.screen)
pygame.display.update()
The main()
method is the main game loop. It takes in window_width
and window_height
parameters, which are the dimensions of the game window. board_size
is set to 8, which represents the size of the game board. tile_width
and tile_height
are calculated based on the size of the window and the size of the board:
def main(self, window_width, window_height):
board_size = 8
tile_width, tile_height = window_width // board_size, window_height // board_size
board = Board(tile_width, tile_height, board_size)
game = Game()
while self.running:
game.check_jump(board)
for self.event in pygame.event.get():
if self.event.type == pygame.QUIT:
self.running = False
if not game.is_game_over(board):
if self.event.type == pygame.MOUSEBUTTONDOWN:
board.handle_click(self.event.pos)
else:
game.message()
self.running = False
self._draw(board)
self.FPS.tick(60)
board
is then initialized with the Board
class, passing in the tile_width
, tile_height
, and board_size
parameters. game
is also initialized with the Game
class.
The while
loop runs as long as self.running
is True
. Within the loop, we call game.check_jump(board)
to check for any available jumps on the board. We then loop through the events in the Pygame event queue using pygame.event.get()
. If the event type is pygame.QUIT
(when the exit button was clicked), we set self.running
to False
to exit the game.
If the game is not over, we check if the user has clicked on a tile on the board using board.handle_click()
. If the game is over, we display a message using game.message()
and exit the game loop by setting self.running
to False
.
We then call the _draw()
method to draw the game board on the screen and update the display using pygame.display.update()
. Finally, we use self.FPS.tick(60)
to limit the game to 60 frames per second.
Below the class, let's call the game with the main()
function.
if __name__ == "__main__":
window_size = (640, 640)
screen = pygame.display.set_mode(window_size)
pygame.display.set_caption("Checkers")
checkers = Checkers(screen)
checkers.main(window_size[0], window_size[1])
We set the window_size
to (640, 640)
, create a Pygame display screen with this size using pygame.display.set_mode(window_size)
, and set the window caption to "Checkers" using pygame.display.set_caption("Checkers")
. Then, we create an instance of the Checkers
class called checkers
, passing in the screen
as a parameter. Then, we call the main()
method on checkers
, passing in the window_size
dimensions. This code sets up the Pygame display screen, creates an instance of the Checkers
class, and starts the game loop by calling the main()
method.
Game
classThe Game
class contains methods for checking if the game is over, checking if there is a jump available, and displaying the winner of the game:
# /* Game.py
class Game:
def __init__(self):
self.winner = None
The __init__()
method initializes the winner
variable to None
.
# checks if both colors still has a piece
def check_piece(self, board):
red_piece = 0
black_piece = 0
for y in range(board.board_size):
for x in range(board.board_size):
tile = board.get_tile_from_pos((x, y))
if tile.occupying_piece != None:
if tile.occupying_piece.color == "red":
red_piece += 1
else:
black_piece += 1
return red_piece, black_piece
The check_piece()
method iterates over each tile on the board and checks if it contains an occupying piece. If it does, it adds to the red_piece
count if the piece color is "red", or to the black_piece
count if the piece color is "black". It then returns a tuple containing the red_piece
and black_piece
counts.
def is_game_over(self, board):
red_piece, black_piece = self.check_piece(board)
if red_piece == 0 or black_piece == 0:
self.winner = "red" if red_piece > black_piece else "black"
return True
else:
return False
The is_game_over()
method calls the check_piece()
method to get the current piece count for each color. If one color has no pieces left, the other color is declared the winner, and the method returns True
. Otherwise, the method returns False
.
def check_jump(self, board):
piece = None
for tile in board.tile_list:
if tile.occupying_piece != None:
piece = tile.occupying_piece
if len(piece.valid_jumps()) != 0 and board.turn == piece.color:
board.is_jump = True
break
else:
board.is_jump = False
if board.is_jump:
board.selected_piece = piece
board.handle_click(piece.pos)
return board.is_jump
The check_jump()
method checks if there is a piece that can make a jump. If there is, it sets the is_jump
attribute of the board to True
, sets the selected_piece
to the first piece that can make a jump, and returns True
. If there isn't, it sets the is_jump
attribute of the board to False
and returns False
. Since we're creating the traditional Checkers where jump moves can't be skipped, the check_jump()
will force the users to jump.
def message(self):
print(f"{self.winner} Wins!!")
The message method simply prints out the winner of the game.
Board
classThe Board
class defines the behavior of a checkers game board and how it responds to user input. It has an __init__()
method that initializes the board with the specified tile_width
, tile_height
, and board_size
. It also initializes various variables such as selected_piece
, turn
, and is_jump
:
# /* Board.py
import pygame
from Tile import Tile
from Pawn import Pawn
class Board:
def __init__(self,tile_width, tile_height, board_size):
self.tile_width = tile_width
self.tile_height = tile_height
self.board_size = board_size
self.selected_piece = None
self.turn = "black"
self.is_jump = False
self.config = [
['', 'bp', '', 'bp', '', 'bp', '', 'bp'],
['bp', '', 'bp', '', 'bp', '', 'bp', ''],
['', 'bp', '', 'bp', '', 'bp', '', 'bp'],
['', '', '', '', '', '', '', ''],
['', '', '', '', '', '', '', ''],
['rp', '', 'rp', '', 'rp', '', 'rp', ''],
['', 'rp', '', 'rp', '', 'rp', '', 'rp'],
['rp', '', 'rp', '', 'rp', '', 'rp', '']
]
self.tile_list = self._generate_tiles()
self._setup()
The self.config
contains the starting setup of the board for our game. The tile_list
calls the _generate_tiles()
method that creates each tile for the board, and the _setup()
method sets the starting position of pawns in the game base on config
.
def _generate_tiles(self):
output = []
for y in range(self.board_size):
for x in range(self.board_size):
output.append(
Tile(x, y, self.tile_width, self.tile_height)
)
return output
def get_tile_from_pos(self, pos):
for tile in self.tile_list:
if (tile.x, tile.y) == (pos[0], pos[1]):
return tile
The _generate_tiles()
method generates a list of tiles using the specified tile width, tile height, and board size. The get_tile_from_pos()
method returns the tile object at a given position.
def _setup(self):
for y_ind, row in enumerate(self.config):
for x_ind, x in enumerate(row):
tile = self.get_tile_from_pos((x_ind, y_ind))
if x != '':
if x[-1] == 'p':
color = 'red' if x[0] == 'r' else 'black'
tile.occupying_piece = Pawn(x_ind, y_ind, color, self)
The _setup()
method sets up the initial board configuration by iterating through the self.config
list, which represents the starting positions of the pieces, and setting the occupying_piece
attribute of the corresponding tile object to a Pawn
object.
def handle_click(self, pos):
x, y = pos[0], pos[-1]
if x >= self.board_size or y >= self.board_size:
x = x // self.tile_width
y = y // self.tile_height
clicked_tile = self.get_tile_from_pos((x, y))
if self.selected_piece is None:
if clicked_tile.occupying_piece is not None:
if clicked_tile.occupying_piece.color == self.turn:
self.selected_piece = clicked_tile.occupying_piece
elif self.selected_piece._move(clicked_tile):
if not self.is_jump:
self.turn = 'red' if self.turn == 'black' else 'black'
else:
if len(clicked_tile.occupying_piece.valid_jumps()) == 0:
self.turn = 'red' if self.turn == 'black' else 'black'
elif clicked_tile.occupying_piece is not None:
if clicked_tile.occupying_piece.color == self.turn:
self.selected_piece = clicked_tile.occupying_piece
The handle_click()
method takes a position (pos
) as an argument, which represents the pixel coordinates of the location on the game board that were clicked by the player.
First, the method extracts the x
and y
coordinates from the pos
argument. If the coordinates are outside the board size, the method calculates which tile was clicked based on the position of the click relative to the size of each tile. Next, the method retrieves the Tile
object that was clicked by calling the get_tile_from_pos()
method, passing in the (x
,y
) coordinates of the clicked tile.
If there is no selected_piece
currently, the method checks if the clicked tile has a Pawn
object on it and whether that Pawn
object belongs to the current player's turn. If there is a Pawn
object on the clicked tile and it belongs to the current player's turn, the selected_piece
attribute of the Board
object is set to that Pawn
object.
If there is a selected_piece
already, the method attempts to move the Pawn
object to the clicked tile by calling the _move()
method of the Pawn
object, passing in the Tile
object as an argument. If the move is successful, the turn
attribute of the Board
object is updated to reflect the next player's turn.
If the move is a jump, the is_jump
attribute of the Board
object is set to True
. The method then checks if there are any more valid jumps available for the same Pawn
object, by calling the valid_jumps()
method of the Pawn
object. If there are no more valid jumps, the turn
attribute of the Board
object is updated to reflect the next player's turn.
If the clicked tile does have a Pawn
object on it and belongs to the current player's turn, the selected_piece
attribute of the Board
object is set to the Pawn
object on the clicked tile.
def draw(self, display):
if self.selected_piece is not None:
self.get_tile_from_pos(self.selected_piece.pos).highlight = True
if not self.is_jump:
for tile in self.selected_piece.valid_moves():
tile.highlight = True
else:
for tile in self.selected_piece.valid_jumps():
tile[0].highlight = True
for tile in self.tile_list:
tile.draw(display)
The draw()
method is used to draw the board and the pieces. It is called to update the display with any changes made by the handle_click()
method. If a piece is selected, the tile it is on and its valid moves or jumps are highlighted.
Tile
classThe Tile
class represents a single tile on the game board.
# /* Tile.py
import pygame
class Tile:
def __init__(self, x, y, tile_width, tile_height):
self.x = x
self.y = y
self.pos = (x, y)
self.tile_width = tile_width
self.tile_height = tile_height
self.abs_x = x * tile_width
self.abs_y = y * tile_height
self.abs_pos = (self.abs_x, self.abs_y)
self.color = 'light' if (x + y) % 2 == 0 else 'dark'
self.draw_color = (220, 189, 194) if self.color == 'light' else (53, 53, 53)
self.highlight_color = (100, 249, 83) if self.color == 'light' else (0, 228, 10)
self.occupying_piece = None
self.coord = self.get_coord()
self.highlight = False
self.rect = pygame.Rect(
self.abs_x,
self.abs_y,
self.tile_width,
self.tile_height
)
The __init__()
method initializes the object's properties, including the x
and y
coordinates of the tile, its width
and height
, its absolute position (abs_x
and abs_y
) and position tuple (abs_pos
).
The color
property is determined by whether the sum of the tile's x
and y
coordinates are even or odd. If it is even, the tile is a light color, otherwise, it is dark. The draw_color
and highlight_color
properties are tuples representing RGB values for the tile's fill color and highlight color, respectively.
The occupying_piece
property is set to None
by default but can be assigned a Piece
object if there is a piece occupying the tile.
Let's also have the get_coord()
and draw()
methods:
def get_coord(self):
columns = 'abcdefgh'
return columns[self.x] + str(self.y + 1)
def draw(self, display):
if self.highlight:
pygame.draw.rect(display, self.highlight_color, self.rect)
else:
pygame.draw.rect(display, self.draw_color, self.rect)
if self.occupying_piece != None:
centering_rect = self.occupying_piece.img.get_rect()
centering_rect.center = self.rect.center
display.blit(self.occupying_piece.img, centering_rect.topleft)
The get_coord()
method returns a string representing the tile's coordinate in standard chess notation, using the lettered columns and numbered rows.
The draw()
method draws the tile on the screen using pygame.draw.rect
, using the draw_color
property as the fill color. If the highlight property is set to True
, the tile is drawn with the highlight_color
instead. If there is an occupying_piece
, the piece's image is blitted onto the center of the tile using pygame.Surface.blit()
. The image is first centered using the get_rect()
and center
properties of the piece's image.
Piece
classThe Piece
class is a parent class for all checker pieces in the game. It contains common attributes and methods that are shared among all pieces such as the position
of the piece on the board, its color
, and the ability to move to certain tiles on the board based on the game rules.
# /* Piece.py
import pygame
class Piece:
def __init__(self, x, y, color, board):
self.x = x
self.y = y
self.pos = (x, y)
self.board = board
self.color = color
For the Pawn
and King
classes specifically, they override some of the methods of the Piece
class to account for the specific rules that apply to these pieces in the game of checkers. For example, the valid_moves()
method of the Pawn
class returns the valid moves that a pawn can make on the board based on the game rules for a pawn (can move or jump forward/against their starting side only). Similarly, the valid_moves()
method of the King
class returns the valid moves that a king can make on the board based on the game rules for a king (can move or jump forward or backward).
In addition, the Pawn
class includes a specific implementation for pawn promotion, which occurs when a pawn reaches the opposite end of the board. The King
class does not require a specific implementation for promotion since kings are already the highest-ranking pieces in the game.
Here in Piece
class, we also define the move()
method for both pieces.
def _move(self, tile):
for i in self.board.tile_list:
i.highlight = False
# ordinary move/s
if tile in self.valid_moves() and not self.board.is_jump:
prev_tile = self.board.get_tile_from_pos(self.pos)
self.pos, self.x, self.y = tile.pos, tile.x, tile.y
prev_tile.occupying_piece = None
tile.occupying_piece = self
self.board.selected_piece = None
self.has_moved = True
# Pawn promotion
if self.notation == 'p':
if self.y == 0 or self.y == 7:
from King import King
tile.occupying_piece = King(
self.x, self.y, self.color, self.board
)
return True
# jump move/s
elif self.board.is_jump:
for move in self.valid_jumps():
if tile in move:
prev_tile = self.board.get_tile_from_pos(self.pos)
jumped_piece = move[-1]
self.pos, self.x, self.y = tile.pos, tile.x, tile.y
prev_tile.occupying_piece = None
jumped_piece.occupying_piece = None
tile.occupying_piece = self
self.board.selected_piece = None
self.has_moved = True
# Pawn promotion
if self.notation == 'p':
if self.y == 0 or self.y == 7:
from King import King
tile.occupying_piece = King(
self.x, self.y, self.color, self.board
)
return True
else:
self.board.selected_piece = None
return False
The _move()
method takes in a tile
object representing the new position for the piece. It first checks if the new position is a valid move for the piece by calling the valid_moves()
method. If the move is valid and no jump is occurring, the method updates the position of the piece and the occupying_piece
attribute of the relevant tiles to reflect the new position of the piece. If the piece is a pawn and reaches the last row, it will be promoted to a king.
If a jump is occurring, the _move()
method checks if the new position is a valid jump move by calling the valid_jumps()
method. If the move is valid, the method updates the position of the piece, the occupying_piece
attribute of the relevant tiles, and removes the jumped_piece
(opponent's piece that caused the jump). The method checks if the pawn has reached the last row and promotes it to a king if necessary.
If the move is not valid, the method simply returns False
.
Pawn
classThe Pawn
class is a subclass of the Piece
class, pawns are the pieces used from the start of the game. The class has an __init__()
method that calls the parent's __init__()
method to initialize the object's x
and y
position, color
, and board
. It also loads an image for the pawn and assigns it a notation of 'p':
# /* Pawn.py
import pygame
from Piece import Piece
class Pawn(Piece):
def __init__(self, x, y, color, board):
super().__init__(x, y, color, board)
img_path = f'images/{color}-pawn.png'
self.img = pygame.image.load(img_path)
self.img = pygame.transform.scale(self.img, (board.tile_width, board.tile_height))
self.notation = 'p'
Let's add three more methods; the _possible_moves()
, the valid_moves()
, and the valid_jumps()
methods:
def _possible_moves(self):
# (x, y) move for left and right
if self.color == "red":
possible_moves = ((-1, -1), (+1, -1))
else:
possible_moves = ((-1, +1), (+1, +1))
return possible_moves
The _possible_moves()
method returns a tuple of possible moves that a pawn can make in the form of (x, y)
coordinates for left and right directions. The possible moves depend on the color of the pawn. For example, a red pawn can move left and right in the (-1, -1) and (+1, -1) directions respectively.
def valid_moves(self):
tile_moves = []
moves = self._possible_moves()
for move in moves:
tile_pos = (self.x + move[0], self.y + move[-1])
if tile_pos[0] < 0 or tile_pos[0] > 7 or tile_pos[-1] < 0 or tile_pos[-1] > 7:
pass
else:
tile = self.board.get_tile_from_pos(tile_pos)
if tile.occupying_piece == None:
tile_moves.append(tile)
return tile_moves
The valid_moves()
method checks all possible moves for a pawn on the board and returns a list of valid moves. It does so by iterating through the possible moves and checking if each move results in a valid tile position on the board that does not contain any occupying piece. If the tile position is valid and empty, it appends the tile object to the list of valid moves.
def valid_jumps(self):
tile_jumps = []
moves = self._possible_moves()
for move in moves:
tile_pos = (self.x + move[0], self.y + move[-1])
if tile_pos[0] < 0 or tile_pos[0] > 7 or tile_pos[-1] < 0 or tile_pos[-1] > 7:
pass
else:
tile = self.board.get_tile_from_pos(tile_pos)
if self.board.turn == self.color:
if tile.occupying_piece != None and tile.occupying_piece.color != self.color:
next_pos = (tile_pos[0] + move[0], tile_pos[-1] + move[-1])
next_tile = self.board.get_tile_from_pos(next_pos)
if next_pos[0] < 0 or next_pos[0] > 7 or next_pos[-1] < 0 or next_pos[-1] > 7:
pass
else:
if next_tile.occupying_piece == None:
tile_jumps.append((next_tile, tile))
return tile_jumps
The valid_jumps()
method returns a list of tuples that represents a valid jump move for the pawn. It checks for jumps in a similar way to valid_moves()
but also checks for a tile position with an opposing piece. If such a tile position exists, it checks the next position in the direction of the jump to ensure that it is empty. If the next position is empty, it appends a tuple containing the current tile and the tile that contains the opponent's piece to the list of valid jumps.
King
classThe King
class is very similar to the Pawn
class in terms of its structure and methods. However, the _possible_moves()
method now returns all the diagonal directions, as a king can move diagonally in any direction.
The valid_moves()
method returns a list of tiles that the king can move to, which are tiles that are empty and within the boundaries of the board. The valid_jumps()
method returns a list of tuples, where each tuple contains two tiles, representing a jump that the king can make over an opposing piece. The implementation of valid_jumps()
is very similar to that of the Pawn
class, with the only difference being that the King
can jump over opposing pieces in any diagonal direction:
# /* King.py
import pygame
from Piece import Piece
class King(Piece):
def __init__(self, x, y, color, board):
super().__init__(x, y, color, board)
img_path = f'images/{color}-king.png'
self.img = pygame.image.load(img_path)
self.img = pygame.transform.scale(self.img, (board.tile_width, board.tile_height))
self.notation = 'k'
def _possible_moves(self):
possible_moves = ((-1, -1), (+1, -1), (-1, +1), (+1, +1))
return possible_moves
def valid_moves(self):
tile_moves = []
moves = self._possible_moves()
for move in moves:
tile_pos = (self.x + move[0], self.y + move[-1])
if tile_pos[0] < 0 or tile_pos[0] > 7 or tile_pos[-1] < 0 or tile_pos[-1] > 7:
pass
else:
tile = self.board.get_tile_from_pos(tile_pos)
if tile.occupying_piece == None:
tile_moves.append(tile)
return tile_moves
def valid_jumps(self):
tile_jumps = []
moves = self._possible_moves()
for move in moves:
tile_pos = (self.x + move[0], self.y + move[-1])
if tile_pos[0] < 0 or tile_pos[0] > 7 or tile_pos[-1] < 0 or tile_pos[-1] > 7:
pass
else:
tile = self.board.get_tile_from_pos(tile_pos)
if self.board.turn == self.color:
if tile.occupying_piece != None and tile.occupying_piece.color != self.color:
next_pos = (tile_pos[0] + move[0], tile_pos[-1] + move[-1])
next_tile = self.board.get_tile_from_pos(next_pos)
if next_pos[0] < 0 or next_pos[0] > 7 or next_pos[-1] < 0 or next_pos[-1] > 7:
pass
else:
if next_tile.occupying_piece == None:
tile_jumps.append((next_tile, tile))
return tile_jumps
And now the game is done! You can try the game by running the Main.py
on your terminal:
$ python Main.py
Here are some of the game snapshots:
Starting the game:
Pawn's move:
Pawn's Jump:
Pawn Promotion:
King's moves:
King's Jump:
In this tutorial, we've explored how to implement a checkers game in Python using the Pygame library. We've covered how to create the game board, pieces, and their movements, as well as the basic rules of the game.
Playing checkers is an enjoyable way to pass the time and test your strategy and critical thinking skills. We hope this article has been helpful in understanding the basics of checkers programming in Python. See you on the next one!
We also have a chess game tutorial, if you want to build one!
Get the complete code here.
Here is a list of other pygame tutorials:
Happy coding ♥
Let our Code Converter simplify your multi-language projects. It's like having a coding translator at your fingertips. Don't miss out!
View Full Code Convert My Code
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!