6.6 Game State Machines
SLO 3
Describe, design, implement, and test structured programs using currently accepted methodology.
Learning Objectives
By the end of this section, you will be able to:
- Use a
switchstatement to organize different game screens. - Implement a state variable that controls which screen is shown.
- Explain why each
caseneeds abreakstatement. - Offer a Continue option only when a save exists, and restore both the saved data and the matching screen together.
Through §6.1-6.5, the courtyard game has only ever had one screen: gameplay, running from the moment setup finishes. A real game needs a title screen before that and a game-over screen after. This section wraps the whole thing in a state machine so it has all three.
6.6.1 What is a State Machine?
A state machine is a programming pattern where a variable (the "state") determines what the program does. In a game, the state might be 'title', 'play', or 'gameover'. Each state has its own code for drawing and handling input.
Think of a state machine like a vending machine. When it's in the "waiting for money" state, it displays prices and accepts coins. When enough money is inserted, it switches to the "select item" state and lights up the buttons.
A programming pattern where a variable (the state) determines what code runs. Different states produce different behavior, and events trigger transitions between states.
Think about the courtyard game as it stands after §6.5. What screens does it need that it doesn't have yet? List at least two, and what should cause the game to move from one to the next.
Solution
It needs a title screen (shown before play starts, transitioning to gameplay on some input like pressing space) and a game-over screen (shown after the player either wins by reaching the end of the level or loses some other way, transitioning back to the title screen so the player can try again). Right now the game just starts running the instant the page loads and never stops — the rest of this section builds exactly the two missing screens.
6.6.2 The Switch Statement
A switch statement reads a value once and runs the matching case block. Placed at the top of draw() and switching on your state variable, each case becomes responsible for its own screen's rendering and input — the pattern this book calls state dispatch.
switch (state) {
case 'title':
// draw title screen
break;
case 'play':
// draw gameplay
break;
case 'gameover':
// draw game over screen
break;
}
A control structure that evaluates an expression once and runs the matching case block. It is an alternative to a chain of if-else statements.
Sketch (as comments, no need to run it) the skeleton of a three-case switch on a state variable for the courtyard game, with one comment per case describing what that screen shows.
Solution
switch (state) {
case 'title':
// "Courtyard" title text, "Press space to start"
break;
case 'play':
// the full game: player, ground, coins, bombs, camera, score/high score HUD
break;
case 'gameover':
// "You win!" or "Game over", final score, high score, "Press space to play again"
break;
}
This is the shape §6.6.3 fills in with real code — the 'play' case is everything §6.1-6.5 already built.
6.6.3 State Dispatch Pattern
The state dispatch pattern puts a switch on the state variable at the top of draw(). Each case block contains all the code for that state — drawing, input handling, and state transitions. To switch states, you just change the state variable: state = 'play';.
The state dispatch pattern keeps your code organized. Instead of one giant draw() function with confusing if statements, you have clean, separate sections for each screen. Adding a new screen (like a pause menu) just means adding a new case.
Wrap the courtyard game in a real state machine: 'title' (press space to start), 'play' (the full game from §6.5, unchanged), and 'gameover' (reached when player.x > 820, near the far end of the 900-wide level — show the final score and high score, press space to return to 'title').
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Load the page and you land on the title screen — the coins and bombs don't even exist yet, since startGame() hasn't run. Press space and the whole §6.5 game starts. Walk to the far end and the state flips to 'gameover', which eases the camera back to center and shows the final tally. Press space again and you're back at the title, ready to run it again.
6.6.4 Break and Fall-Through
Each case needs its own break — without one, execution "falls through" into the next case's code, which is almost never what you want. default: is an optional catch-all case that runs if none of the listed values match.
What happens when a case block does not end with break. Execution continues into the next case block, even if the value doesn't match.
Remove the break from the 'title' case in §6.6.3's solution. What happens when you press space on the title screen now, and why?
Solution
Without break, once state = 'play' runs inside the 'title' case, execution doesn't stop — it falls straight into the 'play' case's code in the same frame, even though the switch only evaluated state once at the top (back when it was still 'title'). In this particular game that mostly goes unnoticed because startGame() already ran, but it means the very first frame of 'play' runs its input-handling code twice as often as intended for one frame, and in a state machine with side effects on entry (like a sound effect or a screen-shake), fall-through would trigger both the title's and the gameplay's entry logic on the same frame. This is exactly why every case needs its own break.
6.6.5 New Game vs. Continue
§6.5.6 gave you a way to save more than one number. Combine that with the state machine and the title screen can now do something more useful than always starting from zero: check whether a save exists, and offer Continue alongside New Game when it does.
getItem('courtyardSave') returns whatever was last saved there, or nothing at all if no save exists yet — testing that return value is enough to decide whether the Continue option even appears. But loading the save is only half the job. A Continue that restores the data but forgets to also set state leaves the player on the correct data and the wrong screen — the save has to carry both the numbers and which screen they belong to, restored together as a pair.
Definition: Save-and-State Pairing
The rule that restoring saved data must also restore the game to the matching screen (state), not just the numbers. Loading data without setting state to match leaves the interface frozen on whatever screen it happened to already be on.
Save { score, highScore } under 'courtyardSave' the moment the run ends (entering 'gameover'). On the title screen, offer New Game (press N, always available) and Continue (press C, only if a save exists) — New Game starts fresh at score = 0; Continue starts a new lap through the level but carries the previous score forward instead of resetting it.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
The title screen only offers Continue when hasSave is true — no save on a fresh visit, no Continue option. Pressing C restores the previous score into a brand-new lap through the level, while N always resets to 0. Notice both branches set state = 'play' right alongside restoring the data — dropping that line would leave Continue's numbers correct but the player stuck watching the title screen, exactly the bug this section is named for.
6.6.6 Challenge: Extend It Yourself
No starter code this time — you build it. Add a fourth state, 'paused': pressing P during 'play' switches to it, freezing all gameplay updates (no movement, no overlap checks) while still drawing the frozen scene underneath a semi-transparent "Paused" overlay. Pressing P again returns to 'play' exactly where it left off.
Hint (try the Challenge yourself first!)
The 'paused' case doesn't need to redraw the player/coins/bombs itself — since nothing deletes or moves them while paused, they're still sitting in memory from the last 'play' frame. You mainly need an overlay rectangle plus the "Paused" text, and a key check that flips state back to 'play'.
Problem Set 6.6
Problem 1. What is a state machine? Give an example from everyday life (not a vending machine).
Solution
Step 1 — Define the pattern: A state machine is a programming pattern where a variable (the state) determines what code runs: each state produces its own behavior, and events trigger transitions from one state to another (Definition 6.6.1). The program is always in exactly one state at a time.
Step 2 — Choose an everyday example: A traffic light works well. Its state is one of three values — 'green', 'yellow', or 'red' — and what the light displays depends entirely on which state it is currently in.
Step 3 — Map the states and transitions: A timer event moves the light between states: green → yellow when the green interval expires, yellow → red when the yellow interval expires, red → green when the red interval expires. The light never shows two colors at once because it can only occupy one state — exactly like the courtyard game being on the title screen, in play, or on the game-over screen.
Answer: A state machine is a pattern where a state variable decides what the program does, with events triggering transitions between states. Everyday example: a traffic light, whose 'green'/'yellow'/'red' state determines what it shows, with timers causing each transition.
Problem 2. What does a switch statement do? How is it different from an if-else chain?
Solution
Step 1 — Say what a switch does: A switch statement evaluates an expression once and runs the code in the matching case block (Definition 6.6.2). It is built for the situation "I have one value and several possible exact matches."
Step 2 — Contrast with an if-else chain: An if-else chain re-tests a condition at every link, and each condition can be anything — a range check like player.x > 820, or a compound test like kb.presses(' ') && player.colliding(ground). A switch instead compares one already-computed value against fixed case values. That single evaluation is also the key behavioral difference: because state is read only once at the top, changing it inside a case (e.g., state = 'play';) does not redirect the current pass — that is why break matters.
Step 3 — Summarize the trade-off: switch is the cleaner tool when one variable is matched against several discrete values; if-else is the right tool when the conditions are genuinely different tests rather than matches on a single value.
Answer: A switch evaluates one expression once and runs the matching case block; an if-else chain re-tests a (possibly different) condition at each step, so it handles ranges and compound conditions that switch's exact-value matching cannot.
Problem 3. What is the state dispatch pattern? Why is it useful for games?
Solution
Step 1 — Define the pattern: State dispatch means putting a switch on the state variable at the top of draw(), with one case per screen. Each case owns everything for that screen: its drawing, its input handling, and its transitions out.
Step 2 — Explain why games need it: draw() runs every frame, but only some code should run per frame — you do not want gameplay overlap checks firing while the player is still reading the title text. Dispatch guarantees exactly one screen's code executes in any given frame.
Step 3 — Note the maintenance payoff: Changing screens is just an assignment like state = 'gameover';, and adding a new screen (say, a pause menu) means adding one new case without touching the other screens' code — the point made in §6.6.3's Insight Note.
Answer: State dispatch is a switch on the state variable at the top of draw(), with one case per screen owning that screen's drawing, input, and transitions. It is useful because exactly one screen's logic runs per frame, transitions are one-line assignments, and new screens are just new cases.
Problem 4. What happens if you forget to put a break at the end of a case block?
Solution
Step 1 — Name the behavior: Forgetting break causes fall-through (Definition 6.6.3): after the matching case's code finishes, execution continues straight into the next case's code — even though that case's value did not match.
Step 2 — Trace it in the game: Remove the break from the 'title' case in §6.6.3's switch. On the frame you press space, the 'title' case runs startGame() and sets state = 'play' — and then execution falls directly into the 'play' case's code in the same frame, even though the switch read state back when it was still 'title'.
Step 3 — State the consequence: Code from two screens runs in one frame. In this particular game it is mostly harmless (the first 'play' frame just runs a little early), but any case with entry side effects — a sound effect, a screen shake, a reset — would fire both screens' logic at once, and the more cases stack up, the worse it gets.
Answer: Execution falls through into the next case's code in the same pass, so more than one case's code runs when you only intended one. Every case needs its own break to exit the switch cleanly.
Problem 5. What does the default: case do in a switch statement? When would you use it?
Solution
Step 1 — Define default: default: is an optional catch-all case that runs when none of the listed case values match the switch expression (see the Key Terms table).
Step 2 — Say when you would use it: Use it whenever a value outside your listed cases is possible and you want defined behavior instead of silence. Two typical uses: (a) a safety net — if state somehow holds a value you never set (a typo like 'tittle'), a default: case can reset state = 'title'; so the game recovers instead of drawing a blank screen; (b) an explicit "everything else" branch when the listed cases cover only the interesting values.
Step 3 — Note the placement convention: default: is conventionally written last and needs no break (there is nothing after it to fall into), though adding one is a good habit in case the cases get reordered later.
Answer: default: runs when no listed case matches; you would use it as a recovery path for unexpected values (e.g., resetting a corrupted state to 'title') or as an explicit else-branch for the switch.
Problem 6. Why does startGame() need to reset score to 0 and recreate the coins/bombs groups every time the title screen transitions to 'play'?
Solution
Step 1 — Remember what the previous run left behind: During play, sprites are permanently removed — each collected coin is deleted with c.remove() and each triggered bomb with b.remove(). By the time the run ends, those objects no longer exist in the world.
Step 2 — Follow a second playthrough without the reset: If startGame() did not recreate the groups, the next run would begin with only the coins and bombs that were never touched last time — possibly none at all, leaving nothing to collect or dodge. And if it did not reset score, the new run would immediately display the old run's total, and the high-score comparison inside the coin overlap callback would be skewed from the very first pickup.
Step 3 — State the principle: Every transition into 'play' must build a fresh, identical level — same coin positions, same bomb positions, and score starting from its correct seed. In §6.6.5 that seed becomes a parameter (startGame(0) for New Game, startGame(saved.score) for Continue), but the rule stays the same: recreate everything so the run starts from a known state.
Answer: Because a repeat run would otherwise inherit the last run's leftovers: removed coins and bombs stay gone, and score carries over its old value. Recreating the groups and resetting the score gives every playthrough identical, predictable starting conditions.
Problem 7. In the state dispatch pattern, where do you put the switch statement? Why?
Solution
Step 1 — Name the location: At the top of draw(), immediately after the background(...) call.
Step 2 — Explain why there: draw() is the heartbeat of the sketch — p5 runs it every frame, roughly 60 times per second. Putting the dispatch there means the state machine is consulted every single frame, so a state change takes effect within one frame of the state variable being assigned, and exactly one case's drawing, input, and transition code executes per frame.
Step 3 — Note what this placement prevents: Because dispatch is the first thing after clearing the canvas, no screen's code can leak into another's frame: the title screen never runs gameplay overlap checks, and the game-over screen never reads movement keys. It also keeps a screen's drawing and its input handling in the same block, so the two cannot drift out of sync.
Answer: At the top of draw() — since draw() runs every frame, the dispatch is re-evaluated every frame, guaranteeing that exactly one screen's code runs per frame and that no screen's logic leaks into another's.
Problem 8. How would you add a "press R to restart mid-game" feature that sends the player back to the title screen without waiting for a win?
Solution
Step 1 — Decide where the check lives: Pressing R is only meaningful while playing, so the key check belongs inside the 'play' case, alongside the existing input handling.
Step 2 — Write the transition: When R is pressed, flip the state back to 'title':
case 'play':
// ... existing movement, overlap, and camera code ...
if (kb.presses('r')) {
allSprites.deleteAll(); // clear the abandoned run's sprites
state = 'title';
}
break;
Step 3 — Handle the cleanup (the easy-to-miss part): The abandoned run leaves sprites behind — the player, the ground, and any uncollected coins or bombs. If they are not removed, they linger in the world alongside the fresh sprites that startGame() creates on the next run. Clearing with allSprites.deleteAll() on the way out — or at the top of startGame() itself, which protects every path back to a new run, whether via R-restart or via game-over — makes the restart genuinely clean.
Answer: Add if (kb.presses('r')) { state = 'title'; } inside the 'play' case, ideally paired with a sprite cleanup like allSprites.deleteAll() so the abandoned run's player, coins, and bombs do not carry over into the next game.
Problem 9. A Continue button restores score and highScore correctly from storage, but the screen stays frozen on the title text. What line is almost certainly missing?
Solution
Step 1 — Diagnose from the symptom: The data is correct but the screen never changes. That means the restore code ran — the numbers loaded fine — but the program was never told to leave the title screen. Since the state dispatch draws whatever screen state names, a state that is never reassigned keeps drawing the title forever.
Step 2 — Name the missing line: state = 'play';, placed immediately after the restore, as a pair:
startGame(saved.score); // restore the DATA... state = 'play'; // ...and the SCREEN, together
Step 3 — Connect it to the section's rule: This is exactly the Save-and-State Pairing definition from §6.6.5: restoring saved data must also restore the matching screen. Data without the state assignment leaves the player holding correct numbers behind the wrong interface.
Answer: state = 'play'; — the code restored the saved numbers but never told the state machine to switch screens, so the display stayed frozen on the title text.
Problem 10. Why does the title screen check hasSave before deciding whether to even show the Continue text, rather than just always showing it?
Solution
Step 1 — Consider the fresh-visit case: On a player's first visit there is no save at all: getItem('courtyardSave') returns nothing (undefined). There is no score to carry forward and no run to resume.
Step 2 — Ask what an unconditional Continue option would do: If the title screen always showed "C: Continue", a new player would press C — and then either get a crash (reading .score off the undefined return value throws an error), see score: undefined printed in the UI, or start a corrupted run. The option would be a trap rather than a feature.
Step 3 — State the principle: The hasSave check makes the offer honest: Continue only appears when loading it will actually work. It is the same instinct as a commercial game greying out "Continue" on its main menu until a save file exists — never show an option the program cannot deliver on.
Answer: Because with no save, Continue would be a non-functional (or crashing) option — getItem('courtyardSave') returns nothing, so there is nothing to load and accessing .score fails. Checking hasSave first ensures the option only appears when it can actually work.
Key Terms
| Term | Definition |
|---|---|
| break | A statement that exits a switch case and prevents fall-through to the next case |
| default | An optional catch-all case in a switch that runs if no other case matches |
| Fall-through | When a case block lacks break and execution continues into the next case |
| State dispatch | A pattern using a switch on a state variable at the top of draw() to organize different game screens |
| State machine | A programming pattern where a variable (state) determines what code runs, with events triggering transitions |
| Save-and-state pairing | Restoring saved data must also set state to the matching screen; restoring data alone leaves the interface on the wrong screen |
| Switch statement | A control structure that evaluates an expression and runs the matching case block |