Session 14 - Capstone: Adventure Game (Part 2)

Session 14 of the Python Fundamentals series, and the second part of the capstone. You finish the adventure game by giving it state that travels through the play-through: a dictionary holding items and a score, rooms that read and update it, a locked door that checks for a key, and real winning and losing endings chosen from the final state. You then polish it with an ask helper and invalid-choice handling. The traps are forgetting to pass the state, so that changes are lost, and reassigning the state dictionary instead of updating it. Every snippet was verified under CPython 3.12, with interactive input shown as the sequence the player types.

Subject: Python Fundamentals · 94 slides · code lesson

Open the interactive version of this deck · Homework for this lesson

What this lesson covers

The lesson, slide by slide

1. Capstone: Adventure Game

Title

Python Fundamentals - Session 14

Part 2 - state, endings, and polish

2. What you will be able to do

Objectives

Part 1 built the skeleton and a couple of rooms. Now we complete and polish it. By the end you can:

  1. Track state - a score or an item - that travels with the player.
  2. Store items and flags in a dictionary and update them in a room.
  3. Use state to unlock a path and to decide the ending.
  1. Give the game a real win and lose ending.
  2. Polish input handling with a helper and invalid-choice re-prompts.
  3. Avoid the lost-state trap.

3. What survived from Session 13 - Capstone: Adventure Game (Part 1)?

Warm-up

Discussion prompt

Before we open Session 14 - Capstone: Adventure Game (Part 2): without looking back, what was the main idea of Session 13 - Capstone: Adventure Game (Part 1), and what could you do by the end of it that you could not do before?

Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.

Answer:

That session plans the map, models rooms as functions that return the next room, and drives them with a while-loop state machine.

4. Tracking State

Section

Part 1

5. The game needs a memory

Concept

A real game remembers things across rooms: an item picked up, a score, whether a switch was flipped.

That remembered information is the game's state, and it must survive from room to room.

6. Break it if you can: The game needs a memory

Counterexample

Discussion prompt

A real game remembers things across rooms: an item picked up, a score, whether a switch was flipped.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

7. State travels with the player

Concept

Keep the state in a variable that the engine passes into each room, so any room can read or change it.

Rooms come and go, but the state variable persists for the whole play-through.

8. By analogy: State travels with the player

Analogy

Discussion prompt

Explain State travels with the player by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

Keep the state in a variable that the engine passes into each room, so any room can read or change it.

9. State is the player's backpack

Intuition

Picture a backpack the player carries everywhere. Rooms can put things in it (a key) or read from it (do I have the key?).

The room changes; the backpack stays. That is exactly what a passed-in state variable is.

10. Teach it back: State is the player's backpack

Explain it

Discussion prompt

Explain State is the player's backpack to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.

Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.

Answer:

Picture a backpack the player carries everywhere. Rooms can put things in it (a key) or read from it (do I have the key?).

11. Pass state into room functions

Concept

Give rooms that need it a state parameter: def room_cave(state):. The engine passes the same state object each call.

Because it is the same object, changes a room makes are visible everywhere - the in-place idea from the scope session.

12. Inventory with a Dict

Section

Part 2

13. A dict holds items and flags

Concept

A dictionary is a natural fit for state: keys name what you track, values hold the current status.

state = {"has_key": False, "score": 0} - a flag and a number, all in one place.

14. Why is this step legal: Read the output

Explain it to yourself

Discussion prompt

In Setting up state this move is made:

Read the output

Why is that legal? Name the rule or definition it rests on before you read on.

Hint: If you can only say "because that is what you do", the rule is the thing to go and find.

Answer:

Verified by execution: False then 0 - the starting state.

15. Setting up state

Worked example

state = {"has_key": False, "score": 0}
print(state["has_key"])
print(state["score"])

One dict, many pieces of state

Why: has_key tracks an item; score tracks points.

Read the output

Why: Verified by execution: False then 0 - the starting state.

keystart value
has_keyFalse
score0

16. Fill in: start value for Setting up state

Comparison

Comparison matrix

From Setting up state: refill the start value column from what you know. The rest of the table is as it appeared.

