dnd-rag-g / e2e_tests /test_adventure_simulation.py
alexchilton's picture
fix: Fix Selenium character dropdown selection in E2E tests
e56b236
#!/usr/bin/env python3
"""
Automated Adventure Simulation - Random Travel & Combat Until Death
This test simulates a full adventure:
1. Load character (Thorin)
2. Explore and travel randomly
3. Fight any creatures encountered
4. Continue until character dies (game over)
5. Show everything in Selenium browser
"""
import time
import random
import re
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
def setup_browser():
"""Setup Chrome with visible window."""
options = Options()
options.add_argument('--start-maximized')
# Don't run headless - we want to SEE it!
driver = webdriver.Chrome(options=options)
return driver
def wait_for_response(driver, previous_count, timeout=30):
"""Wait for new GM response."""
start = time.time()
while time.time() - start < timeout:
messages = driver.find_elements(By.CSS_SELECTOR, '.message')
current_count = len(messages)
if current_count > previous_count:
# Skip "Loading..." messages
last_msg = messages[-1].text if messages else ""
if "Loading" not in last_msg and len(last_msg) > 10:
return current_count
time.sleep(0.5)
return previous_count
def get_chat_messages(driver):
"""Get all chat messages from Gradio chatbot."""
try:
# Try multiple selectors for Gradio chat messages
bot_messages = driver.find_elements(By.CSS_SELECTOR, '.message')
if not bot_messages:
bot_messages = driver.find_elements(By.CSS_SELECTOR, '[data-testid="bot"]')
if not bot_messages:
# Fallback: try finding any div with bot-related class
bot_messages = driver.find_elements(By.CSS_SELECTOR, '.bot, [class*="bot"]')
# Get text from bot messages
messages = []
for msg in bot_messages:
text = msg.text.strip()
if text and "Loading" not in text and text not in messages: # Avoid duplicates
messages.append(text)
return messages
except Exception as e:
print(f"Warning: Could not get messages: {e}")
return []
def send_message(driver, text):
"""Send a message and wait for response."""
print(f" 💬 Sending: {text[:80]}")
# Find and fill input
textarea = driver.find_element(By.CSS_SELECTOR, 'textarea[placeholder*="Type"]')
textarea.clear()
textarea.send_keys(text)
# Click submit
submit_btn = driver.find_element(By.CSS_SELECTOR, 'button.primary')
submit_btn.click()
# Fixed wait for LLM response (more reliable than trying to detect new messages)
time.sleep(10)
# Get the latest chat messages to return
messages = get_chat_messages(driver)
if messages:
return messages[-1]
return "[No messages found]"
def get_hp_from_sheet(driver):
"""Extract HP from character sheet (not chat history)."""
try:
# HP is in the character sheet under "Combat Stats" heading
# Structure: <h3>Combat Stats</h3> <ul> <li><strong>HP</strong>: 28/28</li>
# Find all h3 elements
h3_elements = driver.find_elements(By.TAG_NAME, "h3")
for h3 in h3_elements:
if "Combat Stats" in h3.text:
# Found the Combat Stats section
# Now find the next <ul> sibling and look for HP there
parent = h3.find_element(By.XPATH, "..")
ul_elements = parent.find_elements(By.TAG_NAME, "ul")
for ul in ul_elements:
ul_text = ul.text
if "HP" in ul_text:
# Parse HP from this specific list
match = re.search(r'HP[:\s]+(\d+)/(\d+)', ul_text)
if match:
current_hp = int(match.group(1))
return current_hp
# Fallback: search for HP in markdown container (avoid chat)
# Look for elements with specific structure but not in chat
markdown_divs = driver.find_elements(By.CSS_SELECTOR, "[class*='markdown']")
for div in markdown_divs:
text = div.text
# Only use if it contains "Combat Stats" (character sheet marker)
if "Combat Stats" in text and "HP" in text:
match = re.search(r'HP[:\s]+(\d+)/(\d+)', text)
if match:
return int(match.group(1))
except Exception as e:
print(f"⚠️ Could not parse HP: {e}")
return None
def extract_creatures(response):
"""Try to find creature names in GM response."""
creatures = []
keywords = ['goblin', 'dragon', 'wolf', 'orc', 'skeleton', 'zombie',
'bandit', 'troll', 'spider', 'rat', 'bear', 'snake']
response_lower = response.lower()
for keyword in keywords:
if keyword in response_lower:
creatures.append(keyword.title())
return creatures
def main():
print("=" * 80)
print("🎮 AUTOMATED ADVENTURE SIMULATION")
print("=" * 80)
print("\nStarting Gradio app...")
print("Make sure you run: python web/app_gradio.py")
print("\nPress Enter when app is running...")
input()
driver = setup_browser()
try:
# Navigate to app
print("\n📱 Opening browser...")
driver.get("http://localhost:7860")
time.sleep(3)
# Click "Play Game" tab
print("🎲 Switching to Play Game tab...")
tabs = driver.find_elements(By.CSS_SELECTOR, 'button.tab-nav')
for tab in tabs:
if 'Play Game' in tab.text:
tab.click()
time.sleep(1)
break
# Load character - just type the load command directly
print("⚔️ Loading Thorin Stormshield...")
time.sleep(2)
# Send load command directly via chat
response = send_message(driver, "/load_char Thorin Stormshield")
print(f"Load response: {response[:200]}...")
time.sleep(2)
print("✅ Character loaded!\n")
# Start adventure
print("=" * 80)
print("🗺️ ADVENTURE BEGINS!")
print("=" * 80)
turn = 0
max_turns = 20 # Safety limit
in_combat = False
current_enemy = None
while turn < max_turns:
turn += 1
# Check HP
hp = get_hp_from_sheet(driver)
print(f"\n💚 Turn {turn} - HP: {hp if hp is not None else '???'}")
if hp is not None and hp <= 0:
print("\n" + "=" * 80)
print("💀 GAME OVER - CHARACTER DIED!")
print("=" * 80)
break
# Decide action
if not in_combat:
# Exploration mode - Ask GM what we see/encounter
action_roll = random.random()
if action_roll < 0.3:
# Venture into wilderness/danger
actions = [
"I venture deeper into the wilderness, looking for adventure",
"I follow the path ahead, seeking what dangers or treasures await",
"I explore the area thoroughly, searching for anything unusual",
"I venture into the darker parts of this place",
"I head toward the sounds of potential danger",
"I search for signs of monsters or bandits in the area"
]
action = random.choice(actions)
print(f"\n🔍 Turn {turn}: {action}")
response = send_message(driver, action)
print(f"GM: {response[:300]}...")
# Check for creatures
creatures = extract_creatures(response)
if creatures:
print(f"⚠️ CREATURE SPOTTED: {creatures[0]}!")
current_enemy = creatures[0]
in_combat = True
elif action_roll < 0.6:
# Ask GM about surroundings
actions = [
"I look around carefully. What do I see?",
"I listen for any sounds nearby. What do I hear?",
"What dangers might be lurking here?",
"I investigate the area. Is there anything interesting?",
"Are there any creatures or people nearby?"
]
action = random.choice(actions)
print(f"\n👀 Turn {turn}: {action}")
response = send_message(driver, action)
print(f"GM: {response[:300]}...")
creatures = extract_creatures(response)
if creatures:
print(f"⚠️ CREATURE SPOTTED: {creatures[0]}!")
current_enemy = creatures[0]
in_combat = True
else:
# Move to a new area (use travel or natural language)
if random.random() < 0.5:
destinations = ['Dark Forest', 'Mountain Road', 'Old Ruins',
'Temple District', 'Market Square', 'Forest Path',
'Town Gates', 'Town Square']
dest = random.choice(destinations)
print(f"\n🚶 Turn {turn}: Traveling to {dest}...")
response = send_message(driver, f"I travel to {dest}")
else:
directions = [
"I head north down the path",
"I venture east into the forest",
"I travel south along the road",
"I go west toward the mountains",
"I follow the road ahead",
"I leave this place and continue my journey"
]
direction = random.choice(directions)
print(f"\n🚶 Turn {turn}: {direction}")
response = send_message(driver, direction)
print(f"GM: {response[:300]}...")
# Check for creatures
creatures = extract_creatures(response)
if creatures:
print(f"⚠️ CREATURE SPOTTED: {creatures[0]}!")
current_enemy = creatures[0]
in_combat = True
else:
# Combat mode - attack until someone dies
print(f"\n⚔️ Turn {turn}: COMBAT - Attacking {current_enemy}!")
# Start combat if not already in it
if 'combat' not in response.lower() and 'initiative' not in response.lower():
response = send_message(driver, f"/start_combat {current_enemy}")
print(f"Combat started: {response[:200]}...")
# Attack
attack_variations = [
f"I attack the {current_enemy} with my battleaxe",
f"I swing at the {current_enemy}",
f"I strike the {current_enemy}",
f"I attack {current_enemy}"
]
attack = random.choice(attack_variations)
response = send_message(driver, attack)
print(f"Attack: {response[:250]}...")
# Check if enemy died
if 'dead' in response.lower() or 'dies' in response.lower() or 'falls' in response.lower():
print(f"✅ {current_enemy} defeated!")
in_combat = False
current_enemy = None
# End combat
time.sleep(2)
send_message(driver, "/end_combat")
# Check our HP
time.sleep(1)
hp = get_hp_from_sheet(driver)
if hp is not None and hp <= 0:
print(f"\n💀 Thorin has fallen in battle against {current_enemy}!")
break
# Pause between turns
time.sleep(1)
if turn >= max_turns:
print("\n⏱️ Adventure ended - max turns reached")
print("\n" + "=" * 80)
print("🏁 ADVENTURE COMPLETE")
print("=" * 80)
print(f"\nTotal turns: {turn}")
print(f"Final HP: {get_hp_from_sheet(driver)}")
# Keep browser open
print("\n👀 Browser will stay open for 10 seconds...")
time.sleep(10)
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()
# Keep browser open on error
print("\n👀 Error occurred - browser staying open for inspection...")
time.sleep(60)
finally:
driver.quit()
print("\n✅ Test complete!")
if __name__ == "__main__":
main()