Spaces:
Running
Running
# 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" | |
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() | |
def index(): | |
"""Serve the main page""" | |
return render_template('index.html') | |
# ===== API ROUTES ===== | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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) |