Before we begin
Download the latest Raspbian image from www. raspberrypi.org/downloads. Flash the image to your SD card as you usually would. Instructions can be found at https://www.gadgetdaily.xyz/ tutorials/how-to-set-up-raspberry-pi if needed. You’ll only need to go up to the step where you write the image to the SD card. You’ll have to adapt the instructions slightly for using the newer Raspbian image rather than the Debian one.
Noughts and crosses (or Tic-tac-toe) is a very simple strategy game played across a 3×3 grid. It is often played by young children due to its simple nature, which makes it well suited to the Raspberry Pi. Since we have already covered how to create artificial intelligence players in a previous tutorial, this game will be played by two human players, with the focus instead being on introducing new things such as an algorithm to work out which player has three in a row. We’ll also be working the sizes of everything out in proportion to the size of the display, rather than explicitly specifying their positions in pixels. Without further ado, let’s get started.
Step by Step
Creating our project
Begin by double-clicking on the IDLE (not IDLE 3) icon on the Raspberry Pi’s desktop to open up our Python development environment. IDLE starts as a Python shell by default, so go to the File menu and select New Window to open an Editor Window. Once the Editor is open, select the File menu and then click Save. We don’t actually need to make a project folder for this project as the game will be done entirely in code and not require any external resources such as image files. We called our file O&X.py. Click the Save button once you have named yours.
Starting with the basics
We’re going to start off our project with the usual line of ‘#!/usr/bin/python’, which tells the shell to run the code in the file using Python. Following that, you should write a summary of what your program will do. We’ll also import the Pygame and system modules and import everything thing from the Pygame library, as well as a bunch of constants used by Pygame to make our lives easier. Make sure you keep saving your work with Ctrl+S.
#!/usr/bin/python # A simple, Noughts and Crosses game for two human players implemented using # the PyGame framework. Created by Liam Fraser for a Linux User and Developer # tutorial. import pygame # Provides the PyGame framework import sys # Provides the sys.exit function we use to exit our game loop from pygame import * from pygame.locals import * # Import constants used by PyGame
Starting the Game class
The init function of the Game class is going to look the same as usual and is explained thoroughly by the comments in the following code.
We also keep track of who the current player is, so that we can change it. Notice that the reset function doesn’t reset this value; this means that whoever goes first in the next game is different from whoever went last in the previous one.
# Our game class class OAndX: def __init__(self): # Initialize PyGame pygame.init() # Create a clock to manage the game loop self.clock = time.Clock() # Set the window title display.set_caption(“Noughts and Crosses”) # Create the window with a resolution of 640 x 480 self.displaySize = (640,480) self.screen = display.set_mode(self.displaySize) # Will either be O or X self.player = “O”
The Background class
Our Background class is very simple. We’re going to create the surface and not fill it so that the background is black. After that, we write the text ‘Noughts and Crosses’ at the top of the window, horizontally centred. Below, we pick the font size based on the width of the display. The displaySize tuple contains two values: the width and the height, which are accessed with an index of 0 or 1 respectively. Notice the use of the Pygame Color function, which takes a string which is the name of a colour and returns the RGB value for that colour. The True value passed to the font renderer makes the text look smoother by enabling anti-aliasing.
# The background class class Background: def __init__(self, displaySize): self.image = Surface(displaySize) # Draw a title # Create the font we’ll use self.font = font.Font(None,(displaySize / 12)) # Create the text surface self.text = self.font.render(“Noughts and Crosses”,True,(Color(“white”))) # Work out where to place the text self.textRect = self.text.get_rect() self.textRect.centerx = displaySize / 2 # Add a little margin self.textRect.top = displaySize * 0.02 # Blit the text to the background image self.image.blit(self.text,self.textRect) def draw(self, display): display.blit(self.image, (0,0))
We’re going to have a Grid class which makes up the playing area of the game. However, before we do that, we’re going to make a class for each individual square in the grid. This is so we can know things such as the state of the square (either X or O) and if that state is permanent (or just while the mouse is hovering over the square). The GridSquare class inherits from sprite.Sprite, so that the squares can be added to a sprite group, which can be updated in one go.
The position is a tuple in the form of (column, row). The grid size is a tuple in the form (width, height).
# A class for an individual grid square class GridSquare(sprite.Sprite): def __init__(self, position,gridSize): # Initialise the sprite base class super(GridSquare, self).__init__() # We want to know which row and column we are in self.position = position # State can be “X”, “O” or "" self.state = “” self.permanentState = False self.newState = “” # Work out the position and size of the square width = gridSize / 3 height = gridSize / 3 # Get the x and y coordinate of the top left corner x = (position * width) - width y = (position * height) - height
Grid square surfaces
Each grid square is actually made up of two rectangles. The parent rectangle is white, while the child rectangle is blue and 90% of the size of the parent rectangle. This effectively gives us a blue rectangle with a white border. The new thing here is that the child rectangle image is actually drawn to the parent rectangle surface, which means that all positioning is only relative to the parent rectangle. As far as the child rectangle is concerned, the co-ordinate (0, 0) is the top-left corner of the parent rectangle. We finish off the grid square’s initialiser by creating the font we’ll use to display the O and X letters.
# Create the image, the rect and then position the rect self.image = Surface((width,height)) self.image. fill(Color(“white”)) self.rect = self.image.get_rect() self.rect.topleft = (x, y) # The rect we have is white, which is the parent rect # We will draw another rect in the middle so that we have # a white border but a blue center self.childImage = Surface(((self.rect.w * 0.9), (self.rect.h * 0.9))) self.childImage.fill(Color(“blue”)) self.childRect = self.childImage.get_rect() self.childRect.center = ((width / 2), (height / 2)) self.image.blit(self.childImage, self.childRect) # Create the font we’ll use to display O and X self.font = font.Font(None,(self.childRect.w))
Constructing a grid
So, now that we have the basic structure of our grid squares down, we’re going to create a Grid class to hold them in. The grid is going to be 75% of the size of the screen, and centred. Once all of the positioning code is out of the way, we have a nested loop which makes three rows, each containing three grid squares. Each grid square instance is stored in the self.squares list. We finish the grid initialiser off by putting the sprites into a sprite group, so that they can be updated and drawn as a group.
# A class for the 3x3 grid class Grid: def __init__(self, displaySize): self.image = Surface(displaySize) # Build a collection of grid squares gridSize = (displaySize * 0.75,displaySize * 0.75) # Work out the co-ordinate of the top left corner of the grid # so that it can be centered on the screen self.position = ((displaySize / 2) - (gridSize / 2),(displaySize / 2) - (gridSize / 2)) # An empty array to hold our grid squares in self.squares =  for row in range(1,4): # Loop to make 3 rows for column in range(1,4): # Loop to make 3 columns squarePosition = (column, row) self.squares.appent(GridSquare(squarePosition, gridSize)) # Get the squares into a sprite group self.sprites = sprite.Group() for square in self.squares: self.sprites.add(square)
The grid draw function
The draw function of the Grid class updates each grid square sprite, then draws them to the grid’s surface (self.image), which is then drawn to the display at the position that was calculated in the initialiser function.
def draw(self, display): self.sprites.update() self.sprites.draw(self.image) display.blit(self.image,self.position)
Finishing off the GridSquare class
Now that we have the main parts of our Grid class in place, we can carry on with the GridSquare class. It needs a couple of functions before it is complete. One is the update function, which is mandatory for any class that inherits the Pygame Sprite class, and the other is a simple function that sets the state of the grid square. The setState function sets the newState variable, which in turn means that the grid square will update with the correct state (X, O or blank) on the next clock tick. The setState function can also make the state permanent in the case that the user has clicked on the square.
def update(self): # Need to update if we need to set a new state if (self.state != self.newState): # Refill the childImage blue self.childImage.fill(Color("blue")) text = self.font.render(self.newState, True, (Color("white"))) textRect = text.get_rect() textRect.center = ((self.childRect.w / 2),(self.childRect.h / 2)) # We need to blit twice because the new child image # needs to be blitted to the parent image self.childImage.blit(text, textRect) self.image.blit(self.childImage, self.childRect) # Reset the newState variable self.state = self.newState self.newState = "" def setState(self, newState,permanent=False): if not self.permanentState: self.newState = newState if permanent: self.permanentState = True
Extending the Game Class
Now that we have our Background, and Grid related classes, we should add them into the initialiser of the main OAndX class. However, because we want to be able to reset the board, we’ll put them in a reset function, which recreates the Background and the Grid and can be called many times. Once we have this function, we simply need to call self.reset() from the initialiser of the OAndX class.
def reset(self): # Create an instance of our background and grid class self.background = Background(self.displaySize) self.grid = Grid(self.displaySize)
Finishing off the Grid class
We need to add a couple of helpful functions to the Grid class that will come in very useful when working out who has won the game, or if it’s a draw (if the grid is full).
def getSquareState(self, column, row): # Get the square with the requested position for square in self.squares: if square.position == (column, row): return square.state def full(self): # Finds out if the grid is full count = 0 for square in self.squares: if square.permanentState == True: count += 1 if count == 9: return True else: return False
Working out the winner
This is the part that gets a little tricky. We’re going to add a function to the OAndX class called getWinner, which will either return nothing, the winning player (O or X), or ‘draw’. We start by defining players: a list of things thatweneedtotrytofindarowof.Ifwedon’t find a winner by the time we’ve checked every possibility, we check if the grid is full, in which case we return ‘draw’.
For each player, we go through up to four separate sets of loops (one for each possible direction). The possible ways of achieving three in a row are horizontally, vertically or diagonally, in both forward and reverse directions.
For each direction, a pair of loops check through each grid square and get the next two grid squares away from the current grid square in the direction that we are checking for. If a square doesn’t exist, then nothing is returned by the getSquareState function.
If all three squares have the same state, then the winning player is returned, which stops the function.
def getWinner(self): players = [“X”, “O”] for player in players: # check horizontal spaces for column in range (1, 4): for row in range (1, 4): square1 = self.grid.getSquareState(column, row) square2 = self.grid.getSquareState((column + 1), row) square3 = self.grid.getSquareState((column + 2), row) # Get the player of the square (either O or X) if (square1 == player) and (square2 == player) and (square3 == player): return player # check vertical spaces for column in range (1, 4): for row in range (1, 4): square1 = self.grid.getSquareState(column, row) square2 = self.grid.getSquareState(column, (row + 1)) square3 = self.grid.getSquareState(column, (row + 2)) # Get the player of the square (either O or X) if (square1 == player) and (square2 == player) and (square3 == player): return player # check forwards diagonal spaces for column in range (1, 4): for row in range (1, 4): square1 = self.grid.getSquareState(column, row) square2 = self.grid.getSquareState((column + 1), (row - 1)) square3 = self.grid.getSquareState((column + 2), (row - 2)) # Get the player of the square (either O or X) if (square1 == player) and (square2 == player) and (square3 == player): return player # check backwards diagonal spaces for column in range (1, 4): for row in range (1, 4): square1 = self.grid.getSquareState(column, row) square2 = self.grid.getSquareState((column + 1), (row + 1)) square3 = self.grid.getSquareState((column + 1), (row + 2)) # Get the player of the square (either O or X) if (square1 == player) and (square2 == player) and (square3 == player): return player # Check if grid is full if someone hasn’t won already if self.grid.full(): return “draw”
Displaying a message when a winner is found
We want to be able to display a message when a winner is found, and make it totally separate from the grid. The easiest way is to completely black out the screen and then write to the blank screen. The text is rendered straight to the screen. Notice how the display.update() function needs to be called for anything to go on the screen because the time.wait function will stop the execution of everything, including the game loop that we’re going to write in a moment. The winner message ends by resetting the game to its initial state with a fresh grid.
def winMessage(self, winner): # Display message then reset the game to its initial state # Blank out the screen self.screen.fill(Color(“Black”)) # Create the font we’ll use textFont = font.Font(None, (self.displaySize / 6)) textString = “” if winner == “draw”: textString = “It was a draw!” else: textString = winner + “Wins!” # Create the text surface text = textFont.render(textString, True, (Color(“white”))) textRect = text.get_rect() textRect.centerx = self.displaySize / 2 textRect.centery = self.displaySize / 2 # Blit changes and update the display before we sleep self.screen.blit(text, textRect) display.update() # time.wait comes from pygame libs time.wait(2000) # Reset the game to its initial state self.reset()
The game loop
The game loop for this game is very easy, because all of the code is triggered by something else, namely the handleEvents function that we can begin to write because we’ll have all of the necessary pieces in place once we have finished the game loop. The game can quite happily run at 10fps because there is hardly any animation, simply text being drawn on the screen when the mouse moves between squares.
def run(self): while True: # Our Game loop # Handle events self.handleEvents() # Draw our background and grid self.background.draw(self.screen) self.grid.draw(self.screen) # Update our display display.update() # Limit the game to 10 fps self.clock.tick(10)
Here is the part where everything gets tied together. The only two events important to us are the click event and mouse button up event, so that we know when someone has clicked on a square, indicating that they want to permanently set the state of that square.
We need to get the co-ordinates of the mouse pointer, which are relative to the top left of the screen, but we can make them relative to the top left of the grid by subtracting the co-ordinate of the grid’s top-left corner. Once we have the co-ordinate, we can loop through each square and work out which square the mouse is over using the rect.collidepoint function, which returns True if the co-ordinates that have been passed to it as a parameter are located inside the rectangle.
If the mouse is clicked then we want to set the state of the square, passing through the current player and True, indicating that the change should be permanent. We then change to the next player and call the self.getWinner function. If the getWinner function returns anything that isn’t null (which will be O, X, or draw), then the value is passed to the winMessage which displays a message with whoever won, and then resets the grid.
If the mouse isn’t clicked, then the grid will change state to the current player, but will go blank again once the mouse moves away.
def handleEvents(self): # We need to know if the mouse has been clicked later on mouseClicked = False # Handle events, starting with the quit event for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() if event.type == MOUSEBUTTONUP: mouseClicked = True # Get the co ordinate of the mouse mousex, mousey = mouse.get_pos() # These are relative to the top left of the screen, # we need to make them relative to the top left of the grid mousex -= self.grid.position mousey -= self.grid.position # Find which rect the mouse is in for square in self.grid.squares: if square.rect.collidepoint((mousex, mousey)): if mouseClicked: square.setState(self.player, True) # Change to the next player if self.player == "O": self.player = "X" else: self.player = "O" # Check for a winner winner = self.getWinner() if winner: self.winMessage(winner) else: square.setState(self.player) else: # Set it to blank # Will only happen if permanentState == False square.setState(“”)
The final piece
The final thing to do before our project will run is to add a couple of lines right at the end of the file that will create an instance of the Game class and then call the run function to start the game loop.
if __name__ == ‘__main__’: game = OAndX() game.run()