Spaces:
Sleeping
Sleeping
| """Full turn loop against StubBrain — the entire arc, zero GPU (SPEC §11).""" | |
| from __future__ import annotations | |
| import json | |
| from game.brain import StubBrain | |
| from game.engine import ( | |
| advance_challenge, | |
| confirm_candidate, | |
| current_challenge, | |
| new_session, | |
| run_turn, | |
| ) | |
| from game.ledger import find, has | |
| def test_fresh_sessions_do_not_share_state(): | |
| """Two visitors must not share an alien (SPEC §0/§12).""" | |
| a, b = new_session(), new_session() | |
| assert a.ledger is not b.ledger and a.world is not b.world | |
| run_turn(a, "Hide the blue stone.", StubBrain()) | |
| # mutating a's world/ledger must not touch b | |
| assert b.world.objects["blue_stone"].location == "ground" | |
| assert not has(b.ledger, "hidden_info") or find(b.ledger, "hidden_info").times_applied == 0 | |
| def test_warmup_is_mechanical_no_concept(): | |
| s = new_session() | |
| res = run_turn(s, "Put the red stone in the basket.", StubBrain()) | |
| assert res.won is True and res.newly_won is True | |
| assert res.learn_offer is None | |
| assert s.pending_candidate is None | |
| def test_full_arc_with_teaching_and_generalization(): | |
| s = new_session() | |
| brain = StubBrain() | |
| # 1. warm-up | |
| assert run_turn(s, "Put the red stone in the basket.", StubBrain()).won | |
| assert advance_challenge(s) | |
| assert current_challenge(s).id == "hide" | |
| # 2. teach hidden_info — offered, gated on confirmation | |
| res = run_turn(s, "Hide the blue stone from the other one.", brain) | |
| assert res.won and res.learn_offer and res.learn_offer["id"] == "hidden_info" | |
| assert not has(s.ledger, "hidden_info") # not added until confirmed | |
| assert confirm_candidate(s) == "hiding information" | |
| assert has(s.ledger, "hidden_info") | |
| assert advance_challenge(s) | |
| # 3. teach gift | |
| res = run_turn(s, "Give the other one a present.", brain) | |
| assert res.won and res.learn_offer["id"] == "gift" | |
| confirm_candidate(s) | |
| assert has(s.ledger, "gift") | |
| assert advance_challenge(s) | |
| # 4. GENERALIZATION: surprise = hide + give, with no re-teaching ----------- # | |
| assert current_challenge(s).id == "surprise" | |
| assert current_challenge(s).teaches is None | |
| res = run_turn(s, "Make a surprise for the other one.", brain) | |
| assert res.won is True | |
| # the learned concepts transferred: their times_applied bumped (§5) | |
| assert "hidden_info" in res.reapplied and "gift" in res.reapplied | |
| assert find(s.ledger, "hidden_info").times_applied == 1 | |
| # the alien also coins "surprise", composed from what it knows | |
| assert res.learn_offer and res.learn_offer["id"] == "surprise" | |
| confirm_candidate(s) | |
| assert find(s.ledger, "surprise").built_from == ("hidden_info", "gift") | |
| assert advance_challenge(s) | |
| # 5. second generalization: secret -> reuses hidden_info | |
| res = run_turn(s, "Keep your blue stone a secret from the other one.", brain) | |
| assert res.won is True | |
| assert "hidden_info" in res.reapplied | |
| assert find(s.ledger, "hidden_info").times_applied == 2 # bumped again | |
| assert advance_challenge(s) is False # arc complete | |
| def test_reject_candidate_keeps_ledger_clean(): | |
| s = new_session() | |
| advance_challenge(s) # -> hide | |
| run_turn(s, "Hide the blue stone.", StubBrain()) | |
| assert s.pending_candidate is not None | |
| from game.engine import reject_candidate | |
| reject_candidate(s) | |
| assert s.pending_candidate is None | |
| assert not has(s.ledger, "hidden_info") | |
| def test_unparseable_player_request_is_safe(): | |
| """A request the alien has no concept for never crashes — it waits.""" | |
| s = new_session() | |
| res = run_turn(s, "Please reticulate the splines philosophically.", StubBrain()) | |
| assert res.response.action.verb == "wait" | |
| assert res.won is False | |
| def _scripted(verb, args, cid=None): | |
| """A raw model response with a free-form (non-canonical) concept id — what the | |
| REAL model emits, vs the stub's canonical ids.""" | |
| cand = {"id": cid, "label": cid.replace("_", " "), "understanding": f"{cid} gloss"} if cid else None | |
| return json.dumps({"action": {"verb": verb, "args": args}, | |
| "utterance": "...", "gap": None, "candidate_concept": cand}) | |
| def test_real_model_ids_tracked_by_actual_concepts(): | |
| """The model names concepts freely (concealing/offering, not hidden_info/gift); | |
| lineage + times_applied must follow the concepts actually taught, not the | |
| challenge's canonical relies_on ids.""" | |
| brain = StubBrain(scripted=[ | |
| _scripted("put_in", {"obj_id": "red_stone", "container_id": "basket"}), | |
| _scripted("put_in", {"obj_id": "blue_stone", "container_id": "basket"}, "concealing"), | |
| _scripted("give", {"obj_id": "blue_stone", "agent_id": "other"}, "offering"), | |
| _scripted("put_in", {"obj_id": "blue_stone", "container_id": "basket"}, "the_surprise"), | |
| ]) | |
| s = new_session() | |
| run_turn(s, "put red in basket", brain); advance_challenge(s) | |
| run_turn(s, "hide blue", brain); confirm_candidate(s); advance_challenge(s) | |
| run_turn(s, "give a present", brain); confirm_candidate(s); advance_challenge(s) | |
| res = run_turn(s, "make a surprise", brain) | |
| # reuse lights up the ACTUAL taught concepts, by their real ids | |
| assert set(res.reapplied) == {"concealing", "offering"} | |
| assert find(s.ledger, "concealing").times_applied == 1 | |
| confirm_candidate(s) | |
| surprise = find(s.ledger, "the_surprise") | |
| assert surprise.via_generalization is True | |
| assert surprise.built_from == ("concealing", "offering") # real ids, not phantom | |
| assert find(s.ledger, "concealing").via_generalization is False | |
| def test_generalization_with_nothing_taught_has_no_phantom_lineage(): | |
| """The transcript bug: reach a generalization having taught nothing. The new | |
| concept must be self-reached with EMPTY lineage — not 'from hidden_info+gift' | |
| pointing at concepts that were never learned.""" | |
| from game.engine import reject_candidate | |
| brain = StubBrain(scripted=[ | |
| _scripted("put_in", {"obj_id": "red_stone", "container_id": "basket"}), | |
| _scripted("put_in", {"obj_id": "blue_stone", "container_id": "basket"}, "concealing"), | |
| _scripted("give", {"obj_id": "blue_stone", "agent_id": "other"}, "offering"), | |
| _scripted("put_in", {"obj_id": "blue_stone", "container_id": "basket"}, "the_surprise"), | |
| ]) | |
| s = new_session() | |
| run_turn(s, "put red", brain); advance_challenge(s) | |
| run_turn(s, "hide blue", brain); reject_candidate(s); advance_challenge(s) # taught nothing | |
| run_turn(s, "give present", brain); reject_candidate(s); advance_challenge(s) # taught nothing | |
| res = run_turn(s, "make a surprise", brain) | |
| assert res.reapplied == () # nothing taught to reapply | |
| confirm_candidate(s) | |
| surprise = find(s.ledger, "the_surprise") | |
| assert surprise.via_generalization is True | |
| assert surprise.built_from == () # no phantom lineage | |
| assert [c for c in s.ledger if c.taught_on_turn != 0 and not c.via_generalization] == [] | |