File size: 2,708 Bytes
414dc55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
"""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}."