keystart value
has_keyFalse
score0

17. A room updates the state

Concept

Inside a room, change the dict in place: state["has_key"] = True. That update sticks because every room shares the same dict.

18. Restore the missing line: Grabbing a key

Fill the middle

Fill in the blanks

From Grabbing a key — one line has had its right-hand side removed. Put it back.

def room_cave(state):
print("A shiny key lies on a rock. (grab) or (leave)?")
c = input("> ").strip().lower()
if c == "grab":
state["has_key"] = True
print("You take the key.")
return "door"

Why: state["has_key"] is what everything below it consumes, so the wrong expression here fails later and somewhere else. state["has_key"] = True updates the dict every room can see.

19. Grabbing a key

Worked example

The player types grab:

def room_cave(state):
    print("A shiny key lies on a rock. (grab) or (leave)?")
    c = input("> ").strip().lower()
    if c == "grab":
        state["has_key"] = True
        print("You take the key.")
    return "door"

grab sets the flag in the shared state

Why: state["has_key"] = True updates the dict every room can see.

The room then sends the player to the door

Why: Verified by execution: grabbing the key sets has_key True, then returns "door".

choicehas_key afterreturns
grabTruedoor
leaveFalsedoor

20. What each one costs: Grabbing a key

Trade off

Comparison matrix

From Grabbing a key: every row here is a choice with a cost. Fill the has_key after column, then say which row you would actually pick and what you give up for it.

choicehas_key afterreturns
grabTruedoor
leaveFalsedoor

21. Something is wrong here: state not passed, changes lost

Anomaly

Predict first

A student writes this, and it looks reasonable:

A room makes its own local state instead of using the shared one.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: A brand-new dict is made each call and vanishes when the room returns.

Take state as a parameter and update that.

Why: A brand-new dict is made each call and vanishes when the room returns. The door room never sees the key.

22. Trap: state not passed, changes lost

Trap

The trap

A room makes its own local state instead of using the shared one.

def room_cave():
    state = {"has_key": False}
    state["has_key"] = True
    return "door"

This state is local and thrown away

Why: A brand-new dict is made each call and vanishes when the room returns. The door room never sees the key.

wherehas_key
inside room_caveTrue (local)
everywhere elseunchanged

The fix

Take state as a parameter and update that.

def room_cave(state):
    state["has_key"] = True
    return "door"

The shared dict is updated in place

Why: Now the change persists to the door room. Pass the one state object into every room that touches it.

wherehas_key
after room_cave(state)True everywhere

23. Inspect it line by line: Trap: state not passed, changes lost

Error analysis

Annotate

Walk the callouts on Trap: state not passed, changes lost. Each one is a place this is easy to get subtly wrong.

  • A brand-new dict is made each call and vanishes when the room returns. The door room never sees the key.
  • Now the change persists to the door room. Pass the one state object into every room that touches it.

24. Decisions from State

Section

Part 3

25. A room reads state to decide

Concept

A room can branch on state: if state["has_key"]:. The same room leads to different places depending on what the player carries.

26. The locked door

Worked example

def room_door(state):
    print("A locked door blocks the exit.")
    if state["has_key"]:
        print("Your key fits! You escape.")
        return "win"
    print("It will not budge.")
    return "lose"

The door checks the backpack

Why: If has_key is True, the player wins; otherwise they lose.

State decides the outcome

Why: Verified by execution: with the key, returns "win"; without it, returns "lose".

has_keyreturns
Truewin
Falselose

27. Draw the shape of it: The locked door

Blank canvas

Draw it

Draw what The locked door just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.

28. State makes choices matter

Concept

Because the door reads state set earlier, the player's choice in the cave changes the ending. That connection is what makes a game feel real.

