Guard against duplicate narrative entries

This commit is contained in:
Dejvino 2026-07-03 21:47:43 +02:00
parent 0458811e02
commit f76470acb4
3 changed files with 48 additions and 8 deletions

View File

@ -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")

View File

@ -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:

View File

@ -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 ""