case0 / src /case_zero /generator /crime_profiles.py
HusseinEid's picture
feat: multi-crime cases, scene+exhibit pixel art, background AI generation
80cd1f2 verified
Raw
History Blame
12.9 kB
"""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)]