dnd-rag-g / e2e_tests /test_goblin_combat_npc_extraction.py
alexchilton's picture
Add RAG-enhanced character creation with SRD data integration
5cffd4f
#!/usr/bin/env python3
"""
E2E Test: Goblin Combat with NPC Extraction
Tests the exact scenario from the user's bug report:
1. Player explores a cave
2. GM introduces a goblin and narrates attack
3. Goblin should be auto-added to npcs_present via NPC extraction
4. Player attacks goblin - should work (not rejected)
5. Combat continues until goblin or character dies
"""
import sys
import time
import os
from pathlib import Path
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_helpers import load_character, wait_for_gradio
# Add project to path
sys.path.insert(0, str(Path(__file__).parent.parent))
# Enable debug mode to see NPC extraction
os.environ['GM_DEBUG'] = 'true'
# DEPRECATED: Use selenium_helpers.wait_for_gradio instead
# def wait_for_gradio(driver, timeout=30):
# """Wait for Gradio interface to fully load."""
# print("⏳ Waiting for Gradio to load...")
# WebDriverWait(driver, timeout).until(
# EC.presence_of_element_located((By.TAG_NAME, "gradio-app"))
# )
# time.sleep(2)
# print("✅ Gradio loaded")
# NOTE: This test file has been updated to import from selenium_helpers.py
# Local load_character() and wait_for_gradio() functions have been deprecated.
# The imported versions use the correct Gradio selectors (input[aria-label=...])
# See e2e_tests/README_SELENIUM.md for details.
def find_chat_input(driver):
"""Find the chat input textarea."""
textareas = driver.find_elements(By.TAG_NAME, "textarea")
for ta in textareas:
placeholder = ta.get_attribute("placeholder")
if placeholder and "your action" in placeholder.lower():
return ta
# Fallback: find any visible enabled textarea
for ta in textareas:
if ta.is_displayed() and ta.is_enabled():
return ta
raise Exception("Could not find chat input textarea")
def find_send_button(driver):
"""Find the Send button."""
buttons = driver.find_elements(By.TAG_NAME, "button")
for btn in buttons:
if btn.text.strip().lower() == "send":
return btn
raise Exception("Could not find Send button")
def send_message(driver, message, wait_time=8):
"""Send a message in the chat."""
print(f"\n📤 Player: {message}")
chat_input = find_chat_input(driver)
chat_input.clear()
chat_input.send_keys(message)
send_btn = find_send_button(driver)
send_btn.click()
time.sleep(wait_time) # Wait for GM response + mechanics extraction
# Wait for "Loading content" to clear
max_wait = wait_time + 3
start_time = time.time()
while time.time() - start_time < max_wait:
messages = get_chat_messages(driver)
if messages and messages[-1] != "Loading content":
break
time.sleep(0.5)
def get_chat_messages(driver):
"""Get all chat messages (Gradio 6.x compatible)."""
chat_containers = driver.find_elements(By.CLASS_NAME, "message")
messages = []
seen_texts = set()
for container in chat_containers:
text = container.text.strip()
# Skip loading states and empty messages
if text and text not in seen_texts and text != "Loading content":
messages.append(text)
seen_texts.add(text)
return messages
# DEPRECATED: Use selenium_helpers.load_character instead
# def load_character(driver, char_name="Thorin"):
"""Load a character."""
print(f"\n📝 Loading character: {char_name}")
# Find character dropdown
dropdowns = driver.find_elements(By.TAG_NAME, "select")
char_dropdown = None
for dd in dropdowns:
if dd.is_displayed():
char_dropdown = dd
break
if char_dropdown:
char_dropdown.click()
time.sleep(0.5)
# Find option with character name
options = char_dropdown.find_elements(By.TAG_NAME, "option")
for opt in options:
if char_name.lower() in opt.text.lower():
opt.click()
break
# Click Load Character button
buttons = driver.find_elements(By.TAG_NAME, "button")
for btn in buttons:
if "load character" in btn.text.lower():
btn.click()
time.sleep(3)
break
print(f"✅ Character loaded")
def get_character_sheet_hp(driver):
"""Extract current HP from character sheet."""
try:
import re
# Find markdown components
markdown_divs = driver.find_elements(By.CSS_SELECTOR, "[data-testid='markdown']")
for md_div in markdown_divs:
text = md_div.text
if "HP" in text and "Combat Stats" in text:
# Extract HP from text like "HP: 28"
match = re.search(r'\bHP\b[:\s]+(\d+)', text)
if match:
hp = int(match.group(1))
return hp
return None
except Exception as e:
print(f" ⚠️ HP extraction error: {e}")
return None
def test_goblin_combat_npc_extraction():
"""Test goblin combat with NPC extraction."""
print("\n" + "⚔️" * 40)
print("GOBLIN COMBAT TEST - NPC EXTRACTION")
print("⚔️" * 40)
# Setup Chrome options
options = webdriver.ChromeOptions()
if os.environ.get('HEADLESS', 'false').lower() == 'true':
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--disable-gpu')
driver = webdriver.Chrome(options=options)
try:
# Navigate to Gradio app
print("\n📍 Opening Gradio app at http://localhost:7860")
driver.get("http://localhost:7860")
wait_for_gradio(driver)
# Load Thorin (our brave dwarf fighter)
load_character(driver, "Thorin")
initial_hp = get_character_sheet_hp(driver)
print(f"\n💪 Thorin's starting HP: {initial_hp if initial_hp else 'Unknown'}")
print("\n" + "=" * 80)
print("SCENE 1: Exploring the Cave")
print("=" * 80)
# Explore cave (GM should describe cave but not introduce goblin yet)
send_message(driver, "I want to explore a dangerous cave looking for treasure. What do I find?", wait_time=10)
messages = get_chat_messages(driver)
if messages:
print(f"🎭 GM: {messages[-1]}")
print("\n" + "=" * 80)
print("SCENE 2: Goblin Appears and Attacks (GM introduces NPC)")
print("=" * 80)
# GM should introduce goblin and narrate attack
# Mechanics extractor should detect goblin and add to npcs_present
send_message(driver, "A goblin jumps out and attacks me with a rusty sword", wait_time=10)
messages = get_chat_messages(driver)
if messages:
gm_response = messages[-1]
print(f"🎭 GM: {gm_response}")
# Check HP - should have taken damage
time.sleep(2)
hp_after_goblin = get_character_sheet_hp(driver)
if hp_after_goblin is not None and initial_hp is not None:
if hp_after_goblin < initial_hp:
damage = initial_hp - hp_after_goblin
print(f"\n💥 GOBLIN ATTACK SUCCESSFUL! Took {damage} damage")
print(f" HP: {initial_hp}{hp_after_goblin}")
else:
print(f"\n✅ No damage taken (HP still {hp_after_goblin})")
print("\n" + "=" * 80)
print("SCENE 3: Counter-Attack (THIS IS THE KEY TEST)")
print("=" * 80)
print("⚠️ TESTING: Can we attack the goblin that GM just introduced?")
print(" Without NPC extraction, this would be rejected!")
# Attack goblin - this should work now that NPC extraction is implemented
send_message(driver, "I swing my longsword at the goblin", wait_time=10)
messages = get_chat_messages(driver)
if messages:
gm_response = messages[-1]
print(f"🎭 GM: {gm_response}")
# Check for rejection phrases
rejection_phrases = ["nothing there", "no goblin", "don't see", "can't find", "not present", "empty air"]
is_rejected = any(phrase in gm_response.lower() for phrase in rejection_phrases)
if is_rejected:
print("\n❌ FAIL: Attack was REJECTED - NPC extraction didn't work!")
print(" The goblin was not added to npcs_present")
else:
print("\n✅ SUCCESS: Attack was ACCEPTED - NPC extraction worked!")
print(" The goblin was auto-added to npcs_present via mechanics extractor")
print("\n" + "=" * 80)
print("SCENE 4: Combat Continues Until Death")
print("=" * 80)
# Continue combat for up to 10 rounds or until someone dies
max_rounds = 10
for round_num in range(1, max_rounds + 1):
print(f"\n--- Round {round_num} ---")
current_hp = get_character_sheet_hp(driver)
if current_hp is not None and current_hp <= 0:
print("☠️ Thorin has fallen!")
break
# Attack goblin
send_message(driver, "I attack the goblin with my longsword", wait_time=10)
messages = get_chat_messages(driver)
if messages:
print(f"🎭 GM: {messages[-1][:150]}...")
# Check if goblin is dead
if messages and any(phrase in messages[-1].lower() for phrase in ["goblin dies", "goblin falls", "goblin is defeated", "slain the goblin"]):
print("\n🎉 GOBLIN DEFEATED!")
break
time.sleep(2)
print("\n" + "=" * 80)
print("COMBAT SUMMARY")
print("=" * 80)
final_hp = get_character_sheet_hp(driver)
print(f"\n⚔️ Combat Statistics:")
print(f" Starting HP: {initial_hp if initial_hp is not None else 'Unknown'}")
print(f" Final HP: {final_hp if final_hp is not None else 'Unknown'}")
if initial_hp is not None and final_hp is not None:
total_damage = initial_hp - final_hp
print(f" Net Damage: {total_damage}")
survival_pct = (final_hp/initial_hp*100) if initial_hp > 0 else 0
print(f" HP %: {survival_pct:.1f}%")
print("\n" + "=" * 80)
print("✅ GOBLIN COMBAT TEST COMPLETE")
print("=" * 80)
print("\n💡 Check the logs above to verify:")
print(" 1. Goblin was auto-extracted from GM narrative")
print(" 2. Counter-attack was accepted (not rejected)")
print(" 3. Combat continued properly")
print("=" * 80 + "\n")
except Exception as e:
print(f"\n❌ TEST FAILED: {e}")
import traceback
traceback.print_exc()
# Screenshot for debugging
try:
screenshot_path = "/tmp/goblin_combat_error.png"
driver.save_screenshot(screenshot_path)
print(f"\n📸 Screenshot saved to: {screenshot_path}")
except:
pass
raise
finally:
time.sleep(2)
driver.quit()
if __name__ == "__main__":
print("\n" + "🎮" * 40)
print("GOBLIN COMBAT - NPC EXTRACTION TEST")
print("🎮" * 40)
print("\n🎯 This test verifies that:")
print(" 1. GM can introduce NPCs in narrative")
print(" 2. NPCs are auto-extracted and added to npcs_present")
print(" 3. Subsequent actions against those NPCs work correctly")
print("\n⚔️ Let's test the goblin scenario!\n")
print("=" * 80)
test_goblin_combat_npc_extraction()