# app.py - Fixed D&D Campaign and Character Creator with AI Agents import gradio as gr import logging from typing import Dict, List, Tuple, Optional import json import os from dotenv import load_dotenv from dataclasses import dataclass from enum import Enum import random # Load environment variables load_dotenv() # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Load OpenAI API key try: import openai api_key = os.getenv("OPENAI_API_KEY") if api_key: openai.api_key = api_key logger.info("✅ OpenAI API key loaded") else: logger.warning("⚠️ No OpenAI API key found") except ImportError: logger.warning("⚠️ OpenAI package not installed") # ===== DATA MODELS ===== class Alignment(Enum): LAWFUL_GOOD = "Lawful Good" NEUTRAL_GOOD = "Neutral Good" CHAOTIC_GOOD = "Chaotic Good" LAWFUL_NEUTRAL = "Lawful Neutral" TRUE_NEUTRAL = "True Neutral" CHAOTIC_NEUTRAL = "Chaotic Neutral" LAWFUL_EVIL = "Lawful Evil" NEUTRAL_EVIL = "Neutral Evil" CHAOTIC_EVIL = "Chaotic Evil" @dataclass class CharacterClass: name: str hit_die: int primary_ability: List[str] saving_throws: List[str] skills: List[str] @dataclass class Race: name: str ability_modifiers: Dict[str, int] traits: List[str] languages: List[str] @dataclass class Character: name: str race: str character_class: str level: int gender: str alignment: Alignment abilities: Dict[str, int] hit_points: int skills: List[str] background: str backstory: str portrait_url: Optional[str] = None @dataclass class Campaign: name: str theme: str level_range: str description: str locations: List[str] npcs: List[str] plot_hooks: List[str] @dataclass class NPC: name: str race: str occupation: str personality: str secret: str voice_description: str relationship_to_party: str # ===== AI AGENT CLASSES ===== class DungeonMasterAgent: """AI agent that acts as a Dungeon Master""" def __init__(self): self.personality = "Creative, fair, and engaging storyteller" self.knowledge_areas = ["D&D rules", "storytelling", "world-building", "character development"] def generate_campaign_concept(self, theme: str, level: int, player_count: int) -> Dict: """Generate a complete campaign concept""" try: from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"""As an experienced D&D Dungeon Master, create a campaign concept for: Theme: {theme} Player Level: {level} Number of Players: {player_count} Provide: 1. Campaign Name 2. Core Plot Hook (2-3 sentences) 3. Main Antagonist 4. 3 Key Locations 5. Central Conflict 6. Estimated Campaign Length 7. Unique Elements/Mechanics Make it engaging and ready to play!""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "system", "content": "You are an expert D&D Dungeon Master with 20 years of experience creating memorable campaigns."}, {"role": "user", "content": prompt}], max_tokens=500, temperature=0.8 ) content = response.choices[0].message.content return {"success": True, "content": content} except Exception as e: logger.error(f"Campaign generation failed: {e}") return {"success": False, "error": str(e), "content": f"Mock Campaign: {theme} adventure for {player_count} level {level} characters"} def generate_session_content(self, campaign_context: str, session_number: int) -> Dict: """Generate content for a specific session""" try: from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"""Create session {session_number} content for this campaign: {campaign_context} Generate: 1. Session Opening (scene description) 2. 3 Potential Encounters (combat, social, exploration) 3. Key NPCs for this session 4. Skill challenges or puzzles 5. Potential plot developments 6. Cliffhanger ending options Make it detailed enough for a DM to run immediately.""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "system", "content": "You are a D&D Dungeon Master preparing detailed session content."}, {"role": "user", "content": prompt}], max_tokens=600, temperature=0.7 ) return {"success": True, "content": response.choices[0].message.content} except Exception as e: logger.error(f"Session generation failed: {e}") return {"success": False, "error": str(e), "content": f"Mock Session {session_number}: Adventure continues..."} class NPCAgent: """AI agent specialized in creating and roleplaying NPCs""" def __init__(self): self.personality = "Versatile character actor with deep understanding of motivations" self.specializations = ["Character creation", "Dialogue", "Motivations", "Voice acting"] def generate_npc(self, context: str, role: str, importance: str) -> Dict: """Generate a detailed NPC""" try: from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"""Create a detailed NPC for: Context: {context} Role: {role} Importance: {importance} Generate: 1. Name and basic demographics 2. Personality traits (3-4 key traits) 3. Background and motivation 4. Speech patterns/accent description 5. Physical description 6. Secret or hidden agenda 7. How they react to different party approaches 8. Potential quest hooks they could provide Make them memorable and three-dimensional!""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "system", "content": "You are an expert at creating memorable, three-dimensional NPCs for D&D campaigns."}, {"role": "user", "content": prompt}], max_tokens=400, temperature=0.8 ) return {"success": True, "content": response.choices[0].message.content} except Exception as e: logger.error(f"NPC generation failed: {e}") return {"success": False, "error": str(e), "content": f"Mock NPC: {role} character for {context}"} def roleplay_npc(self, npc_description: str, player_input: str, context: str) -> Dict: """Roleplay as an NPC in response to player actions""" try: from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"""You are roleplaying as this NPC: {npc_description} Context: {context} Player says/does: {player_input} Respond in character with: 1. Dialogue (in quotes) 2. Actions/body language (in italics) 3. Internal thoughts/motivations (in parentheses) Stay true to the character's personality and motivations!""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "system", "content": "You are a skilled voice actor and D&D player, staying in character as NPCs."}, {"role": "user", "content": prompt}], max_tokens=200, temperature=0.9 ) return {"success": True, "content": response.choices[0].message.content} except Exception as e: logger.error(f"NPC roleplay failed: {e}") return {"success": False, "error": str(e), "content": "Mock NPC Response: The character responds appropriately to your action."} class WorldBuilderAgent: """AI agent focused on creating consistent world elements""" def __init__(self): self.personality = "Detail-oriented architect of fictional worlds" self.specializations = ["Geography", "Politics", "Culture", "History", "Economics"] def generate_location(self, location_type: str, theme: str, purpose: str) -> Dict: """Generate a detailed location""" try: from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"""Create a detailed {location_type} with: Theme: {theme} Purpose in campaign: {purpose} Generate: 1. Name and general description 2. Key areas/rooms (at least 5) 3. Notable inhabitants 4. Hidden secrets or mysteries 5. Potential dangers or challenges 6. Valuable resources or rewards 7. Connections to broader world/campaign 8. Sensory details (sights, sounds, smells) Make it feel lived-in and realistic!""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "system", "content": "You are a master world-builder creating immersive D&D locations."}, {"role": "user", "content": prompt}], max_tokens=500, temperature=0.7 ) return {"success": True, "content": response.choices[0].message.content} except Exception as e: logger.error(f"Location generation failed: {e}") return {"success": False, "error": str(e), "content": f"Mock Location: {theme} {location_type} for {purpose}"} class LootMasterAgent: """AI agent specialized in creating balanced loot and magic items""" def __init__(self): self.personality = "Meticulous curator of magical treasures" self.specializations = ["Game balance", "Magic item design", "Treasure distribution"] def generate_loot_table(self, level: int, encounter_type: str, rarity: str) -> Dict: """Generate a balanced loot table""" try: from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"""Create a balanced loot table for: Party Level: {level} Encounter Type: {encounter_type} Rarity Level: {rarity} Generate: 1. Gold/Currency amounts 2. Common items (consumables, gear) 3. Uncommon magical items (if appropriate) 4. Rare items (if high level) 5. Unique/plot-relevant items 6. Alternative treasures (information, allies, etc.) Ensure balance and appropriateness for the level!""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "system", "content": "You are an expert at D&D game balance and treasure design."}, {"role": "user", "content": prompt}], max_tokens=300, temperature=0.6 ) return {"success": True, "content": response.choices[0].message.content} except Exception as e: logger.error(f"Loot generation failed: {e}") return {"success": False, "error": str(e), "content": f"Mock Loot: Level {level} {encounter_type} {rarity} treasures"} def create_custom_magic_item(self, item_concept: str, power_level: str, campaign_theme: str) -> Dict: """Create a custom magic item""" try: from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"""Design a custom magic item: Concept: {item_concept} Power Level: {power_level} Campaign Theme: {campaign_theme} Provide: 1. Item name and basic description 2. Mechanical effects (stats, abilities) 3. Activation requirements 4. Rarity and attunement needs 5. Physical appearance 6. Historical background/lore 7. Potential drawbacks or limitations 8. How it fits the campaign theme Make it balanced and interesting!""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "system", "content": "You are a master craftsperson of magical items for D&D, balancing power with narrative interest."}, {"role": "user", "content": prompt}], max_tokens=400, temperature=0.7 ) return {"success": True, "content": response.choices[0].message.content} except Exception as e: logger.error(f"Magic item creation failed: {e}") return {"success": False, "error": str(e), "content": f"Mock Magic Item: {power_level} {item_concept} with {campaign_theme} theme"} # ===== CHARACTER CREATOR CLASS ===== class CharacterCreator: """Enhanced character creator with AI integration""" def __init__(self): self.classes = self._get_default_classes() self.races = self._get_default_races() self.backgrounds = self._get_backgrounds() def _get_default_classes(self) -> Dict[str, CharacterClass]: return { "Fighter": CharacterClass( name="Fighter", hit_die=10, primary_ability=["Strength", "Dexterity"], saving_throws=["Strength", "Constitution"], skills=["Acrobatics", "Animal Handling", "Athletics", "History", "Insight", "Intimidation", "Perception", "Survival"] ), "Wizard": CharacterClass( name="Wizard", hit_die=6, primary_ability=["Intelligence"], saving_throws=["Intelligence", "Wisdom"], skills=["Arcana", "History", "Insight", "Investigation", "Medicine", "Religion"] ), "Rogue": CharacterClass( name="Rogue", hit_die=8, primary_ability=["Dexterity"], saving_throws=["Dexterity", "Intelligence"], skills=["Acrobatics", "Athletics", "Deception", "Insight", "Intimidation", "Investigation", "Perception", "Performance", "Persuasion", "Sleight of Hand", "Stealth"] ), "Cleric": CharacterClass( name="Cleric", hit_die=8, primary_ability=["Wisdom"], saving_throws=["Wisdom", "Charisma"], skills=["History", "Insight", "Medicine", "Persuasion", "Religion"] ), "Barbarian": CharacterClass( name="Barbarian", hit_die=12, primary_ability=["Strength"], saving_throws=["Strength", "Constitution"], skills=["Animal Handling", "Athletics", "Intimidation", "Nature", "Perception", "Survival"] ), "Bard": CharacterClass( name="Bard", hit_die=8, primary_ability=["Charisma"], saving_throws=["Dexterity", "Charisma"], skills=["Any three of your choice"] ), "Druid": CharacterClass( name="Druid", hit_die=8, primary_ability=["Wisdom"], saving_throws=["Intelligence", "Wisdom"], skills=["Arcana", "Animal Handling", "Insight", "Medicine", "Nature", "Perception", "Religion", "Survival"] ), "Monk": CharacterClass( name="Monk", hit_die=8, primary_ability=["Dexterity", "Wisdom"], saving_throws=["Strength", "Dexterity"], skills=["Acrobatics", "Athletics", "History", "Insight", "Religion", "Stealth"] ), "Paladin": CharacterClass( name="Paladin", hit_die=10, primary_ability=["Strength", "Charisma"], saving_throws=["Wisdom", "Charisma"], skills=["Athletics", "Insight", "Intimidation", "Medicine", "Persuasion", "Religion"] ), "Ranger": CharacterClass( name="Ranger", hit_die=10, primary_ability=["Dexterity", "Wisdom"], saving_throws=["Strength", "Dexterity"], skills=["Animal Handling", "Athletics", "Insight", "Investigation", "Nature", "Perception", "Stealth", "Survival"] ), "Sorcerer": CharacterClass( name="Sorcerer", hit_die=6, primary_ability=["Charisma"], saving_throws=["Constitution", "Charisma"], skills=["Arcana", "Deception", "Insight", "Intimidation", "Persuasion", "Religion"] ), "Warlock": CharacterClass( name="Warlock", hit_die=8, primary_ability=["Charisma"], saving_throws=["Wisdom", "Charisma"], skills=["Arcana", "Deception", "History", "Intimidation", "Investigation", "Nature", "Religion"] ) } def _get_default_races(self) -> Dict[str, Race]: return { "Human": Race( name="Human", ability_modifiers={"All": 1}, traits=["Extra Language", "Extra Skill", "Versatile"], languages=["Common", "One other"] ), "Elf": Race( name="Elf", ability_modifiers={"Dexterity": 2}, traits=["Darkvision", "Keen Senses", "Fey Ancestry", "Trance"], languages=["Common", "Elvish"] ), "Dwarf": Race( name="Dwarf", ability_modifiers={"Constitution": 2}, traits=["Darkvision", "Dwarven Resilience", "Stonecunning"], languages=["Common", "Dwarvish"] ), "Halfling": Race( name="Halfling", ability_modifiers={"Dexterity": 2}, traits=["Lucky", "Brave", "Halfling Nimbleness"], languages=["Common", "Halfling"] ), "Dragonborn": Race( name="Dragonborn", ability_modifiers={"Strength": 2, "Charisma": 1}, traits=["Draconic Ancestry", "Breath Weapon", "Damage Resistance"], languages=["Common", "Draconic"] ), "Gnome": Race( name="Gnome", ability_modifiers={"Intelligence": 2}, traits=["Darkvision", "Gnome Cunning"], languages=["Common", "Gnomish"] ), "Half-Elf": Race( name="Half-Elf", ability_modifiers={"Charisma": 2, "Choice": 1}, traits=["Darkvision", "Fey Ancestry", "Two Skills"], languages=["Common", "Elvish", "One other"] ), "Half-Orc": Race( name="Half-Orc", ability_modifiers={"Strength": 2, "Constitution": 1}, traits=["Darkvision", "Relentless Endurance", "Savage Attacks"], languages=["Common", "Orc"] ), "Tiefling": Race( name="Tiefling", ability_modifiers={"Intelligence": 1, "Charisma": 2}, traits=["Darkvision", "Hellish Resistance", "Infernal Legacy"], languages=["Common", "Infernal"] ) } def _get_backgrounds(self) -> List[str]: return [ "Acolyte", "Criminal", "Folk Hero", "Noble", "Sage", "Soldier", "Charlatan", "Entertainer", "Guild Artisan", "Hermit", "Outlander", "Sailor" ] def roll_ability_scores(self) -> Dict[str, int]: """Roll 4d6, drop lowest, for each ability score""" abilities = {} for ability in ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"]: rolls = [random.randint(1, 6) for _ in range(4)] rolls.sort(reverse=True) abilities[ability] = sum(rolls[:3]) # Take top 3 return abilities def calculate_ability_modifier(self, score: int) -> int: """Calculate ability modifier from score""" return (score - 10) // 2 def calculate_hit_points(self, char_class: str, level: int, constitution_modifier: int) -> int: """Calculate hit points based on class, level, and CON modifier""" hit_die = self.classes[char_class].hit_die base_hp = hit_die + constitution_modifier # Max HP at level 1 # Add average HP for additional levels for _ in range(level - 1): base_hp += (hit_die // 2 + 1) + constitution_modifier return max(1, base_hp) # Minimum 1 HP def apply_racial_modifiers(self, base_abilities: Dict[str, int], race: str) -> Dict[str, int]: """Apply racial ability score modifiers""" modified_abilities = base_abilities.copy() race_data = self.races[race] for ability, modifier in race_data.ability_modifiers.items(): if ability == "All": for ability_name in modified_abilities: modified_abilities[ability_name] += modifier elif ability == "Choice": # For simplicity, add to lowest score lowest_ability = min(modified_abilities, key=modified_abilities.get) modified_abilities[lowest_ability] += modifier else: modified_abilities[ability] += modifier return modified_abilities # ===== IMAGE GENERATION ===== def generate_image(prompt: str) -> str: """Generate image using OpenAI DALL-E""" try: from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) response = client.images.generate( model="dall-e-3", prompt=prompt, size="1024x1024", quality="standard", n=1, ) return response.data[0].url except Exception as e: logger.error(f"Image generation failed: {e}") return "https://via.placeholder.com/512x512/dc2626/ffffff?text=Image+Generation+Failed" # ===== MAIN INTERFACE ===== def create_main_interface(): """Create the main D&D Campaign Manager interface""" # Initialize agents dm_agent = DungeonMasterAgent() npc_agent = NPCAgent() world_agent = WorldBuilderAgent() loot_agent = LootMasterAgent() character_creator = CharacterCreator() custom_css = """ .agent-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 15px; margin: 10px 0; color: white; } .output-box { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 10px; padding: 15px; margin: 10px 0; } """ with gr.Blocks(css=custom_css, title="D&D Campaign Manager") as demo: gr.Markdown(""" # 🏰 Advanced D&D Campaign Manager *Your AI-powered toolkit for epic adventures* **Features:** - 🎭 AI Dungeon Master Assistant - 👥 Intelligent NPC Creator & Roleplay - 🗺️ World Builder Agent - 💰 Loot Master & Magic Item Designer - 🐉 Enhanced Character Creator """) with gr.Tabs(): # ===== CHARACTER CREATOR TAB ===== with gr.TabItem("🐉 Character Creator"): with gr.Row(): with gr.Column(scale=2): gr.Markdown("## 📝 Character Details") character_name = gr.Textbox(label="Character Name", placeholder="Enter name...") with gr.Row(): race_dropdown = gr.Dropdown( choices=list(character_creator.races.keys()), label="Race", value="Human" ) class_dropdown = gr.Dropdown( choices=list(character_creator.classes.keys()), label="Class", value="Fighter" ) gender_dropdown = gr.Dropdown( choices=["Male", "Female", "Non-binary", "Transgender Male", "Transgender Female", "Genderfluid", "Agender", "Other"], label="Gender", value="Male" ) with gr.Row(): level_slider = gr.Slider(minimum=1, maximum=20, step=1, value=1, label="Level") alignment_dropdown = gr.Dropdown( choices=[alignment.value for alignment in Alignment], label="Alignment", value=Alignment.LAWFUL_GOOD.value ) background_dropdown = gr.Dropdown( choices=character_creator._get_backgrounds(), label="Background", value="Folk Hero" ) # AI Enhancement Buttons with gr.Row(): generate_name_btn = gr.Button("✨ AI Generate Name", variant="secondary") generate_backstory_btn = gr.Button("📚 AI Generate Backstory", variant="secondary") backstory = gr.Textbox(label="Backstory", lines=4, placeholder="Character background...") # Ability Scores gr.Markdown("## 🎲 Ability Scores") roll_btn = gr.Button("🎲 Roll Ability Scores", variant="primary") with gr.Row(): str_score = gr.Number(label="Strength", value=10, precision=0) dex_score = gr.Number(label="Dexterity", value=10, precision=0) con_score = gr.Number(label="Constitution", value=10, precision=0) with gr.Row(): int_score = gr.Number(label="Intelligence", value=10, precision=0) wis_score = gr.Number(label="Wisdom", value=10, precision=0) cha_score = gr.Number(label="Charisma", value=10, precision=0) with gr.Column(scale=1): gr.Markdown("## 📊 Character Summary") character_summary = gr.Markdown("*Create character to see summary*") gr.Markdown("## 🎨 Character Portrait") portrait_btn = gr.Button("🎨 Generate AI Portrait", variant="primary") character_portrait = gr.Image(label="Portrait", height=300) gr.Markdown("## 💾 Export") export_btn = gr.Button("📥 Export Character") export_file = gr.File(label="Character JSON") # Hidden state character_data = gr.State() # ===== CAMPAIGN CREATOR TAB ===== with gr.TabItem("🎭 AI Dungeon Master"): gr.Markdown("## �� Campaign Generation", elem_classes=["agent-card"]) with gr.Row(): with gr.Column(): campaign_theme = gr.Dropdown( choices=["High Fantasy", "Dark Fantasy", "Urban Fantasy", "Steampunk", "Horror", "Comedy", "Political Intrigue", "Exploration"], label="Campaign Theme", value="High Fantasy" ) campaign_level = gr.Slider(minimum=1, maximum=20, value=5, label="Starting Level") player_count = gr.Slider(minimum=1, maximum=8, value=4, label="Number of Players") generate_campaign_btn = gr.Button("🎲 Generate Campaign Concept", variant="primary", size="lg") with gr.Column(): campaign_visual_btn = gr.Button("🖼️ Generate Campaign Art") campaign_image = gr.Image(label="Campaign Visual", height=300) campaign_output = gr.Textbox(label="Campaign Concept", lines=10, elem_classes=["output-box"]) gr.Markdown("## 📅 Session Planning") with gr.Row(): session_number = gr.Number(label="Session Number", value=1, precision=0) generate_session_btn = gr.Button("📝 Generate Session Content", variant="secondary") session_output = gr.Textbox(label="Session Content", lines=8, elem_classes=["output-box"]) # ===== NPC CREATOR TAB ===== with gr.TabItem("👥 NPC Agent"): gr.Markdown("## 🎭 NPC Creator & Roleplay Assistant", elem_classes=["agent-card"]) with gr.Row(): with gr.Column(): gr.Markdown("### Create New NPC") npc_context = gr.Textbox(label="Campaign/Scene Context", placeholder="Describe the setting or situation...") npc_role = gr.Dropdown( choices=["Ally", "Neutral", "Antagonist", "Quest Giver", "Merchant", "Authority Figure", "Mysterious Stranger"], label="NPC Role", value="Neutral" ) npc_importance = gr.Dropdown( choices=["Minor", "Moderate", "Major", "Recurring"], label="Importance Level", value="Moderate" ) create_npc_btn = gr.Button("🎭 Generate NPC", variant="primary") npc_portrait_btn = gr.Button("🎨 Generate NPC Portrait") npc_portrait = gr.Image(label="NPC Portrait", height=300) with gr.Column(): npc_output = gr.Textbox(label="Generated NPC", lines=12, elem_classes=["output-box"]) gr.Markdown("### 💬 NPC Roleplay") with gr.Row(): with gr.Column(scale=2): player_input = gr.Textbox(label="Player Action/Dialogue", placeholder="What does the player say or do?") roleplay_context = gr.Textbox(label="Scene Context", placeholder="Describe the current situation...") with gr.Column(scale=1): roleplay_btn = gr.Button("🎭 NPC Response", variant="secondary", size="lg") npc_response = gr.Textbox(label="NPC Response", lines=4, elem_classes=["output-box"]) # Hidden states for NPC current_npc_data = gr.State() # ===== WORLD BUILDER TAB ===== with gr.TabItem("🗺️ World Builder"): gr.Markdown("## 🏗️ Location & World Generator", elem_classes=["agent-card"]) with gr.Row(): with gr.Column(): location_type = gr.Dropdown( choices=["Tavern", "Dungeon", "City", "Forest", "Castle", "Temple", "Shop", "Wilderness", "Mansion", "Cave System"], label="Location Type", value="Tavern" ) location_theme = gr.Dropdown( choices=["Standard Fantasy", "Dark/Gothic", "Mystical", "Abandoned/Ruined", "Luxurious", "Dangerous", "Peaceful", "Mysterious"], label="Theme", value="Standard Fantasy" ) location_purpose = gr.Textbox(label="Purpose in Campaign", placeholder="How will this location be used?") generate_location_btn = gr.Button("🏗️ Generate Location", variant="primary") location_art_btn = gr.Button("🖼️ Generate Location Art") with gr.Column(): location_image = gr.Image(label="Location Visual", height=300) location_output = gr.Textbox(label="Location Details", lines=12, elem_classes=["output-box"]) # ===== LOOT MASTER TAB ===== with gr.TabItem("💰 Loot Master"): gr.Markdown("## 💎 Treasure & Magic Item Generator", elem_classes=["agent-card"]) with gr.Row(): with gr.Column(): gr.Markdown("### 🎲 Loot Table Generator") loot_level = gr.Slider(minimum=1, maximum=20, value=5, label="Party Level") encounter_type = gr.Dropdown( choices=["Boss Fight", "Mini-boss", "Standard Combat", "Exploration Reward", "Quest Completion", "Treasure Hoard"], label="Encounter Type", value="Standard Combat" ) loot_rarity = gr.Dropdown( choices=["Poor", "Standard", "Rich", "Legendary"], label="Treasure Quality", value="Standard" ) generate_loot_btn = gr.Button("💰 Generate Loot Table", variant="primary") with gr.Column(): gr.Markdown("### ✨ Custom Magic Item") item_concept = gr.Textbox(label="Item Concept", placeholder="What kind of item? (sword, ring, staff, etc.)") power_level = gr.Dropdown( choices=["Common", "Uncommon", "Rare", "Very Rare", "Legendary", "Artifact"], label="Power Level", value="Uncommon" ) campaign_theme_item = gr.Textbox(label="Campaign Theme", placeholder="How should it fit your campaign?") create_item_btn = gr.Button("✨ Create Magic Item", variant="secondary") item_art_btn = gr.Button("🎨 Generate Item Art") with gr.Row(): loot_output = gr.Textbox(label="Loot Table", lines=8, elem_classes=["output-box"]) magic_item_output = gr.Textbox(label="Magic Item Details", lines=8, elem_classes=["output-box"]) item_image = gr.Image(label="Magic Item Visual", height=250) # ===== CAMPAIGN TOOLS TAB ===== with gr.TabItem("🛠️ Campaign Tools"): gr.Markdown("## 🎯 Advanced Campaign Management") with gr.Tabs(): with gr.TabItem("📊 Initiative Tracker"): gr.Markdown("### ⚔️ Combat Management") with gr.Row(): add_character = gr.Textbox(label="Character Name", placeholder="Add to initiative...") add_initiative = gr.Number(label="Initiative Roll", value=10) add_btn = gr.Button("➕ Add to Initiative") initiative_list = gr.Dataframe( headers=["Name", "Initiative", "HP", "AC", "Status"], label="Initiative Order", interactive=True ) with gr.Row(): next_turn_btn = gr.Button("⏭️ Next Turn", variant="primary") reset_combat_btn = gr.Button("🔄 Reset Combat") with gr.TabItem("📝 Session Notes"): gr.Markdown("### 📖 Session Management") session_date = gr.Textbox(label="Session Date", value="Session 1") with gr.Row(): with gr.Column(): key_events = gr.Textbox(label="Key Events", lines=4, placeholder="What happened this session?") npc_interactions = gr.Textbox(label="NPC Interactions", lines=3, placeholder="Who did they meet?") with gr.Column(): player_actions = gr.Textbox(label="Notable Player Actions", lines=4, placeholder="What did the players do?") next_session_prep = gr.Textbox(label="Next Session Prep", lines=3, placeholder="What to prepare for next time?") save_notes_btn = gr.Button("💾 Save Session Notes", variant="primary") notes_output = gr.File(label="Session Notes File") with gr.TabItem("🎲 Random Generators"): gr.Markdown("### 🎯 Quick Generators") with gr.Row(): with gr.Column(): gr.Markdown("**Name Generators**") name_type = gr.Dropdown( choices=["Human Male", "Human Female", "Elven", "Dwarven", "Orcish", "Fantasy Place", "Tavern", "Shop"], value="Human Male" ) gen_name_btn = gr.Button("🎲 Generate Name") random_name = gr.Textbox(label="Generated Name") with gr.Column(): gr.Markdown("**Quick Encounters**") encounter_level = gr.Slider(1, 20, value=5, label="Party Level") encounter_difficulty = gr.Dropdown( choices=["Easy", "Medium", "Hard", "Deadly"], value="Medium" ) gen_encounter_btn = gr.Button("⚔️ Generate Encounter") random_encounter = gr.Textbox(label="Encounter", lines=3) with gr.Row(): with gr.Column(): gr.Markdown("**Plot Hooks**") hook_theme = gr.Dropdown( choices=["Mystery", "Adventure", "Political", "Personal", "Rescue", "Exploration"], value="Adventure" ) gen_hook_btn = gr.Button("🎣 Generate Plot Hook") plot_hook = gr.Textbox(label="Plot Hook", lines=2) with gr.Column(): gr.Markdown("**Weather & Atmosphere**") climate = gr.Dropdown( choices=["Temperate", "Tropical", "Arctic", "Desert", "Mountainous"], value="Temperate" ) gen_weather_btn = gr.Button("🌤️ Generate Weather") weather = gr.Textbox(label="Weather & Mood", lines=2) # ===== EVENT HANDLERS ===== # Character Creator Events def roll_abilities(): abilities = character_creator.roll_ability_scores() return ( abilities["Strength"], abilities["Dexterity"], abilities["Constitution"], abilities["Intelligence"], abilities["Wisdom"], abilities["Charisma"] ) def generate_character_name(race, char_class, gender, alignment): try: from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"Generate a {gender} {race} {char_class} name appropriate for D&D. Return only the name." response = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], max_tokens=30 ) return response.choices[0].message.content.strip() except Exception as e: # Fallback to simple name generation names = { "Human": { "Male": ["Garrett", "Marcus", "Thomas", "William", "James"], "Female": ["Elena", "Sarah", "Miranda", "Catherine", "Rose"], "Non-binary": ["Alex", "Jordan", "Riley", "Casey", "Taylor"], "Other": ["River", "Sage", "Phoenix", "Ash", "Rowan"] }, "Elf": { "Male": ["Aelar", "Berrian", "Drannor", "Enna", "Galinndan"], "Female": ["Adrie", "Althaea", "Anastrianna", "Andraste", "Antinua"], "Non-binary": ["Aerdyl", "Ahvir", "Aramil", "Aranea", "Berris"], "Other": ["Dayereth", "Enna", "Galinndan", "Hadarai", "Halimath"] }, "Dwarf": { "Male": ["Adrik", "Baern", "Darrak", "Delg", "Eberk"], "Female": ["Amber", "Bardryn", "Diesa", "Eldeth", "Gunnloda"], "Non-binary": ["Alberich", "Balin", "Dain", "Fundin", "Gloín"], "Other": ["Hávar", "Kíli", "Náli", "Ori", "Thorek"] }, "Halfling": { "Male": ["Alton", "Ander", "Cade", "Corrin", "Eldon"], "Female": ["Andry", "Bree", "Callie", "Cora", "Euphemia"], "Non-binary": ["Finnan", "Garret", "Lindal", "Lyle", "Merric"], "Other": ["Nedda", "Paela", "Portia", "Seraphina", "Shaena"] } } # Handle various gender identities gender_key = gender if gender in ["Transgender Male", "Male"]: gender_key = "Male" elif gender in ["Transgender Female", "Female"]: gender_key = "Female" elif gender in ["Non-binary", "Genderfluid", "Agender"]: gender_key = "Non-binary" else: gender_key = "Other" race_names = names.get(race, names["Human"]) return random.choice(race_names.get(gender_key, race_names["Other"])) def generate_character_backstory(name, race, char_class, gender, alignment, background): try: from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"""Create a compelling backstory for {name}, a {gender} {race} {char_class} with {alignment} alignment and {background} background. Write 2-3 paragraphs about their origins, motivations, and key life events. Be respectful and inclusive in your portrayal.""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], max_tokens=300 ) return response.choices[0].message.content.strip() except Exception as e: return f"{name} is a {gender} {race} {char_class} with a {background} background. Their journey began in their homeland, where they learned the skills that would define their path as an adventurer, embracing their identity and forging their own destiny." def generate_character_portrait(character_data): if not character_data: return "Please create a character first" prompt = f"Fantasy portrait of a {character_data.gender.lower()} {character_data.race.lower()} {character_data.character_class.lower()}, professional D&D character art, respectful and inclusive representation" return generate_image(prompt) def update_character_summary(name, race, char_class, level, gender, alignment, str_val, dex_val, con_val, int_val, wis_val, cha_val, background, backstory): if not all([str_val, dex_val, con_val, int_val, wis_val, cha_val]): return "*Roll ability scores to see summary*", None base_abilities = { "Strength": int(str_val), "Dexterity": int(dex_val), "Constitution": int(con_val), "Intelligence": int(int_val), "Wisdom": int(wis_val), "Charisma": int(cha_val) } final_abilities = character_creator.apply_racial_modifiers(base_abilities, race) con_modifier = character_creator.calculate_ability_modifier(final_abilities["Constitution"]) hit_points = character_creator.calculate_hit_points(char_class, int(level), con_modifier) character = Character( name=name or "Unnamed Character", race=race, character_class=char_class, level=int(level), gender=gender, alignment=Alignment(alignment), abilities=final_abilities, hit_points=hit_points, skills=character_creator.classes[char_class].skills[:2], background=background, backstory=backstory ) summary = f"""**{character.name}** *Level {level} {gender} {race} {char_class} ({alignment})* **Ability Scores:** - STR: {final_abilities['Strength']} ({character_creator.calculate_ability_modifier(final_abilities['Strength']):+d}) - DEX: {final_abilities['Dexterity']} ({character_creator.calculate_ability_modifier(final_abilities['Dexterity']):+d}) - CON: {final_abilities['Constitution']} ({character_creator.calculate_ability_modifier(final_abilities['Constitution']):+d}) - INT: {final_abilities['Intelligence']} ({character_creator.calculate_ability_modifier(final_abilities['Intelligence']):+d}) - WIS: {final_abilities['Wisdom']} ({character_creator.calculate_ability_modifier(final_abilities['Wisdom']):+d}) - CHA: {final_abilities['Charisma']} ({character_creator.calculate_ability_modifier(final_abilities['Charisma']):+d}) **Combat Stats:** - Hit Points: {hit_points} - Hit Die: d{character_creator.classes[char_class].hit_die} **Background:** {background}""" return summary, character # Campaign Events def generate_campaign_concept(theme, level, players): result = dm_agent.generate_campaign_concept(theme, level, players) return result.get("content", "Error generating campaign") def generate_campaign_art(theme, level): prompt = f"{theme} D&D campaign art for level {level} adventurers, epic fantasy illustration" return generate_image(prompt) def generate_session_content(campaign_output, session_num): if not campaign_output: return "Please generate a campaign concept first" result = dm_agent.generate_session_content(campaign_output, session_num) return result.get("content", "Error generating session") # NPC Events def create_npc(context, role, importance): result = npc_agent.generate_npc(context, role, importance) return result.get("content", "Error creating NPC") def generate_npc_portrait(npc_data): if not npc_data: return "Create an NPC first" prompt = "Fantasy portrait of a D&D NPC character, detailed face, professional RPG art" return generate_image(prompt) def npc_roleplay_response(npc_data, player_input, context): if not npc_data or not player_input: return "Please create an NPC and enter player input" result = npc_agent.roleplay_npc(npc_data, player_input, context) return result.get("content", "Error in roleplay") # World Builder Events def create_location(loc_type, theme, purpose): result = world_agent.generate_location(loc_type, theme, purpose) return result.get("content", "Error creating location") def generate_location_art(loc_type, theme): prompt = f"{theme} {loc_type} fantasy location, detailed environment art, D&D setting" return generate_image(prompt) # Loot Events def create_loot_table(level, encounter, rarity): result = loot_agent.generate_loot_table(level, encounter, rarity) return result.get("content", "Error creating loot") def create_magic_item(concept, power, theme): result = loot_agent.create_custom_magic_item(concept, power, theme) return result.get("content", "Error creating item") def generate_item_art(concept, power): prompt = f"{power} magical {concept}, fantasy item illustration, glowing magical effects" return generate_image(prompt) # Random Generator Events def generate_random_name(name_type): names = { "Human Male": ["Garrett", "Marcus", "Thomas", "William", "James"], "Human Female": ["Elena", "Sarah", "Miranda", "Catherine", "Rose"], "Elven": ["Aelar", "Berrian", "Drannor", "Enna", "Galinndan"], "Dwarven": ["Adrik", "Baern", "Darrak", "Delg", "Eberk"], "Orcish": ["Grul", "Thark", "Dench", "Feng", "Gell"], "Fantasy Place": ["Ravenshollow", "Goldbrook", "Thornfield", "Mistral Keep"], "Tavern": ["The Prancing Pony", "Dragon's Rest", "The Silver Tankard"], "Shop": ["Magical Mysteries", "Fine Blades & More", "Potions & Remedies"] } return random.choice(names.get(name_type, ["Unknown"])) def generate_random_encounter(level, difficulty): encounters = [ f"Bandit ambush (adapted for level {level})", f"Wild animal encounter (CR {max(1, level//4)})", f"Mysterious traveler with a quest", f"Ancient ruins with {difficulty.lower()} traps", f"Rival adventuring party", f"Magical phenomenon requiring investigation" ] return random.choice(encounters) def generate_plot_hook(theme): hooks = { "Mystery": "A beloved local figure has vanished without a trace, leaving behind only cryptic clues.", "Adventure": "Ancient maps surface pointing to a legendary treasure thought lost forever.", "Political": "A diplomatic envoy requests secret protection during dangerous negotiations.", "Personal": "A character's past catches up with them in an unexpected way.", "Rescue": "Innocent people are trapped in a dangerous situation and need immediate help.", "Exploration": "Uncharted territories beckon with promises of discovery and danger." } return hooks.get(theme, "A mysterious stranger approaches with an urgent request.") def generate_weather(climate): weather_options = { "Temperate": ["Sunny and mild", "Light rain showers", "Overcast skies", "Gentle breeze"], "Tropical": ["Hot and humid", "Sudden thunderstorm", "Sweltering heat", "Monsoon rains"], "Arctic": ["Bitter cold winds", "Heavy snowfall", "Blizzard conditions", "Icy fog"], "Desert": ["Scorching sun", "Sandstorm approaching", "Cool desert night", "Rare rainfall"], "Mountainous": ["Mountain mist", "Alpine winds", "Rocky terrain", "Sudden weather change"] } return random.choice(weather_options.get(climate, ["Pleasant weather"])) # Wire up all events roll_btn.click(roll_abilities, outputs=[str_score, dex_score, con_score, int_score, wis_score, cha_score]) generate_name_btn.click( generate_character_name, inputs=[race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown], outputs=[character_name] ) generate_backstory_btn.click( generate_character_backstory, inputs=[character_name, race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown, background_dropdown], outputs=[backstory] ) portrait_btn.click( generate_character_portrait, inputs=[character_data], outputs=[character_portrait] ) # Update character summary when inputs change for component in [character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown, str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory]: component.change( update_character_summary, inputs=[character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown, str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory], outputs=[character_summary, character_data] ) # Campaign events generate_campaign_btn.click( generate_campaign_concept, inputs=[campaign_theme, campaign_level, player_count], outputs=[campaign_output] ) campaign_visual_btn.click( generate_campaign_art, inputs=[campaign_theme, campaign_level], outputs=[campaign_image] ) generate_session_btn.click( generate_session_content, inputs=[campaign_output, session_number], outputs=[session_output] ) # NPC events create_npc_btn.click( create_npc, inputs=[npc_context, npc_role, npc_importance], outputs=[npc_output] ) npc_portrait_btn.click( generate_npc_portrait, inputs=[current_npc_data], outputs=[npc_portrait] ) roleplay_btn.click( npc_roleplay_response, inputs=[npc_output, player_input, roleplay_context], outputs=[npc_response] ) # World builder events generate_location_btn.click( create_location, inputs=[location_type, location_theme, location_purpose], outputs=[location_output] ) location_art_btn.click( generate_location_art, inputs=[location_type, location_theme], outputs=[location_image] ) # Loot events generate_loot_btn.click( create_loot_table, inputs=[loot_level, encounter_type, loot_rarity], outputs=[loot_output] ) create_item_btn.click( create_magic_item, inputs=[item_concept, power_level, campaign_theme_item], outputs=[magic_item_output] ) item_art_btn.click( generate_item_art, inputs=[item_concept, power_level], outputs=[item_image] ) # Random generator events gen_name_btn.click(generate_random_name, inputs=[name_type], outputs=[random_name]) gen_encounter_btn.click(generate_random_encounter, inputs=[encounter_level, encounter_difficulty], outputs=[random_encounter]) gen_hook_btn.click(generate_plot_hook, inputs=[hook_theme], outputs=[plot_hook]) gen_weather_btn.click(generate_weather, inputs=[climate], outputs=[weather]) return demo # Main entry point if __name__ == "__main__": logger.info("🏰 Starting Advanced D&D Campaign Manager...") # Check for OpenAI API key api_key = os.getenv("OPENAI_API_KEY") if api_key: logger.info("✅ OpenAI API key found - All AI features enabled!") else: logger.warning("⚠️ No OpenAI API key found - AI features will be limited") logger.info("💡 Add OPENAI_API_KEY=your_key to a .env file to enable all AI features") demo = create_main_interface() try: demo.launch( share=True, server_name="0.0.0.0", server_port=7860, inbrowser=True ) logger.info("🌐 App launched successfully!") logger.info("🔗 Local URL: http://localhost:7860") except Exception as e: logger.error(f"❌ Launch failed: {e}") logger.info("💡 Try: pip install --upgrade gradio")