Official website for Linux User & Developer
FOLLOW US ON:
Mar
2

Make Noughts and Crosses for Raspberry Pi -Tutorial

by Liam Fraser

Liam Fraser demonstrates how to make your own version of this classic game using the Pygame framework

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 http://www.linuxuser.co.uk/ 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.

Raspberry Pi Tic Tac Toe
Noughts and Crosses

Step by Step

Step 01

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.

Step 02

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

Step 03

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”

Step 04

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[0] / 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[0] / 2
        # Add a little margin
        self.textRect.top =
displaySize[1] * 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))

Step 05

Grid squares

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[0] / 3
        height = gridSize[1] / 3
        # Get the x and y coordinate of the top left corner
        x = (position[0] * width) - width
        y = (position[1] * height) - height

Step 06

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))

Step 07

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] * 0.75,displaySize[1] * 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[0] / 2) - (gridSize[0] / 2),(displaySize[1] / 2) - (gridSize[1] / 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)

Step 08

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)

Step 09

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

Step 10

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)

Step 11

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

Step 12

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”

Step 13

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[0] / 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[0] / 2
         textRect.centery = self.displaySize[1] / 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()

Step 14

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)

Step 15

Handling events

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[0]
         mousey -= self.grid.position[1]
         # 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(“”)

Step 16

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()

  • Tell a Friend
  • Follow our Twitter to find out about all the latest Linux news, reviews, previews, interviews, features and a whole more.
    • Fiachra Judge

      Hi
      I’ve painstakingly typed out and corrected this code from the magazine RP for Beginners (me) and attempted to run it. I’ve rebuilt my Raspbian, re-formatted and upgraded. I am at a loss as to why I cannot get this program to run.

      My system pops up an empty black titled window followed by a message in the IDLE window > AttributeError: OAndX instance has no attribute ‘run’ < reported from line 266 in module game.run()

      I've even copied and pasted a second instance of the code from this webpage – after correcting it I get the same.

      If anyone has notes of errata could you please point me to them, thanks

      FoxtrotO

    • Dave

      You have typed in the code for def run(self): but I suspect you have typed it in the wrong place. All the code from step 12 onwards needs to be part of the class OAndX