from __future__ import annotations import json import re from .llm import call_llm from .paths import CHAR_PATH, WORLD_PATH from . import state VALIDATION_PROMPT = """You are a strict RPG game master validating whether a player's action is possible given the game state. Be thorough — check inventory, stats, location, NPCs, story context, and story logic. ## Character {character} ## World {world} ## Session Log *Written in 3rd person with explicit actor names.* {log} ## Recent Story *Written in 3rd person with explicit actor names.* {story} ## Player Action {action} ## Instructions - Is the player trying to use an item they don't have? -> invalid - Are they asserting something that contradicts the state? -> invalid - Is the action nonsensical given the situation? -> invalid - Does the action make sense given the character's abilities and resources? -> valid - Pay close attention to the Recent Story section — entities like monsters, NPCs, and hazards currently present in the scene ARE valid targets for action. - If valid, also check: if they're using a consumable item, note that it must be removed from inventory. Reply with ONLY the JSON object. Examples: ``` {{"valid": true, "reason": "ok"}} ``` or ``` {{"valid": false, "reason": "brief explanation of why the action is impossible"}} ``` """ def validate_action( player_action: str, *, story: str = "", log: str = "", on_debug: callable = None, ) -> tuple[bool, str]: """Ask the LLM whether a player action is valid given the game state. Returns (valid, reason).""" if not player_action: return True, "" char = state.read_file(CHAR_PATH) or "*No character sheet.*" world = state.truncate_world(state.read_file(WORLD_PATH) or "") or "*No world state.*" recent = story.strip() or state.read_recent_book() or "*No prior story.*" log_entries = log.strip() or state.read_recent_log() or "*No recent events.*" prompt = VALIDATION_PROMPT.format(character=char, world=world, log=log_entries, story=recent, action=player_action) text = call_llm( [{"role": "user", "content": prompt}], max_tokens=1024, temperature=0.2, label="Action validation", on_debug=on_debug, ) if not text: return False, "Not sure" cleaned = text.strip() m = re.search(r"```(?:json)?\s*\n?(.*?)```", cleaned, re.DOTALL) if m: cleaned = m.group(1).strip() try: data = json.loads(cleaned) valid = data.get("valid", True) reason = data.get("reason", "") if on_debug: on_debug("action_validation", {"valid": valid, "reason": reason, "action": player_action}) return valid, reason except (json.JSONDecodeError, ValueError): if on_debug: on_debug("action_validation", {"valid": True, "reason": "parse_failed", "raw": text[:200]}) return False, "Unrecognized" def auto_prompt(book_log: str = "") -> str: """Fallback player prompt.""" return "**What do you do?**"