How to make a text adventure game in Python

Learn how to create a text adventure game in Python. This guide covers methods, tips, real-world uses, and debugging common errors.

How to make a text adventure game in Python
Published on: 
Tue
Apr 21, 2026
Updated on: 
Wed
Apr 22, 2026
The Replit Team

A text adventure game in Python is a classic project for new developers. You can practice core programming concepts like loops, functions, and conditional logic in a fun, interactive way.

In this article, we'll walk you through the process. We cover essential techniques, practical tips, real-world applications, and advice to debug your code so you can build a polished game.

Basic text adventure with if-else statements

print("Welcome to the Text Adventure!")
location = "entrance"
while True:
if location == "entrance":
print("You're at the entrance of a dark cave. Go [north] or [quit]?")
choice = input("> ").lower()
if choice == "north":
location = "cave"
elif choice == "quit":
break
elif location == "cave":
print("You're in a dark cave. There's a treasure chest! Go [south]?")
choice = input("> ").lower()
if choice == "south":
location = "entrance"
print("Thanks for playing!")--OUTPUT--Welcome to the Text Adventure!
You're at the entrance of a dark cave. Go [north] or [quit]?
> north
You're in a dark cave. There's a treasure chest! Go [south]?
> south
You're at the entrance of a dark cave. Go [north] or [quit]?
> quit
Thanks for playing!

This approach uses a while True loop to create the main game engine, which runs continuously until you enter quit. The location variable is the key here—it tracks your current state or room within the game.

On each pass of the loop, the if-elif structure checks your location. Based on where you are, the game displays a unique description and prompts for a choice. Your input then changes the location variable, transitioning you to a new state for the next loop.

Structuring your game with functions and data

While the if-elif structure is a great start, you'll need a more organized approach with functions and data structures to build a truly expandable game.

Using functions to organize game areas

def entrance():
print("You're at the entrance of a dark cave. Go [north] or [quit]?")
choice = input("> ").lower()
if choice == "north":
return "cave"
elif choice == "quit":
return "quit"
return "entrance"

def cave():
print("You're in a dark cave. There's a treasure chest! Go [south]?")
choice = input("> ").lower()
if choice == "south":
return "entrance"
return "cave"

print("Welcome to the Text Adventure!")
location = "entrance"
while location != "quit":
if location == "entrance":
location = entrance()
elif location == "cave":
location = cave()
print("Thanks for playing!")--OUTPUT--Welcome to the Text Adventure!
You're at the entrance of a dark cave. Go [north] or [quit]?
> north
You're in a dark cave. There's a treasure chest! Go [south]?
> south
You're at the entrance of a dark cave. Go [north] or [quit]?
> quit
Thanks for playing!

By wrapping each area in a dedicated function like entrance() or cave(), you make the code much more organized. The main game loop is now cleaner; it simply calls the function that matches the current location string.

  • Each function contains its own logic and prompts.
  • Based on your input, the function returns a new string for the next location.
  • The main loop then updates its location variable with this returned value, preparing for the next turn. This makes adding new rooms much simpler.

Adding an inventory system

def display_inventory(items):
print("Inventory:", ", ".join(items) if items else "empty")

print("Welcome to the Text Adventure!")
location = "entrance"
inventory = []
while True:
if location == "entrance":
print("You're at the entrance of a dark cave. Go [north] or [quit]?")
display_inventory(inventory)
choice = input("> ").lower()
if choice == "north":
location = "cave"
elif choice == "quit":
break
elif location == "cave":
print("You're in a dark cave. There's a [treasure] chest! Go [south]?")
display_inventory(inventory)
choice = input("> ").lower()
if choice == "south":
location = "entrance"
elif choice == "treasure" and "treasure" not in inventory:
print("You found gold treasure!")
inventory.append("treasure")
print("Thanks for playing!")--OUTPUT--Welcome to the Text Adventure!
You're at the entrance of a dark cave. Go [north] or [quit]?
Inventory: empty
> north
You're in a dark cave. There's a [treasure] chest! Go [south]?
Inventory: empty
> treasure
You found gold treasure!
You're in a dark cave. There's a [treasure] chest! Go [south]?
Inventory: treasure
> south
You're at the entrance of a dark cave. Go [north] or [quit]?
Inventory: treasure
> quit
Thanks for playing!

