f-id / src /id /engine /accuse.py
marcodsn's picture
Initial Gradio Space
0423b99
Raw
History Blame Contribute Delete
3.62 kB
"""Accusation scoring & debrief (Section 8.7).
The player names culprit + means + motive + opportunity (optionally citing
clues). The engine scores against solution.md: culprit correctness, and whether
each of means/motive/opportunity is supported by a *discovered* clue. Produces a
graded result + a debrief that reveals the solution and missed clues.
"""
from __future__ import annotations
from ..models import AccusationResult, Solution
from ..worldio import World
from .clues import ClueGraph
def _supported(claim_text: str, solution_field: str, discovered_reveals: str) -> bool:
"""Heuristic: the player's stated reason overlaps the truth AND is backed by
something they actually discovered. Token-overlap based (deterministic)."""
claim_tokens = {t for t in claim_text.lower().split() if len(t) > 3}
truth_tokens = {t for t in solution_field.lower().split() if len(t) > 3}
if not claim_tokens or not truth_tokens:
return False
overlaps_truth = len(claim_tokens & truth_tokens) >= 1
backed = bool(claim_tokens & {t for t in discovered_reveals.lower().split() if len(t) > 3})
return overlaps_truth and backed
def score_accusation(
*,
world: World,
clue_graph: ClueGraph,
discovered: set[str],
culprit: str,
means: str,
motive: str,
opportunity: str,
) -> AccusationResult:
sol: Solution = world.solution
culprit_correct = culprit.strip().lower() == sol.culprit.strip().lower()
discovered_reveals = " ".join(
node.reveals for cid in discovered if (node := clue_graph.nodes.get(cid))
)
means_ok = culprit_correct and _supported(means, sol.means, discovered_reveals)
motive_ok = culprit_correct and _supported(motive, sol.motive, discovered_reveals)
opp_ok = culprit_correct and _supported(opportunity, sol.opportunity, discovered_reveals)
if culprit_correct and means_ok and motive_ok and opp_ok:
grade: str = "solved"
elif culprit_correct:
grade = "right_culprit_unproven"
else:
grade = "wrong"
missed = [
cid
for cid, node in clue_graph.nodes.items()
if node.required_for_solution and cid not in discovered
]
debrief = _build_debrief(sol, grade, missed, clue_graph)
return AccusationResult(
culprit_named=culprit,
culprit_correct=culprit_correct,
means_supported=means_ok,
motive_supported=motive_ok,
opportunity_supported=opp_ok,
grade=grade,
debrief=debrief,
missed_clues=missed,
)
def _build_debrief(
sol: Solution, grade: str, missed: list[str], clue_graph: ClueGraph
) -> str:
lines = []
headline = {
"solved": "CASE SOLVED. You named the culprit and proved every element.",
"right_culprit_unproven": (
"RIGHT CULPRIT — but your case wasn't fully proven by discovered evidence."
),
"wrong": "WRONG. The case remains open.",
}[grade]
lines.append(headline)
lines.append("")
lines.append("--- The truth ---")
lines.append(f"Culprit: {sol.culprit}")
lines.append(f"Means: {sol.means}")
lines.append(f"Motive: {sol.motive}")
lines.append(f"Opportunity: {sol.opportunity}")
if sol.true_sequence:
lines.append("")
lines.append(sol.true_sequence.strip())
if missed:
lines.append("")
lines.append("Clues you never surfaced:")
for cid in missed:
node = clue_graph.nodes.get(cid)
if node:
lines.append(f" - {cid}: {node.reveals}")
return "\n".join(lines)