Capstone 2: A text adventure with classes¶
~4 hours ยท prerequisite: Parts 1 to 4 and Part 7 (classes, inheritance, and dunder methods)
Goal¶
Build a small text adventure. The player types commands such as go north, take lamp, and inventory. The world has four rooms, a key, and a locked vault. The player wins by reaching the vault. Use classes to model the parts of the world, so that adding a room or item means adding data rather than new logic.
Requirements and acceptance checks¶
Run each check in a folder that holds text_adventure.py. The > prompt appears before replies, so look for the text after it.
| # | Requirement | Acceptance check |
|---|---|---|
| 1 | The program defines Room, Item, Key, Player, and Game classes. Key inherits from Item. |
grep -n "^class" text_adventure.py lists all five names, and Key(Item) appears. |
| 2 | The game starts in the hall, and look prints the room name, the description, the sorted exits, and the items. |
The first output lines are Hall, the description, and Exits: east, north. |
| 3 | go DIRECTION moves through an exit. Otherwise it prints You cannot go that way. |
go south in the hall prints You cannot go that way. |
| 4 | The door from the library to the vault is locked until the player carries the brass key. | go north then go east prints The door is locked. |
| 5 | take NAME moves an item into the inventory. A missing item prints There is no NAME here. |
take lamp in the hall prints There is no lamp here. |
| 6 | inventory lists the carried items, or prints You carry nothing. |
A new game prints You carry nothing. |
| 7 | drop NAME moves an item back into the room. |
In the kitchen, after take brass key, drop brass key prints Dropped: brass key. |
| 8 | Entering the vault prints You step into the vault. You win! and ends the game. |
The walkthrough below ends with that line. |
| 9 | quit prints Goodbye.. End of input ends the game without a traceback. |
printf 'quit\n' \| python3 -I text_adventure.py ends with Goodbye. |
| 10 | An unknown command prints I do not understand 'NAME'. |
fly prints I do not understand 'fly'. |
| 11 | At least one class defines a dunder method, such as __repr__. |
grep -n "__repr__\|__str__" text_adventure.py shows one. |
Suggested file layout¶
Keep the world in one function, such as build_world, so you can see every room in one place.
Milestones¶
- Rooms as data. Write a
Roomclass with a name, a description, anexitsdictionary, and a list of items. Hint: the dictionary maps a direction to the name of the room it leads to. - Look. Print the name, the description, and
sorted(room.exits). Hint:", ".join(...)turns a list of strings into one line. - Moving. Store the player's room name and look it up in a dictionary of rooms. Hint: check
direction in room.exitsbefore you change anything. - Items. Take and drop by moving an object from one list to another. Hint:
list.removeandlist.appenddo the moving, and a loop finds the item by name. - A subclass. Write
class Key(Item)and override one method, such asdescribe. Hint:super().__init__(...)runs the parent setup when you add fields. - Locks. Store the key name on the room, and check the player's inventory before moving. Hint:
room.locks.get(direction)returnsNonewhen there is no lock. - Winning and stopping. Use a
runningflag. Set it toFalseon the win, onquit, and at end of input. Hint: the loop iswhile self.running:. - Input. Catch
EOFErroraroundinputso a closed input does not print a traceback.
Check it¶
Run this walkthrough from the start:
$ printf 'go east\ntake brass key\ninventory\ngo west\ngo north\ngo east\nlook\n' | python3 -I text_adventure.py
Hall
A dusty hall with a staircase going nowhere.
Exits: east, north
> Kitchen
Cold ovens line the walls.
Exits: west
You see: brass key (a key)
> Taken: brass key.
> You carry: brass key
> Hall
A dusty hall with a staircase going nowhere.
Exits: east, north
> Library
Shelves of books reach the ceiling.
Exits: east, south
You see: lamp
> You step into the vault. You win!
Reference solution¶
Try the milestones first. Open this only after you have a version of your own, or when you are stuck on a milestone.
Reference solution
class Item: def init(self, name, description): self.name = name self.description = description
def describe(self):
return self.name
def __repr__(self):
return f"Item({self.name!r})"
class Key(Item): def describe(self): return f"{self.name} (a key)"
class Room: def init(self, name, description): self.name = name self.description = description self.exits = {} # direction -> name of the room it leads to self.locks = {} # direction -> name of the key that opens it self.items = []
class Player: def init(self, room_name): self.room_name = room_name self.inventory = []
def has(self, item_name):
return any(item.name == item_name for item in self.inventory)
def build_world(): hall = Room("hall", "A dusty hall with a staircase going nowhere.") hall.exits = {"north": "library", "east": "kitchen"}
kitchen = Room("kitchen", "Cold ovens line the walls.")
kitchen.exits = {"west": "hall"}
kitchen.items = [Key("brass key", "A small key, worn smooth.")]
library = Room("library", "Shelves of books reach the ceiling.")
library.exits = {"south": "hall", "east": "vault"}
library.locks = {"east": "brass key"}
library.items = [Item("lamp", "A brass oil lamp.")]
vault = Room("vault", "Gold everywhere.")
vault.exits = {"west": "library"}
return {room.name: room for room in (hall, kitchen, library, vault)}
class Game: def init(self): self.rooms = build_world() self.player = Player("hall") self.running = True
def current(self):
return self.rooms[self.player.room_name]
def look(self):
room = self.current()
print(room.name.capitalize())
print(room.description)
print("Exits: " + ", ".join(sorted(room.exits)))
if room.items:
print("You see: " + ", ".join(item.describe() for item in room.items))
def go(self, direction):
room = self.current()
if direction not in room.exits:
print("You cannot go that way.")
return
key_name = room.locks.get(direction)
if key_name and not self.player.has(key_name):
print("The door is locked.")
return
self.player.room_name = room.exits[direction]
if self.player.room_name == "vault":
print("You step into the vault. You win!")
self.running = False
else:
self.look()
def take(self, name):
room = self.current()
for item in room.items:
if item.name == name:
room.items.remove(item)
self.player.inventory.append(item)
print(f"Taken: {name}.")
return
print(f"There is no {name} here.")
def drop(self, name):
for item in self.player.inventory:
if item.name == name:
self.player.inventory.remove(item)
self.current().items.append(item)
print(f"Dropped: {name}.")
return
print(f"You do not have {name}.")
def show_inventory(self):
if not self.player.inventory:
print("You carry nothing.")
else:
print("You carry: " + ", ".join(item.name for item in self.player.inventory))
def handle(self, line):
words = line.split()
if not words:
return
verb, rest = words[0], " ".join(words[1:])
if verb == "look":
self.look()
elif verb == "go" and rest:
self.go(rest)
elif verb == "take" and rest:
self.take(rest)
elif verb == "drop" and rest:
self.drop(rest)
elif verb == "inventory":
self.show_inventory()
elif verb == "quit":
print("Goodbye.")
self.running = False
else:
print(f"I do not understand '{line.strip()}'.")
def run(self):
self.look()
while self.running:
try:
line = input("> ")
except EOFError:
print()
break
self.handle(line)
if name == "main": Game().run()