mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2026-04-05 20:57:16 +02:00
Some checks are pending
CI / build (push) Waiting to run
* test-cleanup: fix 21 failures, prune stale tests, consolidate fragmented files * test-cleanup: remove permanently-skipped M4/perf tests, fix pydantic ConfigDict warning * docs: update changelog and release notes for test-cleanup changes * ci: fix editorial governance workflow stale test file reference
118 lines
5.7 KiB
Python
118 lines
5.7 KiB
Python
import os
|
|
from code.web.services import sampling
|
|
from code.web.services import card_index
|
|
|
|
|
|
def setup_module(module): # ensure deterministic env weights
|
|
os.environ.setdefault("RARITY_W_MYTHIC", "1.2")
|
|
|
|
|
|
def test_rarity_diminishing():
|
|
# Monkeypatch internal index
|
|
card_index._CARD_INDEX.clear()
|
|
theme = "Test Theme"
|
|
card_index._CARD_INDEX[theme] = [
|
|
{"name": "Mythic One", "tags": [theme], "color_identity": "G", "mana_cost": "G", "rarity": "mythic"},
|
|
{"name": "Mythic Two", "tags": [theme], "color_identity": "G", "mana_cost": "G", "rarity": "mythic"},
|
|
]
|
|
def no_build():
|
|
return None
|
|
sampling.maybe_build_index = no_build
|
|
cards = sampling.sample_real_cards_for_theme(theme, 2, None, synergies=[theme], commander=None)
|
|
rarity_weights = [r for c in cards for r in c["reasons"] if r.startswith("rarity_weight_calibrated")]
|
|
assert len(rarity_weights) >= 2
|
|
v1 = float(rarity_weights[0].split(":")[-1])
|
|
v2 = float(rarity_weights[1].split(":")[-1])
|
|
assert v1 > v2 # diminishing returns
|
|
|
|
|
|
def test_commander_overlap_monotonic_diminishing():
|
|
cmd_tags = {"A","B","C","D"}
|
|
synergy_set = {"A","B","C","D","E"}
|
|
# Build artificial card tag lists with increasing overlaps
|
|
bonus1 = sampling.commander_overlap_scale(cmd_tags, ["A"], synergy_set)
|
|
bonus2 = sampling.commander_overlap_scale(cmd_tags, ["A","B"], synergy_set)
|
|
bonus3 = sampling.commander_overlap_scale(cmd_tags, ["A","B","C"], synergy_set)
|
|
assert 0 < bonus1 < bonus2 < bonus3
|
|
# Diminishing increments: delta shrinks
|
|
assert (bonus2 - bonus1) > 0
|
|
assert (bonus3 - bonus2) < (bonus2 - bonus1)
|
|
|
|
|
|
def test_splash_off_color_penalty_applied():
|
|
card_index._CARD_INDEX.clear()
|
|
theme = "Splash Theme"
|
|
# Commander W U B R (4 colors)
|
|
commander = {"name": "CommanderTest", "tags": [theme], "color_identity": "WUBR", "mana_cost": "", "rarity": "mythic"}
|
|
# Card with single off-color G (W U B R G)
|
|
splash_card = {"name": "CardSplash", "tags": [theme], "color_identity": "WUBRG", "mana_cost": "G", "rarity": "rare"}
|
|
card_index._CARD_INDEX[theme] = [commander, splash_card]
|
|
sampling.maybe_build_index = lambda: None
|
|
cards = sampling.sample_real_cards_for_theme(theme, 2, None, synergies=[theme], commander="CommanderTest")
|
|
splash = next((c for c in cards if c["name"] == "CardSplash"), None)
|
|
assert splash is not None
|
|
assert any(r.startswith("splash_off_color_penalty") for r in splash["reasons"])
|
|
|
|
|
|
def test_role_saturation_penalty_applies(monkeypatch):
|
|
cards = []
|
|
for i in range(30):
|
|
cards.append({"name": f"Payoff{i}", "color_identity": "G", "tags": ["testtheme"], "mana_cost": "1G", "rarity": "common", "color_identity_list": ["G"], "pip_colors": ["G"]})
|
|
monkeypatch.setattr("code.web.services.sampling.get_tag_pool", lambda tag: cards)
|
|
monkeypatch.setattr("code.web.services.sampling.maybe_build_index", lambda: None)
|
|
monkeypatch.setattr("code.web.services.sampling.lookup_commander", lambda name: None)
|
|
chosen = sampling.sample_real_cards_for_theme(theme="testtheme", limit=12, colors_filter=None, synergies=["testtheme"], commander=None)
|
|
penalized = [c for c in chosen if any(r.startswith("role_saturation_penalty") for r in c.get("reasons", []))]
|
|
assert penalized, "Expected at least one card to receive role_saturation_penalty"
|
|
|
|
|
|
def test_adaptive_splash_penalty_scaling(monkeypatch):
|
|
theme = "__AdaptiveSplashTest__"
|
|
commander_name = "Test Commander"
|
|
commander_tags = [theme, "Value", "ETB"]
|
|
commander_entry = {
|
|
"name": commander_name,
|
|
"color_identity": "WUBR",
|
|
"tags": commander_tags,
|
|
"mana_cost": "WUBR",
|
|
"rarity": "mythic",
|
|
"color_identity_list": list("WUBR"),
|
|
"pip_colors": list("WUBR"),
|
|
}
|
|
pool = [commander_entry]
|
|
|
|
def add_card(name: str, color_identity: str, tags: list):
|
|
pool.append({
|
|
"name": name,
|
|
"color_identity": color_identity,
|
|
"tags": tags,
|
|
"mana_cost": "1G",
|
|
"rarity": "uncommon",
|
|
"color_identity_list": list(color_identity),
|
|
"pip_colors": [c for c in "1G" if c in {"W", "U", "B", "R", "G"}],
|
|
})
|
|
|
|
add_card("On Color Card", "WUB", [theme, "ETB"])
|
|
add_card("Splash Card", "WUBG", [theme, "ETB", "Synergy"])
|
|
|
|
from code.web.services import card_index as ci
|
|
monkeypatch.setattr(ci, "lookup_commander", lambda name: commander_entry if name == commander_name else None)
|
|
monkeypatch.setattr(ci, "maybe_build_index", lambda: None)
|
|
monkeypatch.setattr(ci, "get_tag_pool", lambda tag: pool if tag == theme else [])
|
|
monkeypatch.setattr(sampling, "maybe_build_index", lambda: None)
|
|
monkeypatch.setattr(sampling, "get_tag_pool", lambda tag: pool if tag == theme else [])
|
|
monkeypatch.setattr(sampling, "lookup_commander", lambda name: commander_entry if name == commander_name else None)
|
|
monkeypatch.setattr(sampling, "SPLASH_ADAPTIVE_ENABLED", True)
|
|
monkeypatch.setenv("SPLASH_ADAPTIVE", "1")
|
|
monkeypatch.setenv("SPLASH_ADAPTIVE_SCALE", "1:1.0,2:1.0,3:1.0,4:0.5,5:0.25")
|
|
|
|
cards = sampling.sample_real_cards_for_theme(theme, 10, None, synergies=[theme, "ETB", "Synergy"], commander=commander_name)
|
|
by_name = {c["name"]: c for c in cards}
|
|
assert "Splash Card" in by_name, cards
|
|
splash_reasons = [r for r in by_name["Splash Card"]["reasons"] if r.startswith("splash_off_color_penalty")]
|
|
assert splash_reasons, by_name["Splash Card"]["reasons"]
|
|
adaptive_reason = next(r for r in splash_reasons if r.startswith("splash_off_color_penalty_adaptive"))
|
|
parts = adaptive_reason.split(":")
|
|
assert parts[1] == "4"
|
|
penalty_value = float(parts[2])
|
|
assert abs(penalty_value - (-0.3 * 0.5)) < 1e-6
|