Spaces:
Running
Running
File size: 5,226 Bytes
414dc55 80cd1f2 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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | """The PUBLIC wire contract: the exact case shape the Preact client receives.
This mirrors the prototype's ``prototype/js/case.jsx`` (the data-model-by-example) in
camelCase. It is the ONLY case representation that ever reaches the browser before the
verdict. The sealed solution - killer, true motive, key-evidence chain - and every
per-question/per-evidence suspicion *delta* and scripted *answer* are deliberately absent:
those are produced live, server-side (the deltas alone would reveal the killer).
"""
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
class _Wire(BaseModel):
"""camelCase on the wire; snake_case in Python. Frozen - a view is never mutated."""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, frozen=True)
class PublicVictim(_Wire):
name: str
role: str
age: int
sprite: str
bio: str
class SuggestedQuestion(_Wire):
id: str
q: str
class PublicSuspect(_Wire):
id: str
name: str
role: str
age: int
sprite: str
gender: str = "male"
tag: str
baseline_suspicion: int
motive: str # apparent motive shown in the dossier - NOT proof
alibi: str
quote: str
greet: str
suggested_questions: tuple[SuggestedQuestion, ...]
class ThreadMessage(_Wire):
from_: str = Field(alias="from")
who: str
t: str
m: str
class PublicEvidence(_Wire):
id: str
name: str
type: str # PHONE | PAPER | IMAGE | AUDIO | DATA
icon: str
time: str
found: str
summary: str
# Exactly one of these display payloads is populated, per type.
thread: tuple[ThreadMessage, ...] | None = None
detail: str | None = None
transcript: str | None = None
dur: str | None = None
rows: tuple[tuple[str, ...], ...] | None = None
class TimelineBeat(_Wire):
time: str
label: str
locked: bool = False
ev: str | None = None
conflict: bool = False
class FlashbackAccount(_Wire):
who: str
scene: str
lines: tuple[str, ...]
flags: tuple[int, ...] = ()
class PublicFlashback(_Wire):
title: str
a: FlashbackAccount
b: FlashbackAccount
class PublicMotive(_Wire):
id: str
text: str
class StoryBeat(_Wire):
scene: str
kicker: str
title: str
text: str
class PublicCase(_Wire):
id: str
city: str
district: str
title: str
tagline: str
weather: str
victim: PublicVictim
scene: str
tod: str
found: str
cause: str
# Case-kind display labels. Defaulted to homicide so the golden case and every
# pre-kind stored case keep their exact current wording.
kind: str = "homicide"
kind_label: str = "HOMICIDE" # title screen: "A PROCEDURAL {kindLabel}"
division: str = "HOMICIDE DIVISION"
victim_status: str = "DECEASED" # dossier stamp next to the victim
tod_label: str = "T.O.D."
verdict: str = "Homicide" # KEY FACTS verdict line
facts: tuple[tuple[str, str], ...]
boot_lines: tuple[str, ...]
story_beats: tuple[StoryBeat, ...]
suspects: tuple[PublicSuspect, ...]
evidence: tuple[PublicEvidence, ...]
timeline: tuple[TimelineBeat, ...]
flashback: PublicFlashback
motives: tuple[PublicMotive, ...]
def golden_to_public(data: dict) -> PublicCase:
"""Project a sealed golden/stored case dict into the PUBLIC view.
Strips the ``sealed`` block and, per suspect, the scripted ``answer``/``delta`` and
the ``present``/``default`` reply tables - leaving only question text the player sees.
"""
suspects = tuple(
PublicSuspect(
id=s["id"],
name=s["name"],
role=s["role"],
age=s["age"],
sprite=s["sprite"],
gender=s.get("gender", "male"),
tag=s["tag"],
baseline_suspicion=s["suspicion"],
motive=s["motive"],
alibi=s["alibi"],
quote=s["quote"],
greet=s["greet"],
suggested_questions=tuple(
SuggestedQuestion(id=q["id"], q=q["q"]) for q in s["questions"]
),
)
for s in data["suspects"]
)
return PublicCase(
id=data["id"],
city=data["city"],
district=data["district"],
title=data["title"],
tagline=data["tagline"],
weather=data["weather"],
victim=PublicVictim(**data["victim"]),
scene=data["scene"],
tod=data["tod"],
found=data["found"],
cause=data["cause"],
facts=tuple(tuple(pair) for pair in data["facts"]),
boot_lines=tuple(data["bootLines"]),
story_beats=tuple(StoryBeat(**b) for b in data.get("storyBeats", [])),
suspects=suspects,
evidence=tuple(PublicEvidence(**e) for e in data["evidence"]),
timeline=tuple(TimelineBeat(**b) for b in data["timeline"]),
flashback=PublicFlashback(
title=data["flashback"]["title"],
a=FlashbackAccount(**data["flashback"]["a"]),
b=FlashbackAccount(**data["flashback"]["b"]),
),
motives=tuple(PublicMotive(**m) for m in data["motives"]),
)
|