29. Where does each piece belong: Session 14 - Capstone: Adventure Game (Part…

Sorting

Sort into buckets

These are the pieces of Session 14 - Capstone: Adventure Game (Part 2), out of order. Put each one back under the part of the lesson it belongs to.

Tracking State
The game needs a memory; State travels with the player; State is the player's backpack
Inventory with a Dict
A dict holds items and flags; Setting up state; A room updates the state
Decisions from State
A room reads state to decide; The locked door; State makes choices matter
s1
Tracking State is where Session 14 - Capstone: Adventure Game (Part 2) puts The game needs a memory, State travels with the player, State is the player's backpack. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s2
Inventory with a Dict is where Session 14 - Capstone: Adventure Game (Part 2) puts A dict holds items and flags, Setting up state, A room updates the state. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s3
Decisions from State is where Session 14 - Capstone: Adventure Game (Part 2) puts A room reads state to decide, The locked door, State makes choices matter. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.

30. A Score

Section

Part 4

31. Track a score in state

Concept

Add a "score" key and adjust it as the player acts: state["score"] += 10 for a good move.

32. Predict the next row: Awarding points

Pattern

Predict first

The table runs: start | 0 · +10 | 10

In Awarding points, given the rows so far: what is the next one — the row where action is +5?

Correct: +5 | 15

actionscore
start0
+1010
+515

Why: The relationship between the columns, not the individual numbers, is what generates the next row. Each += adds to the running score, just like an accumulator.

33. Awarding points

Worked example

state = {"score": 0}
state["score"] += 10
state["score"] += 5
print(state["score"])

Points accumulate in the dict

Why: Each += adds to the running score, just like an accumulator.

Read the output

Why: Verified by execution: 15.

actionscore
start0
+1010
+515

34. Watch it run: Awarding points

Pattern

Step through it

Step through Awarding points one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: action is start
  2. Step 2: action is +10
  3. Step 3: action is +5

35. Show the score at the end

Concept

After the loop, report the final score with an f-string: print(f"Final score: {state['score']}").

36. Why is this step legal: Pull the value into the message

Explain it to yourself

Discussion prompt

In The final scoreboard this move is made:

Pull the value into the message

Why is that legal? Name the rule or definition it rests on before you read on.

Hint: If you can only say "because that is what you do", the rule is the thing to go and find.

Answer:

The f-string reads state['score'] and drops it in.

37. The final scoreboard

Worked example

state = {"score": 25}
print(f"Final score: {state['score']}")

Pull the value into the message

Why: The f-string reads state['score'] and drops it in.

Read the output

Why: Verified by execution: Final score: 25.

scoreprints
25Final score: 25

38. Win and Lose Endings

Section

Part 5

39. win and lose stop the loop

Concept

Loop while room not in ("win", "lose"):. Either ending name stops the game.

40. Why is this step legal: Read the output

Explain it to yourself

Discussion prompt

In Choosing the ending message this move is made:

Read the output

Why is that legal? Name the rule or definition it rests on before you read on.

Hint: If you can only say "because that is what you do", the rule is the thing to go and find.

Answer:

Verified by execution: with room "win", prints == YOU ESCAPED ==.

41. Choosing the ending message

Worked example

room = "win"
if room == "win":
    print("== YOU ESCAPED ==")
else:
    print("== GAME OVER ==")

After the loop, pick the closing screen

Why: The final value of room says which ending to show.

Read the output

Why: Verified by execution: with room "win", prints == YOU ESCAPED ==.

roomprints
win== YOU ESCAPED ==
lose== GAME OVER ==

42. Two endings make it feel finished

Intuition

A win and a lose ending give the player something to reach for and something to avoid. Even just two endings make a game feel complete.

The stakes come entirely from state: the same rooms end differently based on what the player did.

43. An ending can use the score

Concept

Blend both: after the loop, show the win/lose screen and the final score for a satisfying finish.

44. The Full Game

Section

Part 6

45. Assemble the pieces

Concept

Put it together: a state dict, room functions that take state, the engine loop, and an ending screen. Everything you have learned, in one program.

46. Guess the shape of the answer: The complete game

Estimation

Predict first

A winning play-through: right, then grab:

Commit before you compute: what does The complete game come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.

Correct: Trace the winning path

Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Verified by execution: right -> cave, grab sets has_key True, door -> win, prints WIN.

47. The complete game

Worked example

A winning play-through: right, then grab:

def room_start():
    print("A fork. (left) or (right)?")
    c = input("> ").strip().lower()
    if c == "right":
        return "cave"
    return "start"

def room_cave(state):
    print("A key on a rock. (grab) or (leave)?")
    if input("> ").strip().lower() == "grab":
        state["has_key"] = True
    return "door"

def room_door(state):
    if state["has_key"]:
        return "win"
    return "lose"

state = {"has_key": False}
room = "start"
while room not in ("win", "lose"):
    if room == "start":
        room = room_start()
    elif room == "cave":
        room = room_cave(state)
    elif room == "door":
        room = room_door(state)
print("WIN" if room == "win" else "LOSE")

State flows through the rooms

Why: room_cave sets has_key; room_door reads it to decide the ending.

Trace the winning path

Why: Verified by execution: right -> cave, grab sets has_key True, door -> win, prints WIN.

roomhas_keynext
startFalsecave
cave (grab)Truedoor
doorTruewin

48. Watch it run: The complete game

Pattern

Step through it

Step through The complete game one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: room is start
  2. Step 2: room is cave (grab)
  3. Step 3: room is door

49. Trace the losing path

Worked example

The player types right, then leave:

# right -> cave (leave) -> door (no key) -> lose

Leaving the key changes the ending

Why: Without has_key, room_door returns "lose".

Read the states

Why: Verified by execution: leaving the key leads to LOSE - the same rooms, a different outcome.

roomhas_keynext
cave (leave)Falsedoor
doorFalselose

50. Fill in: next for Trace the losing path

Comparison

Comparison matrix

From Trace the losing path: refill the next column from what you know. The rest of the table is as it appeared.

roomhas_keynext
cave (leave)Falsedoor
doorFalselose

51. Polish

Section

Part 7

52. Handle invalid choices everywhere

Concept

In every room, an unrecognized choice should re-ask (return the same room) rather than silently do nothing or crash.

53. Guess the shape of the answer: Re-prompt on bad input

Estimation

Predict first

The player types dance (invalid), then grab:

Commit before you compute: what does Re-prompt on bad input come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.

Correct: Read the flow

Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Verified by execution: dance re-prompts, grab then takes the key and moves on.

54. Re-prompt on bad input

Worked example

The player types dance (invalid), then grab:

def room_cave(state):
    print("A key. (grab) or (leave)?")
    c = input("> ").strip().lower()
    if c == "grab":
        state["has_key"] = True
        return "door"
    if c == "leave":
        return "door"
    print("You can (grab) or (leave).")
    return "cave"

Unknown choices return the same room

Why: dance matches nothing, so it re-asks the cave.

Read the flow

Why: Verified by execution: dance re-prompts, grab then takes the key and moves on.

choicereturns
dancecave (re-ask)
grabdoor

55. Clean input with a helper

Concept

A single ask helper (input(prompt).strip().lower()) keeps every room forgiving of spaces and capitals, with no repeated code.

56. Small touches, big feel

Concept

Vivid descriptions, a score, a clear win/lose screen, and forgiving input turn a skeleton into a game worth showing off.

None of it is new syntax - it is the Phase 1 toolkit, arranged well.

57. Pitfalls

Section

Part 8

58. Something is wrong here: reassigning the state dict

Anomaly

Predict first

A student writes this, and it looks reasonable:

Replacing state with a new dict inside a room.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: state = {...} points the local name at a new dict; the engine's state is untouched.

Update the existing dict in place.

Why: state = {...} points the local name at a new dict; the engine's state is untouched. The door sees no key - the same rebind trap as with numbers and lists.

59. Trap: reassigning the state dict

Trap

The trap

Replacing state with a new dict inside a room.

def room_cave(state):
    state = {"has_key": True}
    return "door"

This rebinds the local name only

Why: state = {...} points the local name at a new dict; the engine's state is untouched. The door sees no key - the same rebind trap as with numbers and lists.

engine's stateafter
has_keystill False

The fix

Update the existing dict in place.

def room_cave(state):
    state["has_key"] = True
    return "door"

Change a key, do not replace the dict

Why: state["has_key"] = True edits the shared dict, so the change is seen everywhere. Mutate the state; never reassign it.

engine's stateafter
has_keyTrue

60. Which of these survive contact with Session 14 - Capstone: Adventure Game (Part…?

Two truths and a lie

Sort into buckets

Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.

Holds up
A real game remembers things across rooms: an item picked up, a score, whether a switch was flipped.; Keep the state in a variable that the engine passes into each room, so any room can read or change it.; Picture a backpack the player carries everywhere. Rooms can put things in it (a key) or read from it (do I have the key?).
Breaks
A room makes its own local state instead of using the shared one.; Replacing state with a new dict inside a room.
sound
These are stated as this lesson states them — each one survives the edge cases Session 14 - Capstone: Adventure Game (Part 2) puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

61. Initialize state before the loop

Concept

Create the state dict once, above the engine loop. Making it inside the loop would reset the player's progress every pass.

62. Keep every ending reachable

Concept

Double-check there is a real path to "win" and to "lose". An unreachable win means the player can never finish; an unreachable lose removes the stakes.

63. Extending the Game

Section

Part 9

64. An inventory list of items

Concept

Instead of a flag per item, keep a list of what the player carries: state = {"items": []}, then state["items"].append("key").

Check for an item with in: "key" in state["items"]. This scales to any number of items.

65. Why is this step legal: Check with in and and

Explain it to yourself

Discussion prompt

In Collecting several items this move is made:

Check with in and and

Why is that legal? Name the rule or definition it rests on before you read on.

Hint: If you can only say "because that is what you do", the rule is the thing to go and find.

Answer:

Verified by execution: ['key', 'map'], then True - the player has both.

66. Collecting several items

Worked example

state = {"items": []}
state["items"].append("key")
state["items"].append("map")
print(state["items"])
print("key" in state["items"] and "map" in state["items"])

append adds each item to the shared list

Why: The list grows in place, so every room sees the collected items.

Check with in and and

Why: Verified by execution: ['key', 'map'], then True - the player has both.

stepitems
append key['key']
append map['key', 'map']

67. A room needing two items

Concept

Gate a path behind more than one item with and: the player needs the key and the map to proceed.

This lets you build longer quests where earlier rooms set up later ones.

68. Restore the missing line: A door needing key and map

Fill the middle

Fill in the blanks

From A door needing key and map — one line has had its right-hand side removed. Put it back.

def final_door(state):
have = state["items"]
if "key" in have and "map" in have:
return "win"
print("You are missing something.")
return "lose"

print(final_door(___))

Why: have is what everything below it consumes, so the wrong expression here fails later and somewhere else. The and-condition passes only when the player has collected both.

69. A door needing key and map

Worked example

def final_door(state):
    have = state["items"]
    if "key" in have and "map" in have:
        return "win"
    print("You are missing something.")
    return "lose"

print(final_door({"items": ["key", "map"]}))

Both items required

Why: The and-condition passes only when the player has collected both.

Read the outcome

Why: Verified by execution: with both items, returns "win"; missing either, "lose".

itemsreturns
key + mapwin
key onlylose

70. Draw the shape of it: A door needing key and map

Blank canvas

Draw it

Draw what A door needing key and map just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.

71. Add health, and the stakes rise

Concept

Track a "health" number in state; some rooms subtract from it. The game ends in a loss if health drops to 0 or below.

72. Taking damage

Worked example

state = {"health": 100}
state["health"] -= 30
print(state["health"])
print(state["health"] > 0)

Damage lowers health in place

Why: -= 30 subtracts from the running health, like a reverse accumulator.

A check tells if the player survives

Why: Verified by execution: 70, then True (still alive).

stephealthalive?
start100True
-3070True

73. Inspect it line by line: Taking damage

Error analysis

Annotate

Walk the callouts on Taking damage. Each one is a place this is easy to get subtly wrong.

  • -= 30 subtracts from the running health, like a reverse accumulator.
  • Verified by execution: 70, then True (still alive).

74. State is the whole world

Intuition

Items, health, score, doors opened - every changing fact about the game lives in one state dict. The rooms just read and update that world.

Once you see the game as 'state plus rooms that change it', adding features is just adding keys and a few checks.

75. Teach it back: State is the whole world

Explain it

Discussion prompt

Explain State is the whole world to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.

Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.

Answer:

Items, health, score, doors opened - every changing fact about the game lives in one state dict. The rooms just read and update that world.

76. A play-again loop

Concept

Wrap the whole game in an outer while that asks 'play again?' and restarts with fresh state if the player says yes.

77. By analogy: A play-again loop

Analogy

Discussion prompt

Explain A play-again loop by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

Wrap the whole game in an outer while that asks 'play again?' and restarts with fresh state if the player says yes.

78. Restore the missing line: Playing more than once

Fill the middle

Fill in the blanks

From Playing more than once — one line has had its right-hand side removed. Put it back.

again = "y"
rounds = 0
while again == "y":
rounds += 1
print("playing round", rounds)
again = input("Again? (y/n) ").strip().lower()
print("rounds:", rounds)

Why: again is what everything below it consumes, so the wrong expression here fails later and somewhere else. Each round would set up fresh state and run the engine; here we just count rounds.

79. Playing more than once

Worked example

The player types y, then n:

again = "y"
rounds = 0
while again == "y":
    rounds += 1
    print("playing round", rounds)
    again = input("Again? (y/n) ").strip().lower()
print("rounds:", rounds)

The outer loop restarts the game

Why: Each round would set up fresh state and run the engine; here we just count rounds.

Read the output

Why: Verified by execution: two rounds, then rounds: 2.

againrounds
y1
y then n2 -> stop

80. What each one costs: Playing more than once

Trade off

Comparison matrix

From Playing more than once: every row here is a choice with a cost. Fill the rounds column, then say which row you would actually pick and what you give up for it.

againrounds
y1
y then n2 -> stop

81. Where to take it next

Concept

Ideas to grow the game: more rooms and items, an enemy that costs health, a shop that spends score, a locked ending that needs a full inventory.

Every one of these is just more of the same toolkit - functions, loops, conditionals, dicts, strings. You already have everything you need.

82. Break it if you can: Where to take it next

Counterexample

Discussion prompt

Ideas to grow the game: more rooms and items, an enemy that costs health, a shop that spends score, a locked ending that needs a full inventory.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

Answer:

Every one of these is just more of the same toolkit - functions, loops, conditionals, dicts, strings. You already have everything you need.

83. Patterns & Checks

Section

Part 10

84. Adding state to a game

Pattern

1. Make one state dict before the loop

Why: state = {"has_key": False, "score": 0} - items and numbers in one place.

2. Pass state into rooms that use it

Why: def room_x(state): so every room shares the same object.

3. Update in place: state[key] = ...

Why: Mutate the dict; never reassign it, or the change is lost.

4. Read state to branch and to end

Why: if state["has_key"]: unlocks a path; the final state chooses win vs lose.

85. Finishing checklist

Pattern

At least 4 rooms, a way to win, a way to lose

Why: That is the capstone bar - a complete, playable game.

Every room returns a handled name; every choice re-prompts if invalid

Why: No dead ends, no None rooms, no crashes on odd input.

State initialized once and passed everywhere it is needed

Why: So progress and the ending actually depend on what the player did.

86. Where this shows up: Session 14 - Capstone: Adventure Game (Part 2)

Real world

Discussion prompt

Outside this lesson: where does Session 14 - Capstone: Adventure Game (Part 2) actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of Finishing checklist is doing the work in it.

Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.

Answer:

Session 14 of the Python Fundamentals series - the capstone, part 2. Finish the adventure game with state that travels through the play-through: a dictionary holding items and a score, rooms that read and update it, a locked door that checks for a key, and real win/lose endings chosen from the final state.

87. Check: does the change persist?

Check

Trace has_key.

def cave(state):
    state["has_key"] = True

s = {"has_key": False}
cave(s)
print(s["has_key"])
steps['has_key']
after cave(s)?

Check your understanding

What does this print?

  • A. True (correct)
  • B. False
  • C. None
  • D. Error

Answer: A

Why: cave updates the shared dict in place with state["has_key"] = True, so s reflects the change: True. Verified by execution.

Why B tempts people
It would be False only if cave made its own local dict; here it mutates the passed-in one.
Why C tempts people
s['has_key'] holds a bool, not None.
Why D tempts people
Updating a dict key is valid - no error.

88. Check: the reassign trap

Check

This room reassigns state.

def cave(state):
    state = {"has_key": True}

s = {"has_key": False}
cave(s)
print(s["has_key"])
steps['has_key']
after cave(s)?

Check your understanding

What does this print?

  • A. False (correct)
  • B. True
  • C. None
  • D. Error

Answer: A

Why: state = {...} rebinds only the local name to a new dict; the caller's s is unchanged, so it stays False. Update in place (state["has_key"] = True) to make it stick. Verified by execution.

Why B tempts people
Reassigning the parameter does not affect the caller's dict, so s is not True.
Why C tempts people
s['has_key'] was set to False and never removed.
Why D tempts people
The code runs fine - it just does not do what was intended.

89. Check: the ending

Check

The player skipped the key.

def door(state):
    if state["has_key"]:
        return "win"
    return "lose"

print(door({"has_key": False}))
has_keyreturns
False?

Check your understanding

What does this print?

  • A. lose (correct)
  • B. win
  • C. None
  • D. Error

Answer: A

Why: has_key is False, so the if is skipped and the function returns "lose". The earlier choice (skipping the key) determined this ending. Verified by execution.

Why B tempts people
win requires has_key to be True; here it is False.
Why C tempts people
Every path returns a room name, so it is not None.
Why D tempts people
Reading a present key with state["has_key"] is valid - no error.

90. Check: the score

Check

Trace the score.

state = {"score": 0}
state["score"] += 20
state["score"] += 5
print(state["score"])
stepscore
+20?
+5?

Check your understanding

What does this print?

  • A. 25 (correct)
  • B. 20
  • C. 5
  • D. 0

Answer: A

Why: The score accumulates: 0 + 20 = 20, then + 5 = 25. Verified by execution.

Why B tempts people
20 is the score after only the first award; the second += 5 raises it to 25.
Why C tempts people
5 is only the second award, not the total.
Why D tempts people
0 is the starting value; both += lines change it.

91. Fill in: score for Check: the score

Comparison

Comparison matrix

From Check: the score: refill the score column from what you know. The rest of the table is as it appeared.

stepscore
+20?
+5?

92. Check: initialize where?

Check

Where should state be created?

# A: state = {...} above the while loop
# B: state = {...} as the first line inside the while loop
inside loopeffect
resets each pass?

Check your understanding

Where should the state dict be created?

  • A. Above the loop, so it persists (correct)
  • B. Inside the loop, at the top
  • C. Inside each room function
  • D. It does not matter

Answer: A

Why: State must be created once, above the loop, so it carries across rooms. Creating it inside the loop would reset the player's items and score every pass. Verified by reasoning about the loop.

Why B tempts people
Inside the loop, state = {...} runs every pass, wiping progress each time.
Why C tempts people
A fresh dict per room is the lost-state trap - changes would not carry over.
Why D tempts people
It matters: only initializing once, outside the loop, preserves state.

93. Connect it up: Session 14 - Capstone: Adventure Game (Part 2)

Connect it up

Draw it

One page, no notation unless you need it: draw how these connect — Tracking State · Inventory with a Dict · Decisions from State · A Score · Win and Lose Endings · The Full Game. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.

94. You built a game

Recap

You finished the capstone: state in a dict travels with the player, rooms read and update it, and the final state chooses a real win or lose ending.

Phase 1 skillUsed for
functionsone per room, plus helpers
while loopthe game engine
if / elifchoices and endings
dictthe player's state
stringscleaning input
errorshandling odd input

Initialize state once, mutate it in place, keep every ending reachable - and every room forgiving. This game is yours to keep, extend, and show off. Well done finishing Phase 1.

Sources

  1. Python 3 Tutorial - Data Structures (dict) and Control Flow
  2. All snippets executed under CPython 3.12; interactive input shown as typed values. — Author verification run, 2026-07-15 (Python Fundamentals series, Session 14).

Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108