Skip to content

Text-Based Adventure Game

Abstract

Text adventures — Colossal Cave Adventure, Zork, Hitchhiker’s Guide — were the first true interactive fiction. They are also one of the best teaching projects in programming because they force you to think about state (what the player has, where they are, what they have done) and flow (which scene leads to which, under which conditions). In this project you build a fantasy adventure with a cave, a house, a sword, a wicked fairy, multiple endings, and a replay system. Then we refactor it from hard-coded scenes into a JSON-driven engine — the same architecture commercial interactive fiction tools use.

You will learn:

  • How to organize a game as a finite state machine of scenes.
  • How to manage inventory as a list of strings.
  • How to validate player input cleanly with a while not validwhile not valid loop.
  • How to add save/load so a player can resume later.
  • How to extract the story from the code into a data file you can edit without programming.

Prerequisites

  • Python 3.6 or above.
  • A text editor or IDE.
  • Comfort with functions, conditionals, and lists.
  • (Bonus) Familiarity with dictionaries and JSON for the engine refactor.

Getting Started

Create the project

  1. Create a folder named text-adventuretext-adventure.
  2. Inside it, create textbasedadventuregame.pytextbasedadventuregame.py.

Write the code

Text-Based AdventureSource
Text-Based Adventure
# Text Based Adventure Game
 
# Importing Modules
import time
import random
 
# Defining Functions
def print_pause(message_to_print):
    print(message_to_print)
    time.sleep(2)
    
def intro():
    print_pause("You find yourself standing in an open field, filled with grass and yellow wildflowers.")
    print_pause("Rumor has it that a wicked fairie is somewhere around here, and has been terrifying the nearby village.")
    print_pause("In front of you is a house.")
    print_pause("To your right is a dark cave.")
    print_pause("In your hand you hold your trusty (but not very effective) dagger.\n")
    
def house(items):
    print_pause("You approach the door of the house.")
    print_pause("You are about to knock when the door opens and out steps a wicked fairie.")
    print_pause("Eep! This is the wicked fairie's house!")
    print_pause("The wicked fairie attacks you!")
    if "Sword of Ogoroth" not in items:
        print_pause("You feel a bit under-prepared for this, what with only having a tiny dagger.")
    while True:
        choice = input("Would you like to (1) fight or (2) run away?")
        if choice == "1":
            if "Sword of Ogoroth" in items:
                print_pause("As the wicked fairie moves to attack, you unsheath your new sword.")
                print_pause("The Sword of Ogoroth shines brightly in your hand as you brace yourself for the attack.")
                print_pause("But the wicked fairie takes one look at your shiny new toy and runs away!")
                print_pause("You have rid the town of the wicked fairie. You are victorious!")
            else:
                print_pause("You do your best...")
                print_pause("but your dagger is no match for the wicked fairie.")
                print_pause("You have been defeated!")
            play_again()
            break
        if choice == "2":
            print_pause("You run back into the field. Luckily, you don't seem to have been followed.")
            field(items)
            break
        
def cave(items):
    print_pause("You peer cautiously into the cave.")
    if "Sword of Ogoroth" in items:
        print_pause("You've been here before, and gotten all the good stuff. It's just an empty cave now.")
        print_pause("You walk back out to the field.")
        field(items)
    else:
        print_pause("It turns out to be only a very small cave.")
        print_pause("Your eye catches a glint of metal behind a rock.")
        print_pause("You have found the magical Sword of Ogoroth!")
        print_pause("You discard your silly old dagger and take the sword with you.")
        print_pause("You walk back out to the field.")
        items.append("Sword of Ogoroth")
        field(items)
        
def field(items):
    print_pause("Enter 1 to knock on the door of the house.")
    print_pause("Enter 2 to peer into the cave.")
    while True:
        choice = input("What would you like to do?")
        if choice == "1":
            house(items)
            break
        elif choice == "2":
            cave(items)
            break
        
def play_again():
    while True:
        choice = input("Would you like to play again? (y/n)")
        if choice == "y":
            print_pause("Excellent! Restarting the game ...")
            play_game()
            break
        elif choice == "n":
            print_pause("Thanks for playing! See you next time.")
            break
        
