Spaces:
Running on Zero
Running on Zero
| """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 | |
| 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) | |