File size: 6,292 Bytes
c26b033 165ee8f c26b033 165ee8f 2541ca3 1127d3b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | """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 "<table" in html
assert "Patron" in html
for name in g.world.creatures:
assert name in html
assert "#2e2519" in html # forced dark text on the light cells
def test_cutscene_html_never_blank():
g = _game()
before = g.cutscene_html()
assert before # placeholder before any events, never blank
assert "The wood is still." in before
g.step()
after = g.cutscene_html()
assert after
assert "#fff8e6" not in after # the dark surface is never lightened
assert "#161b22" in after # and is the intended dark playbill background
# Judge-facing polish: lab attribution, the story spine, and the raw-output panel.
def test_creature_lab_color_maps_engine_and_falls_back():
from ttw.council import creature_lab_color
assert creature_lab_color("Fenn") == "#76b900" # NVIDIA green (nemotron)
assert creature_lab_color("Oona") == "#10a37f" # OpenAI teal (gptoss)
assert creature_lab_color("Nobody") == "#cdbf9a" # neutral fallback
def test_chapter_markdown_is_html_stepper_with_one_active():
g = _game()
spine = g.chapter_markdown()
# All five chapter short-titles are present as pills.
for title in ("First Whispers", "The Heat Rises", "Reckoning", "The Long Game"):
assert title in spine
# Exactly one pill is the active one (the accent underline appears once).
assert spine.count("border-bottom:3px solid #2f6b3d") == 1
def test_traces_markdown_tags_the_model():
g = _game()
# Thoughts are normally filled from policy.state["raw"]; set them directly to
# test the model tag rendering without a live council.
g.thoughts = {"Oona": "hold the line", "Pip": "buy acorns"}
md = g.traces_markdown()
assert "[gpt-oss-20B]" in md # Oona's model
assert "[ttw-trader-0.5B]" in md # Pip's model
def test_raw_output_markdown_empty_state():
g = _game() # dummy policy exposes no .state
md = g.raw_output_markdown()
assert isinstance(md, str)
assert "No raw model output" in md
def test_raw_output_markdown_tags_and_fence_safe():
g = _game()
g.policy.state = {"raw": {"Oona": '{"thought":"x"} ``` # inject'}}
md = g.raw_output_markdown()
assert "gpt-oss-20B" in md # tagged by model
assert "```json" in md # rendered as a fenced block
# Exactly the open/close fence remain; a stray fence in model output that
# would break out and inject markup is neutralized.
assert md.count("```") == 2
|