def play_game():
    items = []
    intro()
    field(items)
    
# Calling Functions
play_game()
 
Text-Based Adventure
# Text Based Adventure Game
 
# Importing Modules
import time
import random
 
# Defining Functions
def print_pause(message_to_print):
    print(message_to_print)
    time.sleep(2)
    
def intro():
    print_pause("You find yourself standing in an open field, filled with grass and yellow wildflowers.")
    print_pause("Rumor has it that a wicked fairie is somewhere around here, and has been terrifying the nearby village.")
    print_pause("In front of you is a house.")
    print_pause("To your right is a dark cave.")
    print_pause("In your hand you hold your trusty (but not very effective) dagger.\n")
    
def house(items):
    print_pause("You approach the door of the house.")
    print_pause("You are about to knock when the door opens and out steps a wicked fairie.")
    print_pause("Eep! This is the wicked fairie's house!")
    print_pause("The wicked fairie attacks you!")
    if "Sword of Ogoroth" not in items:
        print_pause("You feel a bit under-prepared for this, what with only having a tiny dagger.")
    while True:
        choice = input("Would you like to (1) fight or (2) run away?")
        if choice == "1":
            if "Sword of Ogoroth" in items:
                print_pause("As the wicked fairie moves to attack, you unsheath your new sword.")
                print_pause("The Sword of Ogoroth shines brightly in your hand as you brace yourself for the attack.")
                print_pause("But the wicked fairie takes one look at your shiny new toy and runs away!")
                print_pause("You have rid the town of the wicked fairie. You are victorious!")
            else:
                print_pause("You do your best...")
                print_pause("but your dagger is no match for the wicked fairie.")
                print_pause("You have been defeated!")
            play_again()
            break
        if choice == "2":
            print_pause("You run back into the field. Luckily, you don't seem to have been followed.")
            field(items)
            break
        
def cave(items):
    print_pause("You peer cautiously into the cave.")
    if "Sword of Ogoroth" in items:
        print_pause("You've been here before, and gotten all the good stuff. It's just an empty cave now.")
        print_pause("You walk back out to the field.")
        field(items)
    else:
        print_pause("It turns out to be only a very small cave.")
        print_pause("Your eye catches a glint of metal behind a rock.")
        print_pause("You have found the magical Sword of Ogoroth!")
        print_pause("You discard your silly old dagger and take the sword with you.")
        print_pause("You walk back out to the field.")
        items.append("Sword of Ogoroth")
        field(items)
        
def field(items):
    print_pause("Enter 1 to knock on the door of the house.")
    print_pause("Enter 2 to peer into the cave.")
    while True:
        choice = input("What would you like to do?")
        if choice == "1":
            house(items)
            break
        elif choice == "2":
            cave(items)
            break
        
def play_again():
    while True:
        choice = input("Would you like to play again? (y/n)")
        if choice == "y":
            print_pause("Excellent! Restarting the game ...")
            play_game()
            break
        elif choice == "n":
            print_pause("Thanks for playing! See you next time.")
            break
        
def play_game():
    items = []
    intro()
    field(items)
    
# Calling Functions
play_game()
 

Run it

run
python textbasedadventuregame.py
run
python textbasedadventuregame.py
text
You find yourself standing in front of a cave.
In your hand you hold your trusty (but not very effective) dagger.
 
Enter 1 to knock on the door of the house.
Enter 2 to peer into the cave.
2
 
You have found the magical Sword of Ogoroth!
You are victorious!
 
Would you like to play again? (y/n)
text
You find yourself standing in front of a cave.
In your hand you hold your trusty (but not very effective) dagger.
 
Enter 1 to knock on the door of the house.
Enter 2 to peer into the cave.
2
 
You have found the magical Sword of Ogoroth!
You are victorious!
 
Would you like to play again? (y/n)

Step-by-Step Explanation

1. Slow-print for drama

textbasedadventuregame.py
import time
 
def print_pause(message, seconds=2):
    print(message)
    time.sleep(seconds)
