#!/usr/bin/env python3 """ validation.py — Narrative quality validation for The Chaos engine. Standalone functions — no dependency on GameEngine. """ from __future__ import annotations import re from collections import Counter from .llm import call_llm def auto_prompt(book_log: str = "") -> str: """Fallback player prompt.""" return "**What do you do?**" def validate_narrative( book_log: str, *, model: str, temperature: float, timeout: int, on_debug: callable = None, ) -> tuple[bool, str]: """Check if book_log is acceptable narrative. Returns (ok, reason).""" lines = book_log.strip().split("\n") if not lines: return False, "Empty narrative" common = Counter(lines).most_common(1) if common and common[0][1] >= 5: return False, f"Repetition: '{common[0][0][:60]}' ×{common[0][1]}" mech_lines = [l for l in lines if re.match( r'^\*\*(?:Roll|Damage|Success|Failure|Check|Save|Hit|Miss|' r'Strenght|Dexterity|Willpower|STR|DEX|WIL|' r'(?:[A-Z][a-z]+(?: \(\w+\))?:))', l )] if mech_lines: ratio = len(mech_lines) / len(lines) if ratio > 0.3: return False, f"Game mechanics dominate ({len(mech_lines)}/{len(lines)} lines)" if re.search(r'```(?:tool|json)', book_log): return False, "Contains unprocessed tool blocks" prose = re.sub(r'[*_#>`~\-\d]', '', book_log).strip() if len(prose) < 50: return False, "Too short to be meaningful" text = call_llm([ {"role": "user", "content": f"Rate this RPG narrative quality 1-5.\n" f"1 = unreadable (spam, repetition, pure mechanics, garbled)\n" f"2 = poor (mostly mechanics, little story)\n" f"3 = acceptable (some narrative but rough)\n" f"4 = good (solid prose, minor issues)\n" f"5 = excellent (vivid, engaging)\n" f"Reply with ONLY a single digit 1-5.\n\n" f"{book_log[:600]}"} ], model=model, temperature=temperature, timeout=timeout, max_tokens=2, label="Narrative validation", on_debug=on_debug) if text and text.strip().isdigit(): score = int(text.strip()) if score < 3: return False, f"Quality score: {score}/5" return True, ""