"""Deterministic verdict scoring. The verdict is a pure function of the structured accusation and the discovered/cited evidence - never of any model output. A jailbroken confession in dialogue therefore cannot change the outcome: the case resolves only on the data. """ from __future__ import annotations from ..schemas.accusation import Accusation, Verdict from ..schemas.case import CaseFile from .game_state import GameState _CULPRIT_POINTS = 50 _WEAPON_POINTS = 15 _MOTIVE_POINTS = 15 _EVIDENCE_POINTS = 20 def score_accusation(case: CaseFile, accusation: Accusation, state: GameState) -> Verdict: sol = case.solution culprit_correct = accusation.accused_sus_id == sol.culprit_sus_id weapon_correct = accusation.weapon_id == sol.weapon_id if accusation.weapon_id else False motive_correct = accusation.motive_id == sol.motive_id if accusation.motive_id else False minimal = set(sol.minimal_clue_set) cited = set(accusation.cited_clue_ids) discovered = state.discovered_clue_ids valid_citations = minimal & cited & discovered evidence_ratio = (len(valid_citations) / len(minimal)) if minimal else 0.0 # Weapon, motive, and evidence credit only count once the right person is named: # an incorrect accusation scores zero, so a jailbroken confession earns nothing. culprit_pts = _CULPRIT_POINTS if culprit_correct else 0 weapon_pts = _WEAPON_POINTS if (culprit_correct and weapon_correct) else 0 motive_pts = _MOTIVE_POINTS if (culprit_correct and motive_correct) else 0 evidence_pts = round(_EVIDENCE_POINTS * evidence_ratio) if culprit_correct else 0 score = culprit_pts + weapon_pts + motive_pts + evidence_pts breakdown = ( ("culprit", culprit_pts), ("weapon", weapon_pts), ("motive", motive_pts), ("evidence", evidence_pts), ) rationale = _rationale(case, culprit_correct, valid_citations, minimal) return Verdict( solved=culprit_correct, culprit_correct=culprit_correct, weapon_correct=weapon_correct, motive_correct=motive_correct, score=score, breakdown=breakdown, rationale=rationale, deduction_chain=sol.deduction_chain, ) def _rationale( case: CaseFile, culprit_correct: bool, valid: set[str], minimal: set[str] ) -> str: culprit_name = case.suspect(case.solution.culprit_sus_id).name if culprit_correct: if valid >= minimal and minimal: return f"Correct, and fully evidenced. {culprit_name} did it, and you proved it." return f"You named {culprit_name} - the right person - but the case could be tighter." return f"Not quite. The killer was {culprit_name}."