textbasedadventuregame.py
import time
 
def print_pause(message, seconds=2):
    print(message)
    time.sleep(seconds)

Without delays, the game prints its entire story in milliseconds and loses all sense of pacing. A short sleep after each message lets the player actually read.

2. Intro scene

textbasedadventuregame.py
def intro():
    print_pause("You find yourself standing in an open field, filled with grass and yellow wildflowers.")
    print_pause("Rumor has it that a wicked fairie is somewhere around here...")
    print_pause("In your hand you hold your trusty (but not very effective) dagger.")
textbasedadventuregame.py
def intro():
    print_pause("You find yourself standing in an open field, filled with grass and yellow wildflowers.")
    print_pause("Rumor has it that a wicked fairie is somewhere around here...")
    print_pause("In your hand you hold your trusty (but not very effective) dagger.")

Plain-text scene-setting. The narrative is the gameplay in text adventures, so the writing matters.

3. Scene with a choice

textbasedadventuregame.py
def field(items):
    print_pause("Enter 1 to knock on the door of the house.")
    print_pause("Enter 2 to peer into the cave.")
    while True:
        choice = input("(Please enter 1 or 2.) ").strip()
        if choice == "1":
            house(items)
            break
        elif choice == "2":
            cave(items)
            break
textbasedadventuregame.py
def field(items):
    print_pause("Enter 1 to knock on the door of the house.")
    print_pause("Enter 2 to peer into the cave.")
    while True:
        choice = input("(Please enter 1 or 2.) ").strip()
        if choice == "1":
            house(items)
            break
        elif choice == "2":
            cave(items)
            break

The while Truewhile True + if/elif/breakif/elif/break pattern is the canonical way to handle constrained input. Each branch transitions to a different scene function — that is your state machine.

4. Inventory drives outcomes

textbasedadventuregame.py
def cave(items):
    if "Sword of Ogoroth" in items:
        print_pause("You've been here before, and gotten all the good stuff.")
    else:
        print_pause("You have found the magical Sword of Ogoroth!")
        items.append("Sword of Ogoroth")
    field(items)            # back to the field
textbasedadventuregame.py
def cave(items):
    if "Sword of Ogoroth" in items:
        print_pause("You've been here before, and gotten all the good stuff.")
    else:
        print_pause("You have found the magical Sword of Ogoroth!")
        items.append("Sword of Ogoroth")
    field(items)            # back to the field

The itemsitems list is passed through every scene. Picking up an item is one appendappend away. Branching on whether you have an item is one inin check away.

5. Combat that depends on preparation

textbasedadventuregame.py
def house(items):
    print_pause("You approach the door of the house.")
    print_pause("From within you hear a sinister cackle. The wicked fairie is inside.")
    print_pause("You must decide what to do.")
 
    while True:
        choice = input("(1) fight  (2) run\n> ")
        if choice == "1":
            if "Sword of Ogoroth" in items:
                print_pause("The Sword of Ogoroth shines brightly in your hand...")
                print_pause("You have defeated the fairie. Victory!")
            else:
                print_pause("Your dagger is no match for the fairie.")
                print_pause("You have been defeated.")
            play_again()
            break
        elif choice == "2":
            print_pause("You run back to the field.")
            field(items)
            break
textbasedadventuregame.py
def house(items):
    print_pause("You approach the door of the house.")
    print_pause("From within you hear a sinister cackle. The wicked fairie is inside.")
    print_pause("You must decide what to do.")
 
    while True:
        choice = input("(1) fight  (2) run\n> ")
        if choice == "1":
            if "Sword of Ogoroth" in items:
                print_pause("The Sword of Ogoroth shines brightly in your hand...")
                print_pause("You have defeated the fairie. Victory!")
            else:
                print_pause("Your dagger is no match for the fairie.")
                print_pause("You have been defeated.")
            play_again()
            break
        elif choice == "2":
            print_pause("You run back to the field.")
            field(items)
            break

The same scene has two endings — one rewarding preparation, one punishing recklessness.

6. Replay loop

