optcg-deck-builder / tests /test_deck.py
t22000t's picture
Initial commit: optcg-deck-builder Gradio Space
16eaadc
"""TDD for spaceutil.deck.build_deck.
Hard invariants the builder must always satisfy:
- Total quantity is exactly 50.
- Every card shares at least one color with the leader.
- No card has more than `max_copies` (default 4).
- The leader itself is never in the main deck.
- No other Leader cards are in the main deck.
Soft invariants (style behaviour):
- Aggro decks have lower average cost than midrange.
- Control decks have higher average cost than midrange.
"""
from __future__ import annotations
import numpy as np
import pytest
def _matrix(cards):
return np.stack(
[np.asarray(c["embedding"], dtype=np.float32) for c in cards], axis=0
)
def _strip_emb(cards):
return [{k: v for k, v in c.items() if k != "embedding"} for c in cards]
def _first_leader_idx(cards):
return next(i for i, c in enumerate(cards) if c["card_type"] == "Leader")
class TestDeckSize:
def test_total_quantity_is_50(self, synthetic_cards):
from spaceutil.deck import build_deck
idx = _first_leader_idx(synthetic_cards)
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
deck = build_deck(idx, cards, matrix, style="midrange")
assert deck.total_quantity == 50
def test_total_quantity_is_50_for_each_style(self, synthetic_cards):
from spaceutil.deck import build_deck
idx = _first_leader_idx(synthetic_cards)
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
for style in ("aggro", "midrange", "control"):
deck = build_deck(idx, cards, matrix, style=style)
assert deck.total_quantity == 50, f"{style} produced {deck.total_quantity}"
class TestColorLegality:
def test_all_cards_share_a_color_with_leader(self, synthetic_cards):
from spaceutil.deck import build_deck
idx = _first_leader_idx(synthetic_cards)
leader_colors = set(synthetic_cards[idx]["colors"])
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
deck = build_deck(idx, cards, matrix, style="midrange")
for dc in deck.cards:
assert set(dc.colors) & leader_colors, (
f"{dc.card_id} has {dc.colors}, leader has {leader_colors}"
)
class TestCopyLimit:
def test_no_card_exceeds_max_copies_default(self, synthetic_cards):
from spaceutil.deck import build_deck
idx = _first_leader_idx(synthetic_cards)
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
deck = build_deck(idx, cards, matrix, style="midrange")
for dc in deck.cards:
assert 1 <= dc.quantity <= 4
def test_no_card_exceeds_max_copies_explicit(self, synthetic_cards):
from spaceutil.deck import build_deck
idx = _first_leader_idx(synthetic_cards)
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
deck = build_deck(idx, cards, matrix, style="midrange", max_copies=2)
assert deck.total_quantity == 50
for dc in deck.cards:
assert 1 <= dc.quantity <= 2
class TestLeaderExclusion:
def test_other_leaders_not_in_deck(self, synthetic_cards):
from spaceutil.deck import build_deck
idx = _first_leader_idx(synthetic_cards)
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
deck = build_deck(idx, cards, matrix, style="midrange")
for dc in deck.cards:
assert dc.card_type != "Leader"
def test_chosen_leader_not_in_main_deck(self, synthetic_cards):
from spaceutil.deck import build_deck
idx = _first_leader_idx(synthetic_cards)
leader_id = synthetic_cards[idx]["id"]
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
deck = build_deck(idx, cards, matrix, style="midrange")
for dc in deck.cards:
assert dc.card_id != leader_id
def test_raises_when_index_is_not_leader(self, synthetic_cards):
from spaceutil.deck import build_deck
non_leader_idx = next(
i for i, c in enumerate(synthetic_cards) if c["card_type"] != "Leader"
)
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
with pytest.raises(ValueError, match="not a Leader"):
build_deck(non_leader_idx, cards, matrix)
class TestDeckMetadata:
def test_deck_carries_leader_reference(self, synthetic_cards):
from spaceutil.deck import build_deck
idx = _first_leader_idx(synthetic_cards)
leader_id = synthetic_cards[idx]["id"]
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
deck = build_deck(idx, cards, matrix, style="midrange")
assert deck.leader["id"] == leader_id
assert deck.style == "midrange"
def test_avg_cost_computed(self, synthetic_cards):
from spaceutil.deck import build_deck
idx = _first_leader_idx(synthetic_cards)
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
deck = build_deck(idx, cards, matrix, style="midrange")
assert deck.avg_cost > 0
# 1-9 cost spread, midrange should land somewhere reasonable
assert 1.5 < deck.avg_cost < 8.0
class TestStyleSensitivity:
def test_aggro_cheaper_than_midrange(self, synthetic_cards):
from spaceutil.deck import build_deck
idx = _first_leader_idx(synthetic_cards)
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
aggro = build_deck(idx, cards, matrix, style="aggro")
midrange = build_deck(idx, cards, matrix, style="midrange")
assert aggro.avg_cost < midrange.avg_cost, (
f"aggro avg {aggro.avg_cost:.2f} not < midrange {midrange.avg_cost:.2f}"
)
def test_control_pricier_than_midrange(self, synthetic_cards):
from spaceutil.deck import build_deck
idx = _first_leader_idx(synthetic_cards)
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
midrange = build_deck(idx, cards, matrix, style="midrange")
control = build_deck(idx, cards, matrix, style="control")
assert control.avg_cost > midrange.avg_cost, (
f"control avg {control.avg_cost:.2f} not > midrange {midrange.avg_cost:.2f}"
)
def test_unknown_style_raises(self, synthetic_cards):
from spaceutil.deck import build_deck
idx = _first_leader_idx(synthetic_cards)
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
with pytest.raises(ValueError, match="style"):
build_deck(idx, cards, matrix, style="bogus")
class TestExport:
def test_to_text_format(self, synthetic_cards):
from spaceutil.deck import build_deck, deck_to_text
idx = _first_leader_idx(synthetic_cards)
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
deck = build_deck(idx, cards, matrix, style="midrange")
text = deck_to_text(deck)
assert deck.leader["id"] in text
assert deck.leader["name"] in text
for dc in deck.cards:
assert f"{dc.quantity}x {dc.card_id}" in text
# Sanity: at least one section header so it's human-readable
assert "Leader" in text and "Main deck" in text
def test_to_text_total_quantity_in_summary(self, synthetic_cards):
from spaceutil.deck import build_deck, deck_to_text
idx = _first_leader_idx(synthetic_cards)
matrix = _matrix(synthetic_cards)
cards = _strip_emb(synthetic_cards)
deck = build_deck(idx, cards, matrix, style="midrange")
text = deck_to_text(deck)
assert "50" in text