For our first game, Steve’s Job, our studio developed a puzzle adventure game set in a split-apart reality. In point-and-click fashion, players must talk to characters and obtain items to solve puzzles.
The world of Steve’s Job1 is split into three alternate realities (hate when that happens), and players are meant to freely run around all of them. So, when designing the puzzles and overall game flow, we wanted to make sure players always have multiple things to do at any given point in time – and that they can never get stuck and softlock themselves.
To help design this non-linear game flow, we used a mathematical model known as a Petri net aaaaand I’d like to show you how we did that!
Note: The example Petri nets are rendered with JS, so make sure that’s available and enabled!
Now, Petri nets are perfect for modeling the type of conditions that came up, stuff along the lines of “When the player has item x, talking to character y will give them item z in exchange. Item x can be obtained if the player has item a and … and talks to…”
We can design Petri nets for these conditions and then analyze them to mathematically prove that there’s no way for players to soft-lock themselves.
Alrightyo, let’s look at a very simple Petri net. Most examples in this post are interactive, so you can click the blue box labeled t1 below and watch the circle move from p1 to p2! Good job!
A Petri net is made up from four elements, places, transitions, tokens and arcs between places and transitions. The image above, which is actually a complete Petri net, shows all four.
That’s pretty much it! In reality, the arcs can also be weighted2, but I’ll gloss over that since it won’t be needed for what I’ll be showing – hence we’ll be only looking at so-called ordinary Petri nets where all weights are \(1\).
Let’s look at some more simple Nets. Feel free to play around with them!
Creating and destroying tokens
We can also completely create and destroy tokens.
A source of tokens is a transition without any input places – such a transition is always enabled and will always place a token into each of its outputs.
A sink is a transition without any outputs – it’ll end up destroying its input tokens.
Multiple inputs and outputs
Check the network below.
Here, t1 has two inputs (p1 and p2) and three outputs (p3 through p5).
It can only fire, if there’s tokens in all of its inputs. Upon firing, it will place a token into each output.
Okay, let’s model some game logic. In Steve’s Job, the player can talk to a cult of goose girls – developer self-insert let’s goooo – who will give them a goose. There’s a machine that turns this goose into grease. It costs one coin to operate and can only ever be used once.
The following Petri net models this interaction:

Initial State: The player starts off with a single coin in their inventory. In the Petri net, this is modeled as a token in the Has Coin place. A token in Machine Unused indicates that the machine has not yet been used, and a token in Goose Storage indicates that the goose is still with the cult.
Take Goose: The player joins the mysterious cult and, as a membership reward, is given a goose. This is modeled with the Take Goose transition. It requires that there’s a token in the Goose Storage place, which is the case in the current state.
Goose to Grease: The player walks up to a “Geese-to-Grease” machine. In exchange for a coin and the goose (i.e. by taking a token from the two respective places), the machine commits some goose cruelty and then happily dispenses a tub of grease. The transition also takes a token from Machine Unused, ensuring the player can never use the machine again, even if they obtain another goose and coin.
As mentioned before, the really neat thing about Petri nets is the fact they can be analyzed for certain properties, the most interesting of which is deadlock.
Most Petri net tools should have analysis tools. I used Workcraft, so let’s just check our Petri net for deadlocks:

Aaaaand it’s dead. We’ve already seen that the modeled objective can be completed, but of course, once that’s happened the player is “stuck” in the sense that they’ve finished.
To make our Petri net ready for analysis, we’ll add a Reset_Game transition. It can only be fired when the objective is complete (i.e. the player has the grease), and it restores the Petri net to the initial state.
Let’s check again:

It’s deadlock-free! :confetti: This means we’ve proven that this interaction within the game is logically sound!
Any Petri net can be transformed into a different graph, called the reachability graph (which itself really is just a representation of a state machine).
In this graph, the vertices are all possible states of the Petri net and the edges are transitions.
The above example has three states, i.e. three possible assignments of tokens to places.
We can also rephrase the deadlock-free property: A Petri net is deadlock-free if its reachability graph is strongly connected, i.e. any state can be reached from any other state.
Behold, the big network for the full game! Feel free to play around. The places have tooltips you can hover over that give some more context.
With this Petri net in our hands we can finally prove the game flow actually makes sense – by adding a reset transition and verifying deadlock-freeness.

Neat! And finally, here’s the big reachability graph:
The initial game state is aaaaall the way on the left and as the player progresses through the game, they move towards the right on the reachability graph. The final state, the game’s win condition, is all the way on the right and reached after triggering the Fix_Switch transition – repairing the light switch repairs reality, roll credits etc.
The reachability graph has 82 nodes, meaning that as far as progression is concerned, Steve’s Job has 82 possible game states3.

