I mentioned recently that I was learning Python. I’m working with a few friends on learning the basics. My background is mostly Objective-C and in recent years, Swift. I also programmed in PHP last year for several months working with Laravel, Lumen, Lighthouse, and GraphQL.
To learn Python we decided to begin looking at making a chess game using Pygame. This post shows my attempt to get the chessboard on the screen. I opted to store the coordinates of each square in a class along with their size and if they are black or white. I don’t yet know if this is an efficient way, but for now, it’s helping me learn the Python syntax and work with a few nested loops and a multidimensional list.
One thing to note before I move on to showing my work, I spent just a day on this and my aim wasn’t to separate classes into their own files or work too much on the structure of the program. I just wanted to get it done quickly to see if I could draw the chessboard and if all went well, then begin looking at the structure of the program.
To get more familiar with Pygame I searched and found this tutorial by Jon Fincher at realpython.com. This is a great tutorial and I recommend it if you want a good introduction to Pygame.
Getting going with Pygame
import pygame
from pygame.locals import (
MOUSEBUTTONUP,
K_ESCAPE,
KEYDOWN,
QUIT,
)
pygame.init()
SCREEN_WIDTH = 700
SCREEN_HEIGHT = 700
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
In main.py I used the following code. It first imports pygame. If you haven’t installed this module, then you need to do that first. Please post a comment below if you are stuck on this part.
Next, I import MOUSEBUTTONUP, K_ESCAPE, KEYDOWN, and QUIT from pygame.locals. These four inputs allow me to detect when a mouse button has been clicked when escape has been tapped when any key has been tapped, or if the program has been called. We’ll use these later on in the program.
Next, we initialise pygame. We then set some constants for the screen width and height. For now, I set these at 700 pixels each.
pygame.display.set_mode returns a surface that we can work with. We pass in the width and height as a tuple and this determines the size of our display.
The Board Squares
For this initial version, I thought I would create a custom object that represents each square. For that, I decided it needed x and y coordinates which are for the top left of each square. It would then have a width and height, although I combined this into a single property called width_height because all squares on the board are square; no surprise there, and then another property to show if the square is white or not. I was going to feed in black or white but decided a boolean would suffice.
class BoardSquare:
def __init__(self, x_start, y_start, width_height, is_white):
self.x_start = x_start
self.y_start = y_start
self.width_height = width_height
self.is_white = is_white
The BoardSquare class at the moment just has a default initialiser that accepts x_start, y_start, width_height, and is_white.
The next step was to create a 2D list and fill it with each chessboard square. I created the following to handle this:
chess_board = []
is_white = False
for y in range(8):
chess_row = []
is_white = not is_white
for x in range(8):
chess_row.append(calculate_coordinates(x, y, is_white))
is_white = not is_white
chess_board.append(chess_row)
I first create an empty list called chess_board. At this point it is not multi-dimensional.
I then set is_white to false.
I then created a for loop with a range of 8. I called this y because it will be working through one row at a time starting at row 0, working through to row 7.
In this first, or outer, loop, I initialise an empty list and call it chess_row. This list is the second dimension and will hold a row of eight squares.
Because a chessboard starts with a white square at the top left, I set is_white to not is_white, effectively toggling that boolean back to white.
I then create a nested loop with x as the number. This is the x coordinate and also counts to 8, working left to right.
I create a BoardSquare object and append it. I’ll explain this in more details in a few moments. I then toggle white to black so that on the next iteration of the nested loop, it creates a black square, and then it toggles back to white.
When the nested loop has run eight times, I then append the chess_row to the chess_board array so that item 1 in chess_board, meaning the top row, contains 8 squares.
When this is appended it loops back around the outer loop and white is toggled back to black for the first square of row 2. On the first run of this, I just saw black and white bands across the screen. The reason for this was that I didn’t notice that when the last square on a row is black, the first square on the next row is also black. I added in this extra toggle so that it switches back to the same colour when the row finishes and the new one starts.
calculate_coordinates
This function is used to calculate the x and y coordinates of each square.
def calculate_coordinates(x_array, y_array, is_white):
if SCREEN_WIDTH < SCREEN_HEIGHT or SCREEN_WIDTH == SCREEN_HEIGHT:
width_height = SCREEN_WIDTH / 8
else:
width_height = SCREEN_HEIGHT / 8
x_coordinate = x_array * width_height
y_coordinate = y_array * width_height
return BoardSquare(x_coordinate, y_coordinate, width_height, is_white)
This function is used to calculate the x and y coordinates of each square. Because the constants for SCREEN_WIDTH and SCREEN_HEIGHT can be changed in code before it runs, I decided to make the board flexible as well. If you decide you want a smaller board, the squares will be sized correctly on launching the game.
This function accepts the x and y values from the loops (ie, on the first iteration it would be 0, 0). It also accepts the boolean of the square being white or black.
The first if/else statement checks if the board is taller or wider or square. If taller, we use the shorter edge. If it’s wider, we use the shorter edge. This means that the board will always fit into the game. I store this in width_height which is the width/height of each square.
The next challenge is to calculate the top left corner of each square. To do this I get the x_array value and multiply it by width_height. If we are 4 squares along, the x passed in will be a 3 which will multiple by perhaps 80 making it 240 pixels. The same principle is used for y.
I then return an instantiated BoardSquare with the correct parameters passed into it.
Putting the Chessboard on Screen
Now that I have a multidimensional list of all of the chess squares I next put them on screen.
for row in chess_board:
for square in row:
surf = pygame.Surface((square.width_height, square.width_height))
if square.is_white:
surf.fill((255, 255, 255))
else:
surf.fill((0, 0, 0))
rect = surf.get_rect()
screen.blit(surf, (square.x_start, square.y_start))
pygame.display.flip()
I loop around chess_board and call the value “row”. This means I am beginning on the top row.
I then create a nested loop that works across the row. I call the value “square” as it represents a square on the board.
I create a surface with the squares width_height variable. Because we know that chess squares are square, I just access the same square.width_height for both the width and length.
if square.is_white is true, I set the fill for the box to be white. If not, it gets set to black.
The last three lines of code are Pygame’s way of drawing the surface.
When running the program now, a chessboard will appear.
Main Loop
Now that the board is displayed I have added the main loop, although at the moment it just checks for escape, quit, and mouse clicks.
while running:
for event in pygame.event.get():
if event.type == KEYDOWN:
if event.key == K_ESCAPE:
running = False
if event.type == MOUSEBUTTONUP:
pos = pygame.mouse.get_pos()
highlight_selected_square(get_square_for_position(pos))
elif event.type == QUIT:
running = False
The code above is partly from what I found at realpython.com, but also with MOUSEBUTTONUP added which I call a function and pass in the position of the mouse click. I haven’t finished this yet, but right now the position is passed into get_square_for_position and this function passes back the square object from the multidimensional list.
The other function, highlight_selected_square will be used to test if the correct square has been selected. My plan with that is to put a border around each square as it is clicked on.
get_square_for_position has the following code:
def get_square_for_position(pos):
for row in chess_board:
if row[0].y_start < pos[1] < row[0].y_start + row[0].width_height:
for square in row:
if square.x_start < pos[0] < square.x_start + square.width_height:
return square
The position is passed in which is an x, y coordinate of the mouse click. I learned tonight that this can be accessed with x, y = get_pos(), but in this example, I access pos[0] for x and pos[1] for y.
This looks through the first row of the chess_board list and checks if the y coordinates are within the first row. If true we know that the selected square is on a certain row.
When that is true, I loop through each square in the row and check if the x coordinates are within a certain square, and if true, I return the square.
This doesn’t feel too efficient looping through until the correct square is found. In big-o notation, this wouldn’t be the quickest way when iterating through every item until found, but for now, it will do.
Closing Comments
I am mostly happy with how this turned out. I need to break out the logic a bit more and create a separate file for the BoardSquare class. I don’t know for sure if I will continue down this path of programmatically drawing a chessboard. I may end up just using an image and then calculating which square is clicked a different way, but for now, it works.
The initial purpose of doing this was to become familiar with Python, which I haven’t used much at all. Now that I have some familiarity with it I will be drawing out the structure of the program first to decide what logic should be handled where. Right now, there is no MVC pattern with my example code. I assume people work with MVC when using Python, but that is something I need to read up on to see if it’s best practice for the language.