Introduction
Text-based adventure games have a timeless appeal. They allow players to imagine entire worlds, from shadowy dungeons and towering castles to futuristic spacecraft and mystic realms, all through the power of language. Today, integrating large language models (LLMs), like ChatGPT, into these games takes this concept to new heights by providing dynamically generated descriptions, character dialogue, and more — all on the fly.
Text adventures thrive on imagination. Traditionally, game developers hard-code all the descriptions of rooms, items, and NPC (non-player character) interactions into the game, meaning that each possible path or scenario must be crafted by hand. This works fine for smaller or more static games, but once your project begins to grow, it becomes increasingly cumbersome. On the other hand, using an LLM can give you fresh, varied, and dynamic text without your having to script every detail.
Furthermore, modern LLMs can do more than just generate text; they can carry context from one interaction to the next, enabling them to build coherent narratives, adapt to player choices, or reflect the progress in the story. While we must remain mindful of token and context limitations, the possibilities for synergy between text adventures and language models are vast.
In this tutorial, we’ll walk through the process of building a simple Python-based text adventure game and then elevating it using an LLM. We’ll focus on the following main steps:
- Setting up a simple text adventure framework
- Representing game data with JSON
- Integrating ChatGPT for dynamic room descriptions
- Generating NPC dialogues with ChatGPT
- Putting it all together
When we are finished, we will end up with a complete, working prototype that you can expand upon and tailor to your own creative vision.
This guide assumes some familiarity with basic Python, using APIs, and dealing with JSON formatted data. By the end of this article, you’ll see how straightforward (and powerful) it is to add LLM-driven narrative to even the simplest text-based interactive fiction.
Creating a Simple Text Adventure Framework
Before incorporating any AI magic, let’s build a skeletal text adventure game in Python. We’ll first create a minimal engine that loads game data (rooms, NPCs, items, etc.) from a JSON file and lets players navigate. In subsequent steps, we’ll layer on LLM-driven enhancements.
Basic Project Structure
A typical file structure might look like this:
text_adventure/
├── game_data.json
├── text_adventure.py
└── README.md
The game_data.json file will store our basic game data—room names, descriptions, available exits, items, and so on. The text_adventure.py file will load this JSON file, parse the data, and provide the game loop (user input, responses, state management).
Creating game_data.json
Below is a simple example JSON file to demonstrate how we might structure our data, and give us something to use moving forward. Later, we’ll expand or modify it for LLM integration.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
“rooms”: “room_entrance”: “name”: “Castle Entrance”, “description”: “You are standing at the grand entrance to an ancient stone castle. Torches line the walls.”, “exits”: “north”: “room_hallway”
, “room_hallway”: “name”: “Great Hallway”, “description”: “An expansive hallway stretches before you with doors leading to unknown chambers.”, “exits”: “south”: “room_entrance”, “east”: “room_armory”
, “room_armory”: “name”: “Armory”, “description”: “Shields, swords, and arcane artifacts line the walls of this dimly lit armory.”, “exits”: “west”: “room_hallway”
, “player”: “start_room”: “room_entrance”, “inventory”: []
|
This JSON declares three rooms for our game — room_entrance, room_hallway, and room_armory — each with a name, a description, and a dictionary of exits pointing to other rooms. The player object defines a starting room and an empty inventory. There are numerous alernative ways we could implement this, but this simplistic manner works for our purposes.
Python Framework in text_adventure.py
Let’s write the code for our base text adventure engine. This version excludes any LLM integration — just the bare essentials.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
import json
class TextAdventureGame: def __init__(self, game_data_path): with open(game_data_path, ‘r’) as f: self.game_data = json.load(f)
self.rooms = self.game_data.get(“rooms”, ) self.player_data = self.game_data.get(“player”, ) self.current_room = self.player_data.get(“start_room”, “”) self.inventory = self.player_data.get(“inventory”, [])
def describe_current_room(self): room = self.rooms[self.current_room] print(f“\nroom[‘name’]”) print(room[‘description’])
def get_user_input(self): return input(“\n> “).strip().lower()
def move_player(self, direction): room = self.rooms[self.current_room] exits = room.get(“exits”, ) if direction in exits: self.current_room = exits[direction] print(f“You move direction to the self.rooms[self.current_room][‘name’].”) else: print(“You can’t go that way.”)
def play(self): print(“Welcome to the Text Adventure!”) self.describe_current_room()
while True: command = self.get_user_input()
if command in [“quit”, “exit”]: print(“Thanks for playing!”) break elif command in [“north”, “south”, “east”, “west”]: self.move_player(command) self.describe_current_room() elif command in [“look”, “examine”]: self.describe_current_room() else: print(“I don’t understand that command.”)
if __name__ == “__main__”: game = TextAdventureGame(“game_data.json”) game.play() |
Here is a breakdown of the code:
- Initialization: We load game_data.json and parse it into Python objects
- Game state: We store the available rooms, track the current room, and keep an inventory
- describe_current_room: Prints a short textual description of the current room
- move_player: Allows the player to move between rooms, if a valid exit exists in the game data
- The game loop:
- prompts the player for input
- interprets commands like “north”, “south”, “east”, “west”, or “quit”
- on movement commands, updates the current room and describes it
- if “look” or “examine” is typed, describe_current_room is called
With this structure in place, we have a simple text-based game. Next, we’ll expand our JSON data to hold “meta descriptions” for rooms, integrate an LLM to generate final textual descriptions, and use it for NPC dialogue.
Then run the game by typing:
Here’s what our game looks like at this point:
Integrating ChatGPT for Dynamic Room Descriptions
The core idea is to store a meta description of each room in our JSON, rather than a fully written description. Then, we’ll pass this meta description into ChatGPT along with some instructions, and ask ChatGPT to produce a more detailed or thematically rich final text. This allows your game to have consistent quality and style across all rooms without manually writing paragraphs of text.
Adding Meta Descriptions in JSON
Let’s modify our rooms entries to include a meta_description field. We’ll remove the hand-written description for demonstration, and rely solely on the LLM to produce text.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
“rooms”: “room_entrance”: “name”: “Castle Entrance”, “meta_description”: “castle entrance, lined with torches, stone walls, imposing wooden doors, possible presence of guards”, “exits”: “north”: “room_hallway”
, “room_hallway”: “name”: “Great Hallway”, “meta_description”: “long hallway, stained glass windows, tapestries on the walls, echoes of footsteps, high vaulted ceiling”, “exits”: “south”: “room_entrance”, “east”: “room_armory”
, “room_armory”: “name”: “Armory”, “meta_description”: “room filled with weapons and armor, a faint smell of metal and oil, possible magical artifacts”, “exits”: “west”: “room_hallway”
, “player”: “start_room”: “room_entrance”, “inventory”: []
|
Setting Up ChatGPT API
To integrate ChatGPT, we’ll need:
- An OpenAI API key
- The openai Python package (pip install openai)
Once installed, we can use the API to send ChatGPT prompts. Let’s illustrate a new method in our TextAdventureGame class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
import json import os from openai import OpenAI
class TextAdventureGame: def __init__(self, game_data_path): with open(game_data_path, ‘r’) as f: self.game_data = json.load(f)
self.rooms = self.game_data.get(“rooms”, ) self.player_data = self.game_data.get(“player”, ) self.current_room = self.player_data.get(“start_room”, “”) self.inventory = self.player_data.get(“inventory”, [])
# Initialize OpenAI client self.client = OpenAI(api_key=os.getenv(“OPENAI_API_KEY”))
def describe_current_room(self): room = self.rooms[self.current_room] # We’ll call our LLM function here to get the final description description = self.generate_room_description(room) print(f“\nroom[‘name’]”) print(description)
def generate_room_description(self, room): # Build a prompt with the meta_description prompt = ( “You are a game narrative engine. Given a meta description, produce a vivid, “ “immersive, and thematically consistent room description for players. “ “Do not break character, keep it short (2-3 sentences). “ f“Meta description: room[‘meta_description’]” )
try: response = self.client.chat.completions.create( model=“gpt-3.5-turbo”, messages=[ “role”: “system”, “content”: “You are a game narrative engine.”, “role”: “user”, “content”: prompt ], max_tokens=100, temperature=0.7, n=1, stop=None ) return response.choices[0].message.content.strip() except Exception as e: print(“Error calling OpenAI API:”, e) # Fallback: return the meta_description directly return room[‘meta_description’]
def get_user_input(self): return input(“\n> “).strip().lower()
def move_player(self, direction): room = self.rooms[self.current_room] exits = room.get(“exits”, ) if direction in exits: self.current_room = exits[direction] print(f“You move direction to the self.rooms[self.current_room][‘name’].”) else: print(“You can’t go that way.”)
def play(self): print(“Welcome to the Text Adventure!”) self.describe_current_room()
while True: command = self.get_user_input()
if command in [“quit”, “exit”]: print(“Thanks for playing!”) break elif command in [“north”, “south”, “east”, “west”]: self.move_player(command) self.describe_current_room() elif command in [“look”, “examine”]: self.describe_current_room() else: print(“I don’t understand that command.”)
if __name__ == “__main__”: game = TextAdventureGame(“game_data.json”) game.play() |
Here is how this works:
- We’ve created a new function called generate_room_description that:
- Takes a room dictionary as input
- Constructs a prompt for ChatGPT, referencing the room’s meta_description
- Calls the ChatGPT API to generate a final, fleshed-out description
- Returns that text, which is then printed to the player
- For production, consider customizing the prompt further. For example, you might want to include instructions about game style (e.g., comedic, dark fantasy, high fantasy, sci-fi, etc.), or specify formatting constraints.
- We set a fallback to the meta_description in case of API failures. This ensures your game can still run in an offline scenario or if something goes wrong. Crafting a better default description would likely be a option.
- model=”gpt-3.5-turbo” is used as it performs a little quicker than gpt-4-turbo, and reduces game latency.
With that done, your text adventure game now uses ChatGPT to dynamically produce room descriptions. Every time your player enters a new room, they’ll see fresh text that can be easily modified by adjusting the meta descriptions or the prompt template.
To test the integration, ensure you have an environment variable OPENAI_API_KEY set to your API key by typing this at a command prompt:
export OPENAI_API_KEY=“sk-xxxx” |
Run the game again with the LLM integrations:
Generating NPC Dialogues with ChatGPT
Another compelling feature of using an LLM is generating interactive dialog with characters. You can design a “character” or “NPC” by storing a meta description of their personality, role, or knowledge. Then you prompt ChatGPT with these details plus the recent conversation to produce their lines of dialogue.
Extending the JSON
Let’s add an NPC to our game_data.json. We’ll place the NPC in the hallway with a meta description of its personality and function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
“rooms”: “room_entrance”: “name”: “Castle Entrance”, “meta_description”: “castle entrance, lined with torches, stone walls, imposing wooden doors, possible presence of guards”, “exits”: “north”: “room_hallway”
, “room_hallway”: “name”: “Great Hallway”, “meta_description”: “long hallway, stained glass windows, tapestries on the walls, echoes of footsteps, high vaulted ceiling”, “exits”: “south”: “room_entrance”, “east”: “room_armory” , “npc”: “npc_guard” , “room_armory”: “name”: “Armory”, “meta_description”: “room filled with weapons and armor, a faint smell of metal and oil, possible magical artifacts”, “exits”: “west”: “room_hallway”
, “npcs”: “npc_guard”: “name”: “Castle Guard”, “meta_description”: “stern, loyal, short-tempered guard. Has knowledge of the castle’s secrets but rarely shares.”
, “player”: “start_room”: “room_entrance”, “inventory”: []
|
Note the new npcs section with npc_guard. The room_hallway includes a “npc”: “npc_guard” field to indicate that the guard is present in that room.
Generating NPC Speech
We’ll add functionality to handle NPC interactions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
import os import json import openai
class TextAdventureGame: def __init__(self, game_data_path): with open(game_data_path, ‘r’) as f: self.game_data = json.load(f)
self.rooms = self.game_data.get(“rooms”, ) self.npcs = self.game_data.get(“npcs”, ) self.player_data = self.game_data.get(“player”, ) self.current_room = self.player_data.get(“start_room”, “”) self.inventory = self.player_data.get(“inventory”, [])
openai.api_key = os.getenv(“OPENAI_API_KEY”)
def generate_room_description(self, room): prompt = ( “You are a game narrative engine. Given a meta description, produce a vivid, immersive, “ “and thematically consistent room description for players. Keep it to 2-3 sentences. “ f“Meta description: room[‘meta_description’]” )
try: response = openai.Completion.create( engine=“text-davinci-003”, prompt=prompt, max_tokens=100, temperature=0.7 ) return response.choices[0].text.strip() except Exception as e: print(“Error calling OpenAI API:”, e) return room[‘meta_description’]
def generate_npc_dialogue(self, npc_id, player_input=None): npc_data = self.npcs.get(npc_id, ) prompt = ( f“You are a character in a text adventure game. Your personality: npc_data[‘meta_description’]. “ “The player has asked you something or approached you. Please respond in one or two sentences. “ ) if player_input: prompt += f“\nPlayer input: player_input\n”
try: response = openai.Completion.create( engine=“text-davinci-003”, prompt=prompt, max_tokens=100, temperature=0.7 ) return response.choices[0].text.strip() except Exception as e: print(“Error calling OpenAI API:”, e) return “The NPC stares silently, unable to respond.”
def describe_current_room(self): room = self.rooms[self.current_room] description = self.generate_room_description(room) print(f“\nroom[‘name’]”) print(description) # Check if there’s an NPC npc_id = room.get(“npc”, None) if npc_id: dialogue = self.generate_npc_dialogue(npc_id) npc_name = self.npcs[npc_id][‘name’] print(f“\nnpc_name says: \”dialogue\””)
def get_user_input(self): return input(“\n> “).strip().lower()
def move_player(self, direction): room = self.rooms[self.current_room] exits = room.get(“exits”, ) if direction in exits: self.current_room = exits[direction] print(f“You move direction to the self.rooms[self.current_room][‘name’].”) else: print(“You can’t go that way.”)
def talk_to_npc(self): room = self.rooms[self.current_room] npc_id = room.get(“npc”, None) if npc_id: player_input = input(“What do you say? “) response = self.generate_npc_dialogue(npc_id, player_input) print(f“self.npcs[npc_id][‘name’] replies: \”response\””) else: print(“There’s no one here to talk to.”)
def play(self): print(“Welcome to the Text Adventure!”) self.describe_current_room()
while True: command = self.get_user_input()
if command in [“quit”, “exit”]: print(“Thanks for playing!”) break elif command in [“north”, “south”, “east”, “west”]: self.move_player(command) self.describe_current_room() elif command.startswith(“talk”): self.talk_to_npc() elif command in [“look”, “examine”]: self.describe_current_room() else: print(“I don’t understand that command.”)
if __name__ == “__main__”: game = TextAdventureGame(“game_data.json”) game.play() |
Here’s an explanation of our latest game engine iteration:
- We added an npcs dictionary loaded from JSON
- Inside describe_current_room, if the current room has an NPC, we generate a greeting or initial line of dialogue
- A new method, talk_to_npc, reads what the player says and sends it to the ChatGPT prompt, returning the NPC’s response
- The talk_to_npc method is triggered when the player types something like “talk” or “talk guard”
Then, again, run the game by typing:
You should now be able to move about between rooms with command like “north”, “east”, “west”, or “south”. You can type “talk” or “talk guard” when in the hallway with the guard to have a conversation. When you are done, type “quit” or “exit” to end the game.
If all goes well, you should see dynamically generated descriptions in each room, and the guard should respond with lines reflecting their short-tempered, loyal demeanor.
The prompt structure includes the NPC’s meta description, ensuring ChatGPT can craft a line consistent with the NPC’s personality. Optionally, we can also maintain a conversation history to build deeper, context-based NPC interactions. For brevity, this example keeps it simple with just the single prompt.
Putting It All Together
At this point, our text adventure framework combines:
- JSON-based game data: Storing rooms, item references, NPC info, and meta descriptions
- Python game loop: Handling user input for movement, interaction, and conversation
- ChatGPT integration:
- Room descriptions: Generated from meta descriptions to produce immersive text
- NPC Dialog: Used to create dynamic, personality-driven exchanges with characters
Considerations for Deployment and Scaling
- API Costs: Each call to the ChatGPT API has an associated cost. For heavily trafficked games or persistent worlds, consider how many tokens you’re using. You might use shorter prompts or a specialized, smaller model for quick responses.
- Caching: For repeated room visits, you may want to cache the generated descriptions to avoid re-generating them (and thus incurring more cost) every time a player steps back into a room.
- Context Management: If you want NPCs to remember details over multiple conversations, you’ll need to store conversation history or relevant state. Larger memory can be expensive in terms of tokens, so consider summarizing or limiting context windows.
- Prompt Tuning: The quality and style of LLM output is heavily dependent on how you craft the prompt. By tweaking instructions, you can control length, style, tone, or spoiler content for your game.
Final Thoughts
LLMs such as ChatGPT open up new frontiers for text-based interactive fiction. Instead of tediously writing every line of prose, you can store minimal meta descriptions for rooms, items, or characters, then let the model fill in the details. The method outlined in this article showcases how to seamlessly integrate LLM-driven room descriptions, NPC dialogue, and more, all using Python, JSON, and the ChatGPT API.
To recap:
- We built a simple text adventure framework in Python, loading static game data from JSON
- We added LLM functionality to transform meta descriptions into rich, atmospheric text
- We extended our NPC system to engage in short, personality-driven dialogues with players
With this foundation, the possibilities are nearly endless. You can enhance item descriptions, create dynamic puzzles that adapt to the player’s progress, or even generate entire story arcs on the fly. While caution is warranted—especially concerning token usage, cost, and the unpredictability of generative models—the results can be compelling, immersive, and astonishingly flexible.
So, go forth and experiment. Use LLM-based generation to add an element of surprise and spontaneity to your text adventure. Combine these techniques with your own design creativity, and you’ll find yourself building worlds that feel alive and unbound by static lines of code. You now have the tools to let an LLM shape the stories your players explore. Happy adventure building!