diff --git a/tools/engine.py b/tools/engine.py index b3dafff..89052d9 100644 --- a/tools/engine.py +++ b/tools/engine.py @@ -5,6 +5,7 @@ import random import re import sys from datetime import datetime +from difflib import SequenceMatcher from pathlib import Path from engine_lib.models import TurnResult @@ -117,8 +118,14 @@ class GameEngine: if text: book_log = (book_log + "\n\n" + text) if book_log else text elif name == "finalize_turn": - if args.get("ambience"): - ambience = args["ambience"] + raw = (args.get("ambience") or "").strip().lower() + if raw: + valid = state.get_valid_ambiences() + if raw in valid: + ambience = raw + else: + state.append_llm_log(f"\n[WARN] invalid ambience '{raw}'") + ambience = None if args.get("log_entry"): log_entry = args["log_entry"] elif name == "player_roll": @@ -126,6 +133,25 @@ class GameEngine: elif name not in ("roll",): state_changes.append(tc) + # Duplicate check — reject if narrative is 80%+ similar to last book entry + if book_log: + prev = state.read_recent_book(1) + if prev and prev not in ("*No prior story.*",): + prev_text = re.sub(r"^## Turn \d+\n\n", "", prev, flags=re.MULTILINE).strip() + ratio = SequenceMatcher(None, book_log, prev_text).ratio() + if ratio >= 0.8: + state.append_llm_log(f"\n[TURN DUPLICATE] {ratio:.0%} match with previous turn") + if attempt < MAX_RETRIES: + feedback = "The narrative is nearly identical to the previous turn. Generate something new and different." + state.append_llm_log(f"\n[TURN REGENERATE] (duplicate) attempt {attempt + 2}") + continue + state.append_llm_log(f"\n[TURN DUPLICATE EXCEEDED] cannot generate unique narrative") + return TurnResult( + book_log="", + log_entry="Your action was rejected — could not generate a unique narrative.", + user_prompt=f"*Your action:\n\t\"{player_action}\"\nwas rejected:\nengine failed to produce unique narrative*", + ) + # Validate the generated turn if on_action: on_action("DM is validating the response") @@ -181,8 +207,6 @@ class GameEngine: if name == "narrative": pass - elif name == "finalize_turn": - pass elif name == "player_roll" and on_player_roll: dice = args.get("dice", "1d6") reason = args.get("reason", "a check") diff --git a/tools/engine_lib/state.py b/tools/engine_lib/state.py index 125f066..2ab8be9 100644 --- a/tools/engine_lib/state.py +++ b/tools/engine_lib/state.py @@ -112,8 +112,6 @@ def validate_update_size(name: str, new_content: str, path: Path) -> bool: def apply_state(result: TurnResult) -> None: """Write state changes from a TurnResult to disk.""" - if result.ambience: - AMBIENCE_PATH.write_text(result.ambience.strip().lower() + "\n") if result.changes: CHANGES_PATH.write_text("\n".join(result.changes) + "\n") else: diff --git a/tools/engine_lib/tools_handler.py b/tools/engine_lib/tools_handler.py index 1d3343f..6a52987 100644 --- a/tools/engine_lib/tools_handler.py +++ b/tools/engine_lib/tools_handler.py @@ -4,8 +4,8 @@ import json import random import re -from .paths import CHAR_PATH, WORLD_PATH -from .state import read_file, validate_update_size, update_journal, append_llm_log +from .paths import AMBIENCE_PATH, CHAR_PATH, WORLD_PATH +from .state import read_file, validate_update_size, update_journal, append_llm_log, get_valid_ambiences TOOL_REGISTRY: dict[str, dict] = { @@ -168,6 +168,20 @@ def tool_journal_update(args: dict) -> str: return "Journal updated." +def tool_finalize_turn(args: dict) -> str: + """Validate ambience and write to AMBIENCE_PATH.""" + raw = (args or {}).get("ambience", "").strip().lower() + if not raw: + AMBIENCE_PATH.write_text("silence\n") + return "Ambience set to silence." + valid = get_valid_ambiences() + if raw not in valid: + append_llm_log(f"\n[WARN] invalid ambience '{raw}', allowed: {sorted(valid)}") + return f"**Error:** invalid ambience '{raw}'. Allowed: {', '.join(sorted(valid))}." + AMBIENCE_PATH.write_text(raw + "\n") + return f"Ambience set to {raw}." + + def execute_tool(tool_name: str, args: dict) -> str: """Execute a tool by name. Returns result string.""" fn_map = { @@ -181,6 +195,7 @@ def execute_tool(tool_name: str, args: dict) -> str: "replace_note": tool_replace_note, "world_update": tool_world_update, "journal_update": tool_journal_update, + "finalize_turn": tool_finalize_turn, } fn = fn_map.get(tool_name) if not fn: @@ -227,6 +242,9 @@ def describe_change(tool_name: str, args: dict) -> str: for d in args.get("done", []): parts.append(f"✅ {d}") return "; ".join(parts) if parts else "" + elif tool_name == "finalize_turn": + a = args.get("ambience", "") + return f"♫ {a}" if a else "" return ""