From 586225425543f7018208111cef178a98726931d6 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Tue, 30 Jun 2026 22:32:56 +0200 Subject: [PATCH] Improved validation based on story --- tools/engine.py | 4 +++- tools/engine_lib/validation.py | 16 ++++++++++++++-- tools/test_validation.py | 12 ++++++------ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/tools/engine.py b/tools/engine.py index 3a8a4ef..384683f 100644 --- a/tools/engine.py +++ b/tools/engine.py @@ -51,7 +51,9 @@ class GameEngine: on_debug("config", {"model": model, "temperature": lm.get("temperature"), "max_tokens": lm.get("max_tokens"), "strategy": "tools"}) if player_action: - valid, reason = validate_action(player_action, on_debug=on_debug) + story = state.read_recent_book() + log = state.read_recent_log() + valid, reason = validate_action(player_action, story=story, log=log, on_debug=on_debug) if valid: state.append_llm_log(f"\n[VALIDATION PASSED] {reason}") else: diff --git a/tools/engine_lib/validation.py b/tools/engine_lib/validation.py index 1d35e14..662218f 100644 --- a/tools/engine_lib/validation.py +++ b/tools/engine_lib/validation.py @@ -8,7 +8,7 @@ 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. +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} @@ -16,6 +16,12 @@ VALIDATION_PROMPT = """You are a strict RPG game master validating whether a pla ## World {world} +## Session Log +{log} + +## Recent Story +{story} + ## Player Action {action} @@ -24,6 +30,7 @@ VALIDATION_PROMPT = """You are a strict RPG game master validating whether a pla - 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: @@ -39,6 +46,9 @@ or 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).""" @@ -47,8 +57,10 @@ def validate_action( 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, action=player_action) + prompt = VALIDATION_PROMPT.format(character=char, world=world, log=log_entries, story=recent, action=player_action) text = call_llm( [{"role": "user", "content": prompt}], diff --git a/tools/test_validation.py b/tools/test_validation.py index ca36d4f..b2b1827 100644 --- a/tools/test_validation.py +++ b/tools/test_validation.py @@ -29,7 +29,7 @@ def test_valid_action(mock_call_llm, mock_truncate_world, mock_read_file): mock_truncate_world.return_value = "## Location\nTavern" mock_call_llm.return_value = json.dumps({"valid": True, "reason": "ok"}) - valid, reason = validate_action("I buy a drink") + valid, reason = validate_action("I buy a drink", story="At the tavern", log="- Entered the tavern") assert valid is True assert reason == "ok" @@ -47,7 +47,7 @@ def test_invalid_action(mock_call_llm, mock_truncate_world, mock_read_file): mock_truncate_world.return_value = "## Location\nTavern" mock_call_llm.return_value = json.dumps({"valid": False, "reason": "Not enough gold"}) - valid, reason = validate_action("I buy a drink") + valid, reason = validate_action("I buy a drink", story="At the tavern", log="- Entered the tavern") assert valid is False assert reason == "Not enough gold" @@ -64,7 +64,7 @@ def test_llm_returns_none(mock_call_llm, mock_truncate_world, mock_read_file): mock_truncate_world.return_value = "## Location\nTavern" mock_call_llm.return_value = None - valid, reason = validate_action("I attack the dragon") + valid, reason = validate_action("I attack the dragon", story="A dragon appears!", log="- Dragon spotted") assert valid is False assert reason == "Not sure" @@ -81,7 +81,7 @@ def test_llm_returns_bad_json(mock_call_llm, mock_truncate_world, mock_read_file mock_truncate_world.return_value = "## Location\nTavern" mock_call_llm.return_value = "not valid json at all" - valid, reason = validate_action("I cast a spell") + valid, reason = validate_action("I cast a spell", story="In a dungeon", log="- Found a weird altar") assert valid is False assert reason == "Unrecognized" @@ -98,7 +98,7 @@ def test_missing_character_sheet(mock_truncate_world, mock_read_file): with patch("engine_lib.validation.call_llm") as mock_call_llm: mock_call_llm.return_value = json.dumps({"valid": True, "reason": "ok"}) - valid, reason = validate_action("I look around") + valid, reason = validate_action("I look around", story="In a dark room", log="- Entered the room") assert valid is True print("✓ handles missing character sheet gracefully") @@ -118,7 +118,7 @@ def test_on_debug_called(mock_call_llm, mock_truncate_world, mock_read_file): def debug_cb(key, data): events.append((key, data)) - valid, reason = validate_action("I open the door", on_debug=debug_cb) + valid, reason = validate_action("I open the door", story="In a hallway", log="- Heard noises", on_debug=debug_cb) assert valid is True assert len(events) == 1