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