LEARN PYTHON | Project: A Modular Text Adventure

Lesson objective

Build and test a three-module command-line adventure with fresh world data, explicit player state and reusable action functions.

Learn

04.6 | PROJECT: A MODULAR TEXT ADVENTURE

01 | DEFINE THE GAME BEFORE THE CODE

Download the three-file Python project

The player begins in a hall. The lab is east and contains a key. The observatory is north of the hall but locked until the player holds the key. The goal is to collect it and enter the observatory. A player can quit without winning.

The interface accepts look, help, inventory, go followed by a direction, take followed by an item and quit. Blank text and unknown commands receive feedback. An invalid move or unavailable item must not change the state.

These requirements give you tests before implementation. A winning route is go east, take key, go west, go north. Attempting go north first must leave the player in the hall. Taking the same key twice must not duplicate it.

02 | DIVIDE THE RESPONSIBILITIES

world.py creates the authored room data. engine.py contains parsing and action rules. adventure.py owns input, printed feedback and the main loop. Keep all three files in the same folder and run adventure.py.

The application imports the other two modules. The engine does not import the application or secretly read a player variable from it. The application passes world and state explicitly.

This is deliberately a small design using functions and dictionaries, not classes or a web framework. It uses the collection and control-flow skills you have learned. You can add rooms without copying the entire command loop.

FileResponsibilityExample contract
world.pyFresh story datanew_world returns independent room records
engine.pyReusable action rulesmove changes location only for allowed movement
adventure.pyConsole interactionrun_game returns the final player state

03 | CREATE FRESH WORLD DATA

Each room has a description, an exits dictionary and an items list. An exit maps a direction to another room’s key. The keys hall, lab and observatory identify rooms; they are not positions in a list.

new_world returns fresh nested collections each time it is called. Taking the key mutates the current world’s lab item list. A later game must start with a fresh key, so reusing one mutated global world would be a bug.

Save the code below as world.py. The descriptions are authored text. The lab description mentions the original key even after it is taken, while the separate Items line reports the current contents. Improve that wording if you want dynamic narrative.

def new_world():
    """Create fresh room data for one game."""
    return {
        "hall": {"description": "You are in the hall. A locked observatory is north.",
                 "exits": {"east": "lab", "north": "observatory"}, "items": []},
        "lab": {"description": "A brass key rests in the robotics lab.",
                "exits": {"west": "hall"}, "items": ["key"]},
        "observatory": {"description": "The telescope opens a view of the stars.",
                        "exits": {"south": "hall"}, "items": []}
    }

04 | TRACK STATE AND PARSE COMMANDS

State is the data describing the current game situation. Here it is a dictionary with room and inventory. The room key identifies the current location; the inventory list holds collected item names.

parse_command strips outer whitespace, converts to lowercase and splits at most once. GO EAST becomes action go and argument east. Empty input returns two empty strings. Normalising commands does not normalise arbitrary story text.

The argument may still contain spaces. go east west is therefore not silently treated as go east: the whole argument east west fails the exits lookup. This makes the accepted command grammar clear.

def parse_command(text):
    parts = text.strip().lower().split(maxsplit=1)
    if not parts:
        return "", ""
    action = parts[0]
    argument = parts[1].strip() if len(parts) == 2 else ""
    return action, argument

print(parse_command("  GO EAST  "))
print(parse_command(""))

EXPECTED OUTPUT

('go', 'east')
('', '')

05 | CHECK BEFORE CHANGING DATA

move first checks whether the direction exists. It then checks the lock rule. Only after both checks does it assign the new room. Returning a failure message before mutation keeps state consistent.

take_item checks that the item is currently present before removing it and appending it to inventory. A repeated take key fails because the first successful take removed the key from the room. The authored world assumes distinct item names within this small scenario.

These helpers return feedback for the interface to print. They deliberately mutate world or state where their contracts say so. describe_room reads current data and returns a message without changing the game.

