diff --git a/tools/engine.py b/tools/engine.py index 8db68cd..b1c1101 100644 --- a/tools/engine.py +++ b/tools/engine.py @@ -24,7 +24,7 @@ class GameEngine: def generate_turn( self, player_action: str | None = None, - last_prompt: str | None = None, + recent_narrative: str | None = None, on_thought: callable = None, on_action: callable = None, on_player_roll: callable = None, @@ -36,8 +36,8 @@ class GameEngine: state.append_llm_log(f"{'='*60}") if player_action: state.append_llm_log(f"Player: {player_action}") - elif last_prompt: - state.append_llm_log(f"Resume from: {last_prompt[:120]}") + if recent_narrative is None: + recent_narrative = state.read_recent_book(2) die_roll = random.randint(1, 6) state.append_llm_log(f"Dice: {die_roll} (1d6)") @@ -66,11 +66,11 @@ class GameEngine: system = build_system_prompt() parts = [] - if last_prompt: - parts.append(f"## Situation\n{last_prompt}") + if recent_narrative: + parts.append(f"## Recent Narrative\n{recent_narrative}") if player_action: parts.append(f"## Player's Request\n{player_action}") - if not player_action and not last_prompt: + if not player_action and not recent_narrative: parts.append( "## Instructions\n" "This is a new story. Welcome the player and guide them through the game setup." @@ -166,8 +166,8 @@ class GameEngine: }) log_turn_details( - player_action=player_action or last_prompt or "", - last_prompt=last_prompt or "", + player_action=player_action or "", + last_prompt=recent_narrative or "", strategy_name="tools", die_roll=die_roll, model=model, @@ -196,13 +196,11 @@ def main(): parser = argparse.ArgumentParser(description="The Chaos Game Engine (CLI)") parser.add_argument("--action", "-a", help="Player action text") - parser.add_argument("--last", "-l", help="Last narrative text") args = parser.parse_args() engine = GameEngine() result = engine.generate_turn( player_action=args.action, - last_prompt=args.last, ) if result.error: diff --git a/tools/engine_lib/validation.py b/tools/engine_lib/validation.py index 66fbe57..6a63122 100644 --- a/tools/engine_lib/validation.py +++ b/tools/engine_lib/validation.py @@ -64,32 +64,41 @@ def validate_action( 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, - ) + messages = [{"role": "user", "content": prompt}] - if not text: - return False, "Not sure" + for attempt in range(2): + text = call_llm( + messages, + max_tokens=1024, + temperature=0.2, + label="Action validation", + on_debug=on_debug, + ) - 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" + 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]}) + if attempt == 0: + messages.append({ + "role": "system", + "content": "Your previous response was not valid JSON. Reply with ONLY a JSON object in exactly this format, nothing else:\n\n```json\n{\"valid\": true, \"reason\": \"ok\"}\n```\nor\n```json\n{\"valid\": false, \"reason\": \"brief explanation\"}\n```" + }) + + return False, "Unrecognized" def auto_prompt(book_log: str = "") -> str: diff --git a/tools/run.py b/tools/run.py index f65199a..d01703b 100755 --- a/tools/run.py +++ b/tools/run.py @@ -21,7 +21,7 @@ from engine import GameEngine from engine_lib.models import TurnResult from engine_lib import state from run_utils import ( - BOOK_PATH, CHAR_PATH, LAST_PROMPT_PATH, CHANGES_PATH, SETTINGS_PATH, + BOOK_PATH, CHAR_PATH, CHANGES_PATH, SETTINGS_PATH, TODAY, REFRESH_SECS, clear_llm_log, ensure_log, load_book_pages, ) @@ -105,7 +105,6 @@ class ChaosTUI(App): run_widgets.app_ambience_player = app_ambience_player self.engine = GameEngine() - self._last_prompt: str = "" self._last_result: TurnResult | None = None self._is_processing: bool = False self._spinner_frames = ["◴", "◷", "◶", "◵"] @@ -205,23 +204,18 @@ class ChaosTUI(App): def _begin_game(self): self._last_narrative: str = "" - if LAST_PROMPT_PATH.exists(): - saved = LAST_PROMPT_PATH.read_text().strip() - if saved: - self._last_prompt = saved - pages = load_book_pages() - parts = [] - if pages: - parts.append(pages[-1]) - if CHANGES_PATH.exists(): - changes = [l.strip() for l in CHANGES_PATH.read_text().splitlines() if l.strip()] - if changes: - changes_text = "\n".join(f"> {c}" for c in changes) - parts.append(f"> **Last turn changes:**\n{changes_text}") - parts.append(f"---\n\n{saved}") - self._set_narrative("\n\n".join(parts)) - self._enable_input() - return + pages = load_book_pages() + if pages and pages != ["*The story has not begun.*"]: + parts = [] + parts.append(pages[-1]) + if CHANGES_PATH.exists(): + changes = [l.strip() for l in CHANGES_PATH.read_text().splitlines() if l.strip()] + if changes: + changes_text = "\n".join(f"> {c}" for c in changes) + parts.append(f"> **Last turn changes:**\n{changes_text}") + self._set_narrative("\n\n".join(parts)) + self._enable_input() + return self._call_llm() def _check_ambience(self): @@ -261,8 +255,6 @@ class ChaosTUI(App): def _run_generation(self, player_action: str | None) -> None: import traceback - last_prompt = self._last_prompt or None - def on_thought(thought: str) -> None: self.call_from_thread(self._on_thought, thought) @@ -275,7 +267,6 @@ class ChaosTUI(App): try: result = self.engine.generate_turn( player_action=player_action, - last_prompt=last_prompt, on_thought=on_thought, on_action=on_action, on_player_roll=self._on_player_roll, @@ -458,9 +449,6 @@ class ChaosTUI(App): self._display_scene(result) if result.book_log: self._last_result = result - if result.user_prompt: - LAST_PROMPT_PATH.write_text(result.user_prompt.strip()) - self._last_prompt = result.user_prompt self._append_debug("✔ turn complete") def _on_generation_error(self, error: Exception, traceback_str: str) -> None: diff --git a/tools/run_utils.py b/tools/run_utils.py index 7fea8e5..61db3a4 100644 --- a/tools/run_utils.py +++ b/tools/run_utils.py @@ -8,7 +8,6 @@ JOURNAL_PATH = SESSION / 'journal.md' AMBIENCE_PATH = SESSION / 'ambience.md' AMBIENCE_OPTIONS_PATH = SESSION / 'ambience_options.md' BOOK_PATH = SESSION / 'book.md' -LAST_PROMPT_PATH = SESSION / 'last_prompt.md' CHANGES_PATH = SESSION / 'changes.md' SETTINGS_PATH = SESSION / 'settings.json' AUDIO_DIR = SESSION / 'audio' diff --git a/tools/test_llm_turn.py b/tools/test_llm_turn.py index 25780d4..799b69c 100644 --- a/tools/test_llm_turn.py +++ b/tools/test_llm_turn.py @@ -70,40 +70,35 @@ def section(name: str): def main(): section("First turn — no player action (story opening)") r = engine.generate_turn() - check("Story opening", r, expect_error=False, expect_book=True, expect_prompt=True) + check("Story opening", r, expect_error=False, expect_book=True, expect_prompt=False) section("Valid action — buy a drink") r = engine.generate_turn( player_action="I buy a mug of ale at the Splintered Tankard", - last_prompt="What do you do?", ) - check("Buy ale", r, expect_error=False, expect_book=True, expect_prompt=True) + check("Buy ale", r, expect_error=False, expect_book=True, expect_prompt=False) section("Valid action — talk to an NPC") r = engine.generate_turn( player_action="I ask Mistress Otta about recent rumours", - last_prompt="What do you do?", ) - check("Ask Otta", r, expect_error=False, expect_book=True, expect_prompt=True) + check("Ask Otta", r, expect_error=False, expect_book=True, expect_prompt=False) section("Valid action — use inventory item") r = engine.generate_turn( player_action="I apply my healing salve to restore HP", - last_prompt="What do you do?", ) - check("Use healing salve", r, expect_error=False, expect_book=True, expect_prompt=True) + check("Use healing salve", r, expect_error=False, expect_book=True, expect_prompt=False) section("Valid action — explore") r = engine.generate_turn( player_action="I head to the Market Square to look around", - last_prompt="What do you do?", ) - check("Visit market", r, expect_error=False, expect_book=True, expect_prompt=True) + check("Visit market", r, expect_error=False, expect_book=True, expect_prompt=False) section("Invalid action — use non-existent item") r = engine.generate_turn( player_action="I drink a potion of invisibility", - last_prompt="What do you do?", ) check("Potion of invisibility", r, expect_error=False, expect_book=False) if r.log_entry: @@ -112,7 +107,6 @@ def main(): section("Invalid action — cast spell (not a weaver)") r = engine.generate_turn( player_action="I cast a fireball at the tavern ceiling", - last_prompt="What do you do?", ) check("Fireball spell", r, expect_error=False, expect_book=False) if r.log_entry: @@ -121,17 +115,14 @@ def main(): section("Invalid action — nonsensical") r = engine.generate_turn( player_action="I fly to the moon", - last_prompt="What do you do?", ) check("Fly to moon", r, expect_error=False, expect_book=False) if r.log_entry: print(f" log: {r.log_entry}") - section("Resume from last_prompt (no player action)") - r = engine.generate_turn( - last_prompt="You stand in the market square, surrounded by stalls and bustle. What do you do?", - ) - check("Resume scene", r, expect_error=False, expect_book=True, expect_prompt=True) + section("Resume scene (no player action)") + r = engine.generate_turn() + check("Resume scene", r, expect_error=False, expect_book=True, expect_prompt=False) print(f"\n{'=' * 60}") print(f" Results: {PASS} passed, {FAIL} failed")