Guard against duplicate narrative entries
This commit is contained in:
parent
0458811e02
commit
f76470acb4
@ -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")
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 ""
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user