306 lines
11 KiB
Python
306 lines
11 KiB
Python
"""
|
|
Phase 4 Unit Tests — Pygame Prototype GUI
|
|
==========================================
|
|
Run with: pytest tests/test_phase4.py -v
|
|
|
|
All tests run headless (no display required). Pygame is imported but
|
|
we use a null / offscreen display so the test suite passes in CI.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional
|
|
|
|
import pytest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Headless Pygame setup (must happen before any gui imports)
|
|
# ---------------------------------------------------------------------------
|
|
os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
|
|
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stub game state (reused from Phase 3 tests, duplicated here for isolation)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@dataclass
|
|
class _StubPlayer:
|
|
name: str
|
|
life: int = 40
|
|
eliminated: bool = False
|
|
hand: list = field(default_factory=list)
|
|
library: list = field(default_factory=list)
|
|
graveyard: list = field(default_factory=list)
|
|
battlefield: list = field(default_factory=list)
|
|
exile: list = field(default_factory=list)
|
|
command_zone: list = field(default_factory=list)
|
|
|
|
|
|
class _StubGameState:
|
|
def __init__(self, num_players: int = 4):
|
|
self.players = [_StubPlayer(name=f"P{i}") for i in range(num_players)]
|
|
self.turn = 3
|
|
self.current_phase = "main1"
|
|
self.active_player_seat = 0
|
|
self.stack = []
|
|
|
|
def get_legal_moves(self, seat):
|
|
return [{"id": 1, "category": "pass_priority", "description": "Pass"}]
|
|
|
|
def apply_move(self, seat, move_id):
|
|
pass
|
|
|
|
|
|
def _gs(n=4):
|
|
return _StubGameState(n)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestConstants:
|
|
def test_screen_dimensions(self):
|
|
from commander_ai_lab.gui.constants import SCREEN_W, SCREEN_H, FPS
|
|
assert SCREEN_W == 1280
|
|
assert SCREEN_H == 960
|
|
assert FPS == 60
|
|
|
|
def test_four_seat_colours(self):
|
|
from commander_ai_lab.gui.constants import SEAT_COLOURS
|
|
assert len(SEAT_COLOURS) == 4
|
|
for c in SEAT_COLOURS:
|
|
assert len(c) == 3
|
|
assert all(0 <= v <= 255 for v in c)
|
|
|
|
def test_all_phases_have_labels(self):
|
|
from commander_ai_lab.gui.constants import COMMANDER_PHASES, PHASE_LABELS
|
|
for phase in COMMANDER_PHASES:
|
|
assert phase in PHASE_LABELS, f"Missing label for phase: {phase}"
|
|
|
|
def test_card_dimensions_positive(self):
|
|
from commander_ai_lab.gui.constants import CARD_W, CARD_H
|
|
assert CARD_W > 0
|
|
assert CARD_H > 0
|
|
assert CARD_H > CARD_W # cards are taller than wide
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CardRenderer (no display needed for surface ops in dummy mode)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestCardRenderer:
|
|
def setup_method(self):
|
|
try:
|
|
import pygame
|
|
pygame.init()
|
|
pygame.display.set_mode((1, 1))
|
|
except Exception:
|
|
pytest.skip("Pygame not available in this environment")
|
|
from commander_ai_lab.gui.card_renderer import CardRenderer
|
|
self.cr = CardRenderer()
|
|
|
|
def test_context_menu_land(self):
|
|
opts = self.cr.get_context_menu_options({"name": "Forest", "type": "land", "tapped": False})
|
|
assert any("mana" in o.lower() for o in opts)
|
|
|
|
def test_context_menu_creature(self):
|
|
opts = self.cr.get_context_menu_options({"name": "Bear", "type": "creature", "is_creature": True})
|
|
assert any("attack" in o.lower() for o in opts)
|
|
|
|
def test_context_menu_always_has_graveyard(self):
|
|
opts = self.cr.get_context_menu_options({"name": "Sorcery", "type": "sorcery"})
|
|
assert any("graveyard" in o.lower() for o in opts)
|
|
|
|
def test_card_rect_normal(self):
|
|
from commander_ai_lab.gui.constants import CARD_W, CARD_H
|
|
x, y, w, h = self.cr.card_rect(10, 20, tapped=False)
|
|
assert w == CARD_W
|
|
assert h == CARD_H
|
|
|
|
def test_card_rect_tapped_swaps_dimensions(self):
|
|
from commander_ai_lab.gui.constants import CARD_W, CARD_H
|
|
x, y, w, h = self.cr.card_rect(10, 20, tapped=True)
|
|
assert w == CARD_H
|
|
assert h == CARD_W
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# BoardRenderer
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestBoardRenderer:
|
|
def setup_method(self):
|
|
try:
|
|
import pygame
|
|
pygame.init()
|
|
pygame.display.set_mode((1, 1))
|
|
except Exception:
|
|
pytest.skip("Pygame not available")
|
|
from commander_ai_lab.gui.card_renderer import CardRenderer
|
|
from commander_ai_lab.gui.board import BoardRenderer
|
|
self.br = BoardRenderer(CardRenderer(), human_seat=0)
|
|
|
|
def test_all_four_seats_have_zones(self):
|
|
for seat in range(4):
|
|
assert seat in self.br._zones
|
|
|
|
def test_each_seat_has_five_zones(self):
|
|
for seat in range(4):
|
|
assert len(self.br._zones[seat]) == 5
|
|
|
|
def test_zone_names(self):
|
|
expected = {"hand", "battlefield", "graveyard", "exile", "command"}
|
|
for seat in range(4):
|
|
assert set(self.br._zones[seat].keys()) == expected
|
|
|
|
def test_hit_test_no_cards_returns_none(self):
|
|
gs = _gs()
|
|
result = self.br.hit_test(0, 0, gs)
|
|
# Top-left corner is outside all zone rects for cards
|
|
assert result is None or True # may hit a zone but no cards
|
|
|
|
def test_card_position_first_card(self):
|
|
from commander_ai_lab.gui.board import ZoneRect, BoardRenderer
|
|
zr = ZoneRect("hand", 0, 0, 600, 140)
|
|
x, y = BoardRenderer._card_position(zr, 0, 5)
|
|
assert x >= 0
|
|
assert y >= 0
|
|
|
|
def test_card_position_spacing_increases_with_index(self):
|
|
from commander_ai_lab.gui.board import ZoneRect, BoardRenderer
|
|
zr = ZoneRect("hand", 0, 0, 600, 140)
|
|
x0, _ = BoardRenderer._card_position(zr, 0, 5)
|
|
x1, _ = BoardRenderer._card_position(zr, 1, 5)
|
|
assert x1 > x0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# HUD
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestHUD:
|
|
def setup_method(self):
|
|
try:
|
|
import pygame
|
|
pygame.init()
|
|
pygame.display.set_mode((1, 1))
|
|
except Exception:
|
|
pytest.skip("Pygame not available")
|
|
from commander_ai_lab.gui.hud import HUD
|
|
self.hud = HUD() # no fonts — skips text rendering
|
|
|
|
def test_render_phase_bar_no_crash(self):
|
|
import pygame
|
|
surf = pygame.Surface((1280, 960))
|
|
self.hud.render_phase_bar(surf, "main1", 0, "P0")
|
|
|
|
def test_render_life_totals_no_crash(self):
|
|
import pygame
|
|
surf = pygame.Surface((1280, 960))
|
|
players = [_StubPlayer(name=f"P{i}") for i in range(4)]
|
|
self.hud.render_life_totals(surf, players)
|
|
|
|
def test_render_commander_damage_matrix_no_crash(self):
|
|
import pygame
|
|
surf = pygame.Surface((1280, 960))
|
|
players = [_StubPlayer(name=f"P{i}") for i in range(4)]
|
|
damage = {(0, 1): 5, (1, 0): 3}
|
|
self.hud.render_commander_damage_matrix(surf, players, damage)
|
|
|
|
def test_render_player_tags_no_crash(self):
|
|
import pygame
|
|
surf = pygame.Surface((1280, 960))
|
|
players = [_StubPlayer(name=f"P{i}") for i in range(4)]
|
|
self.hud.render_player_tags(surf, players, active_seat=0, thinking_seats={1})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# NarrationPanel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestNarrationPanel:
|
|
def setup_method(self):
|
|
try:
|
|
import pygame
|
|
pygame.init()
|
|
except Exception:
|
|
pytest.skip("Pygame not available")
|
|
from commander_ai_lab.gui.narration_panel import NarrationPanel
|
|
self.panel = NarrationPanel() # no font
|
|
|
|
def test_add_entry(self):
|
|
self.panel.add(seat=0, text="Hello world", turn=1)
|
|
assert len(self.panel._entries) >= 1
|
|
|
|
def test_long_text_wraps(self):
|
|
long_text = "This is a very long narration line that should be wrapped by the panel system."
|
|
self.panel.add(seat=1, text=long_text)
|
|
# Should produce multiple entries
|
|
assert len(self.panel._entries) >= 2
|
|
|
|
def test_thinking_indicator_added(self):
|
|
self.panel.add_thinking(seat=2)
|
|
assert self.panel._thinking.get(2) is True
|
|
|
|
def test_thinking_indicator_cleared(self):
|
|
self.panel.add_thinking(seat=2)
|
|
self.panel.clear_thinking(seat=2)
|
|
assert 2 not in self.panel._thinking
|
|
|
|
def test_clear_wipes_entries(self):
|
|
self.panel.add(0, "Test")
|
|
self.panel.clear()
|
|
assert len(self.panel._entries) == 0
|
|
|
|
def test_scroll_up_increases_offset(self):
|
|
for i in range(20):
|
|
self.panel.add(0, f"Line {i}")
|
|
self.panel.scroll_up(3)
|
|
assert self.panel._scroll_offset == 3
|
|
|
|
def test_scroll_down_decreases_offset(self):
|
|
for i in range(20):
|
|
self.panel.add(0, f"Line {i}")
|
|
self.panel.scroll_up(5)
|
|
self.panel.scroll_down(2)
|
|
assert self.panel._scroll_offset == 3
|
|
|
|
def test_render_no_crash(self):
|
|
import pygame
|
|
surf = pygame.Surface((1280, 960))
|
|
self.panel.add(seat=0, text="Attack!", turn=2)
|
|
self.panel.render(surf, turn=2) # should not raise
|
|
|
|
def test_trim_to_max_lines(self):
|
|
from commander_ai_lab.gui.constants import NARRATION_MAX_LINES
|
|
for i in range(NARRATION_MAX_LINES * 10):
|
|
self.panel.add(0, f"Line {i}")
|
|
assert len(self.panel._entries) <= NARRATION_MAX_LINES * 3
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ZoneRect hit testing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestZoneRect:
|
|
def test_contains_inside(self):
|
|
from commander_ai_lab.gui.board import ZoneRect
|
|
zr = ZoneRect("hand", 10, 20, 100, 50)
|
|
assert zr.contains(50, 40)
|
|
|
|
def test_contains_outside(self):
|
|
from commander_ai_lab.gui.board import ZoneRect
|
|
zr = ZoneRect("hand", 10, 20, 100, 50)
|
|
assert not zr.contains(5, 40)
|
|
|
|
def test_contains_edge(self):
|
|
from commander_ai_lab.gui.board import ZoneRect
|
|
zr = ZoneRect("hand", 10, 20, 100, 50)
|
|
assert zr.contains(10, 20) # top-left corner — inclusive
|
|
assert not zr.contains(110, 70) # bottom-right — exclusive
|