ghost-logic's picture
Create app.py
1c99143 verified
# 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)