Text Adventure Game with an LLM Game Master

In the 1970s, text adventure games like Colossal Cave Adventure and Zork were the closest thing we had to virtual reality. You typed commands like go north or take lamp and the game described what happened next. Those games were built with hand-crafted parsers and painstakingly authored room descriptions. Today we can build something far more flexible in under 150 lines of Python by handing the storytelling to a large language model.

In the mid-1970s I wrote a popular open source Apple Basic text adventure game. I wrote the game initially on a huge sheet of butcher block paper, each location being a bubble with descriptive text and arrows between the bubbles defining states of a transition network. It was a lot of work writing it out on paper and then manually converting the data to a Basic program. Here, we get better results with a short Python program.

In this chapter we build a terminal-based text adventure game where an LLM acts as the Game Master. The model generates scenes, presents choices, tracks your inventory, and adapts the story to your decisions, all in real time. There is no pre-scripted plot. Every run through the game is different.

The examples for this chapter are in the directory source-code/text-adventure-game.

How It Works

The architecture is simple. We maintain a conversation history as a list of message objects: a system message that defines the game world and rules, then alternating user and assistant messages as the player and Game Master take turns. Each time the player types an action, we append it to the history, send the whole conversation to the model, and append the response. The model’s entire memory of the game state lives in that growing conversation history.

1 ┌──────────┐     ┌─────────────────────┐     ┌──────────────┐
2 │  Player  │────▶│  Conversation       │────▶│  Fireworks   │
3 │  input   │     │  History (messages) │     │  API (LLM)   │
4 └──────────┘     └─────────────────────┘     └──────────────┘
5                         │                           │
6                         │  append reply             │
7                         ◀───────────────────────────┘

The system prompt is the secret sauce. It defines the Game Master’s personality, the setting, the rules, and the narrative style. We load it from a separate file so you can swap in new worlds without touching the code.

The System Prompt

The file story.txt contains the instructions that shape the entire game experience. Let’s look at it:

 1 # story.txt Game Master instructions and world definition
 2 
 3 You are the Game Master of a text adventure game. Your role is to:
 4 
 5 1. Describe the current scene vividly but concisely (2-4 sentences).
 6 2. Present 3-4 clear choices the player can take, labeled as options.
 7 3. Respond to the player's choice by advancing the story in an interesting direction.
 8 4. Keep the story coherent and remember what has happened.
 9 5. Occasionally introduce surprises, puzzles, or challenges.
10 6. End with "What do you do?" after presenting options.
11 
12 The setting: You are in a mysterious ancient valley rumored to hold
13 a powerful artifact. Strange creatures and forgotten magic linger here.
14 The player is an adventurer seeking the artifact.
15 
16 Start the game by describing the entry to the valley and offering
17 initial choices. Keep track of the player's inventory (they start
18 with a map, a dagger, and 3 gold coins). Make choices matter with some
19 lead to treasure, others to peril. Include at least one NPC the player
20 can encounter.

This prompt does several important things at once. It establishes a persona (Game Master), sets constraints (2-4 sentence descriptions, 3-4 choices), defines a world, gives the player starting equipment, and instructs the model to include NPCs and consequences. The numbered list format helps the model follow the rules consistently.

The system prompt is the most important part of any LLM-powered application. When building your own adventures, spend time iterating on the prompt. Add rules, remove rules, change the tone. A small wording change can dramatically alter the game’s personality.

The Game Engine

