tower-learns-you / tests /test_render.py
vknt's picture
Deploy The Tower Learns You (custom gr.Server frontend, hf_inference + mock fallback)
22a027e verified
Raw
History Blame Contribute Delete
10.7 kB
from pathlib import Path
from tower_game.assets import AssetCatalog
from tower_game.registry import SKILLS
from tower_game.render import (
render_latest_log,
render_sidebar,
render_stage,
render_stage_narration,
skill_card_label,
)
from tower_game.state import new_game_state
from tower_game.ui import CSS, JS, _tooltip_data
def test_stage_uses_reserved_huds_and_left_facing_enemy():
state = new_game_state(phase="combat")
state["current_floor"] = 1
state["enemy_name"] = "Test Enemy"
state["enemy_hp"] = 20
state["enemy_max_hp"] = 20
stage = render_stage(state, AssetCatalog.load())
assert 'class="stage-hud player-stage-hud"' in stage
assert 'class="stage-hud enemy-stage-hud"' in stage
assert stage.index('class="stage-hud player-stage-hud"') < stage.index('class="combatant-wrap player-wrap"')
assert "enemy-sprite faces-left" in stage
assert "scaleX(-1)" not in CSS
def test_stage_renders_element_specific_attack_effects():
state = new_game_state(phase="combat")
state["visual_event"] = {
"event_id": 1,
"player_animation": "skill",
"player_element": "Lightning",
"enemy_animation": "idle",
"enemy_variant": "",
"enemy_element": "Physical",
}
stage = render_stage(state, AssetCatalog.load())
assert "attack-fx effect-skill element-lightning" in stage
for element in (
"physical",
"magical",
"fire",
"water",
"ice",
"lightning",
"earth",
"holy",
"demonic",
):
assert f".element-{element}::before" in CSS
def test_sidebar_removes_duplicate_skill_descriptions():
state = new_game_state(phase="combat")
state["active_skills"] = [SKILLS["power_strike"].model_dump()]
state["cooldowns"]["power_strike"] = 2
sidebar = render_sidebar(
state,
AssetCatalog.load(),
{"power_strike": "18 damage"},
)
assert "sidebar-skill" not in sidebar
assert "<h3>Skills</h3>" not in sidebar
assert "Equipment" in sidebar
def test_skill_cards_include_effect_and_cooldown_without_long_description():
state = new_game_state(phase="combat")
skill = SKILLS["power_strike"].model_dump()
state["cooldowns"]["power_strike"] = 2
label = skill_card_label(skill, state, "18 damage")
assert "Power Strike" in label
assert "CD: 2T" in label
assert "18 damage" in label
assert skill["description"] not in label
state["cooldowns"].clear()
assert "CD: 1T" in skill_card_label(skill, state, "18 damage")
shadow = SKILLS["shadow_step"].model_dump()
assert "CD:" not in skill_card_label(shadow, state)
assert "CD: 1T" in skill_card_label(skill, state, active=False)
def test_sidebar_renders_effective_equipment_stats_and_shared_card_tokens():
state = new_game_state(phase="combat")
state["equipment"]["weapon"] = {
"id": "strength_blade",
"name": "Strength Blade",
"slot": "weapon",
"rarity": "Rare",
"asset_id": "weapon_sword_1",
"stat": "strength",
"value": 4,
"element": "Physical",
}
sidebar = render_sidebar(state, AssetCatalog.load(), {})
assert "STR" in sidebar
assert 'class="stat-bonus">(+4)</small>' in sidebar
assert "Max HP" in sidebar
assert "Max AP" in sidebar
assert "AP Regen" in sidebar
assert "--card-bg:#12101a" in CSS
assert "--deck-bg:#12101ae8" in CSS
assert "--card-border:#30283f" in CSS
assert ".action-deck button {" in CSS
assert "background:var(--card-bg) !important" in CSS
assert "background-image:none !important" in CSS
assert "border:1px solid var(--card-border) !important" in CSS
assert "border-radius:0 !important" in CSS
def test_skill_tooltips_explain_cooldown_and_ap_disabling():
state = new_game_state(phase="combat")
state["active_skills"] = [SKILLS["power_strike"].model_dump()]
state["cooldowns"]["power_strike"] = 1
tooltip = _tooltip_data(state, {"power_strike": "18 damage"})
assert "Ready in 1 turn." in tooltip
state["cooldowns"].clear()
state["current_ap"] = 0
tooltip = _tooltip_data(state, {"power_strike": "18 damage"})
assert "Requires 2 AP." in tooltip
def test_tooltip_observer_does_not_watch_its_own_attributes():
assert "childList:true" in JS
assert "attributes:true" not in JS
assert "button.dataset.tip" in JS
assert "#button-tooltip-data [data-target-id]" in JS
assert "tower-tooltip" in JS
assert "requestAnimationFrame" in JS
def test_integrated_command_panel_uses_explicit_compact_rows():
source = Path("tower_game/ui.py").read_text(encoding="utf-8")
assert ".action-deck {" in CSS
assert "position:absolute !important" in CSS
assert "grid-template-columns:repeat(2" in CSS
assert ".skills-deck > .styler" in CSS
assert ".submenu-row" in CSS
assert 'elem_classes=["action-deck", "basic-actions-deck"]' in source
assert 'elem_classes=["action-deck", "skills-deck"]' in source
assert "for row_index in range(3)" in source
assert "for column_index in range(2)" in source
assert "item_toggle.click" not in source
assert "skills_toggle.click" not in source
assert "button.click(item_handler" not in source
assert "submenu-grid" not in source
assert "padding-bottom:clamp" in CSS
def test_player_hud_and_results_layer_above_integrated_commands():
assert ".player-stage-hud { left:3%; bottom:51%; }" in CSS
assert ".stage-overlay" in CSS
assert "z-index:20" in CSS
assert "z-index:8" in CSS
def test_stage_renders_status_icons_enemy_effects_and_accessible_tooltips():
state = new_game_state(phase="combat")
state["player_statuses"] = {"poison": 2, "atkup": 1}
state["enemy_statuses"] = {"defup": 1, "stun": 1}
state["visual_event"] = {
"event_id": 1,
"player_animation": "idle",
"player_element": "Physical",
"enemy_animation": "grunt",
"enemy_variant": "triple-claw",
"enemy_element": "Physical",
}
stage = render_stage(state, AssetCatalog.load())
assert "player-status-rack" in stage
assert "enemy-status-rack" in stage
assert "Poison: Takes damage" in stage
assert "Defense Up: Armor is increased" in stage
assert "status-duration" in stage
assert "enemy-effect-grunt" in stage
assert "enemy-variant-triple-claw" in stage
for variant in (
"silver-slash",
"thrust",
"triple-claw",
"projectile",
"hex-pulse",
"heavy-cleave",
"ground-shockwave",
):
assert f".enemy-variant-{variant}::before" in CSS
def test_stage_uses_cached_static_urls_and_small_payload():
state = new_game_state(phase="combat")
state["current_floor"] = 1
stage = render_stage(state, AssetCatalog.load())
assert "/gradio_api/file=" in stage
assert "data:image" not in stage
assert len(stage.encode("utf-8")) < 20_000
def test_normal_turn_narration_timer_is_removed():
source = Path("tower_game/ui.py").read_text(encoding="utf-8")
assert "gr.Timer" not in source
assert "NARRATION_QUEUE" not in source
assert "update_stage=False" in source
assert "await asyncio.sleep(beat_delay)" in source
assert "3.5" in source and "1.25" in source
def test_stage_narration_is_a_separate_lightweight_overlay():
state = new_game_state(phase="combat")
state["presentation_line"] = "Player: The blade finds its mark."
narration = render_stage_narration(state)
assert "stage-narration" in narration
assert "The blade finds its mark" in narration
assert "battle-frame" not in narration
def test_latest_turn_feed_has_heading_and_three_labelled_events():
state = new_game_state(phase="combat")
state["active_turn"] = 4
state["combat_events"] = [
{"turn": 3, "actor": "System", "text": "older"},
{"turn": 4, "actor": "Player", "text": "You strike."},
{"turn": 4, "actor": "Enemy", "text": "Enemy strikes."},
{"turn": 4, "actor": "Status", "text": "Poison deals damage."},
]
feed = render_latest_log(state)
assert "Turn 4" in feed
assert "older" not in feed
assert "Player" in feed and "Enemy" in feed and "Status" in feed
def test_difficulty_picker_and_percentage_equipment_labels_are_present():
source = Path("tower_game/ui.py").read_text(encoding="utf-8")
assert 'label="Run Difficulty"' in source
assert '("Easy - forgiving combat and lighter defenses", "easy")' in source
assert '("Normal - stronger enemies and bosses", "normal")' in source
state = new_game_state(phase="combat")
state["equipment"]["armor"] = {
"id": "ward",
"name": "Ward",
"slot": "armor",
"rarity": "Rare",
"asset_id": "armor_1",
"bonuses": [
{"stat": "armor", "value": 5},
{"stat": "evasion", "value": 3},
],
}
sidebar = render_sidebar(state, AssetCatalog.load(), {})
assert "+5% Armor" in sidebar
assert "+3% Evasion" in sidebar
def test_skill_scaling_tooltips_and_combat_options_are_present():
source = Path("tower_game/ui.py").read_text(encoding="utf-8")
state = new_game_state(phase="combat")
state["active_skills"] = [SKILLS["power_strike"].model_dump()]
tooltip = _tooltip_data(state, {"power_strike": "18 damage"})
assert "Scales with STR" in tooltip
assert "Cinematic - readable 3.5 second rounds" in source
assert "Fast - current 1.25 second rounds" in source
assert "Improves Strike and STR-scaling technique damage" in source
def test_evolution_hides_raw_dials_until_developer_details():
from tower_game.render import render_evolution
state = new_game_state(phase="evolution_reveal")
state["pending_evolution"] = {
"class_name": "Towerbreaker",
"title": "Stone Answers",
"narrative": "Repeated force becomes law.",
"portrait_id": "player_strength",
"signature_skill_id": "power_strike",
"signature_skill": None,
"dial_changes": [{"dial": "str_scaling_dial", "delta": 0.25}],
}
rendered = render_evolution(state, AssetCatalog.load())
assert "Strength scaling" in rendered
assert "<summary>Developer Details</summary>" in rendered
assert "<code>str_scaling_dial</code>" in rendered