def parse_command(text):
    """Return a lowercase action and optional argument."""
    parts = text.strip().lower().split(maxsplit=1)
    if not parts:
        return "", ""
    action = parts[0]
    argument = parts[1].strip() if len(parts) == 2 else ""
    return action, argument


def describe_room(world, state):
    room = world[state["room"]]
    exits = ", ".join(sorted(room["exits"]))
    items = ", ".join(room["items"]) or "none"
    return f"{room['description']}\nExits: {exits}\nItems: {items}"


def move(world, state, direction):
    """Mutate location only after a valid, unlocked move."""
    exits = world[state["room"]]["exits"]
    if direction not in exits:
        return "You cannot go that way."
    destination = exits[direction]
    if destination == "observatory" and "key" not in state["inventory"]:
        return "The observatory is locked. Find the key."
    state["room"] = destination
    return describe_room(world, state)


def take_item(world, state, item):
    """Transfer a present room item into the player's inventory."""
    items = world[state["room"]]["items"]
    if item not in items:
        return "That item is not here."
    items.remove(item)
    state["inventory"].append(item)
    return f"Taken: {item}"


def has_won(state):
    return state["room"] == "observatory" and "key" in state["inventory"]

06 | KEEP THE INTERFACE IN THE APPLICATION

run_game creates fresh world and state, then repeats until quit or success. Each iteration parses one response and dispatches it to the matching action. Dispatch means selecting the code responsible for that command.

The command checks require no argument for help, look, inventory and quit. quit now is therefore rejected rather than treated as a silent exit. go and take pass their argument to the action rules; absent or unsuitable arguments receive the relevant failure message.

has_won checks the goal after processing an ordinary command. The winning move prints its room description, then the completion message. Returning state ends the loop and leaves a result that development checks can inspect. Save this file as adventure.py.

from world import new_world
from engine import parse_command, describe_room, move, take_item, has_won


def run_game():
    world = new_world()
    state = {"room": "hall", "inventory": []}
    print("Reach the observatory. Type help for commands.")
    print(describe_room(world, state))
    while True:
        action, argument = parse_command(input("Command: "))
        if action == "quit" and not argument:
            print("Thanks for exploring.")
            return state
        if action == "help" and not argument:
            print("look | go east/west/north/south | take key | inventory | quit")
        elif action == "look" and not argument:
            print(describe_room(world, state))
        elif action == "inventory" and not argument:
            print("Inventory:", ", ".join(state["inventory"]) or "empty")
        elif action == "go":
            print(move(world, state, argument))
        elif action == "take":
            print(take_item(world, state, argument))
        else:
            print("Unknown or incomplete command. Type help.")
        if has_won(state):
            print("You unlock the observatory. Adventure complete!")
            return state


if __name__ == "__main__":
    run_game()

07 | RUN AND TRACE A WINNING ROUTE

Open a terminal in the project folder and run python adventure.py, or use your editor’s Run Python File command. On systems configured with python3 instead of python, use python3 adventure.py. No third-party package installation is required.

First try go north: the lock message should appear and location remain hall. Then go east, take key, go west and go north. At the end, state has room observatory and inventory ["key"].

The key stays in inventory; this game does not consume it to unlock a persistent door. The lock rule simply requires carrying it. Write requirements before changing to a consumed-key design, because the current winning check also tests possession.

08 | TRY THE FIXED BROWSER MODEL

This browser model follows the same room and action rules. It does not execute the downloadable Python or save progress. Restart restores a fresh lab key and an empty inventory. Read the response and current state after each command.

09 | TEST ACTIONS WITHOUT TYPING A WHOLE GAME

The helpers can be called directly in a test file saved beside the modules. This checks the important state changes without running input(). Each test sequence begins with fresh world and player data.

Assertions below check the lock, a successful transfer, the failure of a repeated take and the winning destination. They are development evidence rather than validation for an untrusted external world. The engine assumes the authored room keys and record fields exist.

Add a fresh-state test by calling new_world twice, taking a key in the first world and verifying that the second still contains its key. This exposes unintended sharing between separate sessions.

from world import new_world
from engine import move, take_item, has_won

