"""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}", ), ), } # 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, ) 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)]