An inventory system lets you collect and manage items. This is easily handled with a Python list, initialized here as inventory = []. A new display_inventory() function is called in each location to show what you're carrying, making the game state clear on every turn.

  • In the cave, you can now interact with the chest by typing treasure.
  • The code checks if the item is not in inventory before adding it with inventory.append("treasure").
  • This conditional logic is crucial—it prevents you from collecting the same item multiple times.

Implementing game state with dictionaries

rooms = {
"entrance": {
"description": "You're at the entrance of a dark cave.",
"exits": {"north": "cave"},
},
"cave": {
"description": "You're in a dark cave. There's a treasure chest!",
"exits": {"south": "entrance"},
"items": ["treasure"]
}
}

player = {"location": "entrance", "inventory": []}
print("Welcome to the Text Adventure!")

while True:
current = rooms[player["location"]]
print(current["description"])
print("Exits:", ", ".join(current["exits"].keys()))
print("Inventory:", ", ".join(player["inventory"]) or "empty")

action = input("> ").lower().split()
command = action[0] if action else ""

if command == "quit":
break
elif command == "go" and len(action) > 1:
if action[1] in current["exits"]:
player["location"] = current["exits"][action[1]]
elif command == "take" and len(action) > 1 and "items" in current:
if action[1] in current["items"]:
player["inventory"].append(action[1])
current["items"].remove(action[1])
print(f"You picked up the {action[1]}.")

print("Thanks for playing!")--OUTPUT--Welcome to the Text Adventure!
You're at the entrance of a dark cave.
Exits: north
Inventory: empty
> go north
You're in a dark cave. There's a treasure chest!
Exits: south
Inventory: empty
> take treasure
You picked up the treasure.
You're in a dark cave. There's a treasure chest!
Exits: south
Inventory: treasure
> go south
You're at the entrance of a dark cave.
Exits: north
Inventory: treasure
> quit
Thanks for playing!

This method is the most scalable, centralizing all game data into dictionaries. The main rooms dictionary holds nested dictionaries for each location, defining its description, available exits, and any items present. A separate player dictionary tracks your current location and inventory.

  • The game loop is now data-driven. It reads from the rooms dictionary to display information based on your location.
  • Commands like go and take directly modify the player and rooms dictionaries, cleanly separating the game's logic from its content.

Advanced game development techniques

Building on your data-driven foundation, you can now add professional features like a class-based structure, save/load functionality, and a natural language command parser.

Creating a class-based adventure game

class Room:
def __init__(self, name, description):
self.name = name
self.description = description
self.exits = {}
self.items = []

def add_exit(self, direction, room):
self.exits[direction] = room

class Game:
def __init__(self):
self.rooms = {}
self.current_room = None
self.inventory = []
self.is_running = True

def setup_game(self):
entrance = Room("entrance", "You're at the entrance of a dark cave.")
cave = Room("cave", "You're in a dark cave. There's a treasure chest!")
entrance.add_exit("north", "cave")
cave.add_exit("south", "entrance")
cave.items.append("treasure")
self.rooms["entrance"] = entrance
self.rooms["cave"] = cave
self.current_room = self.rooms["entrance"]--OUTPUT--# No direct output - this creates the game architecture
# When run as part of a complete program, it creates a structured
# game with rooms, exits, and items

Transitioning to a class-based structure organizes your game's components into reusable blueprints. This object-oriented approach is a powerful way to model concepts, making your code cleaner and far more scalable than managing separate dictionaries.

  • The Room class acts as a template, bundling a location's description, exits, and items into a single, neat object.
  • A separate Game class manages the overall state, keeping track of your current_room, inventory, and all the Room instances you create in the setup_game() method.

Adding save and load functionality

import json

def save_game(player, rooms, filename="save.json"):
game_state = {"player": player, "rooms": rooms}
with open(filename, 'w') as f:
json.dump(game_state, f)
print("Game saved!")

def load_game(filename="save.json"):
try:
with open(filename, 'r') as f:
game_state = json.load(f)
return game_state["player"], game_state["rooms"]
except FileNotFoundError:
print("No saved game found.")
return None, None

# In the game loop:
if command == "save":
save_game(player, rooms)
elif command == "load":
loaded_player, loaded_rooms = load_game()
if loaded_player and loaded_rooms:
player, rooms = loaded_player, loaded_rooms--OUTPUT--# When save command is used:
Game saved!

# When load command is used (with existing save):
# Game state is restored from file

