"""Crack state machine (Section 8.3). A crack is the designed climax, not an error. It triggers when either the player has satisfied ``cracks_when`` (engine checks discovered clues + confrontations) or the guard cannot produce a safe+consistent reply (logical corner). The reply is generated via ``character/crack.md.j2`` honoring the character's ``crack_behavior``; ``partial_confess`` yields a bounded admission of a *specific* fact only. """ from __future__ import annotations from dataclasses import dataclass from ..llm.client import LLMClient from ..llm.prompts import PromptRegistry from ..models import CharacterCard @dataclass class CrackResult: text: str behavior: str confessed_fact: str = "" class CrackMachine: def __init__(self, client: LLMClient, prompts: PromptRegistry) -> None: self.client = client self.prompts = prompts def should_crack( self, card: CharacterCard, discovered: set[str], confront_count: int ) -> bool: """Heuristic engine check of cracks_when against surfaced pressure. ``cracks_when`` is authored prose; we look for any clue id mentioned in it that the player has discovered, OR a verified confrontation has landed. The character-turn pipeline also forces a crack on a logical corner (guard exhaustion), handled by the caller. """ text = card.cracks_when.lower() mentioned = [cid for cid in discovered if cid.lower() in text] # require ALL clue ids named in cracks_when to be discovered, if any named = [tok for tok in text.replace(",", " ").split() if tok.startswith("clue_")] if named: return all(n in {c.lower() for c in discovered} for n in named) # otherwise crack on first verified confrontation touching this char return bool(mentioned) or confront_count > 0 def crack( self, *, card: CharacterCard, player_message: str, trigger: str, forced_by_corner: bool, ) -> CrackResult: behavior = card.crack_behavior prompt = self.prompts.render( "character/crack.md.j2", name=card.name, voice=card.voice, cover=card.cover, crack_behavior=behavior, cracks_when=card.cracks_when, never_admit=card.never_admit, trigger=trigger, forced_by_corner=forced_by_corner, player_message=player_message, ) resp = self.client.complete( tier="character", task="crack_gen", user=prompt, ) text = resp.text.strip() confessed = "" if behavior == "partial_confess": # The authored crack prompt is instructed to confess exactly one # fact; we surface the trigger as the logged confessed fact. confessed = trigger return CrackResult(text=text, behavior=behavior, confessed_fact=confessed)