case0 / src /case_zero /engine /scoring.py
HusseinEid's picture
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55
raw
history blame
2.71 kB
"""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}."