alexchilton Claude commited on
Commit
fb96ecf
ยท
1 Parent(s): 9ac43de

refactor: Reorganize project structure and enhance Gradio app

Browse files

**Project Structure:**
- Created organized directory structure (characters/, tests/, docs/, web/)
- Moved test files to tests/ directory with updated imports
- Moved documentation to docs/ directory
- Moved Gradio apps to web/ directory
- Created characters/ directory for JSON character files

**Character System:**
- Added image_path field to Character class (placeholder for future GAN generation)
- Exported Thorin and Elara as JSON files in characters/ directory
- Characters now loaded from JSON files instead of hardcoded

**Gradio App Enhancements:**
- Added character creation tab with full interactive UI
- Character creator includes: race, class, ability scores, background, alignment
- Equipment auto-assigned based on class
- Character image display (placeholder for future GAN)
- Characters now dynamically loaded from JSON files
- Improved UI with tabs for Play Game and Create Character

**Docker Updates:**
- Updated start.sh to point to web/app_gradio.py
- Maintained Docker compatibility with new structure

**Testing:**
- All tests passing (20/20)
- Updated test imports for new directory structure
- Fixed syntax error in test_all_collections.py

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

characters/elara_moonwhisper.json ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Elara Moonwhisper",
3
+ "race": "Elf",
4
+ "character_class": "Wizard",
5
+ "level": 2,
6
+ "strength": 8,
7
+ "dexterity": 14,
8
+ "constitution": 12,
9
+ "intelligence": 17,
10
+ "wisdom": 13,
11
+ "charisma": 10,
12
+ "hit_points": 14,
13
+ "armor_class": 12,
14
+ "proficiency_bonus": 2,
15
+ "background": "Sage",
16
+ "alignment": "Neutral Good",
17
+ "race_traits": [
18
+ "Darkvision",
19
+ "Fey Ancestry"
20
+ ],
21
+ "class_features": [
22
+ "Spellcasting",
23
+ "Arcane Recovery"
24
+ ],
25
+ "proficiencies": [
26
+ "Daggers",
27
+ "Quarterstaffs",
28
+ "Light crossbows"
29
+ ],
30
+ "equipment": [
31
+ "Quarterstaff",
32
+ "Spellbook",
33
+ "Component Pouch",
34
+ "Scholar's Pack"
35
+ ],
36
+ "spells": [
37
+ "Fire Bolt",
38
+ "Mage Hand",
39
+ "Magic Missile",
40
+ "Shield"
41
+ ],
42
+ "image_path": null
43
+ }
characters/thorin_stormshield.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Thorin Stormshield",
3
+ "race": "Dwarf",
4
+ "character_class": "Fighter",
5
+ "level": 3,
6
+ "strength": 16,
7
+ "dexterity": 12,
8
+ "constitution": 16,
9
+ "intelligence": 10,
10
+ "wisdom": 13,
11
+ "charisma": 8,
12
+ "hit_points": 28,
13
+ "armor_class": 18,
14
+ "proficiency_bonus": 2,
15
+ "background": "Soldier",
16
+ "alignment": "Lawful Good",
17
+ "race_traits": [
18
+ "Dwarven Resilience",
19
+ "Darkvision"
20
+ ],
21
+ "class_features": [
22
+ "Fighting Style: Defense",
23
+ "Second Wind",
24
+ "Action Surge"
25
+ ],
26
+ "proficiencies": [
27
+ "All armor",
28
+ "All weapons",
29
+ "Shields"
30
+ ],
31
+ "equipment": [
32
+ "Longsword",
33
+ "Shield",
34
+ "Plate Armor",
35
+ "Backpack",
36
+ "50 GP"
37
+ ],
38
+ "spells": [],
39
+ "image_path": null
40
+ }
dnd_rag_system/systems/character_creator.py CHANGED
@@ -52,6 +52,9 @@ class Character:
52
  equipment: List[str] = field(default_factory=list)
