"""Immutable runtime state for a play session. Nothing here is mutated in place. Reducers in ``state_update`` return new instances via ``model_copy``, so the UI can hold snapshots safely and history is never clobbered. """ from __future__ import annotations from enum import StrEnum from pydantic import BaseModel, ConfigDict, Field from ..schemas.accusation import Verdict class Exchange(BaseModel): model_config = ConfigDict(frozen=True) turn_index: int question: str answer: str class NotebookKind(StrEnum): CLUE = "clue" CONTRADICTION = "contradiction" LEAD = "lead" NOTE = "note" class NotebookEntry(BaseModel): model_config = ConfigDict(frozen=True) kind: NotebookKind text: str turn_index: int suspect_id: str | None = None clue_id: str | None = None class SuspectState(BaseModel): """Per-suspect mutable-by-copy interrogation state.""" model_config = ConfigDict(frozen=True) sus_id: str stress: float = Field(default=0.0, ge=0.0, le=1.0) rapport: float = Field(default=0.0, ge=-1.0, le=1.0) turns: int = 0 transcript: tuple[Exchange, ...] = () revealed_fact_ids: frozenset[str] = frozenset() evidence_shown: frozenset[str] = frozenset() broken_lie_ids: frozenset[str] = frozenset() contradictions: tuple[str, ...] = () class GameState(BaseModel): """The whole session snapshot.""" model_config = ConfigDict(frozen=True) case_id: str suspect_states: dict[str, SuspectState] discovered_clue_ids: frozenset[str] = frozenset() current_suspect_id: str | None = None turn_count: int = 0 notebook: tuple[NotebookEntry, ...] = () solved: bool = False verdict: Verdict | None = None def state_for(self, sus_id: str) -> SuspectState: return self.suspect_states[sus_id] def new_game_state(case_id: str, suspect_ids: tuple[str, ...]) -> GameState: return GameState( case_id=case_id, suspect_states={sid: SuspectState(sus_id=sid) for sid in suspect_ids}, current_suspect_id=suspect_ids[0] if suspect_ids else None, )