The Python code is straightforward. It sets up the API client, loads the story, and runs a game loop. Here is the complete program:

  1 # game.py  Text Adventure Game powered by Fireworks.ai LLMs
  2 
  3 import os
  4 import sys
  5 from openai import OpenAI
  6 
  7 
  8 def load_story() -> str:
  9     """Load the game master instructions from story.txt."""
 10     try:
 11         with open("story.txt", "r") as f:
 12             return f.read()
 13     except FileNotFoundError:
 14         print("Error: story.txt not found.")
 15         sys.exit(1)
 16 
 17 
 18 def build_client() -> OpenAI:
 19     """Create a Fireworks.ai OpenAI-compatible client."""
 20     api_key = os.getenv("FIREWORKS_API_KEY")
 21     if not api_key:
 22         print("Error: FIREWORKS_API_KEY environment variable not set.")
 23         print("Set it with: export FIREWORKS_API_KEY='your-api-key'")
 24         sys.exit(1)
 25     return OpenAI(
 26         base_url="https://api.fireworks.ai/inference/v1",
 27         api_key=api_key,
 28     )
 29 
 30 
 31 MODEL = "accounts/fireworks/models/deepseek-v4-flash"
 32 
 33 
 34 def get_ai_response(client: OpenAI, messages: list[dict]) -> str:
 35     """Send conversation history to the model and return its reply."""
 36     response = client.chat.completions.create(
 37         model=MODEL,
 38         messages=messages,
 39     )
 40     return response.choices[0].message.content
 41 
 42 
 43 def main():
 44     story_text = load_story()
 45     client = build_client()
 46 
 47     messages = [
 48         {"role": "system", "content": story_text},
 49     ]
 50 
 51     print("=" * 60)
 52     print("  TEXT ADVENTURE GAME")
 53     print("  Powered by Fireworks.ai — deepseek-v4-flash")
 54     print("=" * 60)
 55     print()
 56     print("Commands: /help  /restart  /quit")
 57     print()
 58 
 59     # Get the opening scene
 60     messages.append({"role": "user", "content": "Start the adventure."})
 61     reply = get_ai_response(client, messages)
 62     messages.append({"role": "assistant", "content": reply})
 63     print(reply)
 64 
 65     # Game loop
 66     while True:
 67         print()
 68         user_input = input("> ").strip()
 69 
 70         if not user_input:
 71             continue
 72 
 73         if user_input.startswith("/"):
 74             cmd = user_input.lower()
 75             if cmd in ("/quit", "/exit", "/q"):
 76                 print("Thanks for playing!")
 77                 break
 78             elif cmd == "/restart":
 79                 messages = [
 80                     {"role": "system", "content": story_text},
 81                     {"role": "user", "content": "Start the adventure."},
 82                 ]
 83                 reply = get_ai_response(client, messages)
 84                 messages.append({"role": "assistant", "content": reply})
 85                 print("\n--- Restarted ---\n")
 86                 print(reply)
 87                 continue
 88             elif cmd == "/help":
 89                 print("Commands: /help  /restart  /quit")
 90                 print("Type your action or choice to advance the story.")
 91                 continue
 92             else:
 93                 print(f"Unknown command: {user_input}")
 94                 continue
 95 
 96         messages.append({"role": "user", "content": user_input})
 97         reply = get_ai_response(client, messages)
 98         messages.append({"role": "assistant", "content": reply})
 99         print()
100         print(reply)
101 
102 
103 if __name__ == "__main__":
104     main()

Walking Through the Code

The client setup uses Fireworks.ai’s OpenAI-compatible endpoint. We covered this pattern in the LLM Public APIs chapter, the only difference from standard OpenAI is the base_url pointing to Fireworks’ servers. We read the API key from the FIREWORKS_API_KEY environment variable and exit with a helpful message if it isn’t set.

The model is DeepSeek V4 Flash, which is fast and inexpensive. Fast responses are important qualities when a player is waiting for the next scene. I prototyped this example using a local model running on Ollama and the game play was not much fun because the responses were very slow. You can substitute any OpenAI-compatible model by changing the MODEL constant.

The conversation history is a Python list of dictionaries, each with a role and content. The system message goes in first to set the ground rules. The opening scene is generated by sending "Start the adventure." as the first user message. After that, every player input and model response is appended to the list.

The game loop is a while True loop that reads input, checks for commands (lines starting with /), sends non-command input to the model, and prints the response. Slash commands let the player quit, restart, or get help without those strings being sent to the model as game actions.

Restarting works by resetting the message list to just the system prompt and a fresh "Start the adventure." so the model generates a completely new opening scene and the adventure begins again.

Error handling wraps the API calls in try/except blocks. If a request fails, we print the error and give the player a chance to continue rather than crashing out of the game.

Playing the Game

Here is an example session to give you a feel for how the game plays:

 1 $ uv run game.py 
 2 ============================================================
 3   TEXT ADVENTURE GAME
 4   Powered by Fireworks.ai — deepseek-v4-flash
 5 ============================================================
 6 
 7 Commands: /help  /restart  /quit
 8 
 9 You stand at the mouth of a narrow pass, its walls of jagged stone rising high on either side. A cold mist curls around your boots, and ancient carvings-weathered swirls and symbols that cover the cliff faces. Somewhere ahead, a faint, rhythmic sound echoes, like water dripping into stone. The air smells of damp earth and old moss.