53
  spells: List[str] = field(default_factory=list)
54
 
 
 
 
55
  def get_ability_modifier(self, score: int) -> int:
56
  """Calculate ability modifier from score."""
57
  return (score - 10) // 2
 
52
  equipment: List[str] = field(default_factory=list)
53
  spells: List[str] = field(default_factory=list)
54
 
55
+ # Character Image (for future GAN generation)
56
+ image_path: Optional[str] = None
57
+
58
  def get_ability_modifier(self, score: int) -> int:
59
  """Calculate ability modifier from score."""
60
  return (score - 10) // 2
HUGGINGFACE_DEPLOYMENT.md โ†’ docs/HUGGINGFACE_DEPLOYMENT.md RENAMED
File without changes
plan_progress.md โ†’ docs/plan_progress.md RENAMED
File without changes
start.sh CHANGED
@@ -19,4 +19,4 @@ fi
19
 
20
  # Start the Gradio application
21
  echo "Starting Gradio app..."
22
- python app_gradio.py
 
19
 
20
  # Start the Gradio application
21
  echo "Starting Gradio app..."
22
+ python web/app_gradio.py
test_all_collections.py โ†’ tests/test_all_collections.py RENAMED
@@ -11,7 +11,7 @@ from pathlib import Path
11
  from typing import Dict, List, Tuple
12
 
13
  # Add project to path
14
- project_root = Path(__file__).parent
15
  sys.path.insert(0, str(project_root))
16
 
17
  from dnd_rag_system.core.chroma_manager import ChromaDBManager
@@ -349,10 +349,11 @@ class RAGTestSuite:
349
  found_darkvision = any(meta.get('darkvision', 0) > 0
350
  for meta in results['metadatas'][0])
351
 
 
352
  self.assert_test(
353
  "Race: Trait search finds races with darkvision",
354
  found_darkvision,
355
- f"Races: {[f\"{m.get('name')} ({m.get('darkvision')}ft)\" for m in results['metadatas'][0][:3] if m.get('darkvision', 0) > 0]}"
356
  )
357
  else:
