mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 23:50:12 +01:00
324 lines
No EOL
11 KiB
Python
324 lines
No EOL
11 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from types import SimpleNamespace
|
|
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
from deck_builder.combined_commander import PartnerMode
|
|
from deck_builder.partner_selection import apply_partner_inputs
|
|
from exceptions import CommanderPartnerError
|
|
|
|
|
|
class _StubBuilder:
|
|
def __init__(self, dataframe: pd.DataFrame) -> None:
|
|
self._df = dataframe
|
|
|
|
def load_commander_data(self) -> pd.DataFrame:
|
|
return self._df.copy(deep=True)
|
|
|
|
|
|
@pytest.fixture()
|
|
def builder() -> _StubBuilder:
|
|
data = [
|
|
{
|
|
"name": "Halana, Kessig Ranger",
|
|
"faceName": "Halana, Kessig Ranger",
|
|
"colorIdentity": ["G"],
|
|
"themeTags": ["Aggro"],
|
|
"text": "Reach\nPartner (You can have two commanders if both have partner.)",
|
|
"type": "Legendary Creature — Human Archer",
|
|
},
|
|
{
|
|
"name": "Alena, Kessig Trapper",
|
|
"faceName": "Alena, Kessig Trapper",
|
|
"colorIdentity": ["R"],
|
|
"themeTags": ["Aggro"],
|
|
"text": "First strike\nPartner",
|
|
"type": "Legendary Creature — Human Scout",
|
|
},
|
|
{
|
|
"name": "Lae'zel, Vlaakith's Champion",
|
|
"faceName": "Lae'zel, Vlaakith's Champion",
|
|
"colorIdentity": ["W"],
|
|
"themeTags": ["Counters"],
|
|
"text": "If you would put one or more counters on a creature... Choose a Background (You can have a Background as a second commander.)",
|
|
"type": "Legendary Creature — Gith Warrior",
|
|
},
|
|
{
|
|
"name": "Commander A",
|
|
"faceName": "Commander A",
|
|
"colorIdentity": ["W"],
|
|
"themeTags": ["Value"],
|
|
"text": "Partner with Commander B (When this creature enters the battlefield, target player may put Commander B into their hand from their library, then shuffle.)",
|
|
"type": "Legendary Creature — Advisor",
|
|
},
|
|
{
|
|
"name": "Commander B",
|
|
"faceName": "Commander B",
|
|
"colorIdentity": ["B"],
|
|
"themeTags": ["Graveyard"],
|
|
"text": "Partner with Commander A",
|
|
"type": "Legendary Creature — Advisor",
|
|
},
|
|
{
|
|
"name": "The Tenth Doctor",
|
|
"faceName": "The Tenth Doctor",
|
|
"colorIdentity": ["U", "R"],
|
|
"themeTags": ["Time", "Doctor"],
|
|
"text": "Whenever you cast a spell with cascade, put a time counter on target permanent",
|
|
"type": "Legendary Creature — Time Lord Doctor",
|
|
},
|
|
{
|
|
"name": "Donna Noble",
|
|
"faceName": "Donna Noble",
|
|
"colorIdentity": ["W"],
|
|
"themeTags": ["Support"],
|
|
"text": "Vigilance\nDoctor's companion (You can have two commanders if the other is a Doctor.)",
|
|
"type": "Legendary Creature — Human Advisor",
|
|
},
|
|
{
|
|
"name": "Amy Pond",
|
|
"faceName": "Amy Pond",
|
|
"colorIdentity": ["R"],
|
|
"themeTags": ["Aggro", "Doctor's Companion", "Partner With"],
|
|
"text": (
|
|
"Partner with Rory Williams\\nWhenever Amy Pond deals combat damage to a player, "
|
|
"choose a suspended card you own and remove that many time counters from it.\\n"
|
|
"Doctor's companion (You can have two commanders if the other is the Doctor.)"
|
|
),
|
|
"type": "Legendary Creature — Human",
|
|
},
|
|
{
|
|
"name": "Rory Williams",
|
|
"faceName": "Rory Williams",
|
|
"colorIdentity": ["W", "U"],
|
|
"themeTags": ["Human", "Doctor's Companion", "Partner With"],
|
|
"text": (
|
|
"Partner with Amy Pond\\nFirst strike, lifelink\\n"
|
|
"Doctor's companion (You can have two commanders if the other is a Doctor.)"
|
|
),
|
|
"type": "Legendary Creature — Human Soldier",
|
|
},
|
|
]
|
|
df = pd.DataFrame(data)
|
|
return _StubBuilder(df)
|
|
|
|
|
|
def _background_catalog() -> SimpleNamespace:
|
|
card = SimpleNamespace(
|
|
name="Scion of Halaster",
|
|
display_name="Scion of Halaster",
|
|
color_identity=("B",),
|
|
themes=("Backgrounds Matter",),
|
|
theme_tags=("Backgrounds Matter",),
|
|
oracle_text="Commander creatures you own have menace.",
|
|
type_line="Legendary Enchantment — Background",
|
|
is_background=True,
|
|
)
|
|
|
|
class _Catalog:
|
|
def __init__(self, entry: SimpleNamespace) -> None:
|
|
self._entry = entry
|
|
self.entries = (entry,)
|
|
|
|
def get(self, name: str) -> SimpleNamespace | None:
|
|
lowered = name.strip().casefold()
|
|
if lowered in {
|
|
self._entry.name.casefold(),
|
|
self._entry.display_name.casefold(),
|
|
}:
|
|
return self._entry
|
|
return None
|
|
|
|
return _Catalog(card)
|
|
|
|
|
|
def test_feature_disabled_returns_none(builder: _StubBuilder) -> None:
|
|
result = apply_partner_inputs(
|
|
builder,
|
|
primary_name="Halana, Kessig Ranger",
|
|
secondary_name="Alena, Kessig Trapper",
|
|
feature_enabled=False,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
assert result is None
|
|
|
|
|
|
def test_conflicting_inputs_raise_error(builder: _StubBuilder) -> None:
|
|
with pytest.raises(CommanderPartnerError):
|
|
apply_partner_inputs(
|
|
builder,
|
|
primary_name="Halana, Kessig Ranger",
|
|
secondary_name="Alena, Kessig Trapper",
|
|
background_name="Scion of Halaster",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
|
|
|
|
def test_background_requires_primary_support(builder: _StubBuilder) -> None:
|
|
with pytest.raises(CommanderPartnerError):
|
|
apply_partner_inputs(
|
|
builder,
|
|
primary_name="Halana, Kessig Ranger",
|
|
background_name="Scion of Halaster",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
|
|
|
|
def test_background_success(builder: _StubBuilder) -> None:
|
|
combined = apply_partner_inputs(
|
|
builder,
|
|
primary_name="Lae'zel, Vlaakith's Champion",
|
|
background_name="Scion of Halaster",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
assert combined is not None
|
|
assert combined.partner_mode is PartnerMode.BACKGROUND
|
|
assert combined.secondary_name == "Scion of Halaster"
|
|
assert combined.color_identity == ("W", "B")
|
|
|
|
|
|
def test_partner_with_detection(builder: _StubBuilder) -> None:
|
|
combined = apply_partner_inputs(
|
|
builder,
|
|
primary_name="Commander A",
|
|
secondary_name="Commander B",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
assert combined is not None
|
|
assert combined.partner_mode is PartnerMode.PARTNER_WITH
|
|
assert combined.color_identity == ("W", "B")
|
|
|
|
|
|
def test_partner_detection(builder: _StubBuilder) -> None:
|
|
combined = apply_partner_inputs(
|
|
builder,
|
|
primary_name="Halana, Kessig Ranger",
|
|
secondary_name="Alena, Kessig Trapper",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
assert combined is not None
|
|
assert combined.partner_mode is PartnerMode.PARTNER
|
|
assert combined.color_identity == ("R", "G")
|
|
|
|
|
|
def test_doctor_companion_pairing(builder: _StubBuilder) -> None:
|
|
combined = apply_partner_inputs(
|
|
builder,
|
|
primary_name="The Tenth Doctor",
|
|
secondary_name="Donna Noble",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
assert combined is not None
|
|
assert combined.partner_mode is PartnerMode.DOCTOR_COMPANION
|
|
assert combined.secondary_name == "Donna Noble"
|
|
assert combined.color_identity == ("W", "U", "R")
|
|
|
|
|
|
def test_doctor_requires_companion(builder: _StubBuilder) -> None:
|
|
with pytest.raises(CommanderPartnerError):
|
|
apply_partner_inputs(
|
|
builder,
|
|
primary_name="The Tenth Doctor",
|
|
secondary_name="Halana, Kessig Ranger",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
|
|
|
|
def test_companion_requires_doctor(builder: _StubBuilder) -> None:
|
|
with pytest.raises(CommanderPartnerError):
|
|
apply_partner_inputs(
|
|
builder,
|
|
primary_name="Donna Noble",
|
|
secondary_name="Commander A",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
|
|
|
|
def test_amy_prefers_partner_with_when_rory_selected(builder: _StubBuilder) -> None:
|
|
combined = apply_partner_inputs(
|
|
builder,
|
|
primary_name="Amy Pond",
|
|
secondary_name="Rory Williams",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
|
|
assert combined is not None
|
|
assert combined.partner_mode is PartnerMode.PARTNER_WITH
|
|
|
|
|
|
def test_amy_can_pair_with_the_doctor(builder: _StubBuilder) -> None:
|
|
combined = apply_partner_inputs(
|
|
builder,
|
|
primary_name="Amy Pond",
|
|
secondary_name="The Tenth Doctor",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
|
|
assert combined is not None
|
|
assert combined.partner_mode is PartnerMode.DOCTOR_COMPANION
|
|
|
|
|
|
def test_rory_can_partner_with_amy(builder: _StubBuilder) -> None:
|
|
combined = apply_partner_inputs(
|
|
builder,
|
|
primary_name="Rory Williams",
|
|
secondary_name="Amy Pond",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
|
|
assert combined is not None
|
|
assert combined.partner_mode is PartnerMode.PARTNER_WITH
|
|
|
|
|
|
def test_logging_emits_partner_mode_selected(caplog: pytest.LogCaptureFixture, builder: _StubBuilder) -> None:
|
|
with caplog.at_level(logging.INFO):
|
|
combined = apply_partner_inputs(
|
|
builder,
|
|
primary_name="Halana, Kessig Ranger",
|
|
secondary_name="Alena, Kessig Trapper",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
)
|
|
|
|
assert combined is not None
|
|
records = [record for record in caplog.records if getattr(record, "event", "") == "partner_mode_selected"]
|
|
assert records, "Expected partner_mode_selected log event"
|
|
payload = getattr(records[-1], "payload", {})
|
|
assert payload.get("mode") == PartnerMode.PARTNER.value
|
|
assert payload.get("commanders", {}).get("primary") == "Halana, Kessig Ranger"
|
|
assert payload.get("commanders", {}).get("secondary") == "Alena, Kessig Trapper"
|
|
assert payload.get("colors_before") == ["G"]
|
|
assert payload.get("colors_after") == ["R", "G"]
|
|
assert payload.get("color_delta", {}).get("added") == ["R"]
|
|
|
|
|
|
def test_logging_includes_selection_source(caplog: pytest.LogCaptureFixture, builder: _StubBuilder) -> None:
|
|
with caplog.at_level(logging.INFO):
|
|
combined = apply_partner_inputs(
|
|
builder,
|
|
primary_name="Halana, Kessig Ranger",
|
|
secondary_name="Alena, Kessig Trapper",
|
|
feature_enabled=True,
|
|
background_catalog=_background_catalog(),
|
|
selection_source="suggestion",
|
|
)
|
|
|
|
assert combined is not None
|
|
records = [record for record in caplog.records if getattr(record, "event", "") == "partner_mode_selected"]
|
|
assert records, "Expected partner_mode_selected log event"
|
|
payload = getattr(records[-1], "payload", {})
|
|
assert payload.get("selection_source") == "suggestion" |