Spaces:
Running
Running
| """Orchestrator: the FSM that binds quests + state + validation. | |
| The Orchestrator owns one (state, quests) pair and the *decisions* on it -- | |
| using quest boundaries it chooses when to advance / complete a stage / win / | |
| lose, and it assembles the player-facing hint (judge hint, else the static | |
| concept_brief, or the reveal on loss). GameState owns its own mutations. | |
| """ | |
| from __future__ import annotations | |
| from config import MAX_ATTEMPT | |
| from core.game_state import GameState | |
| from core.quest_bank import QuestBank | |
| from core.validation import Result, Validator | |
| class Orchestrator: | |
| def __init__(self, bank: QuestBank | None = None, state: GameState | None = None, | |
| validator: Validator | None = None): | |
| self.bank = bank if bank is not None else QuestBank() | |
| self.state = state if state is not None else GameState() | |
| self.validator = validator if validator is not None else Validator() | |
| def current_question(self) -> dict: | |
| """The full current question (shown + check + reveal).""" | |
| return self.bank.stage(self.state.stage_idx)["questions"][self.state.q_idx] | |
| def quest_view(self) -> dict: | |
| """Model-safe READ of the current quest: stage flavor + the SHOWN part only. | |
| Drops `check` and `reveal` here, at the core, so the answer never travels | |
| toward the model. Injected into the prompt instead of being a tool. | |
| """ | |
| stage = self.bank.stage(self.state.stage_idx) | |
| return {"concept": stage["concept"], "fiction": stage["fiction"], | |
| **self.current_question["shown"]} | |
| def submit(self, answer: str) -> Result: | |
| """Validate an answer, drive the state, return (verdict, hint).""" | |
| q = self.current_question | |
| result = self.validator.validate(answer, q["check"]) | |
| if result.verdict == "correct": | |
| n_questions = len(self.bank.stage(self.state.stage_idx)["questions"]) | |
| if self.state.q_idx + 1 < n_questions: | |
| self.state.advance() | |
| elif self.state.stage_idx + 1 < len(self.bank): | |
| self.state.complete_stage() | |
| else: | |
| self.state.win() | |
| return result # no hint needed on correct | |
| # "wrong" or "partial" -> both count as an attempt toward losing | |
| self.state.strike() | |
| if self.state.attempt >= MAX_ATTEMPT: | |
| self.state.lose() | |
| return Result("lost", q["reveal"]) | |
| return Result(result.verdict, result.hint or q["shown"]["concept_brief"]) | |