case0 / src /case_zero /engine /evidence_board.py
HusseinEid's picture
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55
"""Evidence discovery via non-interrogation channels (search / forensic / document).
Every clue in the solution's minimal set is reachable here, so solvability never
depends on a suspect choosing to talk - the discoverability guarantee in play.
"""
from __future__ import annotations
from ..schemas.case import CaseFile
from ..schemas.clue import Clue
from ..schemas.enums import DiscoveryMethod
from .game_state import GameState
from .state_update import discover_clues
_NON_INTERROGATION = frozenset(
{DiscoveryMethod.SEARCH, DiscoveryMethod.FORENSIC, DiscoveryMethod.DOCUMENT}
)
def clues_at(case: CaseFile, loc_id: str) -> tuple[Clue, ...]:
"""Clues physically discoverable by examining a location."""
return tuple(
c
for c in case.clues
if c.discoverable_at_loc_id == loc_id and c.discovery_method in _NON_INTERROGATION
)
def search_location(state: GameState, case: CaseFile, loc_id: str) -> tuple[GameState, tuple[Clue, ...]]:
found = clues_at(case, loc_id)
newly = tuple(c for c in found if c.clue_id not in state.discovered_clue_ids)
new_state = discover_clues(state, tuple(c.clue_id for c in newly), case)
return new_state, newly
def available_evidence(state: GameState, case: CaseFile) -> tuple[Clue, ...]:
"""Discovered clues the player currently holds and may present to suspects."""
return tuple(c for c in case.clues if c.clue_id in state.discovered_clue_ids)