"""Data model (SPEC.md §2). Everything here is a plain dataclass so it can live in ``gr.State`` (Gradio deep-copies session state; dataclasses, lambdas and module-level functions are all deepcopy-safe). Per-user game state is carried in :class:`GameSession`; static content (seed primitives, challenge definitions) lives module-level in ``ledger.py`` / ``challenges.py`` and is never mutated. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Callable, Optional # --------------------------------------------------------------------------- # # 2.1 Concept — an entry in the ledger # --------------------------------------------------------------------------- # @dataclass class Concept: id: str # stable slug, e.g. "hidden_info" label: str # short human label, e.g. "hiding information" player_phrase: str # how the player expressed it when teaching understanding: str # the alien's internal gloss, 1 sentence, alien framing taught_on_turn: int times_applied: int = 0 # Concept ids this one was *composed from* (generalization beat). Lets the UI # draw the constellation edge from e.g. "surprise" back to "hidden_info". built_from: tuple[str, ...] = () # True if reached during a generalization challenge (the alien composed it) # rather than taught directly — drives the finale's "taught vs understood alone" # split. (built_from can't be used for that split: a generalization with no # prior taught concepts has empty built_from but is still self-reached.) via_generalization: bool = False # --------------------------------------------------------------------------- # # 2.2 WorldState — the deterministic sandbox # --------------------------------------------------------------------------- # @dataclass class Obj: id: str name: str # "blue stone", "red stone", "basket" location: str # an Agent id, a Container id, or "ground" hidden: bool = False # concealed from other agents? @dataclass class Agent: id: str # "alien", "other" name: str holding: list[str] = field(default_factory=list) # obj ids # NOTE: not in the SPEC's Agent sketch. Added so the `move_to` verb (which the # SPEC lists in the closed action set) has somewhere to record its effect. # None of the §8 win-predicates depend on it; it is a clean superset. location: str = "ground" @dataclass class WorldState: objects: dict[str, Obj] agents: dict[str, Agent] containers: list[str] # e.g. ["basket"] — things that can conceal log: list[str] = field(default_factory=list) # human-readable action record # --------------------------------------------------------------------------- # # 2.3 Action — the closed, enumerable verb set # --------------------------------------------------------------------------- # @dataclass class Action: verb: str # one of world.ALLOWED_VERBS args: dict = field(default_factory=dict) # --------------------------------------------------------------------------- # # 2.4 Challenge — the current goal, with a MECHANICAL win condition # --------------------------------------------------------------------------- # @dataclass class Challenge: id: str title: str # "Teach the alien to hide the stone" setup_blurb: str # shown to the player (NOT to the model) teaches: Optional[str] # concept id this challenge introduces, or None win_predicate: Callable[[WorldState], bool] # checked after each action # For generalization challenges (teaches=None): concept ids a *previously # learned* concept is expected to be applied through. Not in the SPEC's # Challenge sketch; added to power the §5 "times_applied / concept lights up # on reuse" beat without the model judging semantics. relies_on: tuple[str, ...] = () # A concrete, player-facing objective, shown as a live goal strip in the UI so # "what counts as winning" is legible (not in the SPEC sketch; UX clarity). goal: str = "" # --------------------------------------------------------------------------- # # Per-session container (everything that goes into a single gr.State) # --------------------------------------------------------------------------- # @dataclass class GameSession: """All per-user mutable state. One of these lives in one ``gr.State``. ``challenge_index`` indexes the module-level ``challenges.CHALLENGES`` list rather than carrying a ``Challenge`` (with its callable predicate) through state — keeps the session pure data and the static arc in one place. """ ledger: list[Concept] world: WorldState challenge_index: int = 0 turn: int = 0 # Conversation transcript for rendering. Each entry: # {"who": "player"|"alien"|"system", "text": str, # "gap": str|None, "kind": str|None} history: list[dict] = field(default_factory=list) # A candidate_concept awaiting the player's "Yes, it learned that" (§5). pending_candidate: Optional[dict] = None # Set when the current challenge's win_predicate just became true. won_current: bool = False