Spaces:
Running
Running
File size: 20,416 Bytes
16ff49b | 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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 | """Crime profiles: one frozen table that tells every layer how to talk about a case.
The solver and engine are crime-agnostic - they only check structure (one culprit, a
false alibi contradicted by discoverable evidence, innocents cleared). A profile maps a
``CrimeKind`` to the words each layer needs: how the generator prompts the model, how
the suspect brief states the culprit's deed, and how the dossier/verdict label the case.
Field semantics are REUSED rather than renamed, so no schema churn:
- ``victim`` = the wronged party (the deceased / the one robbed / the missing person);
- ``weapon`` = the instrument or object of the crime (knife / stolen jewel / forged deed);
- ``time_of_death`` = the moment of the incident;
- ``cause_of_death`` = one line describing what happened to the victim.
"""
from __future__ import annotations
from dataclasses import dataclass
from ..schemas.enums import CrimeKind
@dataclass(frozen=True)
class CrimeProfile:
kind: CrimeKind
# --- generator prompts ---
author_noun: str # "murder mystery"
incident_noun: str # "murder" - used in "during the {incident_noun}"
incident_line: str # world prompt: what happened that evening
victim_ask: str # world prompt: who the victim is + what cause_of_death holds
instrument_ask: str # world prompt: what weapon_name/kind hold
deed_line: str # mystery prompt: "{culprit} ... {victim} ... {room} ... {instrument}"
trace_ask: str # mystery prompt: what the two breaker clues prove
# --- engine ---
brief_deed: str # suspect brief, second person: "You killed {victim}."
confession_verbs: str # extra regex alternatives for the confession scrubber
# --- display ---
perp_noun: str # "killer"
division: str # "HOMICIDE DIVISION"
victim_status: str # dossier stamp: "DECEASED"
verdict: str # KEY FACTS verdict line: "Homicide"
kind_label: str # title screen: "A PROCEDURAL {kind_label}"
tod_label: str # "T.O.D." vs "LAST SEEN" vs "INCIDENT"
found_verb: str # "Found in" vs "Last seen in"
timeline_line: str # "{name} is killed. Time of death."
boot_line: str # boot screen: "A death no one saw coming."
fallback_titles: tuple[str, ...]
_P = CrimeProfile
PROFILES: dict[CrimeKind, CrimeProfile] = {
CrimeKind.HOMICIDE: _P(
kind=CrimeKind.HOMICIDE,
author_noun="murder mystery",
incident_noun="murder",
incident_line="The murder happened between {start} and {end} one evening.",
victim_ask="the victim (name, role, cause of death) and which room index (0-based) "
"they were found in",
instrument_ask="the murder weapon (name, kind)",
deed_line="They murdered {victim} in the {room} with the {instrument} between "
"{start} and {end}, then lied that they never left the {claimed}.",
trace_ask="quietly place {culprit} in the {room} during the murder",
brief_deed="You killed {victim}.",
confession_verbs=r"killed|murdered|stabbed|poisoned|strangled|shot|drowned|smothered",
perp_noun="killer",
division="HOMICIDE DIVISION",
victim_status="DECEASED",
verdict="Homicide",
kind_label="HOMICIDE",
tod_label="T.O.D.",
found_verb="Found in",
timeline_line="{name} is killed. Time of death.",
boot_line="A death no one saw coming.",
fallback_titles=(
"A Death in the {room}", "The {room} Affair", "Murder at {setting}",
"The {last} File", "Blood in the {room}", "Last Call at {setting}",
"The {setting} Killing", "The {room} Verdict",
),
),
CrimeKind.THEFT: _P(
kind=CrimeKind.THEFT,
author_noun="heist mystery - a prized object stolen from under everyone's nose",
incident_noun="theft",
incident_line="The theft happened between {start} and {end} one evening.",
victim_ask="the victim - the OWNER of what was stolen (name, role, and in "
"cause_of_death one line on how they discovered the loss) and which room "
"index (0-based) the theft happened in",
instrument_ask="the STOLEN OBJECT itself (name, kind) - one specific prized thing: "
"a jewel, a painting, a rare manuscript, a strongbox",
deed_line="They stole the {instrument} from {victim}'s {room} between {start} and "
"{end}, then lied that they never left the {claimed}.",
trace_ask="quietly place {culprit} in the {room} during the theft",
brief_deed="You stole the {instrument} from {victim}.",
confession_verbs=r"stole|robbed|took\s+the|lifted\s+the|pocketed",
perp_noun="thief",
division="ROBBERY DIVISION",
victim_status="ROBBED",
verdict="Grand Larceny",
kind_label="HEIST",
tod_label="TAKEN AT",
found_verb="Taken from",
timeline_line="The {instrument} vanishes from the {room}.",
boot_line="A theft no one saw happen.",
fallback_titles=(
"The Empty Case in the {room}", "The {setting} Job", "The {last} Collection",
"What Left the {room}", "The {setting} Take", "A Hole in the {room}",
),
),
CrimeKind.FRAUD: _P(
kind=CrimeKind.FRAUD,
author_noun="financial-fraud mystery - a swindle unravelling among people who "
"trusted each other",
incident_noun="fraud",
incident_line="The decisive forged transaction was pushed through between {start} "
"and {end} one evening.",
victim_ask="the victim - the person DEFRAUDED (name, role, and in cause_of_death "
"one line on what they lost and how they found out) and which room index "
"(0-based) the scheme was run from",
instrument_ask="the INSTRUMENT of the fraud (name, kind) - a forged deed, a doctored "
"ledger, a fake certificate, a rigged contract",
deed_line="They defrauded {victim} using the {instrument}, working from the {room} "
"between {start} and {end}, then lied that they never left the {claimed}.",
trace_ask="quietly place {culprit} in the {room} when the {instrument} was made",
brief_deed="You defrauded {victim} using the {instrument}.",
confession_verbs=r"forged|defrauded|swindled|scammed|embezzled|doctored\s+the",
perp_noun="fraudster",
division="FRAUD DIVISION",
victim_status="DEFRAUDED",
verdict="Fraud",
kind_label="SWINDLE",
tod_label="SIGNED AT",
found_verb="Uncovered in",
timeline_line="The {instrument} is signed in the {room}.",
boot_line="A fortune signed away in one evening.",
fallback_titles=(
"The {room} Signature", "The {setting} Accounts", "The {last} Trust",
"Ink in the {room}", "The Paper Fortune", "The {setting} Ledger",
),
),
CrimeKind.BLACKMAIL: _P(
kind=CrimeKind.BLACKMAIL,
author_noun="blackmail mystery - someone is being bled, and the extortionist sat "
"at the same table",
incident_noun="blackmail drop",
incident_line="The latest blackmail demand was planted between {start} and {end} "
"one evening.",
victim_ask="the victim - the person BLACKMAILED (name, role, and in cause_of_death "
"one line on the demand and what it is costing them) and which room index "
"(0-based) the demand was planted in",
instrument_ask="the LEVERAGE (name, kind) - compromising photographs, stolen letters, "
"an incriminating ledger page",
deed_line="They are blackmailing {victim} with the {instrument}, and planted the "
"demand in the {room} between {start} and {end}, then lied that they "
"never left the {claimed}.",
trace_ask="quietly place {culprit} in the {room} when the demand was planted",
brief_deed="You are blackmailing {victim} with the {instrument}.",
confession_verbs=r"blackmailed|blackmailing|extorted|bled\s+(him|her|them)",
perp_noun="blackmailer",
division="EXTORTION UNIT",
victim_status="EXTORTED",
verdict="Extortion",
kind_label="EXTORTION",
tod_label="PLANTED AT",
found_verb="Planted in",
timeline_line="The demand is planted in the {room}.",
boot_line="An envelope no one will admit to leaving.",
fallback_titles=(
"The {room} Envelope", "The Price of {last}", "The {setting} Demand",
"Postage Due at {setting}", "The {room} Letters", "What {last} Pays For",
),
),
CrimeKind.ARSON: _P(
kind=CrimeKind.ARSON,
author_noun="arson mystery - a fire set by a familiar hand",
incident_noun="fire",
incident_line="The fire was set between {start} and {end} one evening.",
victim_ask="the victim - the person whose property BURNED (name, role, and in "
"cause_of_death one line on what the fire took from them) and which room "
"index (0-based) the fire started in",
instrument_ask="the MEANS of the fire (name, kind) - a kerosene tin, an oil lamp, "
"a candle and curtains",
deed_line="They set the fire in {victim}'s {room} using the {instrument} between "
"{start} and {end}, then lied that they never left the {claimed}.",
trace_ask="quietly place {culprit} in the {room} just before the fire started",
brief_deed="You set the fire in {victim}'s {room} using the {instrument}.",
confession_verbs=r"torched|set\s+the\s+fire|burned\s+it|lit\s+the|started\s+the\s+fire",
perp_noun="arsonist",
division="ARSON UNIT",
victim_status="BURNED OUT",
verdict="Arson",
kind_label="ARSON",
tod_label="IGNITED AT",
found_verb="Started in",
timeline_line="The fire starts in the {room}.",
boot_line="A fire that started exactly where it hurt most.",
fallback_titles=(
"The {room} Burn", "Ashes at {setting}", "The {last} Fire",
"What the {room} Kept", "Kindling at {setting}", "The {setting} Blaze",
),
),
CrimeKind.MISSING: _P(
kind=CrimeKind.MISSING,
author_noun="missing-person mystery - someone walked into a room and never walked out",
incident_noun="disappearance",
incident_line="The disappearance happened between {start} and {end} one evening.",
victim_ask="the victim - the person who VANISHED (name, role, and in cause_of_death "
"one line on how their absence was noticed) and which room index (0-based) "
"they were last seen in",
instrument_ask="the LURE (name, kind) - the thing used to draw them away: a forged "
"note, a false telegram, a promised meeting",
deed_line="They are behind {victim}'s disappearance - they lured them from the {room} "
"using the {instrument} between {start} and {end}, then lied that they "
"never left the {claimed}.",
trace_ask="quietly place {culprit} in the {room} when {victim} was last seen",
brief_deed="You are behind {victim}'s disappearance; you lured them away with the "
"{instrument}. {victim} is alive, but you must hide what you did.",
confession_verbs=r"abducted|kidnapped|lured\s+(him|her|them)|took\s+(him|her|them)",
perp_noun="abductor",
division="MISSING PERSONS",
victim_status="MISSING",
verdict="Missing Person",
kind_label="DISAPPEARANCE",
tod_label="LAST SEEN",
found_verb="Last seen in",
timeline_line="{name} is seen for the last time.",
boot_line="A chair still warm, and nobody in it.",
fallback_titles=(
"The Empty Chair in the {room}", "Where {last} Went", "The {setting} Absence",
"Last Seen in the {room}", "The {last} Vanishing", "Gone from {setting}",
),
),
CrimeKind.CON: _P(
kind=CrimeKind.CON,
author_noun="confidence-game mystery - a charming swindler emptied someone's "
"savings through a venture that never existed",
incident_noun="swindle",
incident_line="The decisive payment of the swindle changed hands between {start} "
"and {end} one evening.",
victim_ask="the victim - the person CONNED (name, role, and in cause_of_death "
"one line on what they handed over and the moment they realized the "
"venture was smoke) and which room index (0-based) the deal was closed in",
instrument_ask="the BAIT of the con (name, kind) - a false prospectus, fake share "
"certificates, a salted mine sample, a counterfeit letter of credit",
deed_line="They conned {victim} out of a fortune using the {instrument}, closing "
"the deal in the {room} between {start} and {end}, then lied that they "
"never left the {claimed}.",
trace_ask="quietly place {culprit} in the {room} when the deal was closed",
brief_deed="You conned {victim} out of their savings using the {instrument}.",
confession_verbs=r"conned|swindled|fleeced|duped|took\s+(him|her|them)\s+for",
perp_noun="con artist",
division="BUNCO SQUAD",
victim_status="FLEECED",
verdict="Confidence Fraud",
kind_label="CON GAME",
tod_label="CLOSED AT",
found_verb="Closed in",
timeline_line="The deal is closed in the {room}.",
boot_line="A fortune handed over with a smile.",
fallback_titles=(
"The {room} Prospectus", "The {setting} Promise", "The {last} Venture",
"Paper Gold at {setting}", "The Long Game in the {room}", "The {setting} Pitch",
),
),
CrimeKind.POISONING: _P(
kind=CrimeKind.POISONING,
author_noun="poisoning mystery - a tainted glass passed at a familiar table",
incident_noun="poisoning",
incident_line="The poison was administered between {start} and {end} one evening.",
victim_ask="the victim (name, role, cause of death - what they drank or ate and "
"how it took them) and which room index (0-based) they were stricken in",
instrument_ask="the VECTOR of the poison (name, kind) - a decanter, a teacup, a "
"tonic bottle, a dessert plate",
deed_line="They poisoned {victim} via the {instrument} in the {room} between "
"{start} and {end}, then lied that they never left the {claimed}.",
trace_ask="quietly place {culprit} near the {instrument} before {victim} was stricken",
brief_deed="You poisoned {victim}.",
confession_verbs=r"poisoned|dosed|laced|tainted|spiked",
perp_noun="poisoner",
division="TOXICOLOGY UNIT",
victim_status="POISONED",
verdict="Poisoning",
kind_label="POISONING",
tod_label="T.O.D.",
found_verb="Stricken in",
timeline_line="{name} is stricken. The glass is still warm.",
boot_line="A toast no one should have drunk.",
fallback_titles=(
"The Bitter Glass in the {room}", "The {setting} Toast", "The {last} Dose",
"What the Decanter Held", "Last Pour at {setting}", "The {room} Vintage",
),
),
CrimeKind.RANSOM: _P(
kind=CrimeKind.RANSOM,
author_noun="ransom mystery - someone was taken, and a demand was left where it "
"would be found",
incident_noun="abduction",
incident_line="The abduction happened between {start} and {end} one evening.",
victim_ask="the victim - the person TAKEN (name, role, and in cause_of_death one "
"line on how the ransom demand was discovered) and which room index "
"(0-based) they were taken from",
instrument_ask="the RANSOM NOTE (name, kind) - cut letters, a typed card, a wax-"
"sealed demand",
deed_line="They are behind {victim}'s abduction - they took them from the {room} "
"and left the {instrument} between {start} and {end}, then lied that "
"they never left the {claimed}.",
trace_ask="quietly place {culprit} in the {room} when {victim} was taken",
brief_deed="You took {victim} and left the {instrument}. {victim} is alive and "
"unharmed, but you must hide what you did.",
confession_verbs=r"abducted|kidnapped|took\s+(him|her|them)|snatched|holding\s+(him|her|them)",
perp_noun="kidnapper",
division="RANSOM DETAIL",
victim_status="TAKEN",
verdict="Kidnapping",
kind_label="RANSOM",
tod_label="TAKEN AT",
found_verb="Taken from",
timeline_line="{name} is taken. The note is found.",
boot_line="An empty chair and a demand on the mantel.",
fallback_titles=(
"The {room} Demand", "The Price of {last}", "Taken from {setting}",
"The {setting} Note", "What the {room} Lost", "The {last} Exchange",
),
),
CrimeKind.SABOTAGE: _P(
kind=CrimeKind.SABOTAGE,
author_noun="sabotage mystery - machinery, a performance, or a life's work wrecked "
"by a familiar hand",
incident_noun="sabotage",
incident_line="The sabotage was carried out between {start} and {end} one evening.",
victim_ask="the victim - the person whose WORK WAS WRECKED (name, role, and in "
"cause_of_death one line on what failed and what it cost them) and which "
"room index (0-based) the tampering happened in",
instrument_ask="the TAMPERED MECHANISM (name, kind) - a severed cable, a loosened "
"valve, a doctored mixture, a cut harness",
deed_line="They sabotaged {victim}'s work - tampering with the {instrument} in the "
"{room} between {start} and {end} - then lied that they never left the "
"{claimed}.",
trace_ask="quietly place {culprit} in the {room} when the tampering was done",
brief_deed="You sabotaged {victim}'s work by tampering with the {instrument}. "
"{victim} survived, but you must hide what you did.",
confession_verbs=r"sabotaged|tampered|rigged\s+it|cut\s+the|loosened\s+the|wrecked\s+it",
perp_noun="saboteur",
division="INCIDENT UNIT",
victim_status="RUINED",
verdict="Sabotage",
kind_label="SABOTAGE",
tod_label="FAILED AT",
found_verb="Tampered with in",
timeline_line="The {instrument} gives way in the {room}.",
boot_line="It did not break. It was broken.",
fallback_titles=(
"The {room} Failure", "What Gave Way at {setting}", "The {last} Collapse",
"A Loose Thread in the {room}", "The {setting} Incident", "Made to Fail",
),
),
}
# Weighted draw: homicide stays the backbone of a noir game; the rest add variety.
_KIND_BAG: tuple[CrimeKind, ...] = (
CrimeKind.HOMICIDE, CrimeKind.HOMICIDE, CrimeKind.HOMICIDE,
CrimeKind.THEFT, CrimeKind.THEFT,
CrimeKind.FRAUD, CrimeKind.BLACKMAIL, CrimeKind.ARSON, CrimeKind.MISSING,
CrimeKind.CON, CrimeKind.POISONING, CrimeKind.RANSOM, CrimeKind.SABOTAGE,
)
def profile_for(kind: CrimeKind | str) -> CrimeProfile:
return PROFILES[CrimeKind(kind)]
def kind_for_seed(seed: int) -> CrimeKind:
return _KIND_BAG[seed % len(_KIND_BAG)]
|