DSPy로 창의적인 텍스트 기반 AI 게임 만들기

DSPy로 창의적인 텍스트 기반 AI 게임 만들기

이 튜토리얼에서는 DSPy의 모듈식 프로그래밍 접근 방식으로 인터랙티브한 텍스트 기반 어드벤처 게임을 만드는 방법을 보여드릴게요. AI가 내러티브 생성, 캐릭터 상호작용, 적응형 게임플레이를 담당하는 역동적인 게임을 함께 만들어 볼 거예요.

출처: 문서

본문

무엇을 만들까요

지능적인 텍스트 기반 어드벤처 게임으로, 이런 기능을 갖춰요:

  • 역동적인 스토리 생성과 분기형 내러티브
  • AI 기반 캐릭터 상호작용과 대화
  • 플레이어 선택에 반응하는 적응형 게임플레이
  • 인벤토리와 캐릭터 성장 시스템
  • 게임 상태 저장/불러오기 기능

설정 (Setup)

pip install dspy rich typer

1단계: 핵심 게임 프레임워크

게임의 상태와 플레이어, 게임 엔진을 먼저 정의할게요.

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()

Player는 캐릭터의 능력치와 인벤토리를, GameContext는 현재 위치와 스토리 진행 상황 같은 게임 전역 상태를 관리해요. GameState는 메뉴/플레이/게임오버 같은 화면 상태를 나타내죠.

2단계: AI 기반 스토리 생성

게임의 두뇌 역할을 하는 DSPy 시그니처들을 정의할게요. 스토리 생성, NPC 대화, 행동 해석 세 가지를 담당해요.

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()

GameAI 모듈이 세 시그니처를 dspy.ChainOfThought로 감싸서, 장면 생성·대화·행동 해석을 호출 가능한 메서드로 제공해요. resolve_action은 잘 살펴볼게요. 행동에 "fight", "battle" 같은 단어가 있으면 난이도를 hard로, "look", "examine"이면 easy로 조정해요. 이렇게 액션 유형에 따라 난이도를 달리 하는 거죠.

3단계: 게임 인터페이스와 상호작용

터미널 UI를 구성하는 함수들을 만들어요. Rich 라이브러리로 패널을 그려주고, Typer로 사용자 입력을 받아요.

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")

4단계: 메인 게임 루프

마지막으로 게임을 실제로 돌리는 메인 루프를 만들게요. 캐릭터 생성부터 시작해서, 장면을 생성하고, 행동을 받아 결과를 반영하는 반복 구조예요.

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()

게임 루프의 핵심 흐름이에요. 매 턴마다 장면을 생성하고, 플레이어의 선택을 받아, 선택이 NPC 대화면 대화를 처리하고, 일반 행동이면 resolve_action으로 결과를 계산해 체력·스킬·경험치에 반영해요. 체력이 0이 되면 게임 오버가 되죠.

예시 게임플레이 (Example Gameplay)

게임을 실행하면 이런 경험을 하게 돼요:

캐릭터 생성:

🏰 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!

역동적 장면 생성:

┌──────────── 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 생성 대화:

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

다음 단계

  • 전투 시스템 (Combat System): 전략적인 턴제 전투 추가
  • 마법 시스템 (Magic System): 자원 관리를 포함한 주문 시전
  • 멀티플레이 (Multiplayer): 협동 모험을 위한 네트워크 지원
  • 퀘스트 시스템 (Quest System): 분기 결과가 있는 복잡한 다단계 미션
  • 월드 빌딩 (World Building): 절차적으로 생성되는 장소와 캐릭터
  • 오디오 (Audio): 효과음과 배경 음악 추가

이 튜토리얼은 DSPy의 모듈식 접근 방식이 AI가 창의적 콘텐츠 생성을 담당하면서도 일관된 게임 로직과 플레이어의 행동 주도권을 유지하는 복잡하고 인터랙티브한 시스템을 가능하게 함을 보여줘요.

더 알아보기 (Learn more)