Escape the Station
Three-hour Python session · dict · reading and writing files · a text adventure that remembers your run
Mission
Teacher provides
The room data, ASCII art, gradients, screen drawing, the command loop, and the build checks.
Student builds
Every dict lookup the map needs, the whole bag, and all three file functions.
Not a TODO
No class this week. Nothing in the drawing code or the command parser is homework.
Three-Hour Route
00:00-00:10
Open: list or dict
Ask one question only: how do you find the thing you want? By position, or by name?
00:10-00:45
Warm-Up A: six dict drills
Lookup, safe lookup, looping, dict inside dict, counting, deleting.
00:45-00:58
Play the finished game
Escape once yourself. Then point at the screen and name which part is a dict.
00:58-01:40
TODO 1-4: the map
Four small lookups. Run the checkpoint after each one before moving on.
01:40-01:50
Break
Leave the last passing checkpoint on screen.
01:50-02:15
TODO 5-8: the bag
The bag is a dict of counts. Warm-Up 6 was the rehearsal for TODO 7.
02:15-02:35
Files: r, w, a
Teach the three modes, then run Warm-Up B. Show a real file in the editor.
02:35-02:55
TODO 9-11: the save file
Save, load, append. Then quit the program and continue the same run.
02:55-03:00
Escape and one upgrade
Clear the station, then pick one upgrade and say what data it needs first.
New Idea 1 — dict
# A list: you have to know the position.crew = ["ana", "bo", "cy"]print(crew[1]) # bo# A dict: you use the name instead.deck = {"ana": 2, "bo": 1, "cy": 3}print(deck["bo"]) # 1deck["bo"] = 4 # "bo" already exists -> this CHANGES itdeck["dee"] = 2 # "dee" is new -> this ADDS itdel deck["ana"] # remove the whole pairprint("cy" in deck) # True <- in looks at the KEYSprint(len(deck)) # 3for name in deck: # a loop hands you the KEY, not the value print(name, deck[name])# bo 4# cy 3# dee 2Side by side
| Goal | list | dict |
|---|---|---|
| Make an empty one | things = [] | things = {} |
| Get one out | things[0] | things["rope"] |
| Add one | things.append("rope") | things["rope"] = 2 |
| Change one | things[0] = "rope" | things["rope"] = 5 |
| Remove one | things.remove("rope") | del things["rope"] |
| How many | len(things) | len(things) |
One line does double duty: things["rope"] = 2 adds the pair when the key is new, and changes it when the key is already there.
Three traps
KeyError
Reading a key that is not there stops the program. Check "key in dict" first.
A loop gives keys
for x in bag hands you the key, not the value. Use bag[x] to reach the value.
in checks keys
"rope" in bag asks about the keys. In a list the same words ask about the values.
Warm-Up A — dict
Try each one for two minutes before opening Hint 1. Expected output appears with the Show Solution switch at the top of the page.
Numbers or names
Predict all four printed lines without running the code. Then run it and check.
scores = [90, 75, 88]grades = {"ana": 90, "bo": 75, "cy": 88}print(scores[1])print(grades["bo"])grades["bo"] = 80grades["dee"] = 61print(len(grades))print(grades)Hint 1
Hint 2
Look up without crashing
Return the count. A name that is not in the dict must return 0 instead of crashing.
stock = {"apple": 3, "pear": 0}def how_many(store, name): # TODO passprint(how_many(stock, "apple"))print(how_many(stock, "pear"))print(how_many(stock, "plum"))Hint 1
Hint 2
Walk through a dict
Print one line per item, exactly like: bolt costs 4
prices = {"bolt": 4, "rope": 12, "lamp": 7}# TODO: print one line per itemHint 1
Hint 2
A dict inside a dict
Predict all three lines. This is exactly the shape of today's station map.
crew = { "ana": {"job": "pilot", "deck": 2}, "bo": {"job": "medic", "deck": 1},}print(crew["bo"]["job"])crew["ana"]["deck"] = 3print(crew["ana"])print("cy" in crew)Hint 1
Hint 2
Count with a dict
Count how many times each value appears. Write the loop yourself.
letters = ["a", "b", "a", "c", "a", "b"]def count_all(values): # TODO passprint(count_all(letters))Hint 1
Hint 2
Delete a key
Use one item: lower the count by 1, and remove the key completely once it hits 0.
bag = {"rope": 2, "lamp": 1}def use_one(store, name): # TODO passprint(use_one(bag, "lamp"))print(bag)print(use_one(bag, "torch"))Hint 1
Hint 2
New Idea 2 — files
# "w" creates the file, and empties it if it was already there.with open("crew.txt", "w") as crew_file: crew_file.write("ana 2\n") # write() never adds the line break for you crew_file.write("bo 1\n")# "a" keeps everything already inside and writes at the end.with open("crew.txt", "a") as crew_file: crew_file.write("cy 3\n")# "r" reads it back. One line at a time is usually easiest.with open("crew.txt", "r") as crew_file: for line in crew_file: parts = line.split() # "ana 2\n" -> ["ana", "2"] print(parts[0], "is on deck", int(parts[1]))# ana is on deck 2# bo is on deck 1# cy is on deck 3The three modes
| mode | File is missing | File already exists | Used for |
|---|---|---|---|
| "r" | FileNotFoundError | Opens it | Read |
| "w" | Creates it | Empties it immediately | Write |
| "a" | Creates it | Keeps it, writes at the end | Append |
Five rules
- 01Always use with open(...) as f:. The file closes by itself when the indent ends.
- 02write() adds nothing on its own. You type the line break yourself.
- 03read() gives the whole file as one string. for line in f: gives one line at a time.
- 04Everything read out of a file is text. Numbers need int() before you can add them.
- 05A file may not exist yet. Wrap the read in try / except FileNotFoundError.
Warm-Up B — files
Run these in a folder you can open. After every exercise, look at the real file in the editor before moving on.
Write a file, then read it back
Run it. Then open notes.txt in your editor and confirm it really exists on disk.
with open("notes.txt", "w") as note_file: note_file.write("line one\n") note_file.write("line two\n")with open("notes.txt", "r") as note_file: print(note_file.read())Hint 1
Hint 2
"w" erases, "a" continues
Predict the output. Then answer this: if you run the WHOLE file a second time, does the output change?
with open("diary.txt", "w") as diary: diary.write("day\n")with open("diary.txt", "a") as diary: diary.write("night\n")with open("diary.txt", "r") as diary: print(diary.read())Hint 1
Hint 2
Read lines back into a dict
Turn each line into one dict pair. This is exactly what load_game does later.
with open("stock.txt", "w") as stock_file: stock_file.write("rope 12\n") stock_file.write("lamp 7\n")result = {}with open("stock.txt", "r") as stock_file: for line in stock_file: parts = line.split() # TODO: put parts[0] -> int(parts[1]) into resultprint(result)Hint 1
Hint 2
When the file is missing
Return the text of the file, or None when there is no such file. Do not let it crash.
with open("stock.txt", "w") as stock_file: stock_file.write("rope 12\n")def read_or_none(path): # TODO passprint(read_or_none("stock.txt"))print(read_or_none("ghost.txt"))Hint 1
Hint 2
Play It Before Building It
Run the finished version and escape once. Then point at the screen and say which part of it is stored in a dict.
+--------------------------------------------------------------------------------+| STORAGE BAY moves 2 || || .------. .------. .------. || |######| |######| |######| || |######| |######| |######| || '------' '------' '------' || .------. .------. || |######| |######| || '------' '------' || || Crates everywhere. Something small rolls across the floor when the hull shakes. || || Exits: east, north || You see: flashlight || Bag: (empty) || || > Power Core is sealed. You need the flashlight. || || n s e w | look | take <item> | bag | x <item> | save | load | help | quit |+--------------------------------------------------------------------------------+The map is one dict
+---------------+ +---------------+ +---------------+ | Power Core | | Bridge |----------| Docking Bay | +-------+-------+ +-------+-------+ +---------------+ needs flashlight | GOAL: keycard | | +-------+-------+ +-------+-------+ +---------------+ | Storage Bay |----------| Corridor |----------| Research Lab | +---------------+ +-------+-------+ +---------------+ | +-------+-------+ | Cryo Pod | START +---------------+ Along any line: e moves right, w moves left, n moves up, s moves down. ROOMS["storage"]["exits"] -> {"east": "corridor", "north": "power"} ROOMS["storage"]["items"] -> ["flashlight"]Looked up by name
The room you are in, where each direction leads, what an item does, and how many of it you carry.
Written to disk
station_save.txt holds the current run. station_log.txt keeps the history of every run.
TODO Contract
Read the card before the code. Every function states its purpose, input, return value, state change, and smallest useful test. Complete one at a time and run its checkpoint.
room_name
Purpose
Read one value out of the big ROOMS dict.
Input
room_id: str, for example "lab"
Output / Return
That room's "name" value, or "Unknown Room" when the id is not a key.
State change
Nothing.
Checkpoint
print(room_name("lab")) # Research Labprint(room_name("attic")) # Unknown Roomexits_of
Purpose
Reach the dict that is stored INSIDE a room.
Input
room_id: str, for example "corridor"
Output / Return
That room's "exits" dict, or an empty dict when the room does not exist.
State change
Nothing.
Checkpoint
print(exits_of("pod")) # {'north': 'corridor'}print(exits_of("attic")) # {}next_room
Purpose
Answer the only question walking needs: where does this direction lead?
Input
room_id: str, direction: str such as "east"
Output / Return
The room id you arrive at, or None when there is no exit that way.
State change
Nothing. Reuse exits_of instead of writing the lookup twice.
Checkpoint
print(next_room("corridor", "east")) # labprint(next_room("pod", "east")) # Noneexit_list
Purpose
Collect the direction names so the screen can show them.
Input
room_id: str
Output / Return
A sorted list of the direction names.
State change
Nothing. Looping over a dict gives you its keys.
Checkpoint
print(exit_list("corridor")) # ['east', 'north', 'south', 'west']print(exit_list("attic")) # []add_item
Purpose
Put one item in the bag. The bag counts items instead of listing them.
Input
bag: dict, item: str such as "keycard"
Output / Return
No return value.
State change
Raise that item's count by 1. A brand new item ends up at 1.
Checkpoint
bag = {}add_item(bag, "bolt")add_item(bag, "bolt")print(bag) # {'bolt': 2}has_item
Purpose
Answer one yes-or-no question. Locked doors use this.
Input
bag: dict, item: str
Output / Return
True when the bag holds at least one, otherwise False.
State change
Nothing. Return True or False, not a number.
Checkpoint
print(has_item({"bolt": 1}, "bolt")) # Trueprint(has_item({}, "bolt")) # Falseremove_item
Purpose
Use one item up, and keep the bag tidy afterwards.
Input
bag: dict, item: str
Output / Return
True when one was removed, otherwise False.
State change
Lower the count by 1, and delete the key once the count reaches 0.
Checkpoint
bag = {"bolt": 1}print(remove_item(bag, "bolt")) # Trueprint(bag) # {}print(remove_item(bag, "bolt")) # Falsebag_lines
Purpose
Turn the bag into text the screen can print.
Input
bag: dict
Output / Return
A sorted list of strings like "rope x2". An empty bag gives ["(empty)"].
State change
Nothing.
Checkpoint
print(bag_lines({"rope": 2, "bolt": 1})) # ['bolt x1', 'rope x2']print(bag_lines({})) # ['(empty)']save_game
Purpose
Write the whole run to disk so it survives closing the program.
Input
path: str, state: dict with the keys "room", "moves", "bag"
Output / Return
No return value.
State change
Open with "w" and write one record per line. Add \n yourself.
Checkpoint
save_game("test_save.txt", {"room": "bridge", "moves": 7, "bag": {"keycard": 1}})with open("test_save.txt", "r") as check: print(check.read())# room bridge# moves 7# item keycard 1load_game
Purpose
Rebuild the state dict from the text file.
Input
path: str
Output / Return
A state dict, or None when the file does not exist.
State change
Split every line, and turn number text back into int.
Checkpoint
print(load_game("test_save.txt"))# {'room': 'bridge', 'moves': 7, 'bag': {'keycard': 1}}print(load_game("no_such_file.txt"))# Noneappend_log
Purpose
Keep a history that never erases what came before.
Input
path: str, message: str
Output / Return
No return value.
State change
Add exactly one line at the end. "w" would destroy the history, so use "a".
Checkpoint
append_log("test_log.txt", "one")append_log("test_log.txt", "two")with open("test_log.txt", "r") as check: print(check.read())# one# twoStudent Work Area
student_work.py
# ================================================================# PART 1 - THE MAP# ROOMS is a dict. Every value inside it is another dict.# ================================================================def room_name(room_id): # TODO 1 # Input: room_id, a string such as "lab" # Return: that room's "name" value; "Unknown Room" if room_id is not a key of ROOMS # Change: nothing return ""def exits_of(room_id): # TODO 2 # Input: room_id, a string such as "corridor" # Return: that room's "exits" dict; an empty dict if the room does not exist # Change: nothing return {}def next_room(room_id, direction): # TODO 3 # Input: room_id such as "corridor", direction such as "east" # Return: the room id you arrive at, or None when that direction has no exit # Change: nothing return Nonedef exit_list(room_id): # TODO 4 # Input: room_id, a string # Return: a sorted list of the direction names, for example ["east", "north"] # Change: nothing return []# ================================================================# PART 2 - THE BAG# The bag is a dict: item name -> how many you carry.# {"flashlight": 1, "battery": 2}# ================================================================def add_item(bag, item): # TODO 5 # Input: bag, a dict; item, a string such as "keycard" # Return: nothing # Change: raise that item's count by 1, starting from 0 when it is new passdef has_item(bag, item): # TODO 6 # Input: bag, a dict; item, a string # Return: True when the bag holds at least one of that item, otherwise False # Change: nothing return Falsedef remove_item(bag, item): # TODO 7 # Input: bag, a dict; item, a string # Return: True when one was removed, otherwise False # Change: lower that item's count by 1; delete the key once the count reaches 0 return Falsedef bag_lines(bag): # TODO 8 # Input: bag, a dict # Return: a sorted list of strings like "flashlight x1" # an empty bag returns ["(empty)"] # Change: nothing return []# ================================================================# PART 3 - THE SAVE FILE# The save file is plain text. One record per line:# room bridge# moves 17# item flashlight 1# ================================================================def save_game(path, state): # TODO 9 # Input: path, a file name; state, a dict with keys "room", "moves", "bag" # Return: nothing # Change: write the file in the format shown above, one record per line passdef load_game(path): # TODO 10 # Input: path, a file name # Return: a state dict {"room": ..., "moves": ..., "bag": ...}, # or None when the file does not exist # Change: nothing return Nonedef append_log(path, message): # TODO 11 # Input: path, a file name; message, a string # Return: nothing # Change: add one more line to the end of the file, keeping every older line passProject Template
Copy this whole file once so the game can run. During class, work in the Student Work Area above and search by TODO number.
escape_station.py
import osimport reimport sysfrom time import sleepESC = chr(27) + "["RESET = f"{ESC}0m"USE_COLOR = sys.stdout.isatty()ANIMATE = sys.stdout.isatty()ANSI_RE = re.compile(re.escape(ESC) + r"[0-9;]*m")SCREEN_WIDTH = 82SAVE_PATH = "station_save.txt"LOG_PATH = "station_log.txt"STYLES = { "title": "1;96", "room": "1;95", "item": "1;93", "good": "92", "bad": "91", "dim": "2", "hint": "96",}def paint(text, style_code): if not USE_COLOR: return str(text) return f"{ESC}{style_code}m{text}{RESET}"def style(text, name): return paint(text, STYLES[name])def rgb(text, red, green, blue): if not USE_COLOR: return str(text) return f"{ESC}38;2;{red};{green};{blue}m{text}{RESET}"def gradient_text(text, start=(90, 220, 255), end=(200, 110, 255)): if not USE_COLOR or len(text) <= 1: return str(text) result = "" steps = len(text) - 1 for index, character in enumerate(text): ratio = index / steps red = round(start[0] + (end[0] - start[0]) * ratio) green = round(start[1] + (end[1] - start[1]) * ratio) blue = round(start[2] + (end[2] - start[2]) * ratio) result += rgb(character, red, green, blue) return resultdef visible_len(text): return len(ANSI_RE.sub("", str(text)))def pad_visible(text, width): return str(text) + " " * max(0, width - visible_len(text))def center_visible(text, width): spaces = max(0, width - visible_len(text)) left = spaces // 2 return " " * left + str(text) + " " * (spaces - left)def clear_screen(): if sys.stdout.isatty(): print(f"{ESC}2J{ESC}H", end="")def print_panel(title, lines, width=SCREEN_WIDTH): inside = width - 4 print("+" + "-" * (width - 2) + "+") print("| " + center_visible(title, inside) + " |") print("+" + "-" * (width - 2) + "+") for line in lines: print("| " + pad_visible(line, inside) + " |") print("+" + "-" * (width - 2) + "+")def type_line(text, delay=0.012): if not ANIMATE: print(text) return for character in text: print(character, end="", flush=True) sleep(delay) print()def animate_scan(label): if not ANIMATE: return for filled in range(0, 25, 4): scan = "[" + "=" * filled + "." * (24 - filled) + "]" line = center_visible(f"{label} {scan}", SCREEN_WIDTH - 2) print("\r" + gradient_text(line, (80, 255, 200), (200, 110, 255)), end="", flush=True) sleep(0.05) print("\r" + " " * (SCREEN_WIDTH - 2) + "\r", end="", flush=True)ROOMS = { "pod": { "name": "Cryo Pod", "description": "Frost slides off the glass. One amber lamp blinks above your head.", "exits": {"north": "corridor"}, "items": [], "art": [ "+----------------------+", "| .-. .-. |", "| '-' '-' |", "| |", "| [==========] |", "| |", "+----------------------+", ], }, "corridor": { "name": "Main Corridor", "description": "A long hallway. Emergency strips glow along the floor in both directions.", "exits": {"south": "pod", "east": "lab", "west": "storage", "north": "bridge"}, "items": [], "art": [ " ____________________", "| || || || || ||", "| || || || || ||", "|__||__||__||__||__||", " . . . . . . .", ], }, "lab": { "name": "Research Lab", "description": "Broken sample jars. A cracked terminal still shows a slow star chart.", "exits": {"west": "corridor"}, "items": ["notebook"], "art": [ ".--------. .--------.", "| o . o| | .--. |", "| . o .| | ( oo ) |", "'--------' | '--' |", " | | '--------'", "__|____|________________", ], }, "storage": { "name": "Storage Bay", "description": "Crates everywhere. Something small rolls across the floor when the hull shakes.", "exits": {"east": "corridor", "north": "power"}, "items": ["flashlight"], "art": [ ".------. .------. .------.", "|######| |######| |######|", "|######| |######| |######|", "'------' '------' '------'", ".------. .------.", "|######| |######|", "'------' '------'", ], }, "power": { "name": "Power Core", "description": "Pitch black without a light. Far below, the reactor is still turning.", "exits": {"south": "storage"}, "items": ["keycard"], "needs": "flashlight", "art": [ " .-''''''''''-. ", " .' .--------. '. ", " / | /\\ /\\ | \\ ", " | | \\ \\/ / | | ", " \\ | \\ / | / ", " '. '--------' .' ", " '-..........-' ", ], }, "bridge": { "name": "Bridge", "description": "Wide windows. Outside, the Starfall wreck drifts in slow circles.", "exits": {"south": "corridor", "east": "dock"}, "items": [], "art": [ " ________________________ ", "/ . * . * \\", "| * . * . |", "| . * * |", "\\__ * . . __/", " |__| |__| |__| ", ], }, "dock": { "name": "Docking Bay", "description": "One escape pod is still attached. The hatch light turns green as you enter.", "exits": {"west": "bridge"}, "items": [], "needs": "keycard", "art": [ " .------------. ", " .-' '-. ", " / .----------. \\ ", " | | ( ) | | ", " \\ '----------' / ", " '-. .-' ", " | | | | ", ], },}ITEM_INFO = { "flashlight": "A heavy work light. Bright enough to walk into a dark room.", "keycard": "Crew keycard, level 3. Sealed doors accept it.", "notebook": "A crew notebook. The last page reads: the light is stored west of the corridor.",}START_ROOM = "pod"GOAL_ROOM = "dock"# ================================================================# PART 1 - THE MAP# ROOMS is a dict. Every value inside it is another dict.# ================================================================def room_name(room_id): # TODO 1 # Input: room_id, a string such as "lab" # Return: that room's "name" value; "Unknown Room" if room_id is not a key of ROOMS # Change: nothing return ""def exits_of(room_id): # TODO 2 # Input: room_id, a string such as "corridor" # Return: that room's "exits" dict; an empty dict if the room does not exist # Change: nothing return {}def next_room(room_id, direction): # TODO 3 # Input: room_id such as "corridor", direction such as "east" # Return: the room id you arrive at, or None when that direction has no exit # Change: nothing return Nonedef exit_list(room_id): # TODO 4 # Input: room_id, a string # Return: a sorted list of the direction names, for example ["east", "north"] # Change: nothing return []# ================================================================# PART 2 - THE BAG# The bag is a dict: item name -> how many you carry.# {"flashlight": 1, "battery": 2}# ================================================================def add_item(bag, item): # TODO 5 # Input: bag, a dict; item, a string such as "keycard" # Return: nothing # Change: raise that item's count by 1, starting from 0 when it is new passdef has_item(bag, item): # TODO 6 # Input: bag, a dict; item, a string # Return: True when the bag holds at least one of that item, otherwise False # Change: nothing return Falsedef remove_item(bag, item): # TODO 7 # Input: bag, a dict; item, a string # Return: True when one was removed, otherwise False # Change: lower that item's count by 1; delete the key once the count reaches 0 return Falsedef bag_lines(bag): # TODO 8 # Input: bag, a dict # Return: a sorted list of strings like "flashlight x1" # an empty bag returns ["(empty)"] # Change: nothing return []# ================================================================# PART 3 - THE SAVE FILE# The save file is plain text. One record per line:# room bridge# moves 17# item flashlight 1# ================================================================def save_game(path, state): # TODO 9 # Input: path, a file name; state, a dict with keys "room", "moves", "bag" # Return: nothing # Change: write the file in the format shown above, one record per line passdef load_game(path): # TODO 10 # Input: path, a file name # Return: a state dict {"room": ..., "moves": ..., "bag": ...}, # or None when the file does not exist # Change: nothing return Nonedef append_log(path, message): # TODO 11 # Input: path, a file name; message, a string # Return: nothing # Change: add one more line to the end of the file, keeping every older line passDIRECTION_WORDS = { "n": "north", "north": "north", "s": "south", "south": "south", "e": "east", "east": "east", "w": "west", "west": "west",}HELP_LINES = [ "n / s / e / w walk in that direction", "look describe this room again", "take <item> pick something up", "bag list what you carry", "x <item> inspect an item you carry", "save / load write or read station_save.txt", "help / quit",]def new_state(): return {"room": START_ROOM, "moves": 0, "bag": {}}def items_here(state): visible = [] for item in ROOMS[state["room"]]["items"]: if not has_item(state["bag"], item): visible.append(item) return visibledef render(state, messages): clear_screen() room = ROOMS[state["room"]] inside = SCREEN_WIDTH - 4 header = style(room_name(state["room"]).upper(), "room") counter = style("moves " + str(state["moves"]), "dim") lines = [header + " " + counter, ""] art_width = 0 for art_line in room["art"]: art_width = max(art_width, len(art_line)) for art_line in room["art"]: padded = art_line + " " * (art_width - len(art_line)) lines.append(center_visible(gradient_text(padded), inside)) lines.append("") lines.append(room["description"]) lines.append("") exits = exit_list(state["room"]) if len(exits) == 0: lines.append(style("Exits: none", "dim")) else: lines.append("Exits: " + style(", ".join(exits), "hint")) here = items_here(state) if len(here) > 0: lines.append("You see: " + style(", ".join(here), "item")) lines.append("Bag: " + ", ".join(bag_lines(state["bag"]))) lines.append("") for message in messages: lines.append("> " + message) lines.append("") lines.append(style("n s e w | look | take <item> | bag | x <item> | save | load | help | quit", "dim")) print_panel(gradient_text("ESCAPE THE STATION"), lines)def try_move(state, direction): destination = next_room(state["room"], direction) if destination is None: return "There is no way " + direction + " from here." needed = ROOMS[destination].get("needs") if needed is not None and not has_item(state["bag"], needed): return room_name(destination) + " is sealed. You need the " + needed + "." animate_scan("MOVING " + direction.upper()) state["room"] = destination state["moves"] = state["moves"] + 1 return "You walk " + direction + " into the " + room_name(destination) + "."def try_take(state, item): if item == "": return "Take what? Try: take flashlight" if item not in items_here(state): return "There is no " + item + " here." add_item(state["bag"], item) append_log(LOG_PATH, "picked up " + item + " in " + room_name(state["room"])) return "You take the " + item + "."def try_inspect(state, item): if item == "": return "Inspect what? Try: x notebook" if not has_item(state["bag"], item): return "You are not carrying a " + item + "." if item not in ITEM_INFO: return "You turn it over and learn nothing." return ITEM_INFO[item]def handle_command(state, raw): parts = raw.strip().lower().split() if len(parts) == 0: return "Type a command. Type help to see the list.", False word = parts[0] rest = " ".join(parts[1:]) if word in DIRECTION_WORDS: return try_move(state, DIRECTION_WORDS[word]), False if word == "go": if rest in DIRECTION_WORDS: return try_move(state, DIRECTION_WORDS[rest]), False return "Go where? Try: go north", False if word == "look": return ROOMS[state["room"]]["description"], False if word in ("take", "t", "get"): return try_take(state, rest), False if word in ("bag", "i", "inventory"): return "You carry: " + ", ".join(bag_lines(state["bag"])), False if word in ("x", "inspect", "read"): return try_inspect(state, rest), False if word == "save": save_game(SAVE_PATH, state) append_log(LOG_PATH, "saved at " + room_name(state["room"])) return style("Progress written to " + SAVE_PATH + ".", "good"), False if word == "load": loaded = load_game(SAVE_PATH) if loaded is None: return style("No save file yet. Use save first.", "bad"), False state["room"] = loaded["room"] state["moves"] = loaded["moves"] state["bag"] = loaded["bag"] return style("Save file restored.", "good"), False if word in ("help", "h", "?"): return " | ".join(["n s e w", "look", "take", "bag", "x", "save", "load", "quit"]), False if word in ("quit", "q", "exit"): return "", True return "Unknown command: " + word + ". Type help.", Falsedef intro(): clear_screen() print_panel( gradient_text("ESCAPE THE STATION"), [ "", center_visible("The Starfall station lost power eleven hours ago.", SCREEN_WIDTH - 4), center_visible("You are the only pod that thawed.", SCREEN_WIDTH - 4), "", center_visible(style("Reach the Docking Bay to escape.", "hint"), SCREEN_WIDTH - 4), "", ] + HELP_LINES, ) type_line("") type_line(" Booting life support ...") animate_scan("LIFE SUPPORT")def show_result(state, escaped): clear_screen() if escaped: heading = style("ESCAPE SUCCESSFUL", "good") message = "The pod detaches. The station falls away behind you." else: heading = style("RUN ENDED", "bad") message = "You stayed aboard. The station is still dark." print_panel( heading, [ message, "", "Rooms reached: " + room_name(state["room"]), "Moves used: " + str(state["moves"]), "Carried out: " + ", ".join(bag_lines(state["bag"])), ], )def play(state): messages = ["You wake up. The pod hatch is already open."] while True: render(state, messages) if state["room"] == GOAL_ROOM: append_log(LOG_PATH, "escaped in " + str(state["moves"]) + " moves") return True try: raw = input("Command: ") except EOFError: return False message, should_quit = handle_command(state, raw) if should_quit: return False messages = [message]def run_build_checks(): assert room_name("lab") == "Research Lab", ( "TODO 1 failed: room_name should read the 'name' value out of ROOMS." ) assert room_name("nowhere") == "Unknown Room", ( "TODO 1 failed: an id that is not a key must give 'Unknown Room'." ) assert exits_of("pod") == {"north": "corridor"}, ( "TODO 2 failed: exits_of should return the inner 'exits' dict." ) assert exits_of("nowhere") == {}, ( "TODO 2 failed: a missing room must give an empty dict." ) assert next_room("corridor", "east") == "lab", ( "TODO 3 failed: look the direction up inside the exits dict." ) assert next_room("pod", "east") is None, ( "TODO 3 failed: a direction with no exit must give None." ) assert exit_list("corridor") == ["east", "north", "south", "west"], ( "TODO 4 failed: collect the keys of the exits dict and sort them." ) assert exit_list("nowhere") == [], ( "TODO 4 failed: a missing room must give an empty list." ) bag = {} add_item(bag, "bolt") add_item(bag, "bolt") add_item(bag, "rope") assert bag == {"bolt": 2, "rope": 1}, ( "TODO 5 failed: a new item starts at 1, a repeat item counts up." ) assert has_item(bag, "bolt") is True, ( "TODO 6 failed: has_item must return True or False, not a number." ) assert has_item(bag, "torch") is False, ( "TODO 6 failed: an item that is not a key must give False." ) assert remove_item(bag, "bolt") is True and bag["bolt"] == 1, ( "TODO 7 failed: removing one should lower the count by 1." ) assert remove_item(bag, "bolt") is True and "bolt" not in bag, ( "TODO 7 failed: the key must be deleted once the count reaches 0." ) assert remove_item(bag, "bolt") is False, ( "TODO 7 failed: removing something you do not carry must give False." ) assert bag_lines({}) == ["(empty)"], ( "TODO 8 failed: an empty bag must give ['(empty)']." ) assert bag_lines({"rope": 2, "bolt": 1}) == ["bolt x1", "rope x2"], ( "TODO 8 failed: build 'name xcount' strings in sorted order." ) check_save = "station_check.txt" check_log = "station_check_log.txt" for path in (check_save, check_log): if os.path.exists(path): os.remove(path) assert load_game(check_save) is None, ( "TODO 10 failed: a file that does not exist must give None." ) sample = {"room": "bridge", "moves": 7, "bag": {"keycard": 1, "bolt": 2}} save_game(check_save, sample) assert load_game(check_save) == sample, ( "TODO 9 or 10 failed: what you saved must load back exactly." ) append_log(check_log, "one") append_log(check_log, "two") with open(check_log, "r") as check_file: assert check_file.read() == "one\ntwo\n", ( "TODO 11 failed: append mode must keep the older lines." ) for path in (check_save, check_log): os.remove(path)def main(): try: run_build_checks() except (AssertionError, AttributeError, TypeError, KeyError, IndexError) as error: detail = str(error) if str(error) else "A required result was incorrect." print_panel( "BUILD CHECK FAILED", [ "Finish TODO 1-11 before the station will boot.", "", detail, "", "Fix one TODO, then run the file again.", ], ) return intro() state = new_state() saved = load_game(SAVE_PATH) if saved is not None: answer = input("A save file was found. Continue it? [y/N]: ").strip().lower() if answer in ("y", "yes"): state = saved escaped = play(state) show_result(state, escaped)if __name__ == "__main__": main()Run Checklist
Start the game
python3 escape_station.py# The build check runs first. Only after TODO 1-11 pass does the station boot.# Useful commands during play:# n s e w walk# look describe this room again# take X pick something up# bag list what you carry# x X inspect an item you carry# save / load station_save.txt# help / quitOptional Upgrades
Choose exactly one. Before writing any code, say out loud which dict you are about to change.
A map command
Print every room name and its exits by looping over ROOMS once.
A drop command
Put one item back. remove_item already does the hard part.
One more room
Add a dict entry, and remember to add the exit on BOTH sides of the door.
A door needing two items
Change "needs" from one string into a list, then check every item in it.
Teacher Checkpoints
After TODO 2: ask what type ROOMS["pod"] is, and what type ROOMS["pod"]["exits"] is. Both are dicts.
After TODO 4: ask why we sort. Without sorted() the screen order depends on how the dict was typed.
After TODO 7: ask what the bag prints when the key is deleted, and when a 0 is left behind.
After TODO 9: open station_save.txt in the editor, change moves by hand, then load it in the game.
At the end: the map is a dict and the bag is a dict. Ask the student to say what each one looks up.