A save/load feature makes your game persistent, letting players resume their progress. This is easily achieved with Python's built-in json library, which converts your game's data dictionaries into a text format that can be stored in a file.

  • The save_game() function bundles the current player and rooms state and writes it to a file using json.dump().
  • Conversely, load_game() reads this file, reconstructs the dictionaries with json.load(), and restores your progress. It also gracefully handles cases where no save file is found.

Implementing a command parser for natural language

def parse_command(command_text, player, rooms):
words = command_text.lower().split()
if not words:
return False

command = words[0]
arg = words[1] if len(words) > 1 else None
current = rooms[player["location"]]

if command == "quit":
return True
elif command in ["go", "move", "walk"] and arg:
if arg in current["exits"]:
player["location"] = current["exits"][arg]
elif command in ["take", "get", "grab"] and arg:
if "items" in current and arg in current["items"]:
player["inventory"].append(arg)
current["items"].remove(arg)
print(f"You picked up the {arg}.")
elif command in ["i", "inventory"]:
print("Inventory:", ", ".join(player["inventory"]) or "empty")

return False--OUTPUT--# Example interaction:
> go north
# Player moves north

> take treasure
You picked up the treasure.

> inventory
Inventory: treasure

A command parser makes your game more intuitive by understanding natural phrases. The parse_command() function takes your raw input, converts it to lowercase, and uses split() to separate it into a command and an argument. This lets you handle inputs like "go north" instead of just "north."

  • It supports command aliases, so actions like go, move, and walk all trigger the same logic.
  • The function directly modifies the player and rooms dictionaries to update the game state based on your input.
  • This approach keeps your main game loop clean, as all input-handling logic is neatly contained within this single function.

Move faster with Replit

Replit is an AI-powered development platform that comes with all Python dependencies pre-installed, so you can skip setup and start coding instantly. Describe what you want to build, and Agent 4 handles everything—from writing the code and connecting APIs to deploying it live.

Instead of piecing together techniques, you can describe the app you want and let Agent take it from an idea to a working product:

  • A command-line utility that parses user commands with multiple aliases, like take, get, and grab, to manage a list of items.
  • A simple inventory tracker that can save and load its state to a JSON file, letting you manage a collection of items across sessions.
  • A multi-step form validator that uses a dictionary to manage different states and track user progress, much like the rooms dictionary.

Simply describe your app, and Replit will write the code, test it, and fix issues automatically, all within your browser.

Common errors and challenges

As you build your game, you'll likely run into a few common roadblocks, but they're all fixable with the right techniques.

Handling invalid user input with try-except

User input is unpredictable. If your code expects a specific type of data but receives something else, the program can crash. This is a common scenario that can frustrate players and halt the game.

You can gracefully manage these situations by wrapping your input logic in a try-except block. The code inside the try block runs first. If an error occurs, the program immediately jumps to the except block, where you can handle the error—for example, by printing a message like "Invalid input, please try again."

Fixing infinite loops when input() is not processed

An infinite loop is a classic bug where your game gets stuck repeating the same step. This usually happens when the condition controlling your main while loop is never updated. For instance, if a player's input doesn't match any of your if-elif conditions, the game state won't change, and the loop will repeat from the same point forever.

To fix this, ensure every possible input is accounted for. It's good practice to add a final else statement to your conditional logic. This acts as a catch-all, handling any unexpected input by printing an error message and allowing the loop to continue correctly without getting stuck.

Avoiding KeyError exceptions with dictionaries

When you use dictionaries to manage your game world, a KeyError can stop your game cold. This error happens when your code tries to access a dictionary key that doesn't exist—for example, if a player tries to move in a direction that isn't a valid exit from the current room.

You can prevent this in two simple ways. First, you can check if a key exists using the in keyword before you try to use it. A more direct approach is to use the dictionary's .get() method, which returns None if the key is missing instead of causing an error, allowing your code to handle the situation smoothly.

Handling invalid user input with try-except

Your game can easily crash if a player enters an incomplete command. For example, typing just go when your code expects go north will raise an error. The following code shows exactly how this happens when it tries to access a missing part.

def process_command(command):
parts = command.split()
direction = parts[1] # Error if user enters just "go" without direction
if direction in ["north", "south", "east", "west"]:
return f"Moving {direction}"
return "Cannot go that way"

while True:
user_input = input("Enter command (go [direction]): ")
if user_input == "quit":
break
print(process_command(user_input))

The split() method creates a list with only one item from the input go. Since the list has no second element, trying to access parts[1] causes a crash. The following code shows how to fix this.