…but that’s just a theory
There’s a neat connection here to the more formal approaches to game studies and game theory! Briefly put, a game can be seen as a state machine. Whenever the player takes an action, the rules of the game dictate into which state the game moves next.
In game-theory-lingo, the reachability graph is the game tree.
The player will try to reach some “good” or desirable state – here that’s the single win state. Taking the appropriate choices to influence the game’s state is where the gameplay happens.
The shape of the game tree also tells us about the nature of a game. Take, for example this one, for tic-tac-toe:

I’ve not listed all possible states here – even taking into account symmetries from rotating and mirroring the tic-tac-toe board, there’s 765 different game states. Games with such broad game trees are referred to as games of emergence.
Chess is an even better example with a ginormous game tree – after all we don’t have enough atoms in the universe to enumerate all possible chess games.
Game of emergence generally have a set of simple and general rules. This makes such games “easy to learn,” but because there’s so many possibilities they’re also “hard to master.” There’s never a clear path to victory4 and strategies are requires to (attempt to) navigate the game tree.
Adventure games like Steve’s Job on the other hand fall into the category of games of progression. The rules are more specific (“You need to talk to the cult, and then they give you a goose”) and the game tree is somewhat linear – certainly much less broad than that of even a simple emergent game like tic-tac-toe.
For further reading, check out chapter 3 of Jesper Juul’s Half-Real and unit 2 of Rules of Play by Salen and Zimmerman.
In the actual implementation of Steve’s Job, the Petri net’s places map to inventory items. Some, such as the goose, are visible to the player. For the other places, like our Goose Storage place, we’ve used invisible items.
After talking to the goose cult, we remove the invisible Goose Storage item from the player’s inventory – this makes sure they can’t get a second goose from the cult5.
Now, take a look at the data contained in the savegame – keeping in mind that the savegame really just is a representation of the game state.
[System.Serializable]
public class SaveGame {
public List<int> inventory = new List<int>();
public string scene;
public Vector3 position;
}
Of course, we want the player to respawn in the same position and in
the same scene. But as far as progression is concerned, we really
just need to store the player’s inventory!
I’ve actually found one paper about modelling games with Petri nets. The game the authors apply this to is more of an emergent game, and as such instead of whole-game progression, they’ve modeled complex individual game systems.
In the real world, the closest I’ve seen is the game flow diagram in
the Game Design Document for Sam & Max: Hit the Road
(just google search online for “sam and max gdd pdf”).
It’s more about how the game’s screens are connected, but I’m sure that
in conjunction with the list of puzzles given in the document, one could
also create a Petri net for that game.
The Steve’s Job Petri net was originally designed by fellow goose girl Fionn, with whom I brainstormed the game flow on paper.
I’ve focused on the design of the puzzles, but that doesn’t mean our implementation is necessarily deadlock-free – there may still be bugs.
In theory, we could’ve implemented a representation of the Petri net in our game and have that serve as the back-bone for which interactions are possible, and to which outcomes they lead, at any given point. Doing something like that would probably be pretty interesting – I was considering doing something along those lines for my Bachelor’s thesis before real-time rendering and Vulkan stuff got the better of me :D
Of course, feel free to check out Steve’s Job, available for free on itch.io.
Also, if there’s anything you’d like me to know, reach out to me ^w^
Anyways, thanks for reading :3

🐾 Footnotes:
The game’s free on itch.io and takes around a half hour to complete – if you want to check it out :3 I’m not here to plug the game, it’s just a small student project, but it’s kinda very dear to my heart as it’s what made me fall in love with the whole game development thing :D ↩
Okay, but what if we wanted something to take two tokens, like a vending machine greedily gobbling up two coins? Well, of course we can increase the weight of the respective arc to 2.
But: There’s some Petri net tools that don’t support arc weights, but we can build a simple workaround like this (.webm). As long as there’s at least two tokens across the Has_Coin places, the transitions M1 and M2 can be used to redistribute them so that Goose_To_Grease can fire.
In fact, we can always transform weighted Petri nets into ordinary Petri nets, meaning both kinds can express the same things. ↩
We’ve kind of taken a bird’s eye view of game state here: Of course, the full game state also includes the player’s position, velocity, the audio preferences, timers, the states of all other in-game systems, … ↩
I mean, if we were able to enumerate the game tree of chess, there would always be a clear path to victory – but that’s computationally out of the question, especially for human player. ↩
Technically, we actually give the player a Cult Joined item. I think we did it this way for a lot of the items as it makes the initial inventory state less complicated and is easier to think about – but either way of thinking about this is, of course equivalent. ↩