textbasedadventuregame.py
def play_again():
    while True:
        ans = input("Play again? (y/n) ").strip().lower()
        if ans == "y":
            play_game()
            break
        if ans == "n":
            print_pause("Thanks for playing!")
            quit()
textbasedadventuregame.py
def play_again():
    while True:
        ans = input("Play again? (y/n) ").strip().lower()
        if ans == "y":
            play_game()
            break
        if ans == "n":
            print_pause("Thanks for playing!")
            quit()

quit()quit() terminates the program cleanly when the player is done.

7. The top-level driver

textbasedadventuregame.py
def play_game():
    items = []
    intro()
    field(items)
 
play_game()
textbasedadventuregame.py
def play_game():
    items = []
    intro()
    field(items)
 
play_game()

A fresh items = []items = [] per game guarantees a clean slate.

The State Machine View

Even this small game is a graph:

text
intro → field ──> cave ─→ field (with sword)

                 house ──> fight ─→ (win|lose)
                       └─> run  ─→ field
text
intro → field ──> cave ─→ field (with sword)

                 house ──> fight ─→ (win|lose)
                       └─> run  ─→ field

Every scene is a node. Every choice is an edge. As you add scenes, draw the graph on paper — it stops you writing dead-ends or unreachable scenes.

Add Save / Load

Players appreciate not losing progress.

save.py
import json
from pathlib import Path
 
def save_game(items, location):
    Path("save.json").write_text(json.dumps({"items": items, "location": location}))
 
def load_game():
    if not Path("save.json").exists():
        return None
    return json.loads(Path("save.json").read_text())
save.py
import json
from pathlib import Path
 
def save_game(items, location):
    Path("save.json").write_text(json.dumps({"items": items, "location": location}))
 
def load_game():
    if not Path("save.json").exists():
        return None
    return json.loads(Path("save.json").read_text())

Each scene gets a name. The current scene name and the inventory are all that need persisting in a simple game.

Refactor: Data-Driven Engine

Hard-coded scenes do not scale past 10 nodes. A real engine treats scenes as data:

scenes.json
{
  "field": {
    "text": "You stand in an open field. A house sits to your left and a cave to your right.",
    "choices": [
      { "label": "Knock on the house door", "go": "house" },
      { "label": "Peer into the cave",      "go": "cave"  }
    ]
  },
  "cave": {
    "text": "You find the magical Sword of Ogoroth!",
    "give": "sword",
    "choices": [{ "label": "Return to the field", "go": "field" }]
  },
  "house": {
    "text": "A wicked fairy lives here.",
    "choices": [
      { "label": "Fight", "go": "victory", "require": "sword" },
      { "label": "Fight", "go": "defeat",  "deny": "sword"    },
      { "label": "Run",   "go": "field" }
    ]
  },
  "victory": { "text": "You won!", "end": true },
  "defeat":  { "text": "You lost.", "end": true }
}
scenes.json
{
  "field": {
    "text": "You stand in an open field. A house sits to your left and a cave to your right.",
    "choices": [
      { "label": "Knock on the house door", "go": "house" },
      { "label": "Peer into the cave",      "go": "cave"  }
    ]
  },
  "cave": {
    "text": "You find the magical Sword of Ogoroth!",
    "give": "sword",
    "choices": [{ "label": "Return to the field", "go": "field" }]
  },
  "house": {
    "text": "A wicked fairy lives here.",
    "choices": [
      { "label": "Fight", "go": "victory", "require": "sword" },
      { "label": "Fight", "go": "defeat",  "deny": "sword"    },
      { "label": "Run",   "go": "field" }
    ]
  },
  "victory": { "text": "You won!", "end": true },
  "defeat":  { "text": "You lost.", "end": true }
}

The engine becomes tiny:

engine.py
import json
data = json.load(open("scenes.json"))
state = {"location": "field", "items": []}
 
