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