Text-Based Adventure Game
Abstract
Section titled “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 validloop. - 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
Section titled “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.
flowchart TD n0(["script start"]) n111["cave()"] n112["field()"] n113["house()"] n107["intro()"] n108["play_again()"] n114["play_game()"] n109["print_pause()"] n0 --> n114 n111 --> n112 n111 --> n109 n112 --> n111 n112 --> n113 n112 --> n109 n113 --> n112 n113 --> n108 n113 --> n109 n107 --> n109 n108 --> n114 n108 --> n109 n114 --> n112 n114 --> n107
Getting Started
Section titled “Getting Started”Create the project
Section titled “Create the project”- Create a folder named
text-adventure. - Inside it, create
textbasedadventuregame.py.
Write the code
Section titled “Write the code”Text-Based Adventure
pch.viewSource# 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
Section titled “Run it”python textbasedadventuregame.pyYou 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
Section titled “Step-by-Step Explanation”1. Slow-print for drama
Section titled “1. Slow-print for drama”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
Section titled “2. Intro scene”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
Section titled “3. Scene with a choice”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)
breakThe while True + if/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
Section titled “4. Inventory drives outcomes”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 fieldThe items list is passed through every scene. Picking up an item is one append away. Branching on whether you have an item is one in check away.
5. Combat that depends on preparation
Section titled “5. Combat that depends on preparation”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)
breakThe same scene has two endings — one rewarding preparation, one punishing recklessness.
6. Replay loop
Section titled “6. Replay loop”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() terminates the program cleanly when the player is done.
7. The top-level driver
Section titled “7. The top-level driver”def play_game():
items = []
intro()
field(items)
play_game()A fresh items = [] per game guarantees a clean slate.
The State Machine View
Section titled “The State Machine View”Even this small game is a graph:
intro → field ──> cave ─→ field (with sword)
↓
house ──> fight ─→ (win|lose)
└─> run ─→ fieldEvery 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
Section titled “Add Save / Load”Players appreciate not losing progress.
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
Section titled “Refactor: Data-Driven Engine”Hard-coded scenes do not scale past 10 nodes. A real engine treats scenes as data:
{
"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:
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
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
| Loop never exits | Forgot break after a choice handler | Always break after dispatching |
| State leaks between runs | Globals reused | Build state inside play_game() |
| Bad input crashes the game | int(input()) without try | Validate, or only accept letters/specific strings |
| Scenes call each other recursively forever | No clear terminal scene | Add explicit win/lose endings |
| Long scenes blur together | No pacing | Add time.sleep and section breaks |
Variations to Try
Section titled “Variations to Try”1. Health and damage stats
Section titled “1. Health and damage stats”A health attribute that decreases on bad choices and ends the game at 0.
2. NPC dialogue
Section titled “2. NPC dialogue”A scene where the player chooses what to say; the NPC responds based on prior choices. Track flags like met_wizard = True.
3. Random encounters
Section titled “3. Random encounters”With a 25 % chance, an enemy intercepts the player on a path. Use random.random() < 0.25.
4. Map with directions
Section titled “4. Map with directions”Instead of numbered choices, accept north, south, east, west. Store the map as a dict of rooms with exits.
5. Combat system
Section titled “5. Combat system”Damage dice (1d6 + str), turn-based combat, escape attempts. See Dice Rolling Simulator for the dice mechanic.
6. Multiple character classes
Section titled “6. Multiple character classes”Warrior, mage, rogue — each starts with different equipment and stats; some scenes only unlock for specific classes.
7. Save slots
Section titled “7. Save slots”Multiple JSON files (save1.json, save2.json) plus a menu to pick one.
8. Editor mode
Section titled “8. Editor mode”A separate Python script that lets a designer type scenes interactively and append them to scenes.json.
9. Voice narration
Section titled “9. Voice narration”Pipe each print_pause line through pyttsx3 to read aloud.
10. Web port
Section titled “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
Section titled “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
Section titled “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
Section titled “Next Steps”- Add
time.sleeppacing 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
textuallibrary for a richer TUI with mouse and color support. - Port to Twine or Ink (industry-standard tools) once your story outgrows JSON.
Conclusion
Section titled “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.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading