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 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
- 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
- Create a folder named
text-adventuretext-adventure. - Inside it, create
textbasedadventuregame.pytextbasedadventuregame.py.
Write the code
Text-Based Adventure
Source# 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 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
python textbasedadventuregame.pypython 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)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
import time
def print_pause(message, seconds=2):
print(message)
time.sleep(seconds)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
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.")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
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)
breakdef 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 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
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 fielddef 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 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
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)
breakdef 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
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()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
def play_game():
items = []
intro()
field(items)
play_game()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:
intro → field ──> cave ─→ field (with sword)
↓
house ──> fight ─→ (win|lose)
└─> run ─→ fieldintro → 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
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())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:
{
"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 }
}{
"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"]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
| Problem | Cause | Fix |
|---|---|---|
| Loop never exits | Forgot breakbreak after a choice handler | Always breakbreak after dispatching |
| State leaks between runs | Globals reused | Build state inside play_game()play_game() |
| Bad input crashes the game | int(input())int(input()) without trytry | 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.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.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
textualtextuallibrary 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 coffeeWas this page helpful?
Let us know how we did
