neilA / game /models.py
TriggerFish212's picture
Track taught concepts for lineage and reapplied
3081a6a
Raw
History Blame Contribute Delete
5.29 kB
"""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