Tower Defense Game: Finite State Automata / State Machines
Complex codebases – and games are usually complex – tend to rely on a lot of state, usually captured in variables. Navigating from one screen in a game to another involves a lot of change: you need to render different things; the key bindings you use may change also; and perhaps you need to clear out old objects, like if you change from game play to the score screen.
But instead of having an ever-increasing number of variables to represent what your code is supposed to do – like, is_in_menu
, has_won_game
, is_in_level_editor
, etc. – you should consider formalizing your stateful code using finite state automata, or commonly called a state machine.
To improve our code’s extensibility, it’s time to consider how we can effectively use a simple state machine to represent the state the game is in, and how OOP and inheritance can help with separation of concerns.
What is Statefulness?
All useful computer programs capture some form of state. In a toy application that asks for your name and repeats it back, it could be a variable called name
that makes our application stateful — that is, our program is storing, and can recall at will, information we either gave it explicitly, or that it generated implicitly, by perhaps reading something from a file or database. Regardless of how, we usually store this in variables, or more generically, in memory somewhere, given whatever loose definition of memory we may choose to use.
So why is statefulness important? Because it’s all too easy to ensnare important business logic in simple variables, and add layer upon layer of information of top of that.
Booleans everywhere
Consider an attempt to write a simple game with a handful of screens and how you’d track what screen it’s on:
-
A main menu that you first encounter when you start the game, or quit out to when you exit from the game play or map editor.
-
The main game where you play the actual game
-
A win and loss screen that records your score and how well you did
-
The level editor screen
-
The configuration / options screen
And so on.
Naively, you could store what your game’s currently supposed to display to the user with an endless number of booleans:
in_main_menu = True
in_game_playing = False
in_score_screen = False
in_level_editor = False
in_options_screen = False
# ... etc ...
So, every time you switch from one screen (and functional part of the game) to another you must remember to update all those boolean flags.
If you accidentally set two to True
your game will most probably break in awkward ways. You might render two different things on the screen. Furthermore, you may have a set of paths that a user must navigate through to get to certain screens; for instance, going from the main menu to the score screen is usually not possible.
That’s not to say the boolean approach is bad. Boolean variables are very useful, and the demo makes great use of them.
But there’s a way of capturing the state of a game in a manner that is easier to reason about.
Finite State Automata / State Machines
Finite State Automata is just one facet of the model of computation in Computer Science. We won’t get into the weeds of that at all, as the concept of state machines is very intuitive to most people, and especially to programmers, who often end up creating state machines without necessarily knowing that it’s a formal discipline.
You’re probably familiar with flowcharts, like the included example. Well, that’s a state machine. Starting at the roundel, you can transition from one block to the next by following the direction of the arrows. The example closely mirrors the state machine used in the demo.
It also shows where and what you can or cannot transition to. Though, for this game that is mostly intuitive without a chart, for very large games or applications you’d want a tool capable of drawing them to the screen.
In our case, we can keep it simple. I do not enforce legal transitions in most places in the demo, except where it’s vital to prevent crashes or other serious issues, but in a larger application you definitely want to do that!
Consider an ecommerce website. You’d want to ensure the customer first goes through the has_paid
state before the ecommerce system updates to ship_merchandise
! A large number of logic errors in applications are directly attributable to this sort of mistake.
You can represent states in many ways. I think the simplest way to represent machine states in Python is using the Enum
class from the enum
module.
The Enum class
import enum
class GameState(enum.Enum):
"""
Enum for the Game's State Machine. Every state represents a
known game state for the game engine.
"""
# Unknown state, indicating possible error or misconfiguration.
unknown = "unknown"
# The state the game engine would rightly be set to before
# anything is initialized or configured.
initializing = "initializing"
# The game engine is initialized: pygame is configured, the sprite
# images are loaded, etc.
initialized = "initialized"
# The game engine is in map editing mode
map_editing = "map_editing"
# The game engine is in game playing mode
game_playing = "game_playing"
# The game engine is in the main menu
main_menu = "main_menu"
# The game engine is rendering the game ended screen.
game_ended = "game_ended"
# The game engine is exiting and is unwinding
quitting = "quitting"
class StateError(Exception):
"""
Raised if the game is in an unexpected game state at a point
where we expect it to be in a different state. For instance, to
start the game loop we must be initialized.
"""
You might be tempted to skip this step and just use strings. Avoid that temptation. Strings are useful and they’re perfectly capable of representing information important to your application, but they lack some of the features of enums:
- Enums record a name and associated value for each member
-
An Enum class is just like a regular class. You can write docstrings and add methods. Each enum also records the name (the left-hand side) and its constituent value (the right-hand side)
Each enum element remembers both its
name
andvalue
properties:>>> GameState.starting <GameState.starting: 'starting'>
>>> GameState.starting.value 'starting'
>>> GameState.starting.name 'starting'
They know about valid and invalid names, and you can create an enum element from its value:
>>> GameState("bad name") ValueError: 'bad name' is not a valid GameState
>>> GameState("game_ended") <GameState.game_ended: 'game_ended'>
- Enums are typed, and aid with code completion and type hinting
-
So if you declare a variable or argument as
GameState
your editor will help you code complete. - Enums are iterable and support membership testing
-
So you can use them in loops to capture all the elements:
>>> list(GameState) [<GameState.unknown: 'unknown'>, <GameState.starting: 'starting'>, <GameState.initialized: 'initialized'>, <GameState.map_editing: 'map_editing'>, <GameState.game_playing: 'game_playing'>, <GameState.main_menu: 'main_menu'>, <GameState.game_ended: 'game_ended'>, <GameState.quitting: 'quitting'>]
And check for membership:
>>> GameState.map_editing in GameState True
- Enums are symbolic
-
This is the most important thing to understand. Enums represent a symbol — in
GameState
each value is a string, but it could be an int or some other primitive type, also. But usually the exact value is not important, only that it’s the same and consistent everywhere. And that’s really the thing to keep in mind. You’re not passing around a string, or an integer, but a symbol (GameState
) with aname
and, yes, avalue
.That means all the usual conditional checks you want to do work perfectly. And if you use
IntEnum
you gain the advantage of the enum elements behaving like numbers, meaning>
,<
, and so on, also work.
This course will make heavy use of enums to represent things that’re symbolic.
Putting it all together
Now we can take our new-found knowledge and extend TowerGame
, from the last chapter, with a basic state machine.
@dataclass
class TowerGame:
...
state: GameState
...
@classmethod
def create(cls, ...):
return cls(..., state=GameState.initializing)
def set_state(self, new_state):
self.state = new_state
def assert_state_is(self, *expected_states: GameState):
"""
Asserts that the game engine is one of
`expected_states`. If that assertions fails, raise
`StateError`.
"""
if not self.state in expected_states:
raise StateError(
f"Expected the game state to be one of {expected_states} not {self.state}"
)
def start_game(self):
self.assert_state_is(GameState.initialized)
self.set_state(GameState.main_menu)
self.loop()
def loop(self):
while self.state != GameState.quitting:
if self.state == GameState.main_menu:
# pass control to the game menu's loop
elif self.state == GameState.map_editing:
# ... etc ...
elif self.state == GameState.game_playing:
# ... etc ...
self.quit()
def init(self):
self.assert_state_is(GameState.initializing)
...
self.set_state(GameState.initialized)
Here I’ve added a couple of helper functions to aid with the state transition stuff. We can now assert, whenever we like, that the game state is one or more known states. That way start_game
will error out if we call it before calling init
. And init
itself won’t run if it is not in its GameState.initializing
state.
This means we can now, finally, write out some of our main loop
code also: its while loop will keep spinning as long as we’re not in a quitting
state. Presently, the loop will check if we’re in one of a number of game states and – though it’s not written yet, as we’ve not gotten that far yet – hand off control to another part of our game’s code.
Why do it like this? Because this loop
is a controller. Its aim is not to do any of the heavy lifting of drawing stuff to the screen, nor should it handle keyboard input per se. You could definitely make it do that: you have a screen
to draw on, and you can listen to keyboard and mouse events also. So why not do it like that? Well:
- Every game state represents vastly different requirements
-
Consider a main menu. We want – as we’re hewing somewhat close to the demo – a menu of items and maybe some fancy graphics effects and a logo to show off our cool game. But that’s… not what we want in the map editor. In fact, it’s completely different from the main menu.
So how would you deal with two very conflicting requirements? With
if
statements. Lots of them. And don’t forget, every single object, sprite, or asset you want to draw to the screen, needs to be accessible fromTowerGame
. So you’ll end up with alevel
attribute to store level details for the map editor; amenu_group
for the menu items you render when you’re in a main menu; thescore
andhud
for the game play itself.You’ll end up with dozens of different things that only apply in some circumstances, and only in some game states.
Instead of submitting ourselves to what will be a messy development experience, we’ll instead create a class structure that represents everything we need to draw stuff to the screen; handle keyboard and mouse inputs; and so on, and in a nice, clean and easy-to-understand structure.
The demo’s state machine is basic, but you could do a few things to improve it, if you wish:
- Enforcing only legitimate transitions
-
Currently, you can call
set_state
with any state you like, even if it does not make sense to do so. Like reverting back toinitializing
after it’s alreadyinitialized
. You could extend it to check and enforce that only valid state transitions from the current state is possible. That way your game will error out if you send it to the wrong state. It’s a useful way to catch serious bugs in larger codebases. - Separating the state machine into a new class
-
Instead of integrating it into
TowerGame
you could create a standalone class that also takes a state enum class to use as its source of transitions instead of hardcoding it, like I’ve done inTowerGame
.
Next Steps
We have a skeleton class that can initialize our game, and we have a state machine capable of tracking its current state, and also enforcing that it must be in certain states before proceeding.
There’s a lot more to do there, if you wish, but that’s enough to get us started.
Now we need to build a simple class structure that lets us represent a game screen – and thus a unique game state – and why we’d want to do that. After that, it’s time to start building the game!
Summary
- State Machines are useful abstractions
-
They’re very useful for things that require ordering – initialize first, then show the main menu, for instance – but they are not a panacea. Sometimes a boolean or two is easier to reason about; and you can end up with far too many states that do nothing at all, or are too granular.
It’s a balancing act, so think of state machines are another tool in your toolbox.
- Enums represent symbolic values
-
They have an actual value, and a Python-friendly attribute name, but their main purpose is to remove any ambiguities about what you’re passing around between functions. Strings lose context quickly and require fastidious updating if you change them.