while True:
    scene = data[state["location"]]
    print(scene["text"])
    if scene.get("give"):
        state["items"].append(scene["give"])
    if scene.get("end"):
        break
    options = [c for c in scene["choices"]
               if c.get("require", None) in (None, *state["items"])
               and c.get("deny", "__none__") not in state["items"]]
    for i, c in enumerate(options, 1):
        print(f"  {i}. {c['label']}")
    n = int(input("> ")) - 1
    state["location"] = options[n]["go"]
engine.py
import json
data = json.load(open("scenes.json"))
state = {"location": "field", "items": []}
 
while True:
    scene = data[state["location"]]
    print(scene["text"])
    if scene.get("give"):
        state["items"].append(scene["give"])
    if scene.get("end"):
        break
    options = [c for c in scene["choices"]
               if c.get("require", None) in (None, *state["items"])
               and c.get("deny", "__none__") not in state["items"]]
    for i, c in enumerate(options, 1):
        print(f"  {i}. {c['label']}")
    n = int(input("> ")) - 1
    state["location"] = options[n]["go"]

You now write stories in JSON. Game designers (or you, at 2 AM) can iterate on the narrative without touching Python.

Common Mistakes

ProblemCauseFix
Loop never exitsForgot breakbreak after a choice handlerAlways breakbreak after dispatching
State leaks between runsGlobals reusedBuild state inside play_game()play_game()
Bad input crashes the gameint(input())int(input()) without trytryValidate, or only accept letters/specific strings
Scenes call each other recursively foreverNo clear terminal sceneAdd explicit win/lose endings
Long scenes blur togetherNo pacingAdd time.sleeptime.sleep and section breaks

Variations to Try

1. Health and damage stats

A healthhealth attribute that decreases on bad choices and ends the game at 0.

2. NPC dialogue

A scene where the player chooses what to say; the NPC responds based on prior choices. Track flagsflags like met_wizard = Truemet_wizard = True.

3. Random encounters

With a 25 % chance, an enemy intercepts the player on a path. Use random.random() < 0.25random.random() < 0.25.

4. Map with directions

Instead of numbered choices, accept northnorth, southsouth, easteast, westwest. Store the map as a dict of rooms with exitsexits.

5. Combat system

Damage dice (1d6 + str1d6 + str), turn-based combat, escape attempts. See Dice Rolling Simulator for the dice mechanic.

6. Multiple character classes

Warrior, mage, rogue — each starts with different equipment and stats; some scenes only unlock for specific classes.

7. Save slots

Multiple JSON files (save1.jsonsave1.json, save2.jsonsave2.json) plus a menu to pick one.

8. Editor mode

A separate Python script that lets a designer type scenes interactively and append them to scenes.jsonscenes.json.

9. Voice narration

Pipe each print_pauseprint_pause line through pyttsx3pyttsx3 to read aloud.

10. Web port

Wrap the engine in Flask. Each scene becomes a page; choices become links. The state lives in the user’s session.

Real-World Applications

  • Onboarding flows — branching tutorials that adapt to user knowledge.
  • Chatbots with structured paths — customer-service trees.
  • Educational simulations — choose-your-own-adventure for ethics or training.
  • Decision-tree visualizations — the engine is a generic state machine.
  • Game prototypes — text mode for everything before art and engines.

Educational Value

  • State machines — scenes and transitions are an explicit FSM.
  • Inventory pattern — applicable to shopping carts, permissions, feature flags.
  • Branching logic — what most “config-driven” systems boil down to.
  • Refactoring from script to engine — extracting data from code is a fundamental engineering skill.
  • Narrative design — pacing, payoff, agency.

Next Steps

  • Add time.sleeptime.sleep pacing everywhere — it transforms the feel.
  • Extend with save/load.
  • Refactor to the JSON-driven engine above.
  • Write a longer story — 20+ scenes, 3 endings.
  • Try the textualtextual library for a richer TUI with mouse and color support.
  • Port to Twine or Ink (industry-standard tools) once your story outgrows JSON.

Conclusion

You built a complete branching adventure, learned to manage state and inventory, and saw the refactor path from hard-coded scenes to a real data-driven engine. The same patterns underlie every interactive fiction system, chatbot, and onboarding flow. Full source on GitHub. Explore more game projects on Python Central Hub.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did