10 
11 Where do you begin?
12 
13 **Option 1:** Enter the pass cautiously, following the sound of dripping water.
14 **Option 2:** Study the carvings on the cliffs, hoping they reveal a clue.
15 **Option 3:** Unfold your map and search for a marked path or warning.
16 **Option 4:** Call out into the mist, testing if anything answers.
17 
18 What do you do?
19 
20 > what do the carvings on the cliff look like?
21 
22 You step closer, squinting through the mist. The carvings spiral and weave like the roots of an immense tree. Among the abstract patterns, you make out recurring shapes: a curled serpent, a crescent moon, and a single eye with a vertical pupil. Some sections seem to glow faintly when you brush away moss, as if faintly enchanted. Beneath the largest eye symbol, a line of smaller marks might be letters or a warning but the script is unfamiliar.
23 
24 **Option 1:** Trace the glowing symbols with your finger, hoping to activate something.
25 **Option 2:** Copy the strange script into your journal to decipher later.
26 **Option 3:** Move on entering the pass toward the dripping sound.
27 **Option 4:** Press your ear against the stone and listen for anything hidden.
28 
29 What do you do?
30 
31 > copy the strange script in journal and move towards the dripping sound
32 
33 You quickly scratch the strange script into your journal, the charcoal smudging slightly as you work. The symbols feel oddly familiar in your hand, as if your fingers knew the shapes before your eyes did. You tuck the journal away and step deeper into the pass.
34 
35 The mist thins as you walk, revealing a winding stone corridor that opens into a small, circular clearing. In the center, a weathered stone well stands, its rim draped in pale moss. The dripping sound is louder here, it comes from within the well, though no water spills over the edge. A faint silver light pulses from the depth.
36 
37 Near the well, a small, rusted iron bell hangs from a crooked branch. There is no rope, no clapper, just the bell, motionless.
38 
39 **Option 1:** Peer into the well to see what glows below.
40 **Option 2:** Ring the bell and see what happens.
41 **Option 3:** Search the edges of the clearing for another path or clue.
42 **Option 4:** Drop one of your gold coins into the well as an offering.
43 
44 What do you do?
45 
46 > /quit
47 Thanks for playing!

Every time you play the game the example program generates a unique story. The model remembers your choices — if you befriend an NPC early on, they may reappear later. If you lose your map crossing a river, the model won’t let you consult it later without some creative problem-solving.

Customizing Your Adventure

The best part of this architecture is how easy it is to create entirely different games. Just edit story.txt. Here are a few ideas:

Sci-fi thriller: “You are the security officer aboard the starship Event Horizon. A distress signal has been received from a derelict research station. Strange energy readings suggest something is still alive on board…”

Noir detective: “You are a private eye in 1940s Chicago. A dame in a red dress just walked into your office with a case that smells wrong. The city is baking under a heat wave and your ceiling fan is on its last legs…”

Fantasy tavern: “You are the innkeeper of the Grumpy Griffin, the rowdiest tavern in the realm. Tonight a hooded stranger sits in the corner, a bar brawl is brewing, and the king’s guards just kicked in your door…”

The model adapts to whatever world you define. The numbered rules keep the game structure consistent; the setting paragraph gives the model everything it needs to paint a scene.

For the best results, keep your system prompt between 200 and 500 words. Too short and the model won’t have enough context to maintain a consistent world. Too long and it may struggle to follow all the rules at once.

Why This Matters

Beyond being fun, this project demonstrates a pattern you will use in many real-world LLM applications: maintaining a conversation history where the system prompt defines behavior, user messages carry input, and assistant messages carry responses. This same structure powers customer support chatbots, interactive tutorials, code assistants, and creative writing tools.

The key insight is that the LLM’s context window serves as both its memory and its state. There is no separate database tracking the player’s inventory or location so the model tracks everything implicitly in the conversation. For a game, this works beautifully. For production applications with millions of users, you would eventually want to store state externally and summarize older conversation turns to manage context window limits. But for a personal project or prototype, the pure conversation-history approach is remarkably capable.

Running the Example

Set your Fireworks API key and run the game:

1 export FIREWORKS_API_KEY="your-api-key"
2 uv sync
3 uv run python game.py

Then lose yourself in the ancient valley for a while. The dagger and three gold coins won’t spend themselves.

Summary

You have now built an AI-powered text adventure game. The complete program is under 150 lines of Python, yet it can generate infinite stories across any setting you can describe. The techniques you used like system prompt design, conversation history management, and the game loop pattern all transfer directly to chatbots, interactive fiction, and any application where an LLM needs to maintain state across multiple turns of conversation.

Try swapping in different models, experimenting with the system prompt, or adding features like a save/load system that persists the message history to a JSON file. The framework is simple enough that you can extend it in an afternoon, and the results are genuinely entertaining.