Design Patterns and Video Games

Discover Python and Patterns (8): Game Loop pattern

The time has come to see our first design pattern: the Game Loop Pattern! This pattern can give us many good ideas to refactor our game in a very effective way.

This post is part of the Discover Python and Patterns series

The Game Loop pattern

There is several version of the Game Loop pattern. Here I present a simple case with a single thread. The following 5 functions form this pattern:

The Game Loop pattern
def run():
    init()
    while True:
        processInput()
        update()
        render()

The Game Loop pattern, as for all patterns, is a recipe that gives you ideas on how to solve a problem. There is no unique way to use it: it depends on cases.

Following a pattern forces you to consider problems you may not be thinking about. For instance, splitting user input, data updates, and rendering is not the first thing that came to mind when we created the "guess the number" game. However, according to experimented developers that already created many games, it seems that this splitting is essential. So, right now as beginners, we follow this advice, and later we will understand why it matters. And please trust me: the day you see it all, you will get amazed by all the genius behind these ideas!

Guess the number game: init() function

Let's start using the pattern with the init() function:

def init():    
    return None, random.randint(1,10)

This function returns an initial game state. I named game data "the game state" because games can be seen as finite state machines. For this game, the state is made of:

Bundle all game data is an important task; we'll see that with more details in the next posts.

Guess the number game: processInput() function

def processInput():
    while True:
        word = input("What is the magic number? ")
        if word == "quit":
            return None

        try:
            playerNumber = int(word)
            break
        except ValueError:
            print("Please type a number without decimals!")
            continue

    return playerNumber

This function asks the player for a number. It handles all problems related to user input, like checking that the entered number is correct. It returns the number, or None if the player wants to stop the game.

For users of this function, like the run() function, it is like a magic box that returns instructions from the player. It does not matter how they are collected. It could be from a keyboard, a mouse, a pad, the network, or even from an AI.

Guess the number game: update() function

The update() function updates the game state using the player's instructions:

def update(gameStatus,magicNumber,playerNumber):
    if playerNumber is None:
        gameStatus = "end"
    elif playerNumber == magicNumber:
        gameStatus = "win"
    elif magicNumber < playerNumber:
        gameStatus = "lower"
    elif magicNumber > playerNumber:
        gameStatus = "higher"

    return gameStatus, magicNumber 

In our case, the player's instruction is playerNumber, and the game status and the magic number form the game state. The function updates the game status depending on the value of playerNumber.

Note that we don't use gameStatus as an input, and never change the value of magicNumber. So, we could think that we can remove gameStatus from the arguments and magicNumber from the return values. Except if this is the very last version of this game and that we must reduce computational complexity, this is not a good idea. Maybe in future improvements of the game, we will need to update the game according to gameStatus or change the value of magicNumber. From a design point of view, this current definition of inputs and outputs is robust and has no reason to change.

Guess the number game: render() function

The render() function displays the current game state. It should work whatever happens, to always give a clear view of the game:

def render(gameStatus,magicNumber):
    if gameStatus == "win":
        print("This is correct! You win!")
    elif gameStatus == "end":
        print("Bye!")
    elif gameStatus == "lower":
        print("The magic number is lower")
    elif gameStatus == "higher":
        print("The magic number is higher")
    else:
        raise RuntimeError("Unexpected game status {}".format(gameStatus))

The input of this function is the game state and has no output. The process is simple: display a message according to the value of gameStatus.

Note that we also handle the case where gameStatus has an unexpected value. It is a good habit, it greatly helps the day you update the game and forget to update some parts.

Guess the number game: runGame() function

The runGame() function is the core of the game and uses all the previous functions:

def runGame():
    gameStatus, magicNumber = init()
    while gameStatus != "win" and gameStatus != "end":
        playerNumber = processInput()
        gameStatus, magicNumber = update(gameStatus,magicNumber,playerNumber)
        render(gameStatus,magicNumber)

You can see the flow:

Guess the number game: final code

The final code, with documentation in function headers:

# Import the random package
import random

def init():
    """
    Initialize game

    Outputs:
      * gameStatus
      * magicNumber
    """
    # Generate a random Magic number
    return None, random.randint(1,10)


def processInput():
    """
    Handle player's input

    Output:
      * playerNumber: the number entered by the player, or None if the player wants to stop the game
    """

    while True:
        # Player input
        word = input("What is the magic number? ")
        # Quit if the player types "quit"
        if word == "quit":
            return None

        # Int casting with exception handling
        try:
            playerNumber = int(word)
            break
        except ValueError:
            print("Please type a number without decimals!")
            continue

    return playerNumber

def update(gameStatus,magicNumber,playerNumber):
    """
    Update game state

    Inputs:
      * gameStatus: the status of the game
      * magicNumber: the magic number to find
      * playerNumber: the number entered by the player
    Output:
      * gameStatus: the status of the game
      * magicNumber: the magic number to find
    """
    if playerNumber is None:
        gameStatus = "end"
    elif playerNumber == magicNumber:
        gameStatus = "win"
    elif magicNumber < playerNumber:
        gameStatus = "lower"
    elif magicNumber > playerNumber:
        gameStatus = "higher"

    return gameStatus, magicNumber

def render(gameStatus,magicNumber):
    """
    Render game state

    Input:
      * gameStatus: the status of the game, "win", "end", "lower" or "higher"
    """
    # Cases
    if gameStatus == "win":
        print("This is correct! You win!")
    elif gameStatus == "end":
        print("Bye!")
    elif gameStatus == "lower":
        print("The magic number is lower")
    elif gameStatus == "higher":
        print("The magic number is higher")
    else:
        raise RuntimeError("Unexpected game status {}".format(gameStatus))

def runGame():
    gameStatus, magicNumber = init()
    while gameStatus != "win" and gameStatus != "end":
        playerNumber = processInput()
        gameStatus, magicNumber = update(gameStatus,magicNumber,playerNumber)
        render(gameStatus,magicNumber)


# Launch the game
runGame()

Now we saw the essential basics, in the next post I'll be able to show you how to use game graphics :)