Spaces:
Running
Running
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55 | """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}") | |