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
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:
init()
function is called at startup to initialize the game and its data. In the following, I name this data the game state.processInput()
function is called at each iteration of the game to manage the controls (keyboard, mouse, pad).update()
function changes the game state. The result of processInput()
is used to update the game state, as well as automatic processes.render()
function handles display. It "convert" the game state into visual content.run()
function contains the game loop. In many cases, this loop looks like this one: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!
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:
The game status: this is the general status of the game, represented by a string:
The magic number: the number the player has to find.
Bundle all game data is an important task; we'll see that with more details in the next posts.
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.
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.
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.
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:
init()
function returns an initial game state;processInput()
function collects instructions from the player;update()
function uses the instructions to update the game state;render()
function displays the game state.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 :)