Skip to content
Seriously SQL
Go back

Building a Text Adventure Engine in Python — Part 1

Forest Of Echoes

Where This Started

I grew up in the 80s with no games consoles and only 5 TV channels to choose from. This is when I discovered choose-your-own-adventure books. You took on the role of an adventurer, picked your own route, rolled a dice to simulate fighting, and tracked stamina, skill, luck and inventory on a notepad. I even designed a hand-drawn form with boxes for each stat. This was my first taste of interactive fiction.

Then I finally got my hands on the Sinclair ZX Spectrum along with a text adventure called The Planet of Death and the obligatory Meteor Storm, which was frantic arcade fun. I loved the Planet of Death, but the game that really changed things for me was The Hobbit. It was extraordinary for its time, a proper parser that felt like it actually understood the instructions that I was giving it. I would type a command and the game would talk back, and somehow that felt more immersive than anything with sprites.

Level 9 Computing made some of the best text adventures on the Spectrum. Snowball, Adventure Quest and Price of Magik were proper sprawling games that punched well above what you expected from the hardware. Valhalla was something else entirely, an RPG with characters that wandered around doing their own thing, which felt almost miraculous at the time. Manic Miner and Jet Set Willy were frantic, funny, technically impressive, and the Dizzy series had proper charm. But it was always the text adventures that stuck with me.

I hadn’t thought about this stuff in years. Then I started messing around with Python, using it for automating some other tasks, and I ended up down a rabbit hole. Could I actually build one of those old text adventure engines using Python?

What a Text Adventure Actually Is

Before I wrote any code I spent some time thinking about what these games actually do under the hood. Strip away the nostalgia and it’s surprisingly simple:

  1. Describe where you are
  2. Accept a command from the player
  3. Figure out what they meant
  4. Change the world accordingly
  5. Describe what happened
  6. Go to 1

That’s it, a very simple game loop. The interesting challenges are in steps 2 and 3 which is understanding what the player typed, and of course, building a world that’s interesting enough to be worth exploring.

The original Infocom games (Zork, Hitchhiker’s Guide to the Galaxy, etc.) had surprisingly sophisticated parsers for their time. They could handle sentences like “put the lamp on the shelf then go north” and understand both parts. That’s not trivial. Modern Python gives us proper NLP libraries that make this kind of thing much more achievable.

Figuring Out the Architecture First

My instinct was to just start writing code. I resisted it, which was the right call.

I spent an evening just sketching out what components I’d need and how they’d talk to each other. Coming from a database background, I think in terms of data and operations on that data, which actually turned out to be a useful frame.

Here’s what I landed on:

input → Parser → Intent → Dispatcher → Result → output

                           GameWorld
                     (Rooms, Items, NPCs, Player)

The key thing which took me some time to see (I could actually hear the penny drop) is that the parser should know nothing about the game world. Its only job is to turn raw text into a structured object I called an Intent. Something like:

Intent(verb="take", noun="lamp")
Intent(verb="go", noun="north")
Intent(verb="put", noun="key", preposition="in", noun2="box")

The Dispatcher knows all about the world and is responsible for taking that Intent and figuring out what to actually do with it.

Separating responsibilities like this means I can completely rewrite the parser later, such as swapping out my simple synonym-matching approach for a proper spaCy NLP pipeline without touching any of the game logic. The rest of the engine only ever sees Intent objects. It doesn’t care how they were produced.

The World Model

The other thing I figured out early: keep data out of code.

My first instinct was to define rooms as Python classes or objects with everything hardcoded. That’s a nightmare to maintain. If you want to add a room or simply change the description of a room that already exists then you need to edit Python files.

Instead I went with a simple JSON format for the world data with rooms, items, and NPCs all defined in a single JSON file. The engine loads it at startup. This means:

My initial model was pretty lean:

The flags dict on the Player was one of those lightbulb moments. Rather than adding specific fields for every possible story state, you just set flags: {"met_wizard": True, "chest_opened": False}. This means that I didn’t need to think of every possible thing that I needed to track from the outset.

What I Got Wrong the First Time

I initially built the dispatcher as a big if/elif chain:

if intent.verb == "take":
    # take logic
elif intent.verb == "go":
    # go logic
elif intent.verb == "examine":
    # examine logic
# ...and on and on

This works until it doesn’t. Adding a new command means finding the right place in a growing chain. It’s also untestable in any sensible way.

I settled on using a dictionary to match a verb to a function to handle it.

HANDLERS = {
    "take":      handle_take,
    "go":        handle_go,
    "examine":   handle_examine,
    # ...
}

Adding a new command is now: write a function, add one line to the dict. Much cleaner. Each handler is independently testable.

Where This Goes Next

This first post is the initial “why” and the thinking-before-coding part. In the next post I’ll get into the actual code, starting with the data models, which are the foundation everything else builds on.

The full engine ended up being five Python files and a JSON world definition. It handles:

It’s not Zork or The Hobbit, but it runs and is something I can build on.


Next: Part 2 — Data Models: Rooms, Items, NPCs and the Player


Share this post on:

Next Post
The Resource Pipeline Problem Nobody's Talking About