"""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)]