Hey everyone! A lot of you don't know but I'm involved with an educational NGO called Click To Start. Click To Start provides free digital literacy classes for senior citizens, and in 2017 it provided its first programming class aimed at teenagers! Programming is as rewarding as it is challenging so we tried to incentivise the students by teaching them how to make games - which we all love to do. Python was chosen as the language of instruction as it's one of the more English-like options out there. We then settled on Pygame to teach them game development, as it's pretty straightforward to get into once you know Python. The plus side of helping out was that I had to create games myself! I mean, we need to give them exercises that'll push and entertain them, right? One of the games they worked on was Tic Tac Toe. Creating it in Pygame was fun, so I thought I'd share how to do it.
This Tic Tac Toe iteration will be pretty simple:
- X plays before O
- Each cell is numbered from 1 to 9
- The user presses the number and plays in that position
- Only 2 player, no AI
- After the game is done and winner announced (if any), a user can restart
As it's a one screen game the setup for this is pretty basic. We'll keep all the code in one file.
# Basic imports
The imports and variables are fine for those who used Pygame before; the interesting bit is
os.environ['SDL_VIDEO_CENTERED'] = '1'. SDL, the library which Pygame started off being a wrapper for, uses environment variables for window positioning https://www.libsdl.org/release/SDL-1.2.15/docs/html/sdlenvvars.html.
pygame.init() as it's always required and then do other basic setup: add the fonts we'll be using (one for the Xs and Os and two for the game over screen); setup the screen size and window title; and create the Clock object to keep track of time and manage the framerate.
Before my mind settled into one of the many ways we can represent a game of Tic Tac Toe, I decided to get a visual result to kick it off. What's Tic Tac Toe without 9 cells? Let's draw that first:
def draw_lines(screen, screen_size):
To see the fruits of your (admittedly light) labour we can create the render function and setup the main loop:
def render(screen, screen_size, clock):
In case you don't know, yes you do have to poll for events even though we're not processing any input as yet. Pygame must make a call to the event queue for each frame displayed. If you're writing code that doesn't need to accept events, you may want to consider the
pump function. Check the documentation which talks more about polling events.
Pygame make heavy use of rectangles, the Rect class is absolutely amazing and there's honestly no reason to be representing positions with tuples aside from utter simplicity - you lose out on so many spectacular methods and attributes! We got our screen to show 9 cells, so it seems natural in Pygame to have 9 rects. With that in mind, we'll be drawing our Xs and Os to the centre of a rectangle:
def draw_letter(screen, letter, colour, position_rect):
So far we know we'll have 9 Pygame rectangles, we'll store them in a lists of lists, similar to a matrix. A 3x3 double list would allow us to easily check for winners later on. Along with the rectangle, each cell should say whether a player already used it (don't want a player stealing another one's cell) and whether it has an X or O if played. Let's setup the board.
We show the numbers first to give the users a visual clue of what to press to play. A blank board to someone who doesn't know the setup would not be particularly helpful.
With our visual cues setup, we need to game to accept user input. Before we jump into mapping key presses to board positions, let's see all the mechanics and think about the game loop. Within the loop we accept user input, update the state and render the new state on the screen. For Tic Tac Toe, we'll first setup the board and other flags and then go into a loop: the user presses a number on the keyboard; if the users wins or game ends in stalemate stop the game and let them know; otherwise switch the player's turn and repeat.
Let's start with how the game is updated. From the requirements the first player is always X. After X plays we need to change the player to O and vice versa until the game ends. The game loop is always running, we need to ensure that the player change only occurs after someone made a move.
Now let's define the update function:
def update(game, keys_pressed):
It's straightforward. If someone pressed a number, including those on the numpad (the keys with KP before the number), then save the player on that location and set the someone_played flag to True. Returning someone_played will allow us to switch players after someone plays.
The render function is now updated to show what's on the board: X, O or the keyboard number we set at the beginning. We'll draw X in blue and O in green. The numbers will be black.
def render(screen, screen_size, game, clock):
Now that we're able to play and switch players, we should look at determining the winner or the other end result - a stalemate. Let's start with the win function. It's simple enough, a player wins if she has 3 in a row, 3 in a column or 3 diagonally. All you got to remember is that the board is a list of lists, first index is the row and the second index is the column of that row.
def winner(board, player):
We also have to cater for the case that the game reaches the end and neither player wins. Given that we have a played variable, this just means we need to check whether every board position has played set to True.
Let's include them to the main function now:
We've made a few additions to main update. Firstly, the initial state now includes two flags for winning or stalemate. After the game is updated we check to see if a player won, and then check if it's a stalemate. Note that with our current logic, a game can be won and stalemate at the same time. At this point we want to use these variables to show the user who won, and all allow them to restart the match by hitting spacebar.
Let's allow the game to be played over and over again, we'll do this by rendering two messages: the first stating who won if someone did, the second informing the user to press spacebar to restart. After that we'll modify the main function so that if the game is ended, the user can press spacebar to restart it.
As the messages are essentially the same and just need their content changed, let's create a function to display both messages. We created two fonts earlier, so we'll ensure the function handles both as well.
def end_game_message(screen, screen_size, main_message, main_font,
Pretty straightforward: setup the font, get it's rect and place on the screen. When including this in our render function, we need this message to appear in front of the other objects on the screen (so we render it last) and we need to ensure it only appears when the game has ended.
def render(screen, screen_size, game, clock):
Recall that with our previous logic that if someone wins on the last move, then the game's win and stalemate flags would both be True. To ensure we display the right message, we always show the winning message once someone wins and we only show the stalemate message if it's also true that no one won. If you play a game you'll see the message pop up! Now all we need to do is modify it so that spacebar resets the game.
We only allow the spacebar to reset the game when it's over and to ensure that the user can't play while the game is over we add some checks by the update function and the player switch logic. Without resetting the game with spacebar, the game would not update. And that's our game! If in doubt, you can compare your source code to mine. There are slight differences in the order but this is everything.
I'm sure the vast majority of your friends won't be going on the command line to run Python files and play your game. Let's package it in a format they can easily use. cx_Freeze takes your Python scripts and makes them into executables. It's perfect for what we want. Install with pip
pip install cx_Freeze and create a setup.py file in your directory with the following content:
There isn't much to this script, just telling cx_Freeze where's the file, some metadata and the library needed for the file to work. I'm using macOS, to get my installer I run
python setup.py bdist_dmg. For Windows OSes your last arugment will be bdist_msi. If you just want an executable without an installer, simply use 'build' as the last argument. Definitely read the docs and play with it!
Well done on making it through this tutorial, you got a fully functional game on your hands! There are some ways we can improve it, try your hand at the following:
- Allow for plays to be made with mouse clicks
- Add a victory sound if someone wins
- Make the game fullscreen
- If using cx_Freeze, the build size is quite large. Looks for ways to reduce it (hint: check out the 'excludes' argument)
Hope you enjoyed, happy hacking!