Spaces:
Sleeping
Sleeping
File size: 7,879 Bytes
16eaadc | 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 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | """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
|