world = new_world()
state = {"room": "hall", "inventory": []}
move(world, state, "north")
assert state["room"] == "hall"
move(world, state, "east")
assert take_item(world, state, "key") == "Taken: key"
assert take_item(world, state, "key") == "That item is not here."
assert state["inventory"] == ["key"]
move(world, state, "west")
move(world, state, "north")
assert has_won(state)
print("Adventure checks passed")

EXPECTED OUTPUT

Adventure checks passed

10 | EXTEND ONE REQUIREMENT AT A TIME

Add a room with a return exit, then test both directions. Add a second collectible only after deciding whether item names must be unique and what repeated collection should do. Extend help when the accepted commands change.

If you add a puzzle, give its state a clear home and define the conditions under which it is solved. Avoid putting story-specific decisions in every branch of the application loop. A new action helper can own its rules.

A recursive game loop is unnecessary here. Repeated player commands fit iteration, and recursion would accumulate waiting calls. Reuse the previous lesson’s reasoning to choose a loop deliberately.

11 | HAND OVER A COMPLETE PROJECT

Give a partner the three files together, the startup command, a command list and the goal. Ask them to test an invalid direction, a missing item, repeated take, immediate quit and the winning route without coaching.

The project has no saved progress, user accounts, networking or arbitrary code execution. Room data is trusted authored data. If you later load it from files, validation and persistence become new responsibilities rather than automatic benefits of using modules.

Document how data flows between files. A successful modular design is one where a reader can find story content, action rules and interface behaviour without tracing the entire program.

PUT YOUR SKILLS TO WORK

Use the Section 4 challenges to practise these ideas independently. Work in your Python editor, predict results and keep a short test record.

Section 4 challenges Python course navigation

MATCH THE PYTHON TERM TO ITS MEANING

Terminology

Terminology

Game state

Data describing the current situation of a game.

World data

Authored room, exit and item records.

Room key

An identifier used to access a room record.

Exit mapping

A dictionary connecting a direction to a destination room.

Inventory

The collection of items carried by the player.

Command parser

Code separating input into an action and argument.

Normalisation

Putting command text into a consistent format.

Dispatch

Selecting the handler for an accepted action.

Action helper

A reusable function implementing a game operation.

Guard condition

A check preventing an unsuitable operation before it occurs.

State transition

A change from one game situation to another.

Mutation

Changing an existing state or world object.

Fresh world

Independent room collections created for a new session.

Win condition

A predicate identifying the required successful state.

Entry point

The application file starting the interactive game.

Contract

The inputs, effects and returned result of a helper.

Regression check

Repeating earlier tests after a change.

Persistence

Keeping data beyond the current process or session.

Questions

Questions

CHECK YOUR UNDERSTANDING

Select all correct choices. Each exact set earns one point.

1. Which module owns the console loop?
2. Why does new_world return fresh nested collections?
3. What should a locked north move from the hall do?
4. What does parse_command(" GO EAST ") return?
5. Why does taking the key twice fail on the second attempt?
6. Which sequence wins from fresh state?
7. Which assumptions and limits are stated?
8. Why use a loop for repeated player commands?

EXPLAIN AND COMPARE

Write your answers before revealing each sample.

1. Explain the three-module design.

2. Describe the player state.

3. Explain a failed locked move.

4. Trace collecting the key.

5. Explain fresh-world testing.

6. Construct a winning route.

7. Suggest a room extension and checks.

8. Evaluate the project’s limits.

CODE IT | YOUR INDEPENDENT CHALLENGE

Build the three-file adventure. Test invalid moves, repeated collection, fresh sessions, immediate quit and the winning route.

Work in your Python editor. Keep a working version, explain one design choice and record at least two tests. Use the Learn tab for worked examples before comparing your result.

Flashcards

Flashcards

Click to flip. Select the ideas you need to revisit.

0 cards selected for revision.

    Selections are kept while this page is open.

    Workbook

    Workbook

    COMING SOON

    The workbook for Project: A Modular Text Adventure is coming soon. Complete the matching task and written questions in your exercise book.