Skip to content

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

text-adventure/
  text_adventure.py    # the whole game

Keep the world in one function, such as build_world, so you can see every room in one place.

Milestones

  1. Rooms as data. Write a Room class with a name, a description, an exits dictionary, and a list of items. Hint: the dictionary maps a direction to the name of the room it leads to.
  2. Look. Print the name, the description, and sorted(room.exits). Hint: ", ".join(...) turns a list of strings into one line.
  3. Moving. Store the player's room name and look it up in a dictionary of rooms. Hint: check direction in room.exits before you change anything.
  4. Items. Take and drop by moving an object from one list to another. Hint: list.remove and list.append do the moving, and a loop finds the item by name.
  5. A subclass. Write class Key(Item) and override one method, such as describe. Hint: super().__init__(...) runs the parent setup when you add fields.
  6. Locks. Store the key name on the room, and check the player's inventory before moving. Hint: room.locks.get(direction) returns None when there is no lock.
  7. Winning and stopping. Use a running flag. Set it to False on the win, on quit, and at end of input. Hint: the loop is while self.running:.
  8. Input. Catch EOFError around input so 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()

Sources