358
  self.assert_test(
 
11
  from typing import Dict, List, Tuple
12
 
13
  # Add project to path
14
+ project_root = Path(__file__).parent.parent
15
  sys.path.insert(0, str(project_root))
16
 
17
  from dnd_rag_system.core.chroma_manager import ChromaDBManager
 
349
  found_darkvision = any(meta.get('darkvision', 0) > 0
350
  for meta in results['metadatas'][0])
351
 
352
+ darkvision_races = [f"{m.get('name')} ({m.get('darkvision')}ft)" for m in results['metadatas'][0][:3] if m.get('darkvision', 0) > 0]
353
  self.assert_test(
354
  "Race: Trait search finds races with darkvision",
355
  found_darkvision,
356
+ f"Races: {darkvision_races}"
357
  )
358
  else:
359
  self.assert_test(
test_entity_retrieval.py โ†’ tests/test_entity_retrieval.py RENAMED
@@ -24,7 +24,7 @@ from pathlib import Path
24
  from typing import List, Dict, Set
25
 
26
  # Add project to path
27
- project_root = Path(__file__).parent
28
  sys.path.insert(0, str(project_root))
29
 
30
  from dnd_rag_system.core.chroma_manager import ChromaDBManager
 
24
  from typing import List, Dict, Set
25
 
26
  # Add project to path
27
+ project_root = Path(__file__).parent.parent
28
  sys.path.insert(0, str(project_root))
29
 
30
  from dnd_rag_system.core.chroma_manager import ChromaDBManager
test_rag_search.py โ†’ tests/test_rag_search.py RENAMED
File without changes
test_spell_retrieval.py โ†’ tests/test_spell_retrieval.py RENAMED
@@ -15,7 +15,7 @@ from pathlib import Path
15
  from typing import List, Dict, Tuple
16
 
17
  # Add project to path
18
- project_root = Path(__file__).parent
19
  sys.path.insert(0, str(project_root))
20
 
21
  from dnd_rag_system.core.chroma_manager import ChromaDBManager
 
15
  from typing import List, Dict, Tuple
16
 
17
  # Add project to path
18
+ project_root = Path(__file__).parent.parent
19
  sys.path.insert(0, str(project_root))
20
 
21
  from dnd_rag_system.core.chroma_manager import ChromaDBManager
test_spell_search.py โ†’ tests/test_spell_search.py RENAMED
@@ -7,7 +7,7 @@ import sys
7
  from pathlib import Path
8
 
9
  # Add project to path
10
- project_root = Path(__file__).parent
11
  sys.path.insert(0, str(project_root))
12
 
13
  from dnd_rag_system.core.chroma_manager import ChromaDBManager
 
7
  from pathlib import Path
8
 
9
  # Add project to path
10
+ project_root = Path(__file__).parent.parent
11
  sys.path.insert(0, str(project_root))
12
 
13
  from dnd_rag_system.core.chroma_manager import ChromaDBManager
app.py โ†’ web/app.py RENAMED
File without changes
app_gradio.py โ†’ web/app_gradio.py RENAMED
@@ -3,6 +3,7 @@
3
  D&D Character-Aware Game - Gradio Web Interface
4
 
5
  Web UI for playing D&D with RAG-enhanced AI GM and character tracking.
 
6
  """
7
 
8
  import os
@@ -12,83 +13,97 @@ os.environ['TOKENIZERS_PARALLELISM'] = 'false'
12
  import gradio as gr
13
  import json
14
  from pathlib import Path
 
 
15
 
16
  # Add project to path
17
  import sys
18
- sys.path.insert(0, str(Path(__file__).parent))
19
 
20
  from dnd_rag_system.core.chroma_manager import ChromaDBManager
21
- from dnd_rag_system.systems.character_creator import Character
22
  from dnd_rag_system.systems.gm_dialogue_unified import GameMaster
23
-
24
 
25
  # Initialize system
26
  print("๐ŸŽฒ Initializing D&D RAG System...")
27
  db = ChromaDBManager()
28
  gm = GameMaster(db)
29
 
30
- # Pre-made characters
31
- THORIN = Character(
32
- name="Thorin Stormshield",
33
- race="Dwarf",
34
- character_class="Fighter",
35
- level=3,
36
- strength=16,
37
- dexterity=12,
38
- constitution=16,
39
- intelligence=10,
40
- wisdom=13,
41
- charisma=8,
42
- hit_points=28,
43
- armor_class=18,
44
- proficiency_bonus=2,
45
- background="Soldier",
46
- alignment="Lawful Good",
47
- race_traits=["Dwarven Resilience", "Darkvision"],
48
- class_features=["Fighting Style: Defense", "Second Wind", "Action Surge"],
49
- proficiencies=["All armor", "All weapons", "Shields"],
50
- equipment=["Longsword", "Shield", "Plate Armor", "Backpack", "50 GP"],
51
- spells=[]
52
- )
53
-
54
- ELARA = Character(
55
- name="Elara Moonwhisper",
56
- race="Elf",
57
- character_class="Wizard",
58
- level=2,
59
- strength=8,
60
- dexterity=14,
61
- constitution=12,
62
- intelligence=17,
63
- wisdom=13,
64
- charisma=10,
65
- hit_points=14,
66
- armor_class=12,
67
- proficiency_bonus=2,
68
- background="Sage",
69
- alignment="Neutral Good",
70
- race_traits=["Darkvision", "Fey Ancestry"],
71
- class_features=["Spellcasting", "Arcane Recovery"],
72
- proficiencies=["Daggers", "Quarterstaffs", "Light crossbows"],
73
- equipment=["Quarterstaff", "Spellbook", "Component Pouch", "Scholar's Pack"],
74
- spells=["Fire Bolt", "Mage Hand", "Magic Missile", "Shield"]
75
- )
76
 
77
  # Global state
78
  current_character = None
79
  conversation_history = []
80
 
81
 
82
- def load_character(character_choice):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  """Load selected character and update context."""
84
  global current_character, conversation_history
85
 
86
  conversation_history = []
87
 
88
- if character_choice == "Thorin Stormshield (Dwarf Fighter)":
89
- current_character = THORIN
 
 
 
90
  else:
91
- current_character = ELARA
 
 
 
 
 
 
 
92
 
93
  # Set GM context
94
  char = current_character
@@ -109,11 +124,16 @@ EQUIPMENT: {', '.join(char.equipment[:5])}
109
 
110
  gm.set_context(context)
111
 
112
- # Return character sheet
113
- return format_character_sheet(), "", []
 
 
 
 
 
114
 
115
 
116
- def format_character_sheet():
117
  """Format character sheet for display."""
118
  if not current_character:
119
  return "No character selected"
@@ -155,7 +175,78 @@ def format_character_sheet():
155
  return sheet
156
 
157
 
158
- def chat(message, history):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  """Handle chat messages."""
160
  global conversation_history
161
 
@@ -239,7 +330,7 @@ Otherwise, just type your action and press Enter!"""
239
  ]
240
 
241
 
242
- def clear_history():
243
  """Clear conversation history."""
244
  global conversation_history
245
  conversation_history = []
@@ -247,78 +338,144 @@ def clear_history():
247
 
248
 
249
  # Create Gradio interface
250
- with gr.Blocks(title="D&D RAG Game Master") as demo:
251
  gr.Markdown("""
252
  # ๐ŸŽฒ D&D Character-Aware Game Master
253
 
254
- Play D&D with an AI Game Master powered by RAG (Retrieval-Augmented Generation) and the Qwen3-4B-RPG-Roleplay model.
255
 
256
- **How to Play:**
257
- 1. Select a character
258
- 2. Type your actions (e.g., "I look around", "I cast Magic Missile at the goblin")
259
- 3. Use `/help` to see available commands
 
260
  """)
261
 
262
- with gr.Row():
263
- with gr.Column(scale=1):
264
- gr.Markdown("## Character Selection")
265
-
266
- character_dropdown = gr.Dropdown(
267
- choices=["Thorin Stormshield (Dwarf Fighter)", "Elara Moonwhisper (Elf Wizard)"],
268
- value="Thorin Stormshield (Dwarf Fighter)",
269
- label="Choose Your Character"
270
- )
271
-
272
- load_btn = gr.Button("Load Character", variant="primary")
273
-
274
- gr.Markdown("---")
275
-
276
- character_sheet = gr.Markdown(format_character_sheet())
277
-
278
- with gr.Column(scale=2):
279
- gr.Markdown("## Game Session")
280
-
281
- chatbot = gr.Chatbot(
282
- height=500,
283
- label="Game Master",
284
- show_label=True
285
- )
286
-
287
  with gr.Row():
288
- msg_input = gr.Textbox(
289
- placeholder="Type your action or command here... (e.g., 'I look around' or '/help')",
290
- label="Your Action",
291
- show_label=False,
292
- scale=4
293
- )
294
- submit_btn = gr.Button("Send", variant="primary", scale=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
 
296
  with gr.Row():
297
- clear_btn = gr.Button("Clear History")
298
- gr.Markdown("**Quick Commands:** `/help` | `/context` | `/stats` | `/rag <query>`")
299
-
300
- gr.Markdown("""
301
- ---
302
-
303
- ### ๐Ÿ“– Example Actions
304
- - "I look around and check my surroundings"
305
- - "I draw my weapon and prepare for combat"
306
- - "I cast Magic Missile at the goblin" (Elara)
307
- - "I attack with my longsword" (Thorin)
308
-
309
- ### ๐Ÿ” RAG Commands
310
- - `/rag Magic Missile` - Look up spell details
311
- - `/rag Goblin` - Look up monster stats
312
- - `/rag Fighter` - Look up class features
313
-
314
- **Powered by:** ChromaDB RAG + AI Language Model (Auto-detected: Ollama locally, HF Inference API on Spaces)
315
- """)
316
-
317
- # Event handlers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  load_btn.click(
319
  load_character,
320
  inputs=[character_dropdown],
321
- outputs=[character_sheet, msg_input, chatbot]
322
  )
323
 
324
  submit_btn.click(
@@ -344,11 +501,18 @@ with gr.Blocks(title="D&D RAG Game Master") as demo:
344
  outputs=[chatbot]
345
  )
346
 
 
 
 
 
 
 
 
 
 
347
 
348
- if __name__ == "__main__":
349
- # Load default character
350
- load_character("Thorin Stormshield (Dwarf Fighter)")
351
 
 
352
  # Launch
353
  demo.launch(
354
  server_name="0.0.0.0",
 
3
  D&D Character-Aware Game - Gradio Web Interface
4
 
5
  Web UI for playing D&D with RAG-enhanced AI GM and character tracking.
6
+ Includes character creation, loading, and image display.
7
  """
8
 
9
  import os
 
13
  import gradio as gr
14
  import json
15
  from pathlib import Path
16
+ from typing import Optional, List, Tuple
17
+ from dataclasses import asdict
18
 
19
  # Add project to path
20
  import sys
21
+ sys.path.insert(0, str(Path(__file__).parent.parent))
22
 
23
  from dnd_rag_system.core.chroma_manager import ChromaDBManager
24
+ from dnd_rag_system.systems.character_creator import Character, CharacterCreator
25
  from dnd_rag_system.systems.gm_dialogue_unified import GameMaster
26
+ from dnd_rag_system.config import settings
27
 
28
  # Initialize system
29
  print("๐ŸŽฒ Initializing D&D RAG System...")
30
  db = ChromaDBManager()
31
  gm = GameMaster(db)
32
 
33
+ # Paths
34
+ CHARACTERS_DIR = Path(__file__).parent.parent / "characters"
35
+ CHARACTERS_DIR.mkdir(exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  # Global state
38
  current_character = None
39
  conversation_history = []
40
 
41
 
42
+ def load_character_from_json(filepath: Path) -> Optional[Character]:
43
+ """Load character from JSON file."""
44
+ try:
45
+ with open(filepath, 'r') as f:
46
+ data = json.load(f)
47
+ return Character(**data)
48
+ except Exception as e:
49
+ print(f"Error loading character from {filepath}: {e}")
50
+ return None
51
+
52
+
53
+ def save_character_to_json(character: Character, filename: str) -> str:
54
+ """Save character to JSON file."""
55
+ filepath = CHARACTERS_DIR / filename
56
+ try:
57
+ with open(filepath, 'w') as f:
58
+ json.dump(asdict(character), f, indent=2)
59
+ return str(filepath)
60
+ except Exception as e:
61
+ return f"Error saving character: {e}"
62
+
63
+
64
+ def get_available_characters() -> List[str]:
65
+ """Get list of available character files."""
66
+ characters = []
67
+
68
+ # Load pre-made characters
69
+ thorin_path = CHARACTERS_DIR / "thorin_stormshield.json"
70
+ elara_path = CHARACTERS_DIR / "elara_moonwhisper.json"
71
+
72
+ if thorin_path.exists():
73
+ characters.append("Thorin Stormshield (Dwarf Fighter)")
74
+ if elara_path.exists():
75
+ characters.append("Elara Moonwhisper (Elf Wizard)")
76
+
77
+ # Load custom characters
78
+ for json_file in CHARACTERS_DIR.glob("*.json"):
79
+ if json_file.name not in ["thorin_stormshield.json", "elara_moonwhisper.json"]:
80
+ char = load_character_from_json(json_file)
81
+ if char:
82
+ characters.append(f"{char.name} ({char.race} {char.character_class})")
83
+
84
+ return characters
85
+
86
+
87
+ def load_character(character_choice: str) -> Tuple[str, str, list, Optional[str]]:
88
  """Load selected character and update context."""
89
  global current_character, conversation_history
90
 
91
  conversation_history = []
92
 
93
+ # Map character choice to file
94
+ if "Thorin" in character_choice:
95
+ filepath = CHARACTERS_DIR / "thorin_stormshield.json"
96
+ elif "Elara" in character_choice:
97
+ filepath = CHARACTERS_DIR / "elara_moonwhisper.json"
98
  else:
99
+ # Try to find by character name
100
+ name = character_choice.split("(")[0].strip()
101
+ filepath = CHARACTERS_DIR / f"{name.lower().replace(' ', '_')}.json"
102
+
103
+ current_character = load_character_from_json(filepath)
104
+
105
+ if not current_character:
106
+ return "Error loading character", "", [], None
107
 
108
  # Set GM context
109
  char = current_character
 
124
 
125
  gm.set_context(context)
126
 
127
+ # Get character image if exists
128
+ char_image = None
129
+ if char.image_path and Path(char.image_path).exists():
130
+ char_image = char.image_path
131
+
132
+ # Return character sheet, clear input, empty chat, and image
133
+ return format_character_sheet(), "", [], char_image
134
 
135
 
136
+ def format_character_sheet() -> str:
137
  """Format character sheet for display."""
138
  if not current_character:
139
  return "No character selected"
 
175
  return sheet
176
 
177
 
178
+ def create_character(name: str, race: str, char_class: str, background: str,
179
+ alignment: str, str_val: int, dex_val: int, con_val: int,
180
+ int_val: int, wis_val: int, cha_val: int) -> Tuple[str, str]:
181
+ """Create a new character with given parameters."""
182
+ global current_character
183
+
184
+ if not name:
185
+ return "โŒ Please enter a character name", gr.update()
186
+
187
+ # Create character
188
+ character = Character(
189
+ name=name,
190
+ race=race,
191
+ character_class=char_class,
192
+ level=1,
193
+ strength=str_val,
194
+ dexterity=dex_val,
195
+ constitution=con_val,
196
+ intelligence=int_val,
197
+ wisdom=wis_val,
198
+ charisma=cha_val,
199
+ background=background,
200
+ alignment=alignment
201
+ )
202
+
203
+ # Calculate derived stats
204
+ hit_die_map = {
205
+ "Wizard": 6, "Sorcerer": 6,
206
+ "Rogue": 8, "Bard": 8, "Cleric": 8, "Druid": 8, "Monk": 8, "Warlock": 8,
207
+ "Fighter": 10, "Paladin": 10, "Ranger": 10,
208
+ "Barbarian": 12
209
+ }
210
+ hit_die = hit_die_map.get(char_class, 8)
211
+ character.hit_points = character.calculate_hit_points(hit_die)
212
+
213
+ dex_mod = character.get_ability_modifier(dex_val)
214
+ character.armor_class = 10 + dex_mod
215
+
216
+ # Add basic equipment
217
+ equipment_map = {
218
+ "Fighter": ["Longsword", "Shield", "Chainmail", "Backpack", "50 GP"],
219
+ "Wizard": ["Quarterstaff", "Spellbook", "Robes", "Component Pouch", "25 GP"],
220
+ "Rogue": ["Shortsword", "Leather Armor", "Thieves' Tools", "Backpack", "40 GP"],
221
+ "Cleric": ["Mace", "Shield", "Chainmail", "Holy Symbol", "30 GP"],
222
+ "Paladin": ["Longsword", "Shield", "Plate Armor", "Holy Symbol", "40 GP"],
223
+ "Ranger": ["Longbow", "Arrows (20)", "Leather Armor", "Backpack", "40 GP"],
224
+ "Barbarian": ["Greataxe", "Javelin (4)", "Leather Armor", "Backpack", "50 GP"],
225
+ "Bard": ["Rapier", "Lute", "Leather Armor", "Backpack", "35 GP"],
226
+ "Sorcerer": ["Quarterstaff", "Component Pouch", "Robes", "Backpack", "30 GP"],
227
+ "Warlock": ["Dagger", "Component Pouch", "Leather Armor", "Backpack", "35 GP"],
228
+ "Druid": ["Quarterstaff", "Druidic Focus", "Leather Armor", "Backpack", "30 GP"],
229
+ "Monk": ["Quarterstaff", "Darts (10)", "Robes", "Backpack", "20 GP"]
230
+ }
231
+ character.equipment = equipment_map.get(char_class, ["Basic gear", "50 GP"])
232
+
233
+ # Save character
234
+ filename = f"{name.lower().replace(' ', '_')}.json"
235
+ save_result = save_character_to_json(character, filename)
236
+
237
+ # Set as current character
238
+ current_character = character
239
+
240
+ # Update character list
241
+ new_choices = get_available_characters()
242
+
243
+ return (
244
+ f"โœ… Character '{name}' created successfully!\nSaved to: {save_result}",
245
+ gr.update(choices=new_choices, value=f"{name} ({race} {char_class})")
246
+ )
247
+
248
+
249
+ def chat(message: str, history: list) -> list:
250
  """Handle chat messages."""
251
  global conversation_history
252
 
 
330
  ]
331
 
332
 
333
+ def clear_history() -> list:
334
  """Clear conversation history."""
335
  global conversation_history
336
  conversation_history = []
 
338
 
339
 
340
  # Create Gradio interface
341
+ with gr.Blocks(title="D&D RAG Game Master", theme=gr.themes.Soft()) as demo:
342
  gr.Markdown("""
343
  # ๐ŸŽฒ D&D Character-Aware Game Master
344
 
345
+ Play D&D with an AI Game Master powered by RAG (Retrieval-Augmented Generation).
346
 
347
+ **Features:**
348
+ - Play as pre-made characters (Thorin, Elara)
349
+ - Create your own custom characters
350
+ - Character images (placeholder for future GAN generation)
351
+ - RAG-powered rule lookups
352
  """)
353
 
354
+ with gr.Tabs():
355
+ # Tab 1: Play Game
356
+ with gr.Tab("๐ŸŽฎ Play Game"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  with gr.Row():
358
+ with gr.Column(scale=1):
359
+ gr.Markdown("## Character")
360
+
361
+ character_dropdown = gr.Dropdown(
362
+ choices=get_available_characters(),
363
+ value=get_available_characters()[0] if get_available_characters() else None,
364
+ label="Choose Your Character"
365
+ )
366
+
367
+ load_btn = gr.Button("Load Character", variant="primary")
368
+
369
+ # Character Image
370
+ char_image = gr.Image(
371
+ label="Character Portrait",
372
+ type="filepath",
373
+ interactive=False,
374
+ height=200
375
+ )
376
+
377
+ gr.Markdown("*Image will be generated via GAN in future update*")
378
+
379
+ gr.Markdown("---")
380
+
381
+ character_sheet = gr.Markdown("No character loaded")
382
+
383
+ with gr.Column(scale=2):
384
+ gr.Markdown("## Game Session")
385
+
386
+ chatbot = gr.Chatbot(
387
+ height=500,
388
+ label="Game Master",
389
+ show_label=True
390
+ )
391
+
392
+ with gr.Row():
393
+ msg_input = gr.Textbox(
394
+ placeholder="Type your action or command here... (e.g., 'I look around' or '/help')",
395
+ label="Your Action",
396
+ show_label=False,
397
+ scale=4
398
+ )
399
+ submit_btn = gr.Button("Send", variant="primary", scale=1)
400
+
401
+ with gr.Row():
402
+ clear_btn = gr.Button("Clear History")
403
+ gr.Markdown("**Quick Commands:** `/help` | `/context` | `/stats` | `/rag <query>`")
404
+
405
+ gr.Markdown("""
406
+ ---
407
+
408
+ ### ๐Ÿ“– Example Actions
409
+ - "I look around and check my surroundings"
410
+ - "I draw my weapon and prepare for combat"
411
+ - "I cast Magic Missile at the goblin"
412
+ - "I attack with my longsword"
413
+
414
+ ### ๐Ÿ” RAG Commands
415
+ - `/rag Magic Missile` - Look up spell details
416
+ - `/rag Goblin` - Look up monster stats
417
+ - `/rag Fighter` - Look up class features
418
+ """)
419
+
420
+ # Tab 2: Create Character
421
+ with gr.Tab("โœจ Create Character"):
422
+ gr.Markdown("""
423
+ ## Create Your D&D Character
424
+
425
+ Fill in the fields below to create a new character. Your character will be saved and available in the character dropdown.
426
+ """)
427
 
428
  with gr.Row():
429
+ with gr.Column():
430
+ char_name = gr.Textbox(label="Character Name", placeholder="e.g., Gandalf the Grey")
431
+
432
+ with gr.Row():
433
+ char_race = gr.Dropdown(
434
+ choices=settings.DND_RACES,
435
+ value="Human",
436
+ label="Race"
437
+ )
438
+ char_class = gr.Dropdown(
439
+ choices=settings.DND_CLASSES,
440
+ value="Fighter",
441
+ label="Class"
442
+ )
443
+
444
+ with gr.Row():
445
+ char_background = gr.Textbox(label="Background", placeholder="e.g., Soldier, Sage, Folk Hero")
446
+ char_alignment = gr.Textbox(label="Alignment", placeholder="e.g., Lawful Good, Chaotic Neutral")
447
+
448
+ with gr.Column():
449
+ gr.Markdown("### Ability Scores")
450
+ gr.Markdown("*Standard array: 15, 14, 13, 12, 10, 8*")
451
+
452
+ with gr.Row():
453
+ str_slider = gr.Slider(minimum=3, maximum=20, value=10, step=1, label="Strength")
454
+ dex_slider = gr.Slider(minimum=3, maximum=20, value=10, step=1, label="Dexterity")
455
+
456
+ with gr.Row():
457
+ con_slider = gr.Slider(minimum=3, maximum=20, value=10, step=1, label="Constitution")
458
+ int_slider = gr.Slider(minimum=3, maximum=20, value=10, step=1, label="Intelligence")
459
+
460
+ with gr.Row():
461
+ wis_slider = gr.Slider(minimum=3, maximum=20, value=10, step=1, label="Wisdom")
462
+ cha_slider = gr.Slider(minimum=3, maximum=20, value=10, step=1, label="Charisma")
463
+
464
+ create_btn = gr.Button("Create Character", variant="primary", size="lg")
465
+ create_output = gr.Textbox(label="Status", interactive=False)
466
+
467
+ gr.Markdown("""
468
+ ---
469
+
470
+ **Note:** Equipment and spells will be automatically assigned based on your class.
471
+ Character images can be added later via the GAN generation feature (coming soon).
472
+ """)
473
+
474
+ # Event handlers - Play Game Tab
475
  load_btn.click(
476
  load_character,
477
  inputs=[character_dropdown],
478
+ outputs=[character_sheet, msg_input, chatbot, char_image]
479
  )
480
 
481
  submit_btn.click(
 
501
  outputs=[chatbot]
502
  )
503
 
504
+ # Event handlers - Create Character Tab
505
+ create_btn.click(
506
+ create_character,
507
+ inputs=[
508
+ char_name, char_race, char_class, char_background, char_alignment,
509
+ str_slider, dex_slider, con_slider, int_slider, wis_slider, cha_slider
510
+ ],
511
+ outputs=[create_output, character_dropdown]
512
+ )
513
 
 
 
 
514
 
515
+ if __name__ == "__main__":
516
  # Launch
517
  demo.launch(
518
  server_name="0.0.0.0",