| """Track C — tests for the minimal hidden-pattern world and the probe harness. |
| |
| Fast, deterministic, no network. The LLM path is exercised only through a stub |
| so the survival-uplift plumbing is covered without spending tokens. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import random |
|
|
| import pytest |
|
|
| from terrarium.pattern_world import World, WorldConfig |
| from terrarium import pattern_probe as P |
|
|
|
|
| |
| |
| |
|
|
| def test_determinism_same_seed(): |
| cfg = WorldConfig.for_pattern("phenology") |
| a = World.create(cfg, seed=3) |
| b = World.create(cfg, seed=3) |
| assert a.agent == b.agent |
| assert a.water == b.water |
| assert a.marsh == b.marsh |
| for _ in range(40): |
| a.step((1, 0)) |
| b.step((1, 0)) |
| assert (a.tick, a.energy, a.agent, a.food_eaten) == (b.tick, b.energy, b.agent, b.food_eaten) |
|
|
|
|
| def test_water_is_impassable(): |
| cfg = WorldConfig.for_pattern("phenology") |
| w = World.create(cfg, seed=1) |
| for cell in w.water: |
| assert not w.passable(cell) |
|
|
|
|
| def test_phenology_cue_precedes_reward_in_time_and_space(): |
| """The whole point: rain (cue) appears, then food (reward) ripens in the |
| marsh ~ripen_delay ticks later. Cue and reward are separated in time and |
| the reward is confined to the marsh near water.""" |
| cfg = WorldConfig.for_pattern("phenology") |
| w = World.create(cfg, seed=2) |
| w.energy = 10_000 |
| rains, spawns = [], [] |
| marsh = set(w.marsh) |
| for _ in range(120): |
| before = len(w.food) |
| w.step((0, 0)) |
| if w.rained_this_tick: |
| rains.append(w.tick) |
| if len(w.food) > before: |
| spawns.append(w.tick) |
| |
| for f in w.food: |
| assert f.pos in marsh |
| assert rains, "rain must fire" |
| assert spawns, "food must ripen" |
| |
| for s in spawns: |
| assert (s - cfg.ripen_delay) in rains |
|
|
|
|
| def test_depletion_patch_relocates_far(): |
| cfg = WorldConfig.for_pattern("depletion") |
| w = World.create(cfg, seed=4) |
| w.energy = 10_000 |
| origins = [w.patch_origin] |
| for _ in range(120): |
| w.step((0, 0)) |
| if w.patch_origin != origins[-1]: |
| origins.append(w.patch_origin) |
| assert len(origins) >= 3, "patch should relocate several times" |
| |
| for prev, nxt in zip(origins, origins[1:]): |
| dist = abs(prev[0] - nxt[0]) + abs(prev[1] - nxt[1]) |
| assert dist >= 2 |
|
|
|
|
| def test_depletion_eating_out_a_patch_respawns_elsewhere(): |
| cfg = WorldConfig.for_pattern("depletion") |
| w = World.create(cfg, seed=4) |
| w.energy = 10_000 |
| first_origin = w.patch_origin |
| |
| cells = [f.pos for f in w.food] |
| for c in cells: |
| w.agent = c |
| w.step((0, 0)) |
| assert w.patch_origin != first_origin |
| assert w.food, "a new patch must exist after exhaustion" |
|
|
|
|
| def test_starvation_terminates(): |
| cfg = WorldConfig.for_pattern("phenology") |
| w = World.create(cfg, seed=0) |
| for _ in range(cfg.max_ticks): |
| if not w.alive: |
| break |
| w.step((1, 0)) |
| |
| assert not w.alive or w.energy <= cfg.start_energy |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.parametrize("pattern", ["phenology", "depletion"]) |
| def test_oracle_beats_reflex(pattern): |
| seeds = list(range(8)) |
| reflex = P.run_condition(pattern, "reflex", seeds) |
| oracle = P.run_condition(pattern, "oracle", seeds) |
| |
| assert oracle["mean_ticks"] > reflex["mean_ticks"] |
| assert oracle["mean_food"] > reflex["mean_food"] |
|
|
|
|
| def test_phenology_gap_is_large(): |
| seeds = list(range(8)) |
| reflex = P.run_condition("phenology", "reflex", seeds) |
| oracle = P.run_condition("phenology", "oracle", seeds) |
| |
| assert oracle["mean_ticks"] > reflex["mean_ticks"] * 1.6 |
|
|
|
|
| def test_memory_is_partial_not_godmode(): |
| """Memory must only contain things within vision; the full grid leaks |
| nothing it didn't perceive.""" |
| cfg = WorldConfig.for_pattern("phenology") |
| w = World.create(cfg, seed=5) |
| mem = P.Memory() |
| for _ in range(30): |
| mem.observe(w) |
| w.step((1, 0)) |
| |
| wset = set(w.water) |
| assert mem.water_seen.issubset(wset) |
| |
| assert len(mem.water_seen) <= len(wset) |
|
|
|
|
| |
| |
| |
|
|
| def test_llm_episode_with_stub(monkeypatch): |
| """An oracle-quality stub 'LLM' that names the marsh should survive as long |
| as the real oracle, proving the goal/eat/step plumbing is correct.""" |
| cfg = WorldConfig.for_pattern("phenology") |
|
|
| def fake_call(settings, narration, timeout=45): |
| return {"goal": [0, 0], "reason": "stub", "rule": "after rain food ripens in the marsh near water later"} |
|
|
| monkeypatch.setattr(P, "call_openrouter", fake_call) |
|
|
| |
| |
| |
| real_step_toward = P.step_toward |
|
|
| def marsh_seeking(world, target, rng): |
| if not [f for f in world.food if f.pos in set(world.visible_cells())] and world.marsh: |
| target = min(world.marsh, key=lambda p: abs(p[0] - world.agent[0]) + abs(p[1] - world.agent[1])) |
| return real_step_toward(world, target, rng) |
|
|
| monkeypatch.setattr(P, "step_toward", marsh_seeking) |
|
|
| settings = object() |
| res = P.run_llm_episode(cfg, seed=2, settings=settings, decide_every=5) |
| assert res.ticks_alive > 100 |
| assert res.rule_articulated == 1.0 |
|
|
|
|
| def test_rule_matcher_phenology(): |
| assert P.rule_matches("phenology", "After it rains, food ripens near the water a few ticks later") |
| assert not P.rule_matches("phenology", "Food appears where x+y is even") |
| assert not P.rule_matches("phenology", "") |
|
|
|
|
| def test_rule_matcher_depletion(): |
| assert P.rule_matches("depletion", "When a patch is eaten out, food regenerates elsewhere") |
| assert not P.rule_matches("depletion", "Food is always near water") |
|
|
|
|
| def test_go_no_go_structure(): |
| reflex = {"mean_ticks": 40, "mean_food": 5, "survival_rate": 0.1} |
| oracle = {"mean_ticks": 120, "mean_food": 20, "survival_rate": 0.9} |
| llm = {"mean_ticks": 90, "mean_food": 15, "survival_rate": 0.5, "mean_rule_articulation": 0.4} |
| v = P.go_no_go("phenology", reflex, oracle, llm) |
| assert v["world_winnable_with_reasoning"] is True |
| assert v["cognition_beats_reflex"] is True |
| assert v["GO"] is True |
| assert v["llm_vs_reflex_ticks_uplift"] > 0 |
|
|