84 lines
2.4 KiB
Python
84 lines
2.4 KiB
Python
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, and story logic.
|
|
|
|
## Character
|
|
{character}
|
|
|
|
## World
|
|
{world}
|
|
|
|
## 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
|
|
- 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,
|
|
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.*"
|
|
|
|
prompt = VALIDATION_PROMPT.format(character=char, world=world, action=player_action)
|
|
|
|
text = call_llm(
|
|
[{"role": "user", "content": prompt}],
|
|
max_tokens=512,
|
|
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?**"
|