def process_command(command):
parts = command.split()
if len(parts) < 2:
return "Please specify a direction"

direction = parts[1]
if direction in ["north", "south", "east", "west"]:
return f"Moving {direction}"
return "Cannot go that way"

while True:
user_input = input("Enter command (go [direction]): ")
if user_input == "quit":
break
print(process_command(user_input))

The fix prevents a crash by first checking if the command has enough parts. By adding if len(parts) < 2:, the code validates the input before trying to access parts[1]. This simple check catches incomplete commands like go and returns a helpful message instead of an IndexError. Always validate input length when parsing commands that expect multiple arguments. This ensures your game remains stable and user-friendly, even with unexpected player input.

Fixing infinite loops when input() is not processed

An infinite loop can trap your game in a single state, endlessly repeating the same prompt. This often happens when a function like forest_room() doesn't handle unexpected input, failing to return a new state. The following code shows how this happens.

def forest_room():
print("You're in a forest. Go [north] or [south]?")
choice = input("> ").lower()
if choice == "north":
return "cave"
if choice == "south":
return "entrance"
# No handling for invalid input - will keep asking
# and never change state when invalid input is given

location = "forest"
while location != "quit":
if location == "forest":
location = forest_room()

The forest_room() function only handles "north" and "south". Any other input makes it return None, so the location variable never changes, and the loop repeats endlessly. The following code demonstrates the fix.

def forest_room():
print("You're in a forest. Go [north] or [south]?")
choice = input("> ").lower()
if choice == "north":
return "cave"
if choice == "south":
return "entrance"
if choice == "quit":
return "quit"
print("I don't understand that command.")
return "forest" # Return current room to handle invalid input

location = "forest"
while location != "quit":
if location == "forest":
location = forest_room()

The fix ensures the forest_room() function always returns a valid location. Previously, invalid input made it return None, so the location variable never changed, causing the loop to repeat endlessly. By adding return "forest", the game now re-prompts you in the same room after an unknown command. This guarantees the game state is always updated, preventing it from getting stuck. Always ensure functions that control state can handle all possible inputs.

Avoiding KeyError exceptions with dictionaries

A KeyError will crash your game if a player tries to move in a direction that isn't a valid exit. This happens when your code tries to access a dictionary key that doesn't exist—a common but easily preventable issue.

The following code shows how this error occurs when the move_player() function receives an invalid direction, like "west," from the player's input.

def move_player(direction, current_room, rooms):
next_room = rooms[current_room]["exits"][direction]
return next_room

rooms = {
"entrance": {
"description": "Cave entrance",
"exits": {"north": "tunnel"}
},
"tunnel": {
"description": "Dark tunnel",
"exits": {"south": "entrance"}
}
}

current_room = "entrance"
direction = input("Which direction? ")
next_room = move_player(direction, current_room, rooms)
print(f"Moving to {next_room}")

The move_player() function attempts a direct lookup with ["exits"][direction]. If the provided direction isn't a valid key in the room's exits dictionary, the code raises a KeyError. The following code shows how to fix this.

def move_player(direction, current_room, rooms):
if direction in rooms[current_room]["exits"]:
next_room = rooms[current_room]["exits"][direction]
return next_room
return current_room # Stay in same room if exit doesn't exist

rooms = {
"entrance": {
"description": "Cave entrance",
"exits": {"north": "tunnel"}
},
"tunnel": {
"description": "Dark tunnel",
"exits": {"south": "entrance"}
}
}

current_room = "entrance"
direction = input("Which direction? ")
next_room = move_player(direction, current_room, rooms)
if next_room == current_room:
print("You can't go that way!")
else:
print(f"Moving to {next_room}")

The fix prevents a KeyError by first checking if the direction is a valid key in the room's exits dictionary. The move_player() function now uses the in keyword to validate the input before attempting to access it. If the exit doesn't exist, the function simply returns the current_room, keeping the player in place instead of crashing the game. It's a simple but essential check whenever user input determines a dictionary key.

Real-world applications

The skills for managing game states and parsing commands are directly transferable to building powerful, real-world command-line applications.

  • Creating a simple command-line file explorer: Think of directories as your game's rooms and files as its items. The same logic for player movement can be adapted to navigate a file system, where commands like cd and ls mirror your game's go and look actions.
  • Building a text-based stock market simulation: You can model a financial market where each stock is an item with a fluctuating price. The game loop simulates market changes, and your inventory system becomes a portfolio you manage with buy and sell commands.

Creating a simple command-line file explorer

You can apply this same logic to build a functional file explorer with Python's os module, treating directories as rooms and commands like cd as your movements.

import os

def explore_files():
current_dir = os.getcwd()
running = True

while running:
print(f"\nCurrent location: {current_dir}")
print("Contents:")
for item in os.listdir(current_dir):
prefix = "DIR: " if os.path.isdir(os.path.join(current_dir, item)) else "FILE: "
print(f" {prefix}{item}")

print("\nCommands: cd [directory], up, exit")
command = input("> ").strip().split()

if not command:
continue

if command[0] == "exit":
running = False
elif command[0] == "up":
current_dir = os.path.dirname(current_dir)
elif command[0] == "cd" and len(command) > 1:
target = os.path.join(current_dir, command[1])
if os.path.isdir(target):
current_dir = target
else:
print("Not a valid directory")

explore_files()

This script builds a simple file navigator using Python's os module. The core is a while loop that keeps the program running. In each cycle, it gets the current location with os.getcwd() and lists all files and directories using os.listdir(). It even checks if an item is a directory with os.path.isdir() to label it correctly.

  • The current_dir variable is the key—it tracks your position in the file system.
  • Commands like cd and up update this variable, changing your location for the next loop.

Building a text-based stock market simulation

By treating stocks as items and your portfolio as an inventory, you can create a text-based stock market simulation that evolves with each turn of the game loop.

import random

def stock_market_simulation():
stocks = {
"TECH": {"price": 100, "volatility": 0.1},
"FOOD": {"price": 50, "volatility": 0.05},
"ENERGY": {"price": 75, "volatility": 0.08}
}

portfolio = {"cash": 1000, "stocks": {}}
day = 1

while True:
print(f"\n--- Day {day} ---")
print("\nStock Prices:")
for symbol, data in stocks.items():
print(f"{symbol}: ${data['price']:.2f}")

print("\nYour Portfolio:")
print(f"Cash: ${portfolio['cash']:.2f}")
for symbol, quantity in portfolio['stocks'].items():
value = quantity * stocks[symbol]['price']
print(f"{symbol}: {quantity} shares (${value:.2f})")

print("\nCommands: buy [stock] [amount], sell [stock] [amount], next, exit")
command = input("> ").strip().split()

if not command:
continue

if command[0] == "exit":
break
elif command[0] == "next":
for symbol, data in stocks.items():
change = random.uniform(-data["volatility"], data["volatility"])
data["price"] *= (1 + change)
day += 1
elif command[0] == "buy" and len(command) >= 3:
symbol = command[1].upper()
try:
amount = int(command[2])
if symbol in stocks and amount > 0:
cost = stocks[symbol]["price"] * amount
if cost <= portfolio["cash"]:
portfolio["cash"] -= cost
portfolio["stocks"][symbol] = portfolio["stocks"].get(symbol, 0) + amount
print(f"Bought {amount} shares of {symbol}")
else:
print("Not enough cash")
else:
print("Invalid stock or amount")
except ValueError:
print("Invalid amount")
elif command[0] == "sell" and len(command) >= 3:
symbol = command[1].upper()
try:
amount = int(command[2])
if symbol in portfolio["stocks"] and amount <= portfolio["stocks"][symbol]:
value = stocks[symbol]["price"] * amount
portfolio["cash"] += value
portfolio["stocks"][symbol] -= amount
if portfolio["stocks"][symbol] == 0:
del portfolio["stocks"][symbol]
print(f"Sold {amount} shares of {symbol}")
else:
print("Invalid stock or amount")
except ValueError:
print("Invalid amount")

stock_market_simulation()

This simulation uses a while loop to model passing days in a stock market. You manage your assets using the portfolio dictionary, while the stocks dictionary holds all market data. The code processes your input and updates the game state on each turn.

  • Each turn, you can buy or sell shares, or type next to advance time.
  • When you advance, the random module adjusts stock prices based on their volatility, creating a dynamic market.
  • The code validates your commands—checking for sufficient cash or shares before executing a trade—which keeps the simulation running smoothly.

Get started with Replit

Now, use these skills to build a real tool. Tell Replit Agent: "Build a CLI budget tracker with commands to add expenses" or "Create a task manager that saves progress to a JSON file."

Replit Agent writes the code, tests for errors, and deploys your application. Start building with Replit to turn your concept into a functional app.

Get started free

Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.

Get started free

Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.