# app.py - Flask D&D Campaign Manager for HuggingFace Spaces import os import logging from flask import Flask, render_template, request, jsonify, send_file import json import tempfile import random from datetime import datetime from typing import Dict, List, Optional from dataclasses import dataclass, asdict from enum import Enum # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # ===== D&D 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 Character: name: str race: str character_class: str level: int gender: str alignment: str abilities: Dict[str, int] hit_points: int background: str backstory: str # ===== AI AGENT CLASSES ===== class DungeonMasterAgent: def generate_campaign_concept(self, theme: str, level: int, players: int) -> Dict: try: if os.getenv("OPENAI_API_KEY"): from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"""Create a {theme} D&D campaign for {players} level {level} characters. Include: Campaign name, plot hook, main antagonist, 3 key locations, central conflict.""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], max_tokens=500 ) return {"success": True, "content": response.choices[0].message.content} else: # Fallback content return { "success": True, "content": f"**{theme} Campaign**\n\nA thrilling adventure awaits {players} brave heroes starting at level {level}. Ancient mysteries, dangerous foes, and legendary treasures lie ahead in this epic campaign designed to challenge and inspire." } except Exception as e: logger.error(f"Campaign generation failed: {e}") return {"success": False, "error": str(e)} def generate_session_content(self, campaign_context: str, session_number: int) -> Dict: try: if os.getenv("OPENAI_API_KEY"): 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. Cliffhanger ending options""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], max_tokens=600 ) return {"success": True, "content": response.choices[0].message.content} else: return { "success": True, "content": f"**Session {session_number}**\n\nThe adventure continues with new challenges, mysterious NPCs, and exciting encounters designed to test the heroes' resolve." } except Exception as e: logger.error(f"Session generation failed: {e}") return {"success": False, "error": str(e)} class NPCAgent: def generate_npc(self, context: str, role: str, importance: str, gender: str = "") -> Dict: try: if os.getenv("OPENAI_API_KEY"): from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"""Create a {importance} {role} NPC for: {context} Gender: {gender if gender else 'Choose appropriate'} Include: Name, personality, background, motivation, physical description, speech patterns.""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], max_tokens=400 ) return {"success": True, "content": response.choices[0].message.content} else: return { "success": True, "content": f"**{gender if gender else 'Character'} {role}**\n\nA {importance.lower()} NPC perfect for your campaign context: {context}. This character brings depth and intrigue to your world." } except Exception as e: logger.error(f"NPC generation failed: {e}") return {"success": False, "error": str(e)} def roleplay_npc(self, npc_description: str, player_input: str, context: str) -> Dict: try: if os.getenv("OPENAI_API_KEY"): 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)""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], max_tokens=200 ) return {"success": True, "content": response.choices[0].message.content} else: return { "success": True, "content": f"The NPC responds thoughtfully to your action: '{player_input}'. Their reaction fits their personality and the current situation." } except Exception as e: logger.error(f"NPC roleplay failed: {e}") return {"success": False, "error": str(e)} class WorldBuilderAgent: def generate_location(self, location_type: str, theme: str, purpose: str) -> Dict: try: if os.getenv("OPENAI_API_KEY"): 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""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], max_tokens=500 ) return {"success": True, "content": response.choices[0].message.content} else: return { "success": True, "content": f"**{theme} {location_type}**\n\nA {theme.lower()} {location_type.lower()} designed for {purpose}. This location features multiple areas to explore, interesting NPCs to meet, and secrets to uncover." } except Exception as e: logger.error(f"Location generation failed: {e}") return {"success": False, "error": str(e)} class LootMasterAgent: def generate_loot_table(self, level: int, encounter_type: str, rarity: str) -> Dict: try: if os.getenv("OPENAI_API_KEY"): 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""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], max_tokens=300 ) return {"success": True, "content": response.choices[0].message.content} else: return { "success": True, "content": f"**Level {level} {encounter_type} Loot ({rarity})**\n\nGold: {level * 10}-{level * 20} gp\nItems: Health potions, basic equipment\nSpecial: One rare item appropriate for the encounter" } except Exception as e: logger.error(f"Loot generation failed: {e}") return {"success": False, "error": str(e)} def create_custom_magic_item(self, item_concept: str, power_level: str, campaign_theme: str) -> Dict: try: if os.getenv("OPENAI_API_KEY"): 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""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], max_tokens=400 ) return {"success": True, "content": response.choices[0].message.content} else: return { "success": True, "content": f"**{item_concept.title()} of {campaign_theme}**\n\nRarity: {power_level}\nA magical {item_concept} imbued with the essence of {campaign_theme}. This item grants special abilities and carries ancient power." } except Exception as e: logger.error(f"Magic item creation failed: {e}") return {"success": False, "error": str(e)} class CharacterCreator: def __init__(self): self.classes = { "Fighter": {"hit_die": 10}, "Wizard": {"hit_die": 6}, "Rogue": {"hit_die": 8}, "Cleric": {"hit_die": 8}, "Barbarian": {"hit_die": 12}, "Bard": {"hit_die": 8}, "Druid": {"hit_die": 8}, "Monk": {"hit_die": 8}, "Paladin": {"hit_die": 10}, "Ranger": {"hit_die": 10}, "Sorcerer": {"hit_die": 6}, "Warlock": {"hit_die": 8} } def roll_ability_scores(self) -> Dict[str, int]: 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]) return abilities def generate_image(prompt: str) -> str: """Generate image URL (placeholder for AI image generation)""" try: if os.getenv("OPENAI_API_KEY"): 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 else: # Placeholder image return "https://via.placeholder.com/512x512/7c3aed/ffffff?text=AI+Image" except Exception as e: logger.error(f"Image generation failed: {e}") return "https://via.placeholder.com/512x512/dc2626/ffffff?text=Image+Generation+Failed" # ===== FLASK APP SETUP ===== app = Flask(__name__) app.secret_key = os.getenv('SECRET_KEY', 'your-secret-key-for-hf-spaces') # Initialize components dm_agent = DungeonMasterAgent() npc_agent = NPCAgent() world_agent = WorldBuilderAgent() loot_agent = LootMasterAgent() character_creator = CharacterCreator() @app.route('/') def index(): """Serve the main page""" return render_template('index.html') # ===== API ROUTES ===== @app.route('/api/character/roll-abilities', methods=['POST']) def roll_abilities(): try: abilities = character_creator.roll_ability_scores() return jsonify({'success': True, 'abilities': abilities}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/character/generate-name', methods=['POST']) def generate_character_name(): try: data = request.json race = data.get('race', 'Human') gender = data.get('gender', 'Male') # Try AI generation first if os.getenv("OPENAI_API_KEY"): try: from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"Generate a {gender} {race} 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 ) name = response.choices[0].message.content.strip() return jsonify({'success': True, 'name': name}) except: pass # Fall through to fallback # Fallback name generation names = { "Human": {"Male": ["Garrett", "Marcus", "Thomas"], "Female": ["Elena", "Sarah", "Miranda"]}, "Elf": {"Male": ["Aelar", "Berrian", "Drannor"], "Female": ["Adrie", "Althaea", "Anastrianna"]}, "Dwarf": {"Male": ["Adrik", "Baern", "Darrak"], "Female": ["Amber", "Bardryn", "Diesa"]} } race_names = names.get(race, names["Human"]) gender_key = "Male" if gender in ["Male", "Transgender Male"] else "Female" name_list = race_names.get(gender_key, race_names["Male"]) return jsonify({'success': True, 'name': random.choice(name_list)}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/character/generate-backstory', methods=['POST']) def generate_character_backstory(): try: data = request.json name = data.get('name', 'Character') race = data.get('race', 'Human') char_class = data.get('class', 'Fighter') background = data.get('background', 'Folk Hero') # Try AI generation first if os.getenv("OPENAI_API_KEY"): try: from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) prompt = f"""Create a compelling backstory for {name}, a {race} {char_class} with a {background} background. Write 2-3 paragraphs about their origins, motivations, and key life events.""" response = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], max_tokens=300 ) backstory = response.choices[0].message.content.strip() return jsonify({'success': True, 'backstory': backstory}) except: pass # Fall through to fallback # Fallback backstory backstory = f"{name} is a {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 destiny with courage and determination." return jsonify({'success': True, 'backstory': backstory}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/character/generate-portrait', methods=['POST']) def generate_character_portrait(): try: data = request.json race = data.get('race', 'Human') char_class = data.get('class', 'Fighter') gender = data.get('gender', 'Male') prompt = f"Fantasy portrait of a {gender.lower()} {race.lower()} {char_class.lower()}, professional D&D character art" image_url = generate_image(prompt) return jsonify({'success': True, 'image_url': image_url}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/campaign/generate-concept', methods=['POST']) def generate_campaign_concept(): try: data = request.json theme = data.get('theme', 'High Fantasy') level = data.get('level', 5) players = data.get('players', 4) result = dm_agent.generate_campaign_concept(theme, level, players) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/campaign/generate-session', methods=['POST']) def generate_session_content(): try: data = request.json campaign_context = data.get('campaign_context', 'General D&D campaign') session_number = data.get('session_number', 1) result = dm_agent.generate_session_content(campaign_context, session_number) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/campaign/generate-art', methods=['POST']) def generate_campaign_art(): try: data = request.json theme = data.get('theme', 'High Fantasy') level = data.get('level', 5) prompt = f"{theme} D&D campaign art for level {level} adventurers, epic fantasy illustration" image_url = generate_image(prompt) return jsonify({'success': True, 'image_url': image_url}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/npc/create', methods=['POST']) def create_npc(): try: data = request.json context = data.get('context', '') role = data.get('role', 'Neutral') importance = data.get('importance', 'Moderate') gender = data.get('gender', '') result = npc_agent.generate_npc(context, role, importance, gender) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/npc/roleplay', methods=['POST']) def npc_roleplay(): try: data = request.json npc_description = data.get('npc_description', '') player_input = data.get('player_input', '') context = data.get('context', '') result = npc_agent.roleplay_npc(npc_description, player_input, context) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/world/create-location', methods=['POST']) def create_location(): try: data = request.json location_type = data.get('type', 'Tavern') theme = data.get('theme', 'Standard Fantasy') purpose = data.get('purpose', '') result = world_agent.generate_location(location_type, theme, purpose) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/loot/generate-table', methods=['POST']) def generate_loot_table(): try: data = request.json level = data.get('level', 5) encounter_type = data.get('encounter_type', 'Standard Combat') rarity = data.get('rarity', 'Standard') result = loot_agent.generate_loot_table(level, encounter_type, rarity) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/loot/create-magic-item', methods=['POST']) def create_magic_item(): try: data = request.json concept = data.get('concept', '') power_level = data.get('power_level', 'Uncommon') theme = data.get('theme', '') result = loot_agent.create_custom_magic_item(concept, power_level, theme) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/random/name', methods=['POST']) def generate_random_name(): try: data = request.json name_type = data.get('type', 'Human Male') 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"], "Fantasy Place": ["Ravenshollow", "Goldbrook", "Thornfield", "Mistral Keep"], "Tavern": ["The Prancing Pony", "Dragon's Rest", "The Silver Tankard"] } selected_names = names.get(name_type, ["Unknown"]) random_name = random.choice(selected_names) return jsonify({'success': True, 'name': random_name}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/random/encounter', methods=['POST']) def generate_random_encounter(): try: data = request.json level = data.get('level', 5) difficulty = data.get('difficulty', 'Medium') encounters = [ f"Bandit ambush (adapted for level {level})", f"Wild animal encounter (CR {max(1, level//4)})", "Mysterious traveler with a quest", f"Ancient ruins with {difficulty.lower()} traps", "Rival adventuring party", "Magical phenomenon requiring investigation" ] random_encounter = random.choice(encounters) return jsonify({'success': True, 'encounter': random_encounter}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/random/plot-hook', methods=['POST']) def generate_plot_hook(): try: data = request.json theme = data.get('theme', 'Adventure') 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." } hook = hooks.get(theme, "A mysterious stranger approaches with an urgent request.") return jsonify({'success': True, 'hook': hook}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/random/weather', methods=['POST']) def generate_weather(): try: data = request.json climate = data.get('climate', 'Temperate') 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"] } weathers = weather_options.get(climate, ["Pleasant weather"]) random_weather = random.choice(weathers) return jsonify({'success': True, 'weather': random_weather}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/health') def health_check(): """Health check endpoint for HF Spaces""" return jsonify({"status": "healthy", "service": "D&D Campaign Manager"}) if __name__ == '__main__': # HuggingFace Spaces requires port 7860 port = int(os.environ.get('PORT', 7860)) app.run(host='0.0.0.0', port=port, debug=False)