"""Headless tests for the Game wrapper, using the dummy policy (no GPU/Gradio). Run: python -m pytest tests/ -q """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from ttw import narrate from ttw.dummy import make_random_policy from ttw.game import Game def _game(): return Game(make_random_policy(seed=3), deck_seed=1) def test_baseline_recorded_before_any_step(): g = _game() assert not g.price_frame().empty # turn 0 prices recorded assert g.gini_frame().iloc[0]["turn"] == 0 def test_step_advances_and_grows_history(): g = _game() rows_before = len(g.price_frame()) g.step() assert g.world.turn == 1 assert len(g.price_frame()) > rows_before assert g.gini_frame().iloc[-1]["turn"] == 1 def test_price_frame_shape_is_long_per_good(): g = _game() g.step() pf = g.price_frame() assert set(pf.columns) == {"turn", "good", "price"} # one row per good per recorded turn (turn 0 baseline + turn 1) assert (pf["turn"] == 1).sum() == 5 def test_town_frame_lists_all_creatures_sorted_by_wealth(): g = _game() g.step() tf = g.town_frame() assert len(tf) == 5 nets = list(tf["net worth"]) assert nets == sorted(nets, reverse=True) # richest first def test_tempt_fate_draws_legend_and_tickers(): g = _game() title = g.tempt_fate() assert isinstance(title, str) and title assert any("inspired by" in line for line in g.ticker) def test_shock_gold_rush_echoes_to_ticker(): g = _game() before = sum(c.pebbles for c in g.world.alive()) g.shock("gold_rush", amount=10) assert sum(c.pebbles for c in g.world.alive()) == before + 10 * 5 assert any("windfall" in line.lower() for line in g.ticker) def test_traces_markdown_handles_dummy_policy_gracefully(): """Dummy policy has no raw thoughts; traces view must not crash.""" g = _game() g.step() assert isinstance(g.traces_markdown(), str) # no exception, returns a string def test_legend_markdown_before_and_after_tempt(): g = _game() assert "No legend yet" in g.legend_markdown() g.tempt_fate() md = g.legend_markdown() assert "Inspired by" in md and "✨" in md assert g.current_legend is not None def test_avatar_and_mood_emoji(): assert narrate.avatar("Fenn") == "🦊" assert narrate.avatar("Nobody") == "🐾" # fallback assert narrate.mood_emoji(10) == "😊" assert narrate.mood_emoji(4) == "😣" def test_town_frame_has_avatars_and_mood_faces(): g = _game() g.step() tf = g.town_frame() assert any("🦊" in str(v) for v in tf["creature"]) # avatars present assert all(any(face in str(m) for face in "😊🙂😟😣") for m in tf["mood"]) # Phase 6 operator-console HTML accessors (arity is enforced in app.py). _FIREWALL_BANNED = ("true", "false", "truth", "legend_key") def test_phase6_accessors_return_str(): g = _game() assert isinstance(g.heat_bar_html(), str) assert isinstance(g.relationship_map_html(), str) assert isinstance(g.cutscene_html(), str) def test_heat_bar_html_well_formed(): from ttw.world import INVESTIGATION_THRESHOLD g = _game() html = g.heat_bar_html() assert html # non-empty assert str(INVESTIGATION_THRESHOLD) in html # threshold readout, not hardcoded assert "color:#" in html # inline color set assert not any(b in html for b in _FIREWALL_BANNED) # truth firewall intact def test_relationship_map_html_covers_cast_and_patron(): g = _game() # Drive a couple of moves so at least one sentiment cell is nonzero. g.queue_player_move("bribe", target="Fenn", amount=40) g.step() g.queue_player_move("short", good="berries", amount=20) g.step() html = g.relationship_map_html() assert "