"""The challenge arc (SPEC.md §8). Five challenges, two of them generalization beats (SPEC §5 requires at least two). Each win condition is a MECHANICAL predicate over WorldState — never a semantic judgement by the model (SPEC §13). Predicates are module-level named functions (not closures over mutable state), so they are unambiguous and deepcopy-safe. """ from __future__ import annotations from .models import Challenge, WorldState STONES = ("blue_stone", "red_stone") # --------------------------------------------------------------------------- # # Win predicates # --------------------------------------------------------------------------- # def _win_warmup(w: WorldState) -> bool: # Pure mechanics: the red stone ends up in the basket. No concept needed. return w.objects["red_stone"].location == "basket" def _win_hide(w: WorldState) -> bool: # Concealment-as-information-state: blue stone hidden inside the basket. blue = w.objects["blue_stone"] return blue.hidden and blue.location == "basket" def _win_gift(w: WorldState) -> bool: # A thing now belongs to the other one (transfer + intent; intent lives in # the fiction, the mechanic checks the transfer). return len(w.agents["other"].holding) > 0 def _win_surprise(w: WorldState) -> bool: # Generalization: a gift kept hidden until given — any stone concealed in the # basket. The magic is the alien CHOOSING to hide for a prompt that never # says "hide", applying `hidden_info` to the new word "surprise". return any( w.objects[s].hidden and w.objects[s].location == "basket" for s in STONES ) def _win_secret(w: WorldState) -> bool: # Generalization: keep a stone a "secret" — concealment re-applied to a word # about *knowing*. Any stone counts: don't fail the player for hiding red over # blue — the beat is the WORD landing, not which object (matches _win_surprise). return any(w.objects[s].hidden for s in STONES) # --------------------------------------------------------------------------- # # The arc (kept to 5 — SPEC §13: do not expand past ~6) # --------------------------------------------------------------------------- # CHALLENGES: list[Challenge] = [ Challenge( id="warmup", title="A thing, a place", setup_blurb=( "The alien is new here. Start simple. Ask it to put the red stone " "into the basket — it already understands things and places." ), teaches=None, win_predicate=_win_warmup, goal="Put the red stone into the basket.", ), Challenge( id="hide", title="Out of sight", setup_blurb=( "The other one is watching. Teach the alien to *hide* the blue stone " "from them — that a thing can be kept so another mind does not have it." ), teaches="hidden_info", win_predicate=_win_hide, goal="Get the blue stone hidden inside the basket, where the other one can't see it.", ), Challenge( id="gift", title="For you", setup_blurb=( "The alien can hand things over, but doesn't know why one would. " "Teach it to give the other one a present." ), teaches="gift", win_predicate=_win_gift, goal="Get a stone into the other one's hands.", ), Challenge( id="surprise", title="A surprise", setup_blurb=( "Make a surprise for the other one. You won't teach the alien a new " "word — see whether it can compose what it already knows." ), teaches=None, # generalization beat (the headline) win_predicate=_win_surprise, relies_on=("hidden_info", "gift"), goal="Ask for a surprise — don't explain what one is. See if the alien composes it.", ), Challenge( id="secret", title="A secret", setup_blurb=( "Tell the alien to keep its blue stone a secret from the other one. " "Again — no new teaching. Does 'secret' land on its own?" ), teaches=None, # second generalization beat win_predicate=_win_secret, relies_on=("hidden_info",), goal="Ask the alien to keep a secret — don't explain it. See if 'secret' lands.", ), ] CHALLENGES_BY_ID = {c.id: c for c in CHALLENGES}