Spaces:
Running
Running
File size: 3,707 Bytes
414dc55 | 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 | """The CaseFile: the complete hidden ground truth for one mystery.
This is server-side only. The player never receives it directly - they get a
``PlayerCaseView`` projection (see projections.player_view) with the solution and
all ``is_culprit`` flags stripped. Deep referential and solvability invariants are
enforced by ``solver.checker`` so failures can target a single regenerated slice.
"""
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field
from ..constants import CASE_SCHEMA_VERSION, MAX_SUSPECTS, MIN_SUSPECTS
from .clue import Clue, Fact
from .enums import Difficulty, MotiveCategory
from .suspect import Suspect
from .timeline import Location, TimeWindow
class GenerationKnobs(BaseModel):
"""Light seeds for variety. Creative hints may be empty - then the model is
free to invent setting, era, and tone from scratch."""
model_config = ConfigDict(frozen=True)
setting_hint: str = ""
era_hint: str = ""
tone_hint: str = ""
n_suspects: int = Field(default=4, ge=MIN_SUSPECTS, le=MAX_SUSPECTS)
n_red_herrings: int = Field(default=2, ge=0, le=6)
alibi_tightness: float = Field(default=0.6, ge=0.0, le=1.0)
difficulty: Difficulty = Difficulty.STANDARD
class Setting(BaseModel):
model_config = ConfigDict(frozen=True)
name: str
description: str
locations: tuple[Location, ...]
murder_window: TimeWindow
class Victim(BaseModel):
model_config = ConfigDict(frozen=True)
vic_id: str
name: str
role: str
found_at_loc_id: str
found_at_min: int
cause_of_death: str
time_of_death: TimeWindow
class Weapon(BaseModel):
model_config = ConfigDict(frozen=True)
weapon_id: str
name: str
kind: str
origin_loc_id: str
requires_strength: bool = False
leaves_trace: str = ""
class Relationship(BaseModel):
model_config = ConfigDict(frozen=True)
from_sus_id: str
to_sus_id: str
kind: str
sentiment: float = Field(default=0.0, ge=-1.0, le=1.0)
known_publicly: bool = True
class Motive(BaseModel):
model_config = ConfigDict(frozen=True)
motive_id: str
category: MotiveCategory
summary: str
class AlibiLie(BaseModel):
model_config = ConfigDict(frozen=True)
claimed_loc_id: str
actual_loc_id: str
contradicted_by_clue_ids: tuple[str, ...]
class Culprit(BaseModel):
model_config = ConfigDict(frozen=True)
sus_id: str
true_motive: Motive
method_narrative: str
alibi_lie: AlibiLie
class Solution(BaseModel):
model_config = ConfigDict(frozen=True)
culprit_sus_id: str
weapon_id: str
motive_id: str
minimal_clue_set: tuple[str, ...]
deduction_chain: tuple[str, ...]
class CaseFile(BaseModel):
"""The full, hidden mystery. Frozen: a generated case is never mutated in place."""
model_config = ConfigDict(frozen=True)
case_id: str
seed: int
schema_version: str = CASE_SCHEMA_VERSION
title: str
briefing: str
knobs: GenerationKnobs
setting: Setting
victim: Victim
weapon: Weapon
suspects: tuple[Suspect, ...] = Field(min_length=MIN_SUSPECTS, max_length=MAX_SUSPECTS)
culprit: Culprit
relationships: tuple[Relationship, ...] = ()
facts: tuple[Fact, ...] = ()
clues: tuple[Clue, ...] = ()
solution: Solution
def suspect(self, sus_id: str) -> Suspect:
for s in self.suspects:
if s.sus_id == sus_id:
return s
raise KeyError(f"no suspect {sus_id!r}")
def clue(self, clue_id: str) -> Clue:
for c in self.clues:
if c.clue_id == clue_id:
return c
raise KeyError(f"no clue {clue_id!r}")
|