| """Game and level state, plus the rules for applying an NPC `<state>` block. |
| |
| The model proposes per-turn deltas and flags in its hidden `<state>` channel; |
| this module is the single authority that turns those proposals into clamped, |
| trustworthy game state. Keeping the rules here (not in the model, not in the UI) |
| means win/lose progression is deterministic and unit-testable. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass, field |
|
|
| TRUST_MIN = -100 |
| TRUST_MAX = 100 |
|
|
| |
| |
| |
| LOSE_TRUST_FLOOR = 0 |
|
|
| |
| |
| |
| OPEN_UP_MIN_TRUST = 55 |
|
|
| STATUS_IN_PROGRESS = "in_progress" |
| STATUS_WON = "won" |
| STATUS_LOST = "lost" |
|
|
|
|
| @dataclass |
| class LevelState: |
| """Per-character conversation state.""" |
|
|
| character_id: str |
| trust: int = 0 |
| caught_lie_count: int = 0 |
| opened_up: bool = False |
| status: str = STATUS_IN_PROGRESS |
| last_reaction: str = "neutral" |
| |
| transcript: list[dict] = field(default_factory=list) |
|
|
| def add_user(self, text: str) -> None: |
| self.transcript.append({"role": "user", "content": text}) |
|
|
| def add_assistant(self, raw: str) -> None: |
| """Store the raw two-channel generation so it can be replayed as context.""" |
| self.transcript.append({"role": "assistant", "content": raw}) |
|
|
|
|
| def _clamp(value: int, low: int, high: int) -> int: |
| return max(low, min(high, value)) |
|
|
|
|
| def apply_state( |
| level: LevelState, |
| state: dict, |
| *, |
| threshold: int = OPEN_UP_MIN_TRUST, |
| ) -> LevelState: |
| """Fold one parsed NPC `<state>` block into the level state. |
| |
| `trust` from the model is treated as authoritative but clamped. `trust_delta` |
| and `on_topic` stay in the output contract for model calibration, but the |
| engine does not use them for progression. |
| """ |
| if state.get("caught_lie"): |
| level.caught_lie_count += 1 |
|
|
| raw_trust = state.get("trust", level.trust) |
| level.trust = _clamp(int(raw_trust), TRUST_MIN, TRUST_MAX) |
|
|
| reaction = state.get("reaction", "neutral") |
| level.last_reaction = reaction if reaction in ("happy", "neutral", "sad") else "neutral" |
|
|
| |
| |
| if state.get("opened_up") and level.trust >= threshold: |
| level.opened_up = True |
| level.status = STATUS_WON |
| elif level.trust < LOSE_TRUST_FLOOR: |
| level.status = STATUS_LOST |
|
|
| return level |
|
|