Spaces:
Running on Zero
Running on Zero
| """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) | |