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