"""v4 ("the wood fights back"): wood resistance, informant heat, firewall preserved. GPU-free and deterministic. Drives the engine with a no-op policy (so trading never perturbs prices) and forces a known legend via EventDeck(seed, events=[run]), the same forcing idiom as tests/test_information_war.py. Wariness is set directly on each creature's sentiment toward the Patron, the load-bearing v4 signal. Run: python -m pytest tests/test_wood_resistance.py -q """ import re import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from ttw.events import EVENTS, EventDeck from ttw.game import Game from ttw.narrate import narrate from ttw.world import INFORMANT_HEAT_PER, INVESTIGATION_THRESHOLD, ShortPosition, Relationship def _noop_policy(world, name): return [], [] def _game(seed=1): return Game(_noop_policy, deck_seed=seed) def _run_on_hoard(): return next(e for e in EVENTS if e.key == "run_on_hoard") # crash_mult=0.5, honey def _sour_holders(world, good, count): """Sour `count` creatures toward the Patron AND make them hold `good`, so they count as wary HOLDERS. Deterministic by creature name order.""" soured = 0 for name in sorted(world.creatures): if soured >= count: break c = world.creatures[name] c.relationships["Patron"] = Relationship(sentiment=-3) # at WARY_THRESHOLD c.inventory[good] = c.inventory.get(good, 0) + 1 # ensure it holds the good soured += 1 def _short_pnl_with_wary(n_wary): """Realized P&L of a fixed honey short front-running run_on_hoard, with exactly `n_wary` wary holders blunting the crash.""" g = _game() g.deck = EventDeck(seed=0, events=[_run_on_hoard()]) _sour_holders(g.world, "honey", n_wary) g.world.patron.shorts.append(ShortPosition(good="honey", qty=4, entry_price=10.0)) g.tempt_fate() # binds run_on_hoard (honey) g.world.last_price["honey"] = 10.0 # known pre-crash price; no-op policy won't move it g.step() braced = any(e["type"] == "wood_braces" for e in g.world.log) return g.world.patron.realized_pnl, braced # --- (1) more wary holders -> a strictly smaller short payoff ------------------ def test_resistance_monotonically_blunts_short_pnl(): pnls = [] for n in (0, 1, 2, 3): pnl, braced = _short_pnl_with_wary(n) pnls.append(pnl) if n > 0: assert braced, f"wood_braces should fire with {n} wary holders" # Strictly decreasing: each additional wary holder blunts the crash further, # so the short pays strictly less as the wood wises up. for earlier, later in zip(pnls, pnls[1:]): assert later < earlier, f"P&L must strictly decrease as wariness rises: {pnls}" # --- (1b) resistance plateaus at the MAX_RESIST clamp ------------------------- def test_resistance_plateaus_at_max_resist(): # RESIST_PER=0.25, MAX_RESIST=0.75 -> 3 wary holders hit the cap (3*0.25=0.75), # so a 4th wary holder cannot blunt the crash further. P&L must be EQUAL at the # cap, locking the clamp against regression (not merely "non-increasing"). pnl3, _ = _short_pnl_with_wary(3) pnl4, braced4 = _short_pnl_with_wary(4) assert braced4, "wood_braces should still fire past the clamp" assert pnl4 == pnl3, f"P&L must plateau at the MAX_RESIST cap: n3={pnl3} n4={pnl4}" # --- (2) informant heat: exactly N * INFORMANT_HEAT_PER per turn --------------- def test_informant_heat_adds_per_wary_creature(): # No player moves, no legend, no shorts/tips -> the ONLY heat source is the # informant block, isolating its contribution. g = _game() n = 3 for name in sorted(g.world.creatures)[:n]: g.world.creatures[name].relationships["Patron"] = Relationship(sentiment=-3) heat0 = g.world.patron.heat g.step() assert g.world.patron.heat == heat0 + n * INFORMANT_HEAT_PER assert any(e["type"] == "wood_testifies" and e["n_wary"] == n for e in g.world.log) # --- (3) informant heat alone can open Heron's inquiry ------------------------- def test_informant_heat_opens_investigation_faster(): g = _game() # Sour the whole cast and pre-load heat so informant heat tips it over. for c in g.world.creatures.values(): c.relationships["Patron"] = Relationship(sentiment=-3) n_wary = len(g.world.creatures) g.world.patron.heat = INVESTIGATION_THRESHOLD - n_wary * INFORMANT_HEAT_PER assert g.world.patron.heat < INVESTIGATION_THRESHOLD # below before the wood talks g.step() assert g.world.investigation is not None assert g.world.investigation.stage == "testimony" # --- (4) the two new events narrate cleanly (firewall: no banned substring) ---- def test_new_events_narrate_clean(): banned = ("true", "false", "truth", "legend_key") braces = narrate({"type": "wood_braces", "turn": 1, "good": "honey", "resistance": 0.5}) testifies = narrate({"type": "wood_testifies", "turn": 1, "n_wary": 2}) for line in (braces, testifies): assert line is not None low = line.lower() for token in banned: assert token not in low, f"{token!r} leaked into a v4 ticker line" # --- (6) the exposure meter shows payoff + wariness and reacts to souring ------- def _exposure_with_wary(n_wary): """exposure_markdown() for a honey short front-running run_on_hoard, with exactly `n_wary` wary holders blunting the projected crash. peek() (not draw) drives the meter, so no legend is bound and the world is never stepped.""" g = _game() g.deck = EventDeck(seed=0, events=[_run_on_hoard()]) # peek -> run_on_hoard (honey) _sour_holders(g.world, "honey", n_wary) g.world.patron.shorts.append(ShortPosition(good="honey", qty=4, entry_price=10.0)) g.world.last_price["honey"] = 10.0 # known pre-crash mark return g.exposure_markdown() def _payoff_figure(md): """Parse the signed pebble payoff the exposure panel headlines.""" return int(re.search(r"payoff if you Tempt Fate now: \*\*([+-]?\d+)p\*\*", md).group(1)) def test_exposure_markdown_shows_payoff_and_wariness(): md0 = _exposure_with_wary(0) # The wood-wariness fraction is shown (0 of the full cast wary in a fresh world). n_alive = len(_game().world.alive()) assert f"0/{n_alive}" in md0 # A real, positive expected payoff is headlined (the honey short into the crash). assert _payoff_figure(md0) > 0 # Souring a honey holder blunts the projected crash, so the displayed expected # payoff DROPS -- the v4 causation made visible in the meter. md1 = _exposure_with_wary(1) assert f"1/{n_alive}" in md1 assert _payoff_figure(md1) < _payoff_figure(md0) # Firewall: the meter never surfaces the hidden tip-truth or the legend key. for banned in ("truth", "legend_key"): assert banned not in md0.lower() assert banned not in md1.lower() # --- (5) a fresh (no-wary) world keeps the existing run_on_hoard crash to 5.0 --- def test_run_on_hoard_zero_wary_still_crashes_to_5(): g = _game() g.deck = EventDeck(seed=0, events=[_run_on_hoard()]) g.tempt_fate() # binds run_on_hoard (honey) g.world.last_price["honey"] = 10.0 # known pre-crash price; no-op policy won't move it g.step() assert g.world.last_price["honey"] == 5.0 # resistance 0 -> effective_crash == crash (0.5) assert not any(e["type"] == "wood_braces" for e in g.world.log)