| """Transparent scoring — fairness by showing the work. |
| |
| The reveal shows three columns: what you SAID, what the artist DREW from it, |
| and the TRUTH. Score is objective: per-attribute accuracy (salience-weighted) |
| + the lineup verdict. No fuzzy similarity, no rigged feel. |
| """ |
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
|
|
| from .face import FaceSpec, VOCAB, SALIENCE |
|
|
| LABELS_EN = { |
| "sex": "Sex", "age": "Age", |
| "face_shape": "Face", "skin": "Skin", "hair_style": "Hair", "hair_color": "Hair color", |
| "brows": "Brows", "eyes": "Eyes", "glasses": "Glasses", "nose": "Nose", |
| "mouth": "Mouth", "facial_hair": "Facial hair", "hat": "Headwear", "extra": "Marks", |
| } |
|
|
|
|
| @dataclass |
| class TestimonyReport: |
| rows: list[tuple[str, str, str, str]] |
| hits: int |
| misses: int |
| silents: int |
| accuracy_pct: int |
| weighted_pct: int |
|
|
|
|
| def grade_testimony(described: dict[str, str | None], truth: FaceSpec) -> TestimonyReport: |
| rows, hits, misses, silents = [], 0, 0, 0 |
| w_total = w_earned = 0.0 |
| for attr in VOCAB: |
| label = LABELS_EN[attr] |
| truth_v = getattr(truth, attr) |
| said_v = described.get(attr) |
| w = SALIENCE.get(attr, 1.0) |
| w_total += w |
| if said_v is None: |
| silents += 1 |
| verdict = "silent" |
| said_txt = "—" |
| elif said_v == truth_v: |
| hits += 1 |
| w_earned += w |
| verdict = "hit" |
| said_txt = said_v.replace("_", " ") |
| else: |
| misses += 1 |
| verdict = "miss" |
| said_txt = said_v.replace("_", " ") |
| rows.append((label, said_txt, truth_v.replace("_", " "), verdict)) |
| n_said = hits + misses |
| return TestimonyReport( |
| rows=rows, hits=hits, misses=misses, silents=silents, |
| accuracy_pct=round(100 * hits / n_said) if n_said else 0, |
| weighted_pct=round(100 * w_earned / w_total), |
| ) |
|
|
|
|
| def detective_rating(report: TestimonyReport, picked_culprit: bool, glimpse_s: float) -> tuple[str, str]: |
| """(badge, one-liner) for the verdict screen.""" |
| if picked_culprit and report.weighted_pct >= 60: |
| return "★ STAR WITNESS", "The sketch artist wants to shake your hand." |
| if picked_culprit: |
| return "LUCKY BADGE", "Terrible description. Correct arrest. We'll take it." |
| if report.weighted_pct >= 55: |
| return "ALMOST", "Great memory — wrong arrest. He walked right past you." |
| return "GOLDFISH CLEARANCE", f"{glimpse_s:.0f} seconds was apparently not enough. The pigeons remember more." |
|
|