In the game "Mystic Realm," after completing each non-BOSS layer, players encounter an inter-layer event. This event presents a special scenario where players can make choices to potentially earn a wildcard card. The task involves implementing a function called generate_layer_event that utilizes the Deepseek large language model to dynamically generate event descriptions, four selectable options, their corresponding outcomes, and identify exactly one trap option that prevents the player from obtaining a wildcard. Module Structure and Dependencies
File Location: src/game_logic/layer_event_generator.pyRequired Imports:- llm_hint - Provides the shared LLM client instance and API key validation
langchain_core.prompts- For constructing prompt templatesscene_generator- Reuse_parse_llm_responsehelper function- Standard libraries:
logging,json,re,os,typing
Function Implementation
import logging
import os
import json
import re
from typing import Dict, List, Optional, Any
from langchain_core.prompts import ChatPromptTemplate
from .llm_hint import llm, check_api_key
from .scene_generator import _parse_llm_response
logger = logging.getLogger(__name__)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
PROMPT_FILE = os.path.join(BASE_DIR, "prompts", "layer_event_prompt.txt")
# Load template on module initialization
_event_template = None
def _load_template() -> Optional[str]:
global _event_template
if _event_template is not None:
return _event_template
try:
with open(PROMPT_FILE, "r", encoding="utf-8") as handle:
_event_template = handle.read()
logger.info(f"Layer event template loaded: {PROMPT_FILE}")
except FileNotFoundError:
logger.error(f"Template file missing: {PROMPT_FILE}")
return _event_template
def generate_layer_event(
current_layer: int,
moral_alignment: Optional[Dict[str, float]] = None
) -> Optional[Dict[str, Any]]:
"""
Generate inter-layer event data based on player progress and moral standing.
Args:
current_layer: Current dungeon level (range 1-10)
moral_alignment: Player's moral distribution dictionary
Example: {'good': 0.3, 'neutral': 0.2, 'evil': 0.5}
Returns:
Dictionary containing event data on success, None on failure.
Structure:
{
"description": str,
"options": {
"A": {"text": str, "is_trap": bool, "outcome": str,
"card_name": Optional[str], "card_description": Optional[str]},
"B": {...},
"C": {...},
"D": {...}
}
}
"""
template = _load_template()
if not template:
return _get_fallback_event()
if not check_api_key():
logger.warning("API key validation failed")
return _get_fallback_event()
# Extract moral ratios with defaults
good_val = moral_alignment.get('good', 0.0) if moral_alignment else 0.0
neutral_val = moral_alignment.get('neutral', 0.0) if moral_alignment else 0.0
evil_val = moral_alignment.get('evil', 0.0) if moral_alignment else 0.0
# Build and format prompt
prompt_builder = ChatPromptTemplate.from_template(template)
final_prompt = prompt_builder.format(
layer=current_layer,
good_ratio=good_val,
neutral_ratio=neutral_val,
evil_ratio=evil_val
)
# Invoke LLM with retry logic
for attempt in range(3):
try:
response = llm.invoke(final_prompt)
parsed = _parse_llm_response(response.content)
event_data = json.loads(parsed)
if _validate_event_structure(event_data):
return event_data
else:
logger.warning(f"Validation failed on attempt {attempt + 1}")
except Exception as e:
logger.error(f"LLM call failed (attempt {attempt + 1}): {e}")
return _get_fallback_event()
def _validate_event_structure(data: Dict) -> bool:
"""Validate that event data contains all required fields and exactly one trap."""
if not isinstance(data, dict):
return False
if "description" not in data or "options" not in data:
return False
options = data["options"]
required_keys = {"A", "B", "C", "D"}
if set(options.keys()) != required_keys:
return False
trap_count = 0
required_fields = ["text", "is_trap", "outcome", "card_name", "card_description"]
for key, option in options.items():
if not all(field in option for field in required_fields):
return False
if option["is_trap"]:
trap_count += 1
# Trap options should not have card rewards
if option.get("card_name") or option.get("card_description"):
return False
else:
# Non-trap options must have card rewards
if not option.get("card_name") or not option.get("card_description"):
return False
return trap_count == 1
def _get_fallback_event() -> Dict:
"""Return a predefined fallback event when LLM generation fails."""
return {
"description": "The realm trembles slightly as a faint silhouette streaks past.",
"options": {
"A": {
"text": "Pursue the shadow",
"is_trap": False,
"outcome": "You catch the shadow, which transforms into a glowing talisman.",
"card_name": "Shadow Talisman",
"card_description": "A captured remnant, usable as any card."
},
"B": {
"text": "Wait and observe",
"is_trap": False,
"outcome": "The shadow circles you, leaving behind a wisp that solidifies.",
"card_name": "Wisp Card",
"card_description": "Born from patient observation, holds mystical essence."
},
"C": {
"text": "Strike immediately",
"is_trap": True,
"outcome": "Your attack shatters the illusion, leaving nothing behind.",
"card_name": None,
"card_description": None
},
"D": {
"text": "Ignore it",
"is_trap": False,
"outcome": "Before fading, the shadow deposits a card at your feet.",
"card_name": "Parting Gift",
"card_description": "A farewell offering from the fleeting shadow."
}
}
}
Prompt Template Configuration
Create the file prompts/layer_event_prompt.txt with the following content: ```
You are the Spirit of the Mystic Realm - an ancient cultivator's trial spirit left behind before ascension. You possess conflicting desires: to test disciples, yet yearn for companionship.
The player has completed layer {layer}. A disturbance ripples through the realm, requiring you to generate an inter-layer event. This special encounter presents a brief but crucial choice that may yield a wildcard card.
Current moral alignment: Good {good_ratio:.0%}, Neutral {neutral_ratio:.0%}, Evil {evil_ratio:.0%}. This influences the event's atmosphere subtly.
Generate a JSON object with this structure:
{ "description": "Event description (50-100 characters), evoking mystery and unease", "options": { "A": { "text": "Option text (max 15 chars)", "is_trap": false, "outcome": "Narrative result (30-50 chars), describing consequences", "card_name": "Card name (2-5 chars, e.g., 'Mirror Bloom')", "card_description": "Card lore (10-20 chars)" }, "B": {...}, "C": {...}, "D": {...} } }
Critical rules:
- Exactly ONE option must have is_trap set to true
- Trap options must have null for card_name and card_description
- Non-trap options must include card_name and card_description
- Option text should be concise and immediately clear
- Descriptions should be vivid and atmospheric
- Moral ratios can subtly influence tone without being explicit
Output only JSON, no additional text.
Return Data Structure
---------------------
**Fields Explanation:**- `description` (str): Event narrative text, 50-100 characters, establishing a mysterious atmosphere
- `options` (dict): Four options (A, B, C, D), each containing:
- `text` (str): Option label, maximum 15 characters
- `is_trap` (bool): Whether selecting this option yields no card
- `outcome` (str): Result narrative, 30-50 characters
- `card_name` (str|null): Card name for non-trap options
- `card_description` (str|null): Card flavor text for non-trap options
**Important:** Exactly one of the four options must have `is_trap` set to `true`. Unit Testing Guidelines
-----------------------
Create `tests/test_layer_event_generator.py` with coverage for: **1. Successful Generation**```
def test_successful_event_generation(mocker):
mock_response = mocker.Mock()
mock_response.content = '{"description": "Test", "options": {...valid structure...}}'
mocker.patch('layer_event_generator.llm.invoke', return_value=mock_response)
result = generate_layer_event(5, {'good': 0.5, 'neutral': 0.3, 'evil': 0.2})
assert result is not None
assert 'description' in result
assert len(result['options']) == 4
2. JSON Parsing Failure```
def test_invalid_json_returns_fallback(mocker): mocker.patch('layer_event_generator.llm.invoke', side_effect=Exception("Parse error"))
result = generate_layer_event(3)
assert result == _get_fallback_event()
**3. Validation Failure Handling**```
def test_missing_fields_returns_fallback(mocker):
mock_response = mocker.Mock()
mock_response.content = '{"description": "Incomplete", "options": {"A": {"text": "Test"}}}'
mocker.patch('layer_event_generator.llm.invoke', return_value=mock_response)
result = generate_layer_event(2)
assert result == _get_fallback_event()
4. Template Missing Scenario```
def test_missing_template_returns_none(mocker): mocker.patch('layer_event_generator._load_template', return_value=None)
result = generate_layer_event(1)
assert result is not None # Returns fallback
**5. Trap Count Validation**```
def test_exactly_one_trap_required():
invalid_event = {
"description": "Test",
"options": {
"A": {"is_trap": True, "card_name": None, "card_description": None, ...},
"B": {"is_trap": True, ...} # Two traps - invalid
}
}
assert _validate_event_structure(invalid_event) == False
Deliverables
src/game_logic/layer_event_generator.py- Main implementationprompts/layer_event_prompt.txt- LLM prompt templatetests/test_layer_event_generator.py- Comprehensive unit tests