MelkortheCorrupt commited on
Commit
237e002
Β·
1 Parent(s): 409063d

Initial D&D Campaign Manager

Browse files
Files changed (2) hide show
  1. app(local).py +1274 -0
  2. app.py +215 -51
app(local).py ADDED
@@ -0,0 +1,1274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py - Fixed D&D Campaign and Character Creator with AI Agents
2
+
3
+ import gradio as gr
4
+ import logging
5
+ from typing import Dict, List, Tuple, Optional
6
+ import json
7
+ import os
8
+ from dotenv import load_dotenv
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+ import random
12
+
13
+ # Load environment variables
14
+ load_dotenv()
15
+
16
+ # Set up logging
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Load OpenAI API key
21
+ try:
22
+ import openai
23
+ api_key = os.getenv("OPENAI_API_KEY")
24
+ if api_key:
25
+ openai.api_key = api_key
26
+ logger.info("βœ… OpenAI API key loaded")
27
+ else:
28
+ logger.warning("⚠️ No OpenAI API key found")
29
+ except ImportError:
30
+ logger.warning("⚠️ OpenAI package not installed")
31
+
32
+ # ===== DATA MODELS =====
33
+ class Alignment(Enum):
34
+ LAWFUL_GOOD = "Lawful Good"
35
+ NEUTRAL_GOOD = "Neutral Good"
36
+ CHAOTIC_GOOD = "Chaotic Good"
37
+ LAWFUL_NEUTRAL = "Lawful Neutral"
38
+ TRUE_NEUTRAL = "True Neutral"
39
+ CHAOTIC_NEUTRAL = "Chaotic Neutral"
40
+ LAWFUL_EVIL = "Lawful Evil"
41
+ NEUTRAL_EVIL = "Neutral Evil"
42
+ CHAOTIC_EVIL = "Chaotic Evil"
43
+
44
+ @dataclass
45
+ class CharacterClass:
46
+ name: str
47
+ hit_die: int
48
+ primary_ability: List[str]
49
+ saving_throws: List[str]
50
+ skills: List[str]
51
+
52
+ @dataclass
53
+ class Race:
54
+ name: str
55
+ ability_modifiers: Dict[str, int]
56
+ traits: List[str]
57
+ languages: List[str]
58
+
59
+ @dataclass
60
+ class Character:
61
+ name: str
62
+ race: str
63
+ character_class: str
64
+ level: int
65
+ gender: str
66
+ alignment: Alignment
67
+ abilities: Dict[str, int]
68
+ hit_points: int
69
+ skills: List[str]
70
+ background: str
71
+ backstory: str
72
+ portrait_url: Optional[str] = None
73
+
74
+ @dataclass
75
+ class Campaign:
76
+ name: str
77
+ theme: str
78
+ level_range: str
79
+ description: str
80
+ locations: List[str]
81
+ npcs: List[str]
82
+ plot_hooks: List[str]
83
+
84
+ @dataclass
85
+ class NPC:
86
+ name: str
87
+ race: str
88
+ occupation: str
89
+ personality: str
90
+ secret: str
91
+ voice_description: str
92
+ relationship_to_party: str
93
+
94
+ # ===== AI AGENT CLASSES =====
95
+ class DungeonMasterAgent:
96
+ """AI agent that acts as a Dungeon Master"""
97
+
98
+ def __init__(self):
99
+ self.personality = "Creative, fair, and engaging storyteller"
100
+ self.knowledge_areas = ["D&D rules", "storytelling", "world-building", "character development"]
101
+
102
+ def generate_campaign_concept(self, theme: str, level: int, player_count: int) -> Dict:
103
+ """Generate a complete campaign concept"""
104
+ try:
105
+ from openai import OpenAI
106
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
107
+
108
+ prompt = f"""As an experienced D&D Dungeon Master, create a campaign concept for:
109
+ Theme: {theme}
110
+ Player Level: {level}
111
+ Number of Players: {player_count}
112
+
113
+ Provide:
114
+ 1. Campaign Name
115
+ 2. Core Plot Hook (2-3 sentences)
116
+ 3. Main Antagonist
117
+ 4. 3 Key Locations
118
+ 5. Central Conflict
119
+ 6. Estimated Campaign Length
120
+ 7. Unique Elements/Mechanics
121
+
122
+ Make it engaging and ready to play!"""
123
+
124
+ response = client.chat.completions.create(
125
+ model="gpt-4",
126
+ messages=[{"role": "system", "content": "You are an expert D&D Dungeon Master with 20 years of experience creating memorable campaigns."},
127
+ {"role": "user", "content": prompt}],
128
+ max_tokens=500,
129
+ temperature=0.8
130
+ )
131
+
132
+ content = response.choices[0].message.content
133
+ return {"success": True, "content": content}
134
+
135
+ except Exception as e:
136
+ logger.error(f"Campaign generation failed: {e}")
137
+ return {"success": False, "error": str(e), "content": f"Mock Campaign: {theme} adventure for {player_count} level {level} characters"}
138
+
139
+ def generate_session_content(self, campaign_context: str, session_number: int) -> Dict:
140
+ """Generate content for a specific session"""
141
+ try:
142
+ from openai import OpenAI
143
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
144
+
145
+ prompt = f"""Create session {session_number} content for this campaign:
146
+ {campaign_context}
147
+
148
+ Generate:
149
+ 1. Session Opening (scene description)
150
+ 2. 3 Potential Encounters (combat, social, exploration)
151
+ 3. Key NPCs for this session
152
+ 4. Skill challenges or puzzles
153
+ 5. Potential plot developments
154
+ 6. Cliffhanger ending options
155
+
156
+ Make it detailed enough for a DM to run immediately."""
157
+
158
+ response = client.chat.completions.create(
159
+ model="gpt-4",
160
+ messages=[{"role": "system", "content": "You are a D&D Dungeon Master preparing detailed session content."},
161
+ {"role": "user", "content": prompt}],
162
+ max_tokens=600,
163
+ temperature=0.7
164
+ )
165
+
166
+ return {"success": True, "content": response.choices[0].message.content}
167
+
168
+ except Exception as e:
169
+ logger.error(f"Session generation failed: {e}")
170
+ return {"success": False, "error": str(e), "content": f"Mock Session {session_number}: Adventure continues..."}
171
+
172
+ class NPCAgent:
173
+ """AI agent specialized in creating and roleplaying NPCs"""
174
+
175
+ def __init__(self):
176
+ self.personality = "Versatile character actor with deep understanding of motivations"
177
+ self.specializations = ["Character creation", "Dialogue", "Motivations", "Voice acting"]
178
+
179
+ def generate_npc(self, context: str, role: str, importance: str) -> Dict:
180
+ """Generate a detailed NPC"""
181
+ try:
182
+ from openai import OpenAI
183
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
184
+
185
+ prompt = f"""Create a detailed NPC for:
186
+ Context: {context}
187
+ Role: {role}
188
+ Importance: {importance}
189
+
190
+ Generate:
191
+ 1. Name and basic demographics
192
+ 2. Personality traits (3-4 key traits)
193
+ 3. Background and motivation
194
+ 4. Speech patterns/accent description
195
+ 5. Physical description
196
+ 6. Secret or hidden agenda
197
+ 7. How they react to different party approaches
198
+ 8. Potential quest hooks they could provide
199
+
200
+ Make them memorable and three-dimensional!"""
201
+
202
+ response = client.chat.completions.create(
203
+ model="gpt-4",
204
+ messages=[{"role": "system", "content": "You are an expert at creating memorable, three-dimensional NPCs for D&D campaigns."},
205
+ {"role": "user", "content": prompt}],
206
+ max_tokens=400,
207
+ temperature=0.8
208
+ )
209
+
210
+ return {"success": True, "content": response.choices[0].message.content}
211
+
212
+ except Exception as e:
213
+ logger.error(f"NPC generation failed: {e}")
214
+ return {"success": False, "error": str(e), "content": f"Mock NPC: {role} character for {context}"}
215
+
216
+ def roleplay_npc(self, npc_description: str, player_input: str, context: str) -> Dict:
217
+ """Roleplay as an NPC in response to player actions"""
218
+ try:
219
+ from openai import OpenAI
220
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
221
+
222
+ prompt = f"""You are roleplaying as this NPC:
223
+ {npc_description}
224
+
225
+ Context: {context}
226
+ Player says/does: {player_input}
227
+
228
+ Respond in character with:
229
+ 1. Dialogue (in quotes)
230
+ 2. Actions/body language (in italics)
231
+ 3. Internal thoughts/motivations (in parentheses)
232
+
233
+ Stay true to the character's personality and motivations!"""
234
+
235
+ response = client.chat.completions.create(
236
+ model="gpt-4",
237
+ messages=[{"role": "system", "content": "You are a skilled voice actor and D&D player, staying in character as NPCs."},
238
+ {"role": "user", "content": prompt}],
239
+ max_tokens=200,
240
+ temperature=0.9
241
+ )
242
+
243
+ return {"success": True, "content": response.choices[0].message.content}
244
+
245
+ except Exception as e:
246
+ logger.error(f"NPC roleplay failed: {e}")
247
+ return {"success": False, "error": str(e), "content": "Mock NPC Response: The character responds appropriately to your action."}
248
+
249
+ class WorldBuilderAgent:
250
+ """AI agent focused on creating consistent world elements"""
251
+
252
+ def __init__(self):
253
+ self.personality = "Detail-oriented architect of fictional worlds"
254
+ self.specializations = ["Geography", "Politics", "Culture", "History", "Economics"]
255
+
256
+ def generate_location(self, location_type: str, theme: str, purpose: str) -> Dict:
257
+ """Generate a detailed location"""
258
+ try:
259
+ from openai import OpenAI
260
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
261
+
262
+ prompt = f"""Create a detailed {location_type} with:
263
+ Theme: {theme}
264
+ Purpose in campaign: {purpose}
265
+
266
+ Generate:
267
+ 1. Name and general description
268
+ 2. Key areas/rooms (at least 5)
269
+ 3. Notable inhabitants
270
+ 4. Hidden secrets or mysteries
271
+ 5. Potential dangers or challenges
272
+ 6. Valuable resources or rewards
273
+ 7. Connections to broader world/campaign
274
+ 8. Sensory details (sights, sounds, smells)
275
+
276
+ Make it feel lived-in and realistic!"""
277
+
278
+ response = client.chat.completions.create(
279
+ model="gpt-4",
280
+ messages=[{"role": "system", "content": "You are a master world-builder creating immersive D&D locations."},
281
+ {"role": "user", "content": prompt}],
282
+ max_tokens=500,
283
+ temperature=0.7
284
+ )
285
+
286
+ return {"success": True, "content": response.choices[0].message.content}
287
+
288
+ except Exception as e:
289
+ logger.error(f"Location generation failed: {e}")
290
+ return {"success": False, "error": str(e), "content": f"Mock Location: {theme} {location_type} for {purpose}"}
291
+
292
+ class LootMasterAgent:
293
+ """AI agent specialized in creating balanced loot and magic items"""
294
+
295
+ def __init__(self):
296
+ self.personality = "Meticulous curator of magical treasures"
297
+ self.specializations = ["Game balance", "Magic item design", "Treasure distribution"]
298
+
299
+ def generate_loot_table(self, level: int, encounter_type: str, rarity: str) -> Dict:
300
+ """Generate a balanced loot table"""
301
+ try:
302
+ from openai import OpenAI
303
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
304
+
305
+ prompt = f"""Create a balanced loot table for:
306
+ Party Level: {level}
307
+ Encounter Type: {encounter_type}
308
+ Rarity Level: {rarity}
309
+
310
+ Generate:
311
+ 1. Gold/Currency amounts
312
+ 2. Common items (consumables, gear)
313
+ 3. Uncommon magical items (if appropriate)
314
+ 4. Rare items (if high level)
315
+ 5. Unique/plot-relevant items
316
+ 6. Alternative treasures (information, allies, etc.)
317
+
318
+ Ensure balance and appropriateness for the level!"""
319
+
320
+ response = client.chat.completions.create(
321
+ model="gpt-4",
322
+ messages=[{"role": "system", "content": "You are an expert at D&D game balance and treasure design."},
323
+ {"role": "user", "content": prompt}],
324
+ max_tokens=300,
325
+ temperature=0.6
326
+ )
327
+
328
+ return {"success": True, "content": response.choices[0].message.content}
329
+
330
+ except Exception as e:
331
+ logger.error(f"Loot generation failed: {e}")
332
+ return {"success": False, "error": str(e), "content": f"Mock Loot: Level {level} {encounter_type} {rarity} treasures"}
333
+
334
+ def create_custom_magic_item(self, item_concept: str, power_level: str, campaign_theme: str) -> Dict:
335
+ """Create a custom magic item"""
336
+ try:
337
+ from openai import OpenAI
338
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
339
+
340
+ prompt = f"""Design a custom magic item:
341
+ Concept: {item_concept}
342
+ Power Level: {power_level}
343
+ Campaign Theme: {campaign_theme}
344
+
345
+ Provide:
346
+ 1. Item name and basic description
347
+ 2. Mechanical effects (stats, abilities)
348
+ 3. Activation requirements
349
+ 4. Rarity and attunement needs
350
+ 5. Physical appearance
351
+ 6. Historical background/lore
352
+ 7. Potential drawbacks or limitations
353
+ 8. How it fits the campaign theme
354
+
355
+ Make it balanced and interesting!"""
356
+
357
+ response = client.chat.completions.create(
358
+ model="gpt-4",
359
+ messages=[{"role": "system", "content": "You are a master craftsperson of magical items for D&D, balancing power with narrative interest."},
360
+ {"role": "user", "content": prompt}],
361
+ max_tokens=400,
362
+ temperature=0.7
363
+ )
364
+
365
+ return {"success": True, "content": response.choices[0].message.content}
366
+
367
+ except Exception as e:
368
+ logger.error(f"Magic item creation failed: {e}")
369
+ return {"success": False, "error": str(e), "content": f"Mock Magic Item: {power_level} {item_concept} with {campaign_theme} theme"}
370
+
371
+ # ===== CHARACTER CREATOR CLASS =====
372
+ class CharacterCreator:
373
+ """Enhanced character creator with AI integration"""
374
+
375
+ def __init__(self):
376
+ self.classes = self._get_default_classes()
377
+ self.races = self._get_default_races()
378
+ self.backgrounds = self._get_backgrounds()
379
+
380
+ def _get_default_classes(self) -> Dict[str, CharacterClass]:
381
+ return {
382
+ "Fighter": CharacterClass(
383
+ name="Fighter", hit_die=10,
384
+ primary_ability=["Strength", "Dexterity"],
385
+ saving_throws=["Strength", "Constitution"],
386
+ skills=["Acrobatics", "Animal Handling", "Athletics", "History", "Insight", "Intimidation", "Perception", "Survival"]
387
+ ),
388
+ "Wizard": CharacterClass(
389
+ name="Wizard", hit_die=6,
390
+ primary_ability=["Intelligence"],
391
+ saving_throws=["Intelligence", "Wisdom"],
392
+ skills=["Arcana", "History", "Insight", "Investigation", "Medicine", "Religion"]
393
+ ),
394
+ "Rogue": CharacterClass(
395
+ name="Rogue", hit_die=8,
396
+ primary_ability=["Dexterity"],
397
+ saving_throws=["Dexterity", "Intelligence"],
398
+ skills=["Acrobatics", "Athletics", "Deception", "Insight", "Intimidation", "Investigation", "Perception", "Performance", "Persuasion", "Sleight of Hand", "Stealth"]
399
+ ),
400
+ "Cleric": CharacterClass(
401
+ name="Cleric", hit_die=8,
402
+ primary_ability=["Wisdom"],
403
+ saving_throws=["Wisdom", "Charisma"],
404
+ skills=["History", "Insight", "Medicine", "Persuasion", "Religion"]
405
+ ),
406
+ "Barbarian": CharacterClass(
407
+ name="Barbarian", hit_die=12,
408
+ primary_ability=["Strength"],
409
+ saving_throws=["Strength", "Constitution"],
410
+ skills=["Animal Handling", "Athletics", "Intimidation", "Nature", "Perception", "Survival"]
411
+ ),
412
+ "Bard": CharacterClass(
413
+ name="Bard", hit_die=8,
414
+ primary_ability=["Charisma"],
415
+ saving_throws=["Dexterity", "Charisma"],
416
+ skills=["Any three of your choice"]
417
+ ),
418
+ "Druid": CharacterClass(
419
+ name="Druid", hit_die=8,
420
+ primary_ability=["Wisdom"],
421
+ saving_throws=["Intelligence", "Wisdom"],
422
+ skills=["Arcana", "Animal Handling", "Insight", "Medicine", "Nature", "Perception", "Religion", "Survival"]
423
+ ),
424
+ "Monk": CharacterClass(
425
+ name="Monk", hit_die=8,
426
+ primary_ability=["Dexterity", "Wisdom"],
427
+ saving_throws=["Strength", "Dexterity"],
428
+ skills=["Acrobatics", "Athletics", "History", "Insight", "Religion", "Stealth"]
429
+ ),
430
+ "Paladin": CharacterClass(
431
+ name="Paladin", hit_die=10,
432
+ primary_ability=["Strength", "Charisma"],
433
+ saving_throws=["Wisdom", "Charisma"],
434
+ skills=["Athletics", "Insight", "Intimidation", "Medicine", "Persuasion", "Religion"]
435
+ ),
436
+ "Ranger": CharacterClass(
437
+ name="Ranger", hit_die=10,
438
+ primary_ability=["Dexterity", "Wisdom"],
439
+ saving_throws=["Strength", "Dexterity"],
440
+ skills=["Animal Handling", "Athletics", "Insight", "Investigation", "Nature", "Perception", "Stealth", "Survival"]
441
+ ),
442
+ "Sorcerer": CharacterClass(
443
+ name="Sorcerer", hit_die=6,
444
+ primary_ability=["Charisma"],
445
+ saving_throws=["Constitution", "Charisma"],
446
+ skills=["Arcana", "Deception", "Insight", "Intimidation", "Persuasion", "Religion"]
447
+ ),
448
+ "Warlock": CharacterClass(
449
+ name="Warlock", hit_die=8,
450
+ primary_ability=["Charisma"],
451
+ saving_throws=["Wisdom", "Charisma"],
452
+ skills=["Arcana", "Deception", "History", "Intimidation", "Investigation", "Nature", "Religion"]
453
+ )
454
+ }
455
+
456
+ def _get_default_races(self) -> Dict[str, Race]:
457
+ return {
458
+ "Human": Race(
459
+ name="Human",
460
+ ability_modifiers={"All": 1},
461
+ traits=["Extra Language", "Extra Skill", "Versatile"],
462
+ languages=["Common", "One other"]
463
+ ),
464
+ "Elf": Race(
465
+ name="Elf",
466
+ ability_modifiers={"Dexterity": 2},
467
+ traits=["Darkvision", "Keen Senses", "Fey Ancestry", "Trance"],
468
+ languages=["Common", "Elvish"]
469
+ ),
470
+ "Dwarf": Race(
471
+ name="Dwarf",
472
+ ability_modifiers={"Constitution": 2},
473
+ traits=["Darkvision", "Dwarven Resilience", "Stonecunning"],
474
+ languages=["Common", "Dwarvish"]
475
+ ),
476
+ "Halfling": Race(
477
+ name="Halfling",
478
+ ability_modifiers={"Dexterity": 2},
479
+ traits=["Lucky", "Brave", "Halfling Nimbleness"],
480
+ languages=["Common", "Halfling"]
481
+ ),
482
+ "Dragonborn": Race(
483
+ name="Dragonborn",
484
+ ability_modifiers={"Strength": 2, "Charisma": 1},
485
+ traits=["Draconic Ancestry", "Breath Weapon", "Damage Resistance"],
486
+ languages=["Common", "Draconic"]
487
+ ),
488
+ "Gnome": Race(
489
+ name="Gnome",
490
+ ability_modifiers={"Intelligence": 2},
491
+ traits=["Darkvision", "Gnome Cunning"],
492
+ languages=["Common", "Gnomish"]
493
+ ),
494
+ "Half-Elf": Race(
495
+ name="Half-Elf",
496
+ ability_modifiers={"Charisma": 2, "Choice": 1},
497
+ traits=["Darkvision", "Fey Ancestry", "Two Skills"],
498
+ languages=["Common", "Elvish", "One other"]
499
+ ),
500
+ "Half-Orc": Race(
501
+ name="Half-Orc",
502
+ ability_modifiers={"Strength": 2, "Constitution": 1},
503
+ traits=["Darkvision", "Relentless Endurance", "Savage Attacks"],
504
+ languages=["Common", "Orc"]
505
+ ),
506
+ "Tiefling": Race(
507
+ name="Tiefling",
508
+ ability_modifiers={"Intelligence": 1, "Charisma": 2},
509
+ traits=["Darkvision", "Hellish Resistance", "Infernal Legacy"],
510
+ languages=["Common", "Infernal"]
511
+ )
512
+ }
513
+
514
+ def _get_backgrounds(self) -> List[str]:
515
+ return [
516
+ "Acolyte", "Criminal", "Folk Hero", "Noble", "Sage", "Soldier",
517
+ "Charlatan", "Entertainer", "Guild Artisan", "Hermit", "Outlander", "Sailor"
518
+ ]
519
+
520
+ def roll_ability_scores(self) -> Dict[str, int]:
521
+ """Roll 4d6, drop lowest, for each ability score"""
522
+ abilities = {}
523
+ for ability in ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"]:
524
+ rolls = [random.randint(1, 6) for _ in range(4)]
525
+ rolls.sort(reverse=True)
526
+ abilities[ability] = sum(rolls[:3]) # Take top 3
527
+ return abilities
528
+
529
+ def calculate_ability_modifier(self, score: int) -> int:
530
+ """Calculate ability modifier from score"""
531
+ return (score - 10) // 2
532
+
533
+ def calculate_hit_points(self, char_class: str, level: int, constitution_modifier: int) -> int:
534
+ """Calculate hit points based on class, level, and CON modifier"""
535
+ hit_die = self.classes[char_class].hit_die
536
+ base_hp = hit_die + constitution_modifier # Max HP at level 1
537
+
538
+ # Add average HP for additional levels
539
+ for _ in range(level - 1):
540
+ base_hp += (hit_die // 2 + 1) + constitution_modifier
541
+
542
+ return max(1, base_hp) # Minimum 1 HP
543
+
544
+ def apply_racial_modifiers(self, base_abilities: Dict[str, int], race: str) -> Dict[str, int]:
545
+ """Apply racial ability score modifiers"""
546
+ modified_abilities = base_abilities.copy()
547
+ race_data = self.races[race]
548
+
549
+ for ability, modifier in race_data.ability_modifiers.items():
550
+ if ability == "All":
551
+ for ability_name in modified_abilities:
552
+ modified_abilities[ability_name] += modifier
553
+ elif ability == "Choice":
554
+ # For simplicity, add to lowest score
555
+ lowest_ability = min(modified_abilities, key=modified_abilities.get)
556
+ modified_abilities[lowest_ability] += modifier
557
+ else:
558
+ modified_abilities[ability] += modifier
559
+
560
+ return modified_abilities
561
+
562
+ # ===== IMAGE GENERATION =====
563
+ def generate_image(prompt: str) -> str:
564
+ """Generate image using OpenAI DALL-E"""
565
+ try:
566
+ from openai import OpenAI
567
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
568
+
569
+ response = client.images.generate(
570
+ model="dall-e-3",
571
+ prompt=prompt,
572
+ size="1024x1024",
573
+ quality="standard",
574
+ n=1,
575
+ )
576
+
577
+ return response.data[0].url
578
+
579
+ except Exception as e:
580
+ logger.error(f"Image generation failed: {e}")
581
+ return "https://via.placeholder.com/512x512/dc2626/ffffff?text=Image+Generation+Failed"
582
+
583
+ # ===== MAIN INTERFACE =====
584
+ def create_main_interface():
585
+ """Create the main D&D Campaign Manager interface"""
586
+
587
+ # Initialize agents
588
+ dm_agent = DungeonMasterAgent()
589
+ npc_agent = NPCAgent()
590
+ world_agent = WorldBuilderAgent()
591
+ loot_agent = LootMasterAgent()
592
+ character_creator = CharacterCreator()
593
+
594
+ custom_css = """
595
+ .agent-card {
596
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
597
+ padding: 20px;
598
+ border-radius: 15px;
599
+ margin: 10px 0;
600
+ color: white;
601
+ }
602
+
603
+ .output-box {
604
+ background: #f8f9fa;
605
+ border: 1px solid #dee2e6;
606
+ border-radius: 10px;
607
+ padding: 15px;
608
+ margin: 10px 0;
609
+ }
610
+ """
611
+
612
+ with gr.Blocks(css=custom_css, title="D&D Campaign Manager") as demo:
613
+ gr.Markdown("""
614
+ # 🏰 Advanced D&D Campaign Manager
615
+
616
+ *Your AI-powered toolkit for epic adventures*
617
+
618
+ **Features:**
619
+ - 🎭 AI Dungeon Master Assistant
620
+ - πŸ‘₯ Intelligent NPC Creator & Roleplay
621
+ - πŸ—ΊοΈ World Builder Agent
622
+ - πŸ’° Loot Master & Magic Item Designer
623
+ - πŸ‰ Enhanced Character Creator
624
+ """)
625
+
626
+ with gr.Tabs():
627
+ # ===== CHARACTER CREATOR TAB =====
628
+ with gr.TabItem("πŸ‰ Character Creator"):
629
+ with gr.Row():
630
+ with gr.Column(scale=2):
631
+ gr.Markdown("## πŸ“ Character Details")
632
+
633
+ character_name = gr.Textbox(label="Character Name", placeholder="Enter name...")
634
+
635
+ with gr.Row():
636
+ race_dropdown = gr.Dropdown(
637
+ choices=list(character_creator.races.keys()),
638
+ label="Race", value="Human"
639
+ )
640
+ class_dropdown = gr.Dropdown(
641
+ choices=list(character_creator.classes.keys()),
642
+ label="Class", value="Fighter"
643
+ )
644
+ gender_dropdown = gr.Dropdown(
645
+ choices=["Male", "Female", "Non-binary", "Transgender Male", "Transgender Female", "Genderfluid", "Agender", "Other"],
646
+ label="Gender", value="Male"
647
+ )
648
+
649
+ with gr.Row():
650
+ level_slider = gr.Slider(minimum=1, maximum=20, step=1, value=1, label="Level")
651
+ alignment_dropdown = gr.Dropdown(
652
+ choices=[alignment.value for alignment in Alignment],
653
+ label="Alignment", value=Alignment.LAWFUL_GOOD.value
654
+ )
655
+
656
+ background_dropdown = gr.Dropdown(
657
+ choices=character_creator._get_backgrounds(),
658
+ label="Background", value="Folk Hero"
659
+ )
660
+
661
+ # AI Enhancement Buttons
662
+ with gr.Row():
663
+ generate_name_btn = gr.Button("✨ AI Generate Name", variant="secondary")
664
+ generate_backstory_btn = gr.Button("πŸ“š AI Generate Backstory", variant="secondary")
665
+
666
+ backstory = gr.Textbox(label="Backstory", lines=4, placeholder="Character background...")
667
+
668
+ # Ability Scores
669
+ gr.Markdown("## 🎲 Ability Scores")
670
+ roll_btn = gr.Button("🎲 Roll Ability Scores", variant="primary")
671
+
672
+ with gr.Row():
673
+ str_score = gr.Number(label="Strength", value=10, precision=0)
674
+ dex_score = gr.Number(label="Dexterity", value=10, precision=0)
675
+ con_score = gr.Number(label="Constitution", value=10, precision=0)
676
+
677
+ with gr.Row():
678
+ int_score = gr.Number(label="Intelligence", value=10, precision=0)
679
+ wis_score = gr.Number(label="Wisdom", value=10, precision=0)
680
+ cha_score = gr.Number(label="Charisma", value=10, precision=0)
681
+
682
+ with gr.Column(scale=1):
683
+ gr.Markdown("## πŸ“Š Character Summary")
684
+ character_summary = gr.Markdown("*Create character to see summary*")
685
+
686
+ gr.Markdown("## 🎨 Character Portrait")
687
+ portrait_btn = gr.Button("🎨 Generate AI Portrait", variant="primary")
688
+ character_portrait = gr.Image(label="Portrait", height=300)
689
+
690
+ gr.Markdown("## πŸ’Ύ Export")
691
+ export_btn = gr.Button("πŸ“₯ Export Character")
692
+ export_file = gr.File(label="Character JSON")
693
+
694
+ # Hidden state
695
+ character_data = gr.State()
696
+
697
+ # ===== CAMPAIGN CREATOR TAB =====
698
+ with gr.TabItem("🎭 AI Dungeon Master"):
699
+ gr.Markdown("## οΏ½οΏ½ Campaign Generation", elem_classes=["agent-card"])
700
+
701
+ with gr.Row():
702
+ with gr.Column():
703
+ campaign_theme = gr.Dropdown(
704
+ choices=["High Fantasy", "Dark Fantasy", "Urban Fantasy", "Steampunk", "Horror", "Comedy", "Political Intrigue", "Exploration"],
705
+ label="Campaign Theme", value="High Fantasy"
706
+ )
707
+ campaign_level = gr.Slider(minimum=1, maximum=20, value=5, label="Starting Level")
708
+ player_count = gr.Slider(minimum=1, maximum=8, value=4, label="Number of Players")
709
+
710
+ generate_campaign_btn = gr.Button("🎲 Generate Campaign Concept", variant="primary", size="lg")
711
+
712
+ with gr.Column():
713
+ campaign_visual_btn = gr.Button("πŸ–ΌοΈ Generate Campaign Art")
714
+ campaign_image = gr.Image(label="Campaign Visual", height=300)
715
+
716
+ campaign_output = gr.Textbox(label="Campaign Concept", lines=10, elem_classes=["output-box"])
717
+
718
+ gr.Markdown("## πŸ“… Session Planning")
719
+ with gr.Row():
720
+ session_number = gr.Number(label="Session Number", value=1, precision=0)
721
+ generate_session_btn = gr.Button("πŸ“ Generate Session Content", variant="secondary")
722
+
723
+ session_output = gr.Textbox(label="Session Content", lines=8, elem_classes=["output-box"])
724
+
725
+ # ===== NPC CREATOR TAB =====
726
+ with gr.TabItem("πŸ‘₯ NPC Agent"):
727
+ gr.Markdown("## 🎭 NPC Creator & Roleplay Assistant", elem_classes=["agent-card"])
728
+
729
+ with gr.Row():
730
+ with gr.Column():
731
+ gr.Markdown("### Create New NPC")
732
+ npc_context = gr.Textbox(label="Campaign/Scene Context", placeholder="Describe the setting or situation...")
733
+ npc_role = gr.Dropdown(
734
+ choices=["Ally", "Neutral", "Antagonist", "Quest Giver", "Merchant", "Authority Figure", "Mysterious Stranger"],
735
+ label="NPC Role", value="Neutral"
736
+ )
737
+ npc_importance = gr.Dropdown(
738
+ choices=["Minor", "Moderate", "Major", "Recurring"],
739
+ label="Importance Level", value="Moderate"
740
+ )
741
+
742
+ create_npc_btn = gr.Button("🎭 Generate NPC", variant="primary")
743
+
744
+ npc_portrait_btn = gr.Button("🎨 Generate NPC Portrait")
745
+ npc_portrait = gr.Image(label="NPC Portrait", height=300)
746
+
747
+ with gr.Column():
748
+ npc_output = gr.Textbox(label="Generated NPC", lines=12, elem_classes=["output-box"])
749
+
750
+ gr.Markdown("### πŸ’¬ NPC Roleplay")
751
+ with gr.Row():
752
+ with gr.Column(scale=2):
753
+ player_input = gr.Textbox(label="Player Action/Dialogue", placeholder="What does the player say or do?")
754
+ roleplay_context = gr.Textbox(label="Scene Context", placeholder="Describe the current situation...")
755
+
756
+ with gr.Column(scale=1):
757
+ roleplay_btn = gr.Button("🎭 NPC Response", variant="secondary", size="lg")
758
+
759
+ npc_response = gr.Textbox(label="NPC Response", lines=4, elem_classes=["output-box"])
760
+
761
+ # Hidden states for NPC
762
+ current_npc_data = gr.State()
763
+
764
+ # ===== WORLD BUILDER TAB =====
765
+ with gr.TabItem("πŸ—ΊοΈ World Builder"):
766
+ gr.Markdown("## πŸ—οΈ Location & World Generator", elem_classes=["agent-card"])
767
+
768
+ with gr.Row():
769
+ with gr.Column():
770
+ location_type = gr.Dropdown(
771
+ choices=["Tavern", "Dungeon", "City", "Forest", "Castle", "Temple", "Shop", "Wilderness", "Mansion", "Cave System"],
772
+ label="Location Type", value="Tavern"
773
+ )
774
+ location_theme = gr.Dropdown(
775
+ choices=["Standard Fantasy", "Dark/Gothic", "Mystical", "Abandoned/Ruined", "Luxurious", "Dangerous", "Peaceful", "Mysterious"],
776
+ label="Theme", value="Standard Fantasy"
777
+ )
778
+ location_purpose = gr.Textbox(label="Purpose in Campaign", placeholder="How will this location be used?")
779
+
780
+ generate_location_btn = gr.Button("πŸ—οΈ Generate Location", variant="primary")
781
+ location_art_btn = gr.Button("πŸ–ΌοΈ Generate Location Art")
782
+
783
+ with gr.Column():
784
+ location_image = gr.Image(label="Location Visual", height=300)
785
+
786
+ location_output = gr.Textbox(label="Location Details", lines=12, elem_classes=["output-box"])
787
+
788
+ # ===== LOOT MASTER TAB =====
789
+ with gr.TabItem("πŸ’° Loot Master"):
790
+ gr.Markdown("## πŸ’Ž Treasure & Magic Item Generator", elem_classes=["agent-card"])
791
+
792
+ with gr.Row():
793
+ with gr.Column():
794
+ gr.Markdown("### 🎲 Loot Table Generator")
795
+ loot_level = gr.Slider(minimum=1, maximum=20, value=5, label="Party Level")
796
+ encounter_type = gr.Dropdown(
797
+ choices=["Boss Fight", "Mini-boss", "Standard Combat", "Exploration Reward", "Quest Completion", "Treasure Hoard"],
798
+ label="Encounter Type", value="Standard Combat"
799
+ )
800
+ loot_rarity = gr.Dropdown(
801
+ choices=["Poor", "Standard", "Rich", "Legendary"],
802
+ label="Treasure Quality", value="Standard"
803
+ )
804
+
805
+ generate_loot_btn = gr.Button("πŸ’° Generate Loot Table", variant="primary")
806
+
807
+ with gr.Column():
808
+ gr.Markdown("### ✨ Custom Magic Item")
809
+ item_concept = gr.Textbox(label="Item Concept", placeholder="What kind of item? (sword, ring, staff, etc.)")
810
+ power_level = gr.Dropdown(
811
+ choices=["Common", "Uncommon", "Rare", "Very Rare", "Legendary", "Artifact"],
812
+ label="Power Level", value="Uncommon"
813
+ )
814
+ campaign_theme_item = gr.Textbox(label="Campaign Theme", placeholder="How should it fit your campaign?")
815
+
816
+ create_item_btn = gr.Button("✨ Create Magic Item", variant="secondary")
817
+ item_art_btn = gr.Button("🎨 Generate Item Art")
818
+
819
+ with gr.Row():
820
+ loot_output = gr.Textbox(label="Loot Table", lines=8, elem_classes=["output-box"])
821
+ magic_item_output = gr.Textbox(label="Magic Item Details", lines=8, elem_classes=["output-box"])
822
+
823
+ item_image = gr.Image(label="Magic Item Visual", height=250)
824
+
825
+ # ===== CAMPAIGN TOOLS TAB =====
826
+ with gr.TabItem("πŸ› οΈ Campaign Tools"):
827
+ gr.Markdown("## 🎯 Advanced Campaign Management")
828
+
829
+ with gr.Tabs():
830
+ with gr.TabItem("πŸ“Š Initiative Tracker"):
831
+ gr.Markdown("### βš”οΈ Combat Management")
832
+
833
+ with gr.Row():
834
+ add_character = gr.Textbox(label="Character Name", placeholder="Add to initiative...")
835
+ add_initiative = gr.Number(label="Initiative Roll", value=10)
836
+ add_btn = gr.Button("βž• Add to Initiative")
837
+
838
+ initiative_list = gr.Dataframe(
839
+ headers=["Name", "Initiative", "HP", "AC", "Status"],
840
+ label="Initiative Order",
841
+ interactive=True
842
+ )
843
+
844
+ with gr.Row():
845
+ next_turn_btn = gr.Button("⏭️ Next Turn", variant="primary")
846
+ reset_combat_btn = gr.Button("πŸ”„ Reset Combat")
847
+
848
+ with gr.TabItem("πŸ“ Session Notes"):
849
+ gr.Markdown("### πŸ“– Session Management")
850
+
851
+ session_date = gr.Textbox(label="Session Date", value="Session 1")
852
+
853
+ with gr.Row():
854
+ with gr.Column():
855
+ key_events = gr.Textbox(label="Key Events", lines=4, placeholder="What happened this session?")
856
+ npc_interactions = gr.Textbox(label="NPC Interactions", lines=3, placeholder="Who did they meet?")
857
+
858
+ with gr.Column():
859
+ player_actions = gr.Textbox(label="Notable Player Actions", lines=4, placeholder="What did the players do?")
860
+ next_session_prep = gr.Textbox(label="Next Session Prep", lines=3, placeholder="What to prepare for next time?")
861
+
862
+ save_notes_btn = gr.Button("πŸ’Ύ Save Session Notes", variant="primary")
863
+ notes_output = gr.File(label="Session Notes File")
864
+
865
+ with gr.TabItem("🎲 Random Generators"):
866
+ gr.Markdown("### 🎯 Quick Generators")
867
+
868
+ with gr.Row():
869
+ with gr.Column():
870
+ gr.Markdown("**Name Generators**")
871
+ name_type = gr.Dropdown(
872
+ choices=["Human Male", "Human Female", "Elven", "Dwarven", "Orcish", "Fantasy Place", "Tavern", "Shop"],
873
+ value="Human Male"
874
+ )
875
+ gen_name_btn = gr.Button("🎲 Generate Name")
876
+ random_name = gr.Textbox(label="Generated Name")
877
+
878
+ with gr.Column():
879
+ gr.Markdown("**Quick Encounters**")
880
+ encounter_level = gr.Slider(1, 20, value=5, label="Party Level")
881
+ encounter_difficulty = gr.Dropdown(
882
+ choices=["Easy", "Medium", "Hard", "Deadly"],
883
+ value="Medium"
884
+ )
885
+ gen_encounter_btn = gr.Button("βš”οΈ Generate Encounter")
886
+ random_encounter = gr.Textbox(label="Encounter", lines=3)
887
+
888
+ with gr.Row():
889
+ with gr.Column():
890
+ gr.Markdown("**Plot Hooks**")
891
+ hook_theme = gr.Dropdown(
892
+ choices=["Mystery", "Adventure", "Political", "Personal", "Rescue", "Exploration"],
893
+ value="Adventure"
894
+ )
895
+ gen_hook_btn = gr.Button("🎣 Generate Plot Hook")
896
+ plot_hook = gr.Textbox(label="Plot Hook", lines=2)
897
+
898
+ with gr.Column():
899
+ gr.Markdown("**Weather & Atmosphere**")
900
+ climate = gr.Dropdown(
901
+ choices=["Temperate", "Tropical", "Arctic", "Desert", "Mountainous"],
902
+ value="Temperate"
903
+ )
904
+ gen_weather_btn = gr.Button("🌀️ Generate Weather")
905
+ weather = gr.Textbox(label="Weather & Mood", lines=2)
906
+
907
+ # ===== EVENT HANDLERS =====
908
+
909
+ # Character Creator Events
910
+ def roll_abilities():
911
+ abilities = character_creator.roll_ability_scores()
912
+ return (
913
+ abilities["Strength"], abilities["Dexterity"], abilities["Constitution"],
914
+ abilities["Intelligence"], abilities["Wisdom"], abilities["Charisma"]
915
+ )
916
+
917
+ def generate_character_name(race, char_class, gender, alignment):
918
+ try:
919
+ from openai import OpenAI
920
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
921
+
922
+ prompt = f"Generate a {gender} {race} {char_class} name appropriate for D&D. Return only the name."
923
+
924
+ response = client.chat.completions.create(
925
+ model="gpt-4",
926
+ messages=[{"role": "user", "content": prompt}],
927
+ max_tokens=30
928
+ )
929
+
930
+ return response.choices[0].message.content.strip()
931
+ except Exception as e:
932
+ # Fallback to simple name generation
933
+ names = {
934
+ "Human": {
935
+ "Male": ["Garrett", "Marcus", "Thomas", "William", "James"],
936
+ "Female": ["Elena", "Sarah", "Miranda", "Catherine", "Rose"],
937
+ "Non-binary": ["Alex", "Jordan", "Riley", "Casey", "Taylor"],
938
+ "Other": ["River", "Sage", "Phoenix", "Ash", "Rowan"]
939
+ },
940
+ "Elf": {
941
+ "Male": ["Aelar", "Berrian", "Drannor", "Enna", "Galinndan"],
942
+ "Female": ["Adrie", "Althaea", "Anastrianna", "Andraste", "Antinua"],
943
+ "Non-binary": ["Aerdyl", "Ahvir", "Aramil", "Aranea", "Berris"],
944
+ "Other": ["Dayereth", "Enna", "Galinndan", "Hadarai", "Halimath"]
945
+ },
946
+ "Dwarf": {
947
+ "Male": ["Adrik", "Baern", "Darrak", "Delg", "Eberk"],
948
+ "Female": ["Amber", "Bardryn", "Diesa", "Eldeth", "Gunnloda"],
949
+ "Non-binary": ["Alberich", "Balin", "Dain", "Fundin", "GloΓ­n"],
950
+ "Other": ["HΓ‘var", "KΓ­li", "NΓ‘li", "Ori", "Thorek"]
951
+ },
952
+ "Halfling": {
953
+ "Male": ["Alton", "Ander", "Cade", "Corrin", "Eldon"],
954
+ "Female": ["Andry", "Bree", "Callie", "Cora", "Euphemia"],
955
+ "Non-binary": ["Finnan", "Garret", "Lindal", "Lyle", "Merric"],
956
+ "Other": ["Nedda", "Paela", "Portia", "Seraphina", "Shaena"]
957
+ }
958
+ }
959
+
960
+ # Handle various gender identities
961
+ gender_key = gender
962
+ if gender in ["Transgender Male", "Male"]:
963
+ gender_key = "Male"
964
+ elif gender in ["Transgender Female", "Female"]:
965
+ gender_key = "Female"
966
+ elif gender in ["Non-binary", "Genderfluid", "Agender"]:
967
+ gender_key = "Non-binary"
968
+ else:
969
+ gender_key = "Other"
970
+
971
+ race_names = names.get(race, names["Human"])
972
+ return random.choice(race_names.get(gender_key, race_names["Other"]))
973
+
974
+ def generate_character_backstory(name, race, char_class, gender, alignment, background):
975
+ try:
976
+ from openai import OpenAI
977
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
978
+
979
+ prompt = f"""Create a compelling backstory for {name}, a {gender} {race} {char_class} with {alignment} alignment and {background} background.
980
+ Write 2-3 paragraphs about their origins, motivations, and key life events. Be respectful and inclusive in your portrayal."""
981
+
982
+ response = client.chat.completions.create(
983
+ model="gpt-4",
984
+ messages=[{"role": "user", "content": prompt}],
985
+ max_tokens=300
986
+ )
987
+
988
+ return response.choices[0].message.content.strip()
989
+ except Exception as e:
990
+ 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."
991
+
992
+ def generate_character_portrait(character_data):
993
+ if not character_data:
994
+ return "Please create a character first"
995
+
996
+ 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"
997
+ return generate_image(prompt)
998
+
999
+ def update_character_summary(name, race, char_class, level, gender, alignment,
1000
+ str_val, dex_val, con_val, int_val, wis_val, cha_val, background, backstory):
1001
+ if not all([str_val, dex_val, con_val, int_val, wis_val, cha_val]):
1002
+ return "*Roll ability scores to see summary*", None
1003
+
1004
+ base_abilities = {
1005
+ "Strength": int(str_val), "Dexterity": int(dex_val), "Constitution": int(con_val),
1006
+ "Intelligence": int(int_val), "Wisdom": int(wis_val), "Charisma": int(cha_val)
1007
+ }
1008
+
1009
+ final_abilities = character_creator.apply_racial_modifiers(base_abilities, race)
1010
+ con_modifier = character_creator.calculate_ability_modifier(final_abilities["Constitution"])
1011
+ hit_points = character_creator.calculate_hit_points(char_class, int(level), con_modifier)
1012
+
1013
+ character = Character(
1014
+ name=name or "Unnamed Character", race=race, character_class=char_class,
1015
+ level=int(level), gender=gender, alignment=Alignment(alignment), abilities=final_abilities,
1016
+ hit_points=hit_points, skills=character_creator.classes[char_class].skills[:2],
1017
+ background=background, backstory=backstory
1018
+ )
1019
+
1020
+ summary = f"""**{character.name}**
1021
+ *Level {level} {gender} {race} {char_class} ({alignment})*
1022
+
1023
+ **Ability Scores:**
1024
+ - STR: {final_abilities['Strength']} ({character_creator.calculate_ability_modifier(final_abilities['Strength']):+d})
1025
+ - DEX: {final_abilities['Dexterity']} ({character_creator.calculate_ability_modifier(final_abilities['Dexterity']):+d})
1026
+ - CON: {final_abilities['Constitution']} ({character_creator.calculate_ability_modifier(final_abilities['Constitution']):+d})
1027
+ - INT: {final_abilities['Intelligence']} ({character_creator.calculate_ability_modifier(final_abilities['Intelligence']):+d})
1028
+ - WIS: {final_abilities['Wisdom']} ({character_creator.calculate_ability_modifier(final_abilities['Wisdom']):+d})
1029
+ - CHA: {final_abilities['Charisma']} ({character_creator.calculate_ability_modifier(final_abilities['Charisma']):+d})
1030
+
1031
+ **Combat Stats:**
1032
+ - Hit Points: {hit_points}
1033
+ - Hit Die: d{character_creator.classes[char_class].hit_die}
1034
+
1035
+ **Background:** {background}"""
1036
+
1037
+ return summary, character
1038
+
1039
+ # Campaign Events
1040
+ def generate_campaign_concept(theme, level, players):
1041
+ result = dm_agent.generate_campaign_concept(theme, level, players)
1042
+ return result.get("content", "Error generating campaign")
1043
+
1044
+ def generate_campaign_art(theme, level):
1045
+ prompt = f"{theme} D&D campaign art for level {level} adventurers, epic fantasy illustration"
1046
+ return generate_image(prompt)
1047
+
1048
+ def generate_session_content(campaign_output, session_num):
1049
+ if not campaign_output:
1050
+ return "Please generate a campaign concept first"
1051
+ result = dm_agent.generate_session_content(campaign_output, session_num)
1052
+ return result.get("content", "Error generating session")
1053
+
1054
+ # NPC Events
1055
+ def create_npc(context, role, importance):
1056
+ result = npc_agent.generate_npc(context, role, importance)
1057
+ return result.get("content", "Error creating NPC")
1058
+
1059
+ def generate_npc_portrait(npc_data):
1060
+ if not npc_data:
1061
+ return "Create an NPC first"
1062
+ prompt = "Fantasy portrait of a D&D NPC character, detailed face, professional RPG art"
1063
+ return generate_image(prompt)
1064
+
1065
+ def npc_roleplay_response(npc_data, player_input, context):
1066
+ if not npc_data or not player_input:
1067
+ return "Please create an NPC and enter player input"
1068
+ result = npc_agent.roleplay_npc(npc_data, player_input, context)
1069
+ return result.get("content", "Error in roleplay")
1070
+
1071
+ # World Builder Events
1072
+ def create_location(loc_type, theme, purpose):
1073
+ result = world_agent.generate_location(loc_type, theme, purpose)
1074
+ return result.get("content", "Error creating location")
1075
+
1076
+ def generate_location_art(loc_type, theme):
1077
+ prompt = f"{theme} {loc_type} fantasy location, detailed environment art, D&D setting"
1078
+ return generate_image(prompt)
1079
+
1080
+ # Loot Events
1081
+ def create_loot_table(level, encounter, rarity):
1082
+ result = loot_agent.generate_loot_table(level, encounter, rarity)
1083
+ return result.get("content", "Error creating loot")
1084
+
1085
+ def create_magic_item(concept, power, theme):
1086
+ result = loot_agent.create_custom_magic_item(concept, power, theme)
1087
+ return result.get("content", "Error creating item")
1088
+
1089
+ def generate_item_art(concept, power):
1090
+ prompt = f"{power} magical {concept}, fantasy item illustration, glowing magical effects"
1091
+ return generate_image(prompt)
1092
+
1093
+ # Random Generator Events
1094
+ def generate_random_name(name_type):
1095
+ names = {
1096
+ "Human Male": ["Garrett", "Marcus", "Thomas", "William", "James"],
1097
+ "Human Female": ["Elena", "Sarah", "Miranda", "Catherine", "Rose"],
1098
+ "Elven": ["Aelar", "Berrian", "Drannor", "Enna", "Galinndan"],
1099
+ "Dwarven": ["Adrik", "Baern", "Darrak", "Delg", "Eberk"],
1100
+ "Orcish": ["Grul", "Thark", "Dench", "Feng", "Gell"],
1101
+ "Fantasy Place": ["Ravenshollow", "Goldbrook", "Thornfield", "Mistral Keep"],
1102
+ "Tavern": ["The Prancing Pony", "Dragon's Rest", "The Silver Tankard"],
1103
+ "Shop": ["Magical Mysteries", "Fine Blades & More", "Potions & Remedies"]
1104
+ }
1105
+ return random.choice(names.get(name_type, ["Unknown"]))
1106
+
1107
+ def generate_random_encounter(level, difficulty):
1108
+ encounters = [
1109
+ f"Bandit ambush (adapted for level {level})",
1110
+ f"Wild animal encounter (CR {max(1, level//4)})",
1111
+ f"Mysterious traveler with a quest",
1112
+ f"Ancient ruins with {difficulty.lower()} traps",
1113
+ f"Rival adventuring party",
1114
+ f"Magical phenomenon requiring investigation"
1115
+ ]
1116
+ return random.choice(encounters)
1117
+
1118
+ def generate_plot_hook(theme):
1119
+ hooks = {
1120
+ "Mystery": "A beloved local figure has vanished without a trace, leaving behind only cryptic clues.",
1121
+ "Adventure": "Ancient maps surface pointing to a legendary treasure thought lost forever.",
1122
+ "Political": "A diplomatic envoy requests secret protection during dangerous negotiations.",
1123
+ "Personal": "A character's past catches up with them in an unexpected way.",
1124
+ "Rescue": "Innocent people are trapped in a dangerous situation and need immediate help.",
1125
+ "Exploration": "Uncharted territories beckon with promises of discovery and danger."
1126
+ }
1127
+ return hooks.get(theme, "A mysterious stranger approaches with an urgent request.")
1128
+
1129
+ def generate_weather(climate):
1130
+ weather_options = {
1131
+ "Temperate": ["Sunny and mild", "Light rain showers", "Overcast skies", "Gentle breeze"],
1132
+ "Tropical": ["Hot and humid", "Sudden thunderstorm", "Sweltering heat", "Monsoon rains"],
1133
+ "Arctic": ["Bitter cold winds", "Heavy snowfall", "Blizzard conditions", "Icy fog"],
1134
+ "Desert": ["Scorching sun", "Sandstorm approaching", "Cool desert night", "Rare rainfall"],
1135
+ "Mountainous": ["Mountain mist", "Alpine winds", "Rocky terrain", "Sudden weather change"]
1136
+ }
1137
+ return random.choice(weather_options.get(climate, ["Pleasant weather"]))
1138
+
1139
+ # Wire up all events
1140
+ roll_btn.click(roll_abilities, outputs=[str_score, dex_score, con_score, int_score, wis_score, cha_score])
1141
+
1142
+ generate_name_btn.click(
1143
+ generate_character_name,
1144
+ inputs=[race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown],
1145
+ outputs=[character_name]
1146
+ )
1147
+
1148
+ generate_backstory_btn.click(
1149
+ generate_character_backstory,
1150
+ inputs=[character_name, race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown, background_dropdown],
1151
+ outputs=[backstory]
1152
+ )
1153
+
1154
+ portrait_btn.click(
1155
+ generate_character_portrait,
1156
+ inputs=[character_data],
1157
+ outputs=[character_portrait]
1158
+ )
1159
+
1160
+ # Update character summary when inputs change
1161
+ for component in [character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown,
1162
+ str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory]:
1163
+ component.change(
1164
+ update_character_summary,
1165
+ inputs=[character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown,
1166
+ str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory],
1167
+ outputs=[character_summary, character_data]
1168
+ )
1169
+
1170
+ # Campaign events
1171
+ generate_campaign_btn.click(
1172
+ generate_campaign_concept,
1173
+ inputs=[campaign_theme, campaign_level, player_count],
1174
+ outputs=[campaign_output]
1175
+ )
1176
+
1177
+ campaign_visual_btn.click(
1178
+ generate_campaign_art,
1179
+ inputs=[campaign_theme, campaign_level],
1180
+ outputs=[campaign_image]
1181
+ )
1182
+
1183
+ generate_session_btn.click(
1184
+ generate_session_content,
1185
+ inputs=[campaign_output, session_number],
1186
+ outputs=[session_output]
1187
+ )
1188
+
1189
+ # NPC events
1190
+ create_npc_btn.click(
1191
+ create_npc,
1192
+ inputs=[npc_context, npc_role, npc_importance],
1193
+ outputs=[npc_output]
1194
+ )
1195
+
1196
+ npc_portrait_btn.click(
1197
+ generate_npc_portrait,
1198
+ inputs=[current_npc_data],
1199
+ outputs=[npc_portrait]
1200
+ )
1201
+
1202
+ roleplay_btn.click(
1203
+ npc_roleplay_response,
1204
+ inputs=[npc_output, player_input, roleplay_context],
1205
+ outputs=[npc_response]
1206
+ )
1207
+
1208
+ # World builder events
1209
+ generate_location_btn.click(
1210
+ create_location,
1211
+ inputs=[location_type, location_theme, location_purpose],
1212
+ outputs=[location_output]
1213
+ )
1214
+
1215
+ location_art_btn.click(
1216
+ generate_location_art,
1217
+ inputs=[location_type, location_theme],
1218
+ outputs=[location_image]
1219
+ )
1220
+
1221
+ # Loot events
1222
+ generate_loot_btn.click(
1223
+ create_loot_table,
1224
+ inputs=[loot_level, encounter_type, loot_rarity],
1225
+ outputs=[loot_output]
1226
+ )
1227
+
1228
+ create_item_btn.click(
1229
+ create_magic_item,
1230
+ inputs=[item_concept, power_level, campaign_theme_item],
1231
+ outputs=[magic_item_output]
1232
+ )
1233
+
1234
+ item_art_btn.click(
1235
+ generate_item_art,
1236
+ inputs=[item_concept, power_level],
1237
+ outputs=[item_image]
1238
+ )
1239
+
1240
+ # Random generator events
1241
+ gen_name_btn.click(generate_random_name, inputs=[name_type], outputs=[random_name])
1242
+ gen_encounter_btn.click(generate_random_encounter, inputs=[encounter_level, encounter_difficulty], outputs=[random_encounter])
1243
+ gen_hook_btn.click(generate_plot_hook, inputs=[hook_theme], outputs=[plot_hook])
1244
+ gen_weather_btn.click(generate_weather, inputs=[climate], outputs=[weather])
1245
+
1246
+ return demo
1247
+
1248
+ # Main entry point
1249
+ if __name__ == "__main__":
1250
+ logger.info("🏰 Starting Advanced D&D Campaign Manager...")
1251
+
1252
+ # Check for OpenAI API key
1253
+ api_key = os.getenv("OPENAI_API_KEY")
1254
+ if api_key:
1255
+ logger.info("βœ… OpenAI API key found - All AI features enabled!")
1256
+ else:
1257
+ logger.warning("⚠️ No OpenAI API key found - AI features will be limited")
1258
+ logger.info("πŸ’‘ Add OPENAI_API_KEY=your_key to a .env file to enable all AI features")
1259
+
1260
+ demo = create_main_interface()
1261
+
1262
+ try:
1263
+ demo.launch(
1264
+ share=True,
1265
+ server_name="0.0.0.0",
1266
+ server_port=7860,
1267
+ inbrowser=True
1268
+ )
1269
+ logger.info("🌐 App launched successfully!")
1270
+ logger.info("πŸ”— Local URL: http://localhost:7860")
1271
+
1272
+ except Exception as e:
1273
+ logger.error(f"❌ Launch failed: {e}")
1274
+ logger.info("πŸ’‘ Try: pip install --upgrade gradio")
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # app.py - Expanded D&D Campaign and Character Creator with AI Agents
2
 
3
  import gradio as gr
4
  import logging
@@ -12,22 +12,22 @@ import random
12
 
13
  # Load environment variables
14
  load_dotenv()
 
 
 
 
 
15
  # Load OpenAI API key
16
  try:
17
  import openai
18
- openai.api_key = os.getenv("OPENAI_API_KEY")
19
-
20
- if not openai.api_key:
21
- logger.warning("⚠️ No OpenAI API key found β€” AI features will be disabled.")
22
  else:
23
- logger.info("πŸ”‘ OpenAI key loaded successfully.")
24
  except ImportError:
25
- openai = None
26
- logger.error("❌ openai package not installed β€” check requirements.txt")
27
-
28
- # Set up logging
29
- logging.basicConfig(level=logging.INFO)
30
- logger = logging.getLogger(__name__)
31
 
32
  # ===== DATA MODELS =====
33
  class Alignment(Enum):
@@ -62,6 +62,7 @@ class Character:
62
  race: str
63
  character_class: str
64
  level: int
 
65
  alignment: Alignment
66
  abilities: Dict[str, int]
67
  hit_points: int
@@ -133,7 +134,7 @@ class DungeonMasterAgent:
133
 
134
  except Exception as e:
135
  logger.error(f"Campaign generation failed: {e}")
136
- return {"success": False, "error": str(e)}
137
 
138
  def generate_session_content(self, campaign_context: str, session_number: int) -> Dict:
139
  """Generate content for a specific session"""
@@ -166,7 +167,7 @@ class DungeonMasterAgent:
166
 
167
  except Exception as e:
168
  logger.error(f"Session generation failed: {e}")
169
- return {"success": False, "error": str(e)}
170
 
171
  class NPCAgent:
172
  """AI agent specialized in creating and roleplaying NPCs"""
@@ -210,7 +211,7 @@ class NPCAgent:
210
 
211
  except Exception as e:
212
  logger.error(f"NPC generation failed: {e}")
213
- return {"success": False, "error": str(e)}
214
 
215
  def roleplay_npc(self, npc_description: str, player_input: str, context: str) -> Dict:
216
  """Roleplay as an NPC in response to player actions"""
@@ -243,7 +244,7 @@ class NPCAgent:
243
 
244
  except Exception as e:
245
  logger.error(f"NPC roleplay failed: {e}")
246
- return {"success": False, "error": str(e)}
247
 
248
  class WorldBuilderAgent:
249
  """AI agent focused on creating consistent world elements"""
@@ -286,7 +287,7 @@ class WorldBuilderAgent:
286
 
287
  except Exception as e:
288
  logger.error(f"Location generation failed: {e}")
289
- return {"success": False, "error": str(e)}
290
 
291
  class LootMasterAgent:
292
  """AI agent specialized in creating balanced loot and magic items"""
@@ -328,7 +329,7 @@ class LootMasterAgent:
328
 
329
  except Exception as e:
330
  logger.error(f"Loot generation failed: {e}")
331
- return {"success": False, "error": str(e)}
332
 
333
  def create_custom_magic_item(self, item_concept: str, power_level: str, campaign_theme: str) -> Dict:
334
  """Create a custom magic item"""
@@ -365,7 +366,7 @@ class LootMasterAgent:
365
 
366
  except Exception as e:
367
  logger.error(f"Magic item creation failed: {e}")
368
- return {"success": False, "error": str(e)}
369
 
370
  # ===== CHARACTER CREATOR CLASS =====
371
  class CharacterCreator:
@@ -640,6 +641,10 @@ def create_main_interface():
640
  choices=list(character_creator.classes.keys()),
641
  label="Class", value="Fighter"
642
  )
 
 
 
 
643
 
644
  with gr.Row():
645
  level_slider = gr.Slider(minimum=1, maximum=20, step=1, value=1, label="Level")
@@ -909,12 +914,12 @@ def create_main_interface():
909
  abilities["Intelligence"], abilities["Wisdom"], abilities["Charisma"]
910
  )
911
 
912
- def generate_character_name(race, char_class, alignment):
913
  try:
914
  from openai import OpenAI
915
  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
916
 
917
- prompt = f"Generate a {race} {char_class} name appropriate for D&D. Return only the name."
918
 
919
  response = client.chat.completions.create(
920
  model="gpt-4",
@@ -924,15 +929,55 @@ def create_main_interface():
924
 
925
  return response.choices[0].message.content.strip()
926
  except Exception as e:
927
- return f"Error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
928
 
929
- def generate_character_backstory(name, race, char_class, alignment, background):
930
  try:
931
  from openai import OpenAI
932
  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
933
 
934
- prompt = f"""Create a compelling backstory for {name}, a {race} {char_class} with {alignment} alignment and {background} background.
935
- Write 2-3 paragraphs about their origins, motivations, and key life events."""
936
 
937
  response = client.chat.completions.create(
938
  model="gpt-4",
@@ -942,16 +987,16 @@ def create_main_interface():
942
 
943
  return response.choices[0].message.content.strip()
944
  except Exception as e:
945
- return f"Error generating backstory: {str(e)}"
946
 
947
  def generate_character_portrait(character_data):
948
  if not character_data:
949
  return "Please create a character first"
950
 
951
- prompt = f"Fantasy portrait of a {character_data.race.lower()} {character_data.character_class.lower()}, professional D&D character art"
952
  return generate_image(prompt)
953
 
954
- def update_character_summary(name, race, char_class, level, alignment,
955
  str_val, dex_val, con_val, int_val, wis_val, cha_val, background, backstory):
956
  if not all([str_val, dex_val, con_val, int_val, wis_val, cha_val]):
957
  return "*Roll ability scores to see summary*", None
@@ -967,13 +1012,13 @@ def create_main_interface():
967
 
968
  character = Character(
969
  name=name or "Unnamed Character", race=race, character_class=char_class,
970
- level=int(level), alignment=Alignment(alignment), abilities=final_abilities,
971
  hit_points=hit_points, skills=character_creator.classes[char_class].skills[:2],
972
  background=background, backstory=backstory
973
  )
974
 
975
  summary = f"""**{character.name}**
976
- *Level {level} {race} {char_class} ({alignment})*
977
 
978
  **Ability Scores:**
979
  - STR: {final_abilities['Strength']} ({character_creator.calculate_ability_modifier(final_abilities['Strength']):+d})
@@ -994,7 +1039,7 @@ def create_main_interface():
994
  # Campaign Events
995
  def generate_campaign_concept(theme, level, players):
996
  result = dm_agent.generate_campaign_concept(theme, level, players)
997
- return result.get("content", "Error generating campaign") if result["success"] else f"Error: {result['error']}"
998
 
999
  def generate_campaign_art(theme, level):
1000
  prompt = f"{theme} D&D campaign art for level {level} adventurers, epic fantasy illustration"
@@ -1004,29 +1049,31 @@ def create_main_interface():
1004
  if not campaign_output:
1005
  return "Please generate a campaign concept first"
1006
  result = dm_agent.generate_session_content(campaign_output, session_num)
1007
- return result.get("content", "Error generating session") if result["success"] else f"Error: {result['error']}"
1008
 
1009
  # NPC Events
1010
  def create_npc(context, role, importance):
1011
  result = npc_agent.generate_npc(context, role, importance)
1012
- return result.get("content", "Error creating NPC") if result["success"] else f"Error: {result['error']}"
1013
 
1014
  def generate_npc_portrait(npc_data):
1015
  if not npc_data:
1016
- return "Create an NPC first"
1017
- prompt = "Fantasy portrait of a D&D NPC character, detailed face, professional RPG art"
 
 
1018
  return generate_image(prompt)
1019
 
1020
  def npc_roleplay_response(npc_data, player_input, context):
1021
  if not npc_data or not player_input:
1022
  return "Please create an NPC and enter player input"
1023
  result = npc_agent.roleplay_npc(npc_data, player_input, context)
1024
- return result.get("content", "Error in roleplay") if result["success"] else f"Error: {result['error']}"
1025
 
1026
  # World Builder Events
1027
  def create_location(loc_type, theme, purpose):
1028
  result = world_agent.generate_location(loc_type, theme, purpose)
1029
- return result.get("content", "Error creating location") if result["success"] else f"Error: {result['error']}"
1030
 
1031
  def generate_location_art(loc_type, theme):
1032
  prompt = f"{theme} {loc_type} fantasy location, detailed environment art, D&D setting"
@@ -1035,11 +1082,11 @@ def create_main_interface():
1035
  # Loot Events
1036
  def create_loot_table(level, encounter, rarity):
1037
  result = loot_agent.generate_loot_table(level, encounter, rarity)
1038
- return result.get("content", "Error creating loot") if result["success"] else f"Error: {result['error']}"
1039
 
1040
  def create_magic_item(concept, power, theme):
1041
  result = loot_agent.create_custom_magic_item(concept, power, theme)
1042
- return result.get("content", "Error creating item") if result["success"] else f"Error: {result['error']}"
1043
 
1044
  def generate_item_art(concept, power):
1045
  prompt = f"{power} magical {concept}, fantasy item illustration, glowing magical effects"
@@ -1096,13 +1143,13 @@ def create_main_interface():
1096
 
1097
  generate_name_btn.click(
1098
  generate_character_name,
1099
- inputs=[race_dropdown, class_dropdown, alignment_dropdown],
1100
  outputs=[character_name]
1101
  )
1102
 
1103
  generate_backstory_btn.click(
1104
  generate_character_backstory,
1105
- inputs=[character_name, race_dropdown, class_dropdown, alignment_dropdown, background_dropdown],
1106
  outputs=[backstory]
1107
  )
1108
 
@@ -1113,11 +1160,11 @@ def create_main_interface():
1113
  )
1114
 
1115
  # Update character summary when inputs change
1116
- for component in [character_name, race_dropdown, class_dropdown, level_slider, alignment_dropdown,
1117
  str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory]:
1118
  component.change(
1119
  update_character_summary,
1120
- inputs=[character_name, race_dropdown, class_dropdown, level_slider, alignment_dropdown,
1121
  str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory],
1122
  outputs=[character_summary, character_data]
1123
  )
@@ -1150,7 +1197,7 @@ def create_main_interface():
1150
 
1151
  npc_portrait_btn.click(
1152
  generate_npc_portrait,
1153
- inputs=[current_npc_data],
1154
  outputs=[npc_portrait]
1155
  )
1156
 
@@ -1197,6 +1244,124 @@ def create_main_interface():
1197
  gen_encounter_btn.click(generate_random_encounter, inputs=[encounter_level, encounter_difficulty], outputs=[random_encounter])
1198
  gen_hook_btn.click(generate_plot_hook, inputs=[hook_theme], outputs=[plot_hook])
1199
  gen_weather_btn.click(generate_weather, inputs=[climate], outputs=[weather])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1200
 
1201
  return demo
1202
 
@@ -1210,20 +1375,19 @@ if __name__ == "__main__":
1210
  logger.info("βœ… OpenAI API key found - All AI features enabled!")
1211
  else:
1212
  logger.warning("⚠️ No OpenAI API key found - AI features will be limited")
1213
- logger.info("πŸ’‘ Add OPENAI_API_KEY=your_key to a .env file to enable all AI features")
1214
 
1215
  demo = create_main_interface()
1216
 
1217
  try:
 
1218
  demo.launch(
1219
- share=True,
1220
- server_name="0.0.0.0",
1221
- server_port=7860,
1222
- inbrowser=True
1223
  )
1224
  logger.info("🌐 App launched successfully!")
1225
- logger.info("πŸ”— Local URL: http://localhost:7860")
1226
 
1227
  except Exception as e:
1228
  logger.error(f"❌ Launch failed: {e}")
1229
- logger.info("πŸ’‘ Try: pip install --upgrade gradio")
 
 
1
+ # app.py - Fixed D&D Campaign and Character Creator with AI Agents
2
 
3
  import gradio as gr
4
  import logging
 
12
 
13
  # Load environment variables
14
  load_dotenv()
15
+
16
+ # Set up logging
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
  # Load OpenAI API key
21
  try:
22
  import openai
23
+ api_key = os.getenv("OPENAI_API_KEY")
24
+ if api_key:
25
+ openai.api_key = api_key
26
+ logger.info("βœ… OpenAI API key loaded")
27
  else:
28
+ logger.warning("⚠️ No OpenAI API key found")
29
  except ImportError:
30
+ logger.warning("⚠️ OpenAI package not installed")
 
 
 
 
 
31
 
32
  # ===== DATA MODELS =====
33
  class Alignment(Enum):
 
62
  race: str
63
  character_class: str
64
  level: int
65
+ gender: str
66
  alignment: Alignment
67
  abilities: Dict[str, int]
68
  hit_points: int
 
134
 
135
  except Exception as e:
136
  logger.error(f"Campaign generation failed: {e}")
137
+ return {"success": False, "error": str(e), "content": f"Mock Campaign: {theme} adventure for {player_count} level {level} characters"}
138
 
139
  def generate_session_content(self, campaign_context: str, session_number: int) -> Dict:
140
  """Generate content for a specific session"""
 
167
 
168
  except Exception as e:
169
  logger.error(f"Session generation failed: {e}")
170
+ return {"success": False, "error": str(e), "content": f"Mock Session {session_number}: Adventure continues..."}
171
 
172
  class NPCAgent:
173
  """AI agent specialized in creating and roleplaying NPCs"""
 
211
 
212
  except Exception as e:
213
  logger.error(f"NPC generation failed: {e}")
214
+ return {"success": False, "error": str(e), "content": f"Mock NPC: {role} character for {context}"}
215
 
216
  def roleplay_npc(self, npc_description: str, player_input: str, context: str) -> Dict:
217
  """Roleplay as an NPC in response to player actions"""
 
244
 
245
  except Exception as e:
246
  logger.error(f"NPC roleplay failed: {e}")
247
+ return {"success": False, "error": str(e), "content": "Mock NPC Response: The character responds appropriately to your action."}
248
 
249
  class WorldBuilderAgent:
250
  """AI agent focused on creating consistent world elements"""
 
287
 
288
  except Exception as e:
289
  logger.error(f"Location generation failed: {e}")
290
+ return {"success": False, "error": str(e), "content": f"Mock Location: {theme} {location_type} for {purpose}"}
291
 
292
  class LootMasterAgent:
293
  """AI agent specialized in creating balanced loot and magic items"""
 
329
 
330
  except Exception as e:
331
  logger.error(f"Loot generation failed: {e}")
332
+ return {"success": False, "error": str(e), "content": f"Mock Loot: Level {level} {encounter_type} {rarity} treasures"}
333
 
334
  def create_custom_magic_item(self, item_concept: str, power_level: str, campaign_theme: str) -> Dict:
335
  """Create a custom magic item"""
 
366
 
367
  except Exception as e:
368
  logger.error(f"Magic item creation failed: {e}")
369
+ return {"success": False, "error": str(e), "content": f"Mock Magic Item: {power_level} {item_concept} with {campaign_theme} theme"}
370
 
371
  # ===== CHARACTER CREATOR CLASS =====
372
  class CharacterCreator:
 
641
  choices=list(character_creator.classes.keys()),
642
  label="Class", value="Fighter"
643
  )
644
+ gender_dropdown = gr.Dropdown(
645
+ choices=["Male", "Female", "Non-binary", "Transgender Male", "Transgender Female", "Genderfluid", "Agender", "Other"],
646
+ label="Gender", value="Male"
647
+ )
648
 
649
  with gr.Row():
650
  level_slider = gr.Slider(minimum=1, maximum=20, step=1, value=1, label="Level")
 
914
  abilities["Intelligence"], abilities["Wisdom"], abilities["Charisma"]
915
  )
916
 
917
+ def generate_character_name(race, char_class, gender, alignment):
918
  try:
919
  from openai import OpenAI
920
  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
921
 
922
+ prompt = f"Generate a {gender} {race} {char_class} name appropriate for D&D. Return only the name."
923
 
924
  response = client.chat.completions.create(
925
  model="gpt-4",
 
929
 
930
  return response.choices[0].message.content.strip()
931
  except Exception as e:
932
+ # Fallback to simple name generation
933
+ names = {
934
+ "Human": {
935
+ "Male": ["Garrett", "Marcus", "Thomas", "William", "James"],
936
+ "Female": ["Elena", "Sarah", "Miranda", "Catherine", "Rose"],
937
+ "Non-binary": ["Alex", "Jordan", "Riley", "Casey", "Taylor"],
938
+ "Other": ["River", "Sage", "Phoenix", "Ash", "Rowan"]
939
+ },
940
+ "Elf": {
941
+ "Male": ["Aelar", "Berrian", "Drannor", "Enna", "Galinndan"],
942
+ "Female": ["Adrie", "Althaea", "Anastrianna", "Andraste", "Antinua"],
943
+ "Non-binary": ["Aerdyl", "Ahvir", "Aramil", "Aranea", "Berris"],
944
+ "Other": ["Dayereth", "Enna", "Galinndan", "Hadarai", "Halimath"]
945
+ },
946
+ "Dwarf": {
947
+ "Male": ["Adrik", "Baern", "Darrak", "Delg", "Eberk"],
948
+ "Female": ["Amber", "Bardryn", "Diesa", "Eldeth", "Gunnloda"],
949
+ "Non-binary": ["Alberich", "Balin", "Dain", "Fundin", "GloΓ­n"],
950
+ "Other": ["HΓ‘var", "KΓ­li", "NΓ‘li", "Ori", "Thorek"]
951
+ },
952
+ "Halfling": {
953
+ "Male": ["Alton", "Ander", "Cade", "Corrin", "Eldon"],
954
+ "Female": ["Andry", "Bree", "Callie", "Cora", "Euphemia"],
955
+ "Non-binary": ["Finnan", "Garret", "Lindal", "Lyle", "Merric"],
956
+ "Other": ["Nedda", "Paela", "Portia", "Seraphina", "Shaena"]
957
+ }
958
+ }
959
+
960
+ # Handle various gender identities
961
+ gender_key = gender
962
+ if gender in ["Transgender Male", "Male"]:
963
+ gender_key = "Male"
964
+ elif gender in ["Transgender Female", "Female"]:
965
+ gender_key = "Female"
966
+ elif gender in ["Non-binary", "Genderfluid", "Agender"]:
967
+ gender_key = "Non-binary"
968
+ else:
969
+ gender_key = "Other"
970
+
971
+ race_names = names.get(race, names["Human"])
972
+ return random.choice(race_names.get(gender_key, race_names["Other"]))
973
 
974
+ def generate_character_backstory(name, race, char_class, gender, alignment, background):
975
  try:
976
  from openai import OpenAI
977
  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
978
 
979
+ prompt = f"""Create a compelling backstory for {name}, a {gender} {race} {char_class} with {alignment} alignment and {background} background.
980
+ Write 2-3 paragraphs about their origins, motivations, and key life events. Be respectful and inclusive in your portrayal."""
981
 
982
  response = client.chat.completions.create(
983
  model="gpt-4",
 
987
 
988
  return response.choices[0].message.content.strip()
989
  except Exception as e:
990
+ 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."
991
 
992
  def generate_character_portrait(character_data):
993
  if not character_data:
994
  return "Please create a character first"
995
 
996
+ 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"
997
  return generate_image(prompt)
998
 
999
+ def update_character_summary(name, race, char_class, level, gender, alignment,
1000
  str_val, dex_val, con_val, int_val, wis_val, cha_val, background, backstory):
1001
  if not all([str_val, dex_val, con_val, int_val, wis_val, cha_val]):
1002
  return "*Roll ability scores to see summary*", None
 
1012
 
1013
  character = Character(
1014
  name=name or "Unnamed Character", race=race, character_class=char_class,
1015
+ level=int(level), gender=gender, alignment=Alignment(alignment), abilities=final_abilities,
1016
  hit_points=hit_points, skills=character_creator.classes[char_class].skills[:2],
1017
  background=background, backstory=backstory
1018
  )
1019
 
1020
  summary = f"""**{character.name}**
1021
+ *Level {level} {gender} {race} {char_class} ({alignment})*
1022
 
1023
  **Ability Scores:**
1024
  - STR: {final_abilities['Strength']} ({character_creator.calculate_ability_modifier(final_abilities['Strength']):+d})
 
1039
  # Campaign Events
1040
  def generate_campaign_concept(theme, level, players):
1041
  result = dm_agent.generate_campaign_concept(theme, level, players)
1042
+ return result.get("content", "Error generating campaign")
1043
 
1044
  def generate_campaign_art(theme, level):
1045
  prompt = f"{theme} D&D campaign art for level {level} adventurers, epic fantasy illustration"
 
1049
  if not campaign_output:
1050
  return "Please generate a campaign concept first"
1051
  result = dm_agent.generate_session_content(campaign_output, session_num)
1052
+ return result.get("content", "Error generating session")
1053
 
1054
  # NPC Events
1055
  def create_npc(context, role, importance):
1056
  result = npc_agent.generate_npc(context, role, importance)
1057
+ return result.get("content", "Error creating NPC")
1058
 
1059
  def generate_npc_portrait(npc_data):
1060
  if not npc_data:
1061
+ prompt = "Fantasy portrait of a D&D NPC character, detailed face, professional RPG art"
1062
+ else:
1063
+ # Extract key details from NPC description for better portraits
1064
+ prompt = f"Fantasy portrait of a D&D NPC character based on: {npc_data[:200]}..., detailed face, professional RPG art"
1065
  return generate_image(prompt)
1066
 
1067
  def npc_roleplay_response(npc_data, player_input, context):
1068
  if not npc_data or not player_input:
1069
  return "Please create an NPC and enter player input"
1070
  result = npc_agent.roleplay_npc(npc_data, player_input, context)
1071
+ return result.get("content", "Error in roleplay")
1072
 
1073
  # World Builder Events
1074
  def create_location(loc_type, theme, purpose):
1075
  result = world_agent.generate_location(loc_type, theme, purpose)
1076
+ return result.get("content", "Error creating location")
1077
 
1078
  def generate_location_art(loc_type, theme):
1079
  prompt = f"{theme} {loc_type} fantasy location, detailed environment art, D&D setting"
 
1082
  # Loot Events
1083
  def create_loot_table(level, encounter, rarity):
1084
  result = loot_agent.generate_loot_table(level, encounter, rarity)
1085
+ return result.get("content", "Error creating loot")
1086
 
1087
  def create_magic_item(concept, power, theme):
1088
  result = loot_agent.create_custom_magic_item(concept, power, theme)
1089
+ return result.get("content", "Error creating item")
1090
 
1091
  def generate_item_art(concept, power):
1092
  prompt = f"{power} magical {concept}, fantasy item illustration, glowing magical effects"
 
1143
 
1144
  generate_name_btn.click(
1145
  generate_character_name,
1146
+ inputs=[race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown],
1147
  outputs=[character_name]
1148
  )
1149
 
1150
  generate_backstory_btn.click(
1151
  generate_character_backstory,
1152
+ inputs=[character_name, race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown, background_dropdown],
1153
  outputs=[backstory]
1154
  )
1155
 
 
1160
  )
1161
 
1162
  # Update character summary when inputs change
1163
+ for component in [character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown,
1164
  str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory]:
1165
  component.change(
1166
  update_character_summary,
1167
+ inputs=[character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown,
1168
  str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory],
1169
  outputs=[character_summary, character_data]
1170
  )
 
1197
 
1198
  npc_portrait_btn.click(
1199
  generate_npc_portrait,
1200
+ inputs=[npc_output], # Changed from current_npc_data to npc_output
1201
  outputs=[npc_portrait]
1202
  )
1203
 
 
1244
  gen_encounter_btn.click(generate_random_encounter, inputs=[encounter_level, encounter_difficulty], outputs=[random_encounter])
1245
  gen_hook_btn.click(generate_plot_hook, inputs=[hook_theme], outputs=[plot_hook])
1246
  gen_weather_btn.click(generate_weather, inputs=[climate], outputs=[weather])
1247
+
1248
+ # Additional missing event handlers
1249
+
1250
+ # Export character functionality
1251
+ def export_character_json(character_data):
1252
+ if not character_data:
1253
+ return None
1254
+
1255
+ try:
1256
+ # Convert character to dict for JSON export
1257
+ char_dict = {
1258
+ "name": character_data.name,
1259
+ "race": character_data.race,
1260
+ "class": character_data.character_class,
1261
+ "level": character_data.level,
1262
+ "gender": character_data.gender,
1263
+ "alignment": character_data.alignment.value,
1264
+ "abilities": character_data.abilities,
1265
+ "hit_points": character_data.hit_points,
1266
+ "skills": character_data.skills,
1267
+ "background": character_data.background,
1268
+ "backstory": character_data.backstory
1269
+ }
1270
+
1271
+ # Create temporary file
1272
+ import tempfile
1273
+ import json
1274
+
1275
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
1276
+ json.dump(char_dict, f, indent=2)
1277
+ return f.name
1278
+
1279
+ except Exception as e:
1280
+ logger.error(f"Character export failed: {e}")
1281
+ return None
1282
+
1283
+ export_btn.click(
1284
+ export_character_json,
1285
+ inputs=[character_data],
1286
+ outputs=[export_file]
1287
+ )
1288
+
1289
+ # Session notes functionality
1290
+ def save_session_notes(session_date, key_events, npc_interactions, player_actions, next_session_prep):
1291
+ try:
1292
+ import tempfile
1293
+
1294
+ notes_content = f"""# {session_date}
1295
+
1296
+ ## Key Events
1297
+ {key_events}
1298
+
1299
+ ## NPC Interactions
1300
+ {npc_interactions}
1301
+
1302
+ ## Notable Player Actions
1303
+ {player_actions}
1304
+
1305
+ ## Next Session Preparation
1306
+ {next_session_prep}
1307
+
1308
+ ---
1309
+ *Generated by D&D Campaign Manager*
1310
+ """
1311
+
1312
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
1313
+ f.write(notes_content)
1314
+ return f.name
1315
+
1316
+ except Exception as e:
1317
+ logger.error(f"Session notes save failed: {e}")
1318
+ return None
1319
+
1320
+ save_notes_btn.click(
1321
+ save_session_notes,
1322
+ inputs=[session_date, key_events, npc_interactions, player_actions, next_session_prep],
1323
+ outputs=[notes_output]
1324
+ )
1325
+
1326
+ # Initiative tracker functionality
1327
+ initiative_data = gr.State([])
1328
+
1329
+ def add_to_initiative(current_data, char_name, initiative_roll):
1330
+ if not char_name:
1331
+ return current_data
1332
+
1333
+ if current_data is None:
1334
+ current_data = []
1335
+
1336
+ # Add new character
1337
+ new_entry = [char_name, int(initiative_roll), "Full", "10", "Active"]
1338
+ current_data.append(new_entry)
1339
+
1340
+ # Sort by initiative (descending)
1341
+ current_data.sort(key=lambda x: x[1], reverse=True)
1342
+
1343
+ return current_data
1344
+
1345
+ def reset_initiative():
1346
+ return []
1347
+
1348
+ add_btn.click(
1349
+ add_to_initiative,
1350
+ inputs=[initiative_data, add_character, add_initiative],
1351
+ outputs=[initiative_data]
1352
+ )
1353
+
1354
+ # Update the dataframe when initiative_data changes
1355
+ initiative_data.change(
1356
+ lambda data: data,
1357
+ inputs=[initiative_data],
1358
+ outputs=[initiative_list]
1359
+ )
1360
+
1361
+ reset_combat_btn.click(
1362
+ reset_initiative,
1363
+ outputs=[initiative_data]
1364
+ )
1365
 
1366
  return demo
1367
 
 
1375
  logger.info("βœ… OpenAI API key found - All AI features enabled!")
1376
  else:
1377
  logger.warning("⚠️ No OpenAI API key found - AI features will be limited")
1378
+ logger.info("πŸ’‘ Add OPENAI_API_KEY as a repository secret in HF Spaces settings")
1379
 
1380
  demo = create_main_interface()
1381
 
1382
  try:
1383
+ # HF Spaces compatible launch
1384
  demo.launch(
1385
+ share=False, # HF Spaces handles sharing
1386
+ inbrowser=False # Don't try to open browser in cloud environment
 
 
1387
  )
1388
  logger.info("🌐 App launched successfully!")
 
1389
 
1390
  except Exception as e:
1391
  logger.error(f"❌ Launch failed: {e}")
1392
+ # Fallback launch for HF Spaces
1393
+ demo.launch()