Building a Creative Text-Based AI Game with DSPy
This tutorial demonstrates how to create an interactive text-based adventure game using DSPy's modular programming approach. You'll build a dynamic game where AI handles narrative generation, character interactions, and adaptive gameplay.
What You'll Build
An intelligent text-based adventure game featuring:
- Dynamic story generation and branching narratives
- AI-powered character interactions and dialogue
- Adaptive gameplay that responds to player choices
- Inventory and character progression systems
- Save/load game state functionality
Setup
Step 1: Core Game Framework
import dspy
import json
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field
from enum import Enum
import random
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
import typer
# Configure DSPy
lm = dspy.LM(model='openai/gpt-4o-mini')
dspy.configure(lm=lm)
console = Console()
class GameState(Enum):
MENU = "menu"
PLAYING = "playing"
INVENTORY = "inventory"
CHARACTER = "character"
GAME_OVER = "game_over"
@dataclass
class Player:
name: str
health: int = 100
level: int = 1
experience: int = 0
inventory: List[str] = field(default_factory=list)
skills: Dict[str, int] = field(default_factory=lambda: {
"strength": 10,
"intelligence": 10,
"charisma": 10,
"stealth": 10
})
def add_item(self, item: str):
self.inventory.append(item)
console.print(f"[green]Added {item} to inventory![/green]")
def remove_item(self, item: str) -> bool:
if item in self.inventory:
self.inventory.remove(item)
return True
return False
def gain_experience(self, amount: int):
self.experience += amount
old_level = self.level
self.level = 1 + (self.experience // 100)
if self.level > old_level:
console.print(f"[bold yellow]Level up! You are now level {self.level}![/bold yellow]")
@dataclass
class GameContext:
current_location: str = "Village Square"
story_progress: int = 0
visited_locations: List[str] = field(default_factory=list)
npcs_met: List[str] = field(default_factory=list)
completed_quests: List[str] = field(default_factory=list)
game_flags: Dict[str, bool] = field(default_factory=dict)
def add_flag(self, flag: str, value: bool = True):
self.game_flags[flag] = value
def has_flag(self, flag: str) -> bool:
return self.game_flags.get(flag, False)
class GameEngine:
def __init__(self):
self.player = None
self.context = GameContext()
self.state = GameState.MENU
self.running = True
def save_game(self, filename: str = "savegame.json"):
"""Save current game state."""
save_data = {
"player": {
"name": self.player.name,
"health": self.player.health,
"level": self.player.level,
"experience": self.player.experience,
"inventory": self.player.inventory,
"skills": self.player.skills
},
"context": {
"current_location": self.context.current_location,
"story_progress": self.context.story_progress,
"visited_locations": self.context.visited_locations,
"npcs_met": self.context.npcs_met,
"completed_quests": self.context.completed_quests,
"game_flags": self.context.game_flags
}
}
with open(filename, 'w') as f:
json.dump(save_data, f, indent=2)
console.print(f"[green]Game saved to {filename}![/green]")
def load_game(self, filename: str = "savegame.json") -> bool:
"""Load game state from file."""
try:
with open(filename, 'r') as f:
save_data = json.load(f)
# Reconstruct player
player_data = save_data["player"]
self.player = Player(
name=player_data["name"],
health=player_data["health"],
level=player_data["level"],
experience=player_data["experience"],
inventory=player_data["inventory"],
skills=player_data["skills"]
)
# Reconstruct context
context_data = save_data["context"]
self.context = GameContext(
current_location=context_data["current_location"],
story_progress=context_data["story_progress"],
visited_locations=context_data["visited_locations"],
npcs_met=context_data["npcs_met"],
completed_quests=context_data["completed_quests"],
game_flags=context_data["game_flags"]
)
console.print(f"[green]Game loaded from {filename}![/green]")
return True
except FileNotFoundError:
console.print(f"[red]Save file {filename} not found![/red]")
return False
except Exception as e:
console.print(f"[red]Error loading game: {e}![/red]")
return False
# Initialize game engine
game = GameEngine()
Step 2: AI-Powered Story Generation
class StoryGenerator(dspy.Signature):
"""Generate dynamic story content based on current game state."""
location: str = dspy.InputField(desc="Current location")
player_info: str = dspy.InputField(desc="Player information and stats")
story_progress: int = dspy.InputField(desc="Current story progress level")
recent_actions: str = dspy.InputField(desc="Player's recent actions")
scene_description: str = dspy.OutputField(desc="Vivid description of current scene")
available_actions: List[str] = dspy.OutputField(desc="List of possible player actions")
npcs_present: List[str] = dspy.OutputField(desc="NPCs present in this location")
items_available: List[str] = dspy.OutputField(desc="Items that can be found or interacted with")
class DialogueGenerator(dspy.Signature):
"""Generate NPC dialogue and responses."""
npc_name: str = dspy.InputField(desc="Name and type of NPC")
npc_personality: str = dspy.InputField(desc="NPC personality and background")
player_input: str = dspy.InputField(desc="What the player said or did")
context: str = dspy.InputField(desc="Current game context and history")
npc_response: str = dspy.OutputField(desc="NPC's dialogue response")
mood_change: str = dspy.OutputField(desc="How NPC's mood changed (positive/negative/neutral)")
quest_offered: bool = dspy.OutputField(desc="Whether NPC offers a quest")
information_revealed: str = dspy.OutputField(desc="Any important information shared")
class ActionResolver(dspy.Signature):
"""Resolve player actions and determine outcomes."""
action: str = dspy.InputField(desc="Player's chosen action")
player_stats: str = dspy.InputField(desc="Player's current stats and skills")
context: str = dspy.InputField(desc="Current game context")
difficulty: str = dspy.InputField(desc="Difficulty level of the action")
success: bool = dspy.OutputField(desc="Whether the action succeeded")
outcome_description: str = dspy.OutputField(desc="Description of what happened")
stat_changes: Dict[str, int] = dspy.OutputField(desc="Changes to player stats")
items_gained: List[str] = dspy.OutputField(desc="Items gained from this action")
experience_gained: int = dspy.OutputField(desc="Experience points gained")
class GameAI(dspy.Module):
"""Main AI module for game logic and narrative."""
def __init__(self):
super().__init__()
self.story_gen = dspy.ChainOfThought(StoryGenerator)
self.dialogue_gen = dspy.ChainOfThought(DialogueGenerator)
self.action_resolver = dspy.ChainOfThought(ActionResolver)
def generate_scene(self, player: Player, context: GameContext, recent_actions: str = "") -> Dict:
"""Generate current scene description and options."""
player_info = f"Level {player.level} {player.name}, Health: {player.health}, Skills: {player.skills}"
scene = self.story_gen(
location=context.current_location,
player_info=player_info,
story_progress=context.story_progress,
recent_actions=recent_actions
)
return {
"description": scene.scene_description,
"actions": scene.available_actions,
"npcs": scene.npcs_present,
"items": scene.items_available
}
def handle_dialogue(self, npc_name: str, player_input: str, context: GameContext) -> Dict:
"""Handle conversation with NPCs."""
# Create NPC personality based on name and context
personality_map = {
"Village Elder": "Wise, knowledgeable, speaks in riddles, has ancient knowledge",
"Merchant": "Greedy but fair, loves to bargain, knows about valuable items",
"Guard": "Dutiful, suspicious of strangers, follows rules strictly",
"Thief": "Sneaky, untrustworthy, has information about hidden things",
"Wizard": "Mysterious, powerful, speaks about magic and ancient forces"
}
personality = personality_map.get(npc_name, "Friendly villager with local knowledge")
game_context = f"Location: {context.current_location}, Story progress: {context.story_progress}"
response = self.dialogue_gen(
npc_name=npc_name,
npc_personality=personality,
player_input=player_input,
context=game_context
)
return {
"response": response.npc_response,
"mood": response.mood_change,
"quest": response.quest_offered,
"info": response.information_revealed
}
def resolve_action(self, action: str, player: Player, context: GameContext) -> Dict:
"""Resolve player actions and determine outcomes."""
player_stats = f"Level {player.level}, Health {player.health}, Skills: {player.skills}"
game_context = f"Location: {context.current_location}, Progress: {context.story_progress}"
# Determine difficulty based on action type
difficulty = "medium"
if any(word in action.lower() for word in ["fight", "battle", "attack"]):
difficulty = "hard"
elif any(word in action.lower() for word in ["look", "examine", "talk"]):
difficulty = "easy"
result = self.action_resolver(
action=action,
player_stats=player_stats,
context=game_context,
difficulty=difficulty
)
return {
"success": result.success,
"description": result.outcome_description,
"stat_changes": result.stat_changes,
"items": result.items_gained,
"experience": result.experience_gained
}
# Initialize AI
ai = GameAI()
Step 3: Game Interface and Interaction
def display_game_header():
"""Display the game header."""
header = Text("🏰 MYSTIC REALM ADVENTURE 🏰", style="bold magenta")
console.print(Panel(header, style="bright_blue"))
def display_player_status(player: Player):
"""Display player status panel."""
status = f"""
[bold]Name:[/bold] {player.name}
[bold]Level:[/bold] {player.level} (XP: {player.experience})
[bold]Health:[/bold] {player.health}/100
[bold]Skills:[/bold]
• Strength: {player.skills['strength']}
• Intelligence: {player.skills['intelligence']}
• Charisma: {player.skills['charisma']}
• Stealth: {player.skills['stealth']}
[bold]Inventory:[/bold] {len(player.inventory)} items
"""
console.print(Panel(status.strip(), title="Player Status", style="green"))
def display_location(context: GameContext, scene: Dict):
"""Display current location and scene."""
location_panel = f"""
[bold yellow]{context.current_location}[/bold yellow]
{scene['description']}
"""
if scene['npcs']:
location_panel += f"\n\n[bold]NPCs present:[/bold] {', '.join(scene['npcs'])}"
if scene['items']:
location_panel += f"\n[bold]Items visible:[/bold] {', '.join(scene['items'])}"
console.print(Panel(location_panel.strip(), title="Current Location", style="cyan"))
def display_actions(actions: List[str]):
"""Display available actions."""
action_text = "\n".join([f"{i+1}. {action}" for i, action in enumerate(actions)])
console.print(Panel(action_text, title="Available Actions", style="yellow"))
def get_player_choice(max_choices: int) -> int:
"""Get player's choice with input validation."""
while True:
try:
choice = typer.prompt("Choose an action (number)")
choice_num = int(choice)
if 1 <= choice_num <= max_choices:
return choice_num - 1
else:
console.print(f"[red]Please enter a number between 1 and {max_choices}[/red]")
except ValueError:
console.print("[red]Please enter a valid number[/red]")
def show_inventory(player: Player):
"""Display player inventory."""
if not player.inventory:
console.print(Panel("Your inventory is empty.", title="Inventory", style="red"))
else:
items = "\n".join([f"• {item}" for item in player.inventory])
console.print(Panel(items, title="Inventory", style="green"))
def main_menu():
"""Display main menu and handle selection."""
console.clear()
display_game_header()
menu_options = [
"1. New Game",
"2. Load Game",
"3. How to Play",
"4. Exit"
]
menu_text = "\n".join(menu_options)
console.print(Panel(menu_text, title="Main Menu", style="bright_blue"))
choice = typer.prompt("Select an option")
return choice
def show_help():
"""Display help information."""
help_text = """
[bold]How to Play:[/bold]
• This is a text-based adventure game powered by AI
• Make choices by selecting numbered options
• Talk to NPCs to learn about the world and get quests
• Explore different locations to find items and adventures
• Your choices affect the story and character development
• Use 'inventory' to check your items
• Use 'status' to see your character info
• Type 'save' to save your progress
• Type 'quit' to return to main menu
[bold]Tips:[/bold]
• Different skills affect your success in various actions
• NPCs remember your previous interactions
• Explore thoroughly - there are hidden secrets!
• Your reputation affects how NPCs treat you
"""
console.print(Panel(help_text.strip(), title="Game Help", style="blue"))
typer.prompt("Press Enter to continue")
Step 4: Main Game Loop
def create_new_character():
"""Create a new player character."""
console.clear()
display_game_header()
name = typer.prompt("Enter your character's name")
# Character creation with skill point allocation
console.print("\n[bold]Character Creation[/bold]")
console.print("You have 10 extra skill points to distribute among your skills.")
console.print("Base skills start at 10 each.\n")
skills = {"strength": 10, "intelligence": 10, "charisma": 10, "stealth": 10}
points_remaining = 10
for skill in skills.keys():
if points_remaining > 0:
console.print(f"Points remaining: {points_remaining}")
while True:
try:
points = int(typer.prompt(f"Points to add to {skill} (0-{points_remaining})"))
if 0 <= points <= points_remaining:
skills[skill] += points
points_remaining -= points
break
else:
console.print(f"[red]Enter a number between 0 and {points_remaining}[/red]")
except ValueError:
console.print("[red]Please enter a valid number[/red]")
player = Player(name=name, skills=skills)
console.print(f"\n[green]Welcome to Mystic Realm, {name}![/green]")
return player
def game_loop():
"""Main game loop."""
recent_actions = ""
while game.running and game.state == GameState.PLAYING:
console.clear()
display_game_header()
# Generate current scene
scene = ai.generate_scene(game.player, game.context, recent_actions)
# Display game state
display_player_status(game.player)
display_location(game.context, scene)
# Add standard actions
all_actions = scene['actions'] + ["Check inventory", "Character status", "Save game", "Quit to menu"]
display_actions(all_actions)
# Get player choice
choice_idx = get_player_choice(len(all_actions))
chosen_action = all_actions[choice_idx]
# Handle special commands
if chosen_action == "Check inventory":
show_inventory(game.player)
typer.prompt("Press Enter to continue")
continue
elif chosen_action == "Character status":
display_player_status(game.player)
typer.prompt("Press Enter to continue")
continue
elif chosen_action == "Save game":
game.save_game()
typer.prompt("Press Enter to continue")
continue
elif chosen_action == "Quit to menu":
game.state = GameState.MENU
break
# Handle game actions
if chosen_action in scene['actions']:
# Check if it's dialogue with an NPC
npc_target = None
for npc in scene['npcs']:
if npc.lower() in chosen_action.lower():
npc_target = npc
break
if npc_target:
# Handle NPC interaction
console.print(f"\n[bold]Talking to {npc_target}...[/bold]")
dialogue = ai.handle_dialogue(npc_target, chosen_action, game.context)
console.print(f"\n[italic]{npc_target}:[/italic] \"{dialogue['response']}\"")
if dialogue['quest']:
console.print(f"[yellow]💼 Quest opportunity detected![/yellow]")
if dialogue['info']:
console.print(f"[blue]ℹ️ {dialogue['info']}[/blue]")
# Add NPC to met list
if npc_target not in game.context.npcs_met:
game.context.npcs_met.append(npc_target)
recent_actions = f"Talked to {npc_target}: {chosen_action}"
else:
# Handle general action
result = ai.resolve_action(chosen_action, game.player, game.context)
console.print(f"\n{result['description']}")
# Apply results
if result['success']:
console.print("[green]✅ Success![/green]")
# Apply stat changes
for stat, change in result['stat_changes'].items():
if stat in game.player.skills:
game.player.skills[stat] += change
if change > 0:
console.print(f"[green]{stat.title()} increased by {change}![/green]")
elif stat == "health":
game.player.health = max(0, min(100, game.player.health + change))
if change > 0:
console.print(f"[green]Health restored by {change}![/green]")
elif change < 0:
console.print(f"[red]Health decreased by {abs(change)}![/red]")
# Add items
for item in result['items']:
game.player.add_item(item)
# Give experience
if result['experience'] > 0:
game.player.gain_experience(result['experience'])
# Update story progress
game.context.story_progress += 1
else:
console.print("[red]❌ The action didn't go as planned...[/red]")
recent_actions = f"Attempted: {chosen_action}"
# Check for game over conditions
if game.player.health <= 0:
console.print("\n[bold red]💀 You have died! Game Over![/bold red]")
game.state = GameState.GAME_OVER
break
typer.prompt("\nPress Enter to continue")
def main():
"""Main game function."""
while game.running:
if game.state == GameState.MENU:
choice = main_menu()
if choice == "1":
game.player = create_new_character()
game.context = GameContext()
game.state = GameState.PLAYING
console.print("\n[italic]Your adventure begins...[/italic]")
typer.prompt("Press Enter to start")
elif choice == "2":
if game.load_game():
game.state = GameState.PLAYING
typer.prompt("Press Enter to continue")
elif choice == "3":
show_help()
elif choice == "4":
game.running = False
console.print("[bold]Thanks for playing! Goodbye![/bold]")
elif game.state == GameState.PLAYING:
game_loop()
elif game.state == GameState.GAME_OVER:
console.print("\n[bold]Game Over[/bold]")
restart = typer.confirm("Would you like to return to the main menu?")
if restart:
game.state = GameState.MENU
else:
game.running = False
if __name__ == "__main__":
main()
Example Gameplay
When you run the game, you'll experience:
Character Creation:
🏰 MYSTIC REALM ADVENTURE 🏰
Enter your character's name: Aria
Character Creation
You have 10 extra skill points to distribute among your skills.
Base skills start at 10 each.
Points remaining: 10
Points to add to strength (0-10): 2
Points to add to intelligence (0-8): 4
Points to add to charisma (0-4): 3
Points to add to stealth (0-1): 1
Welcome to Mystic Realm, Aria!
Dynamic Scene Generation:
┌──────────── Current Location ────────────┐
│ Village Square │
│ │
│ You stand in the bustling heart of │
│ Willowbrook Village. The ancient stone │
│ fountain bubbles cheerfully as merchants │
│ hawk their wares and children play. A │
│ mysterious hooded figure lurks near the │
│ shadows of the old oak tree. │
│ │
│ NPCs present: Village Elder, Merchant │
│ Items visible: Strange Medallion, Herbs │
└──────────────────────────────────────────┘
┌────────── Available Actions ─────────────┐
│ 1. Approach the hooded figure │
│ 2. Talk to the Village Elder │
│ 3. Browse the merchant's wares │
│ 4. Examine the strange medallion │
│ 5. Gather herbs near the fountain │
│ 6. Head to the forest path │
└───────────────────────────────────────────┘
AI-Generated Dialogue:
Talking to Village Elder...
Village Elder: "Ah, young traveler, I sense a great destiny
surrounds you like morning mist. The ancient prophecy speaks
of one who would come bearing the mark of courage. Tell me,
have you noticed anything... unusual in your travels?"
💼 Quest opportunity detected!
ℹ️ The Village Elder knows about an ancient prophecy that might involve you
Next Steps
- Combat System: Add turn-based battles with strategy
- Magic System: Spellcasting with resource management
- Multiplayer: Network support for cooperative adventures
- Quest System: Complex multi-step missions with branching outcomes
- World Building: Procedurally generated locations and characters
- Audio: Add sound effects and background music
This tutorial demonstrates how DSPy's modular approach enables complex, interactive systems where AI handles creative content generation while maintaining consistent game logic and player agency.