feat(tests): add pytest infrastructure and test coverage for models, rules, and sim engine (closes #28)
This commit is contained in:
1 parent
8c89119ba1
commit
812e5b34ca
6 files changed
+575
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,75 @@
|
|||||||
|
"""Shared pytest fixtures for Commander AI Lab tests."""
|
||||||
|
import pytest
|
||||||
|
from commander_ai_lab.sim.models import Card, Player, PlayerStats, SimState
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def basic_creature():
|
||||||
|
return Card(name="Grizzly Bears", type_line="Creature — Bear", cmc=2, pt="2/2", power="2", toughness="2")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def flying_creature():
|
||||||
|
return Card(
|
||||||
|
name="Serra Angel",
|
||||||
|
type_line="Creature — Angel",
|
||||||
|
cmc=5,
|
||||||
|
pt="4/4",
|
||||||
|
power="4",
|
||||||
|
toughness="4",
|
||||||
|
oracle_text="Flying, vigilance",
|
||||||
|
keywords=["Flying", "Vigilance"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def basic_land():
|
||||||
|
return Card(name="Forest", type_line="Basic Land — Forest", cmc=0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sol_ring():
|
||||||
|
return Card(name="Sol Ring", type_line="Artifact", cmc=1, oracle_text="{T}: Add {C}{C}", is_ramp=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def removal_spell():
|
||||||
|
return Card(name="Murder", type_line="Instant", cmc=3, oracle_text="Destroy target creature.", is_removal=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def board_wipe():
|
||||||
|
return Card(name="Wrath of God", type_line="Sorcery", cmc=4, oracle_text="Destroy all creatures.", is_board_wipe=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def commander_card():
|
||||||
|
return Card(
|
||||||
|
name="Korvold, Fae-Cursed King",
|
||||||
|
type_line="Legendary Creature — Dragon Noble",
|
||||||
|
cmc=5,
|
||||||
|
pt="4/4",
|
||||||
|
power="4",
|
||||||
|
toughness="4",
|
||||||
|
is_commander=True,
|
||||||
|
oracle_text="Flying, haste\nWhenever Korvold, Fae-Cursed King enters the battlefield or attacks, sacrifice another permanent.",
|
||||||
|
keywords=["Flying", "Haste"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_player(basic_creature, basic_land, sol_ring):
|
||||||
|
p = Player(name="Alice", life=40, owner_id=0)
|
||||||
|
p.library = [basic_creature.clone(), basic_land.clone(), sol_ring.clone()] * 20
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def two_player_state(sample_player):
|
||||||
|
import copy
|
||||||
|
p2 = copy.deepcopy(sample_player)
|
||||||
|
p2.name = "Bob"
|
||||||
|
p2.owner_id = 1
|
||||||
|
state = SimState(players=[sample_player, p2])
|
||||||
|
state.init_battlefields(2)
|
||||||
|
return state
|
||||||
Whitespace-only changes.
@@ -0,0 +1,97 @@
|
|||||||
|
"""Seeded simulation tests for the game engine."""
|
||||||
|
import random
|
||||||
|
import pytest
|
||||||
|
from commander_ai_lab.sim.models import Card, Player, PlayerStats
|
||||||
|
from commander_ai_lab.sim.rules import enrich_card
|
||||||
|
from commander_ai_lab.sim.engine import GameEngine
|
||||||
|
|
||||||
|
|
||||||
|
def _make_deck(seed: int, size: int = 60) -> list[Card]:
|
||||||
|
"""Build a deterministic test deck using a seeded RNG."""
|
||||||
|
rng = random.Random(seed)
|
||||||
|
card_pool = [
|
||||||
|
Card(name="Forest"),
|
||||||
|
Card(name="Sol Ring"),
|
||||||
|
Card(name="Cultivate"),
|
||||||
|
Card(name="Murder"),
|
||||||
|
Card(name="Wrath of God"),
|
||||||
|
Card(name="Grizzly Bears"),
|
||||||
|
Card(name="Serra Angel"),
|
||||||
|
]
|
||||||
|
for c in card_pool:
|
||||||
|
enrich_card(c)
|
||||||
|
deck = []
|
||||||
|
for i in range(size):
|
||||||
|
deck.append(card_pool[i % len(card_pool)].clone())
|
||||||
|
rng.shuffle(deck)
|
||||||
|
return deck
|
||||||
|
|
||||||
|
|
||||||
|
class TestGameEngineTwoPlayer:
|
||||||
|
def test_game_completes(self):
|
||||||
|
"""A 2-player seeded game should finish without error."""
|
||||||
|
engine = GameEngine(seed=42)
|
||||||
|
deck_a = _make_deck(seed=1)
|
||||||
|
deck_b = _make_deck(seed=2)
|
||||||
|
result = engine.run_game(
|
||||||
|
deck_a=[c.clone() for c in deck_a],
|
||||||
|
deck_b=[c.clone() for c in deck_b],
|
||||||
|
player_a_name="Alice",
|
||||||
|
player_b_name="Bob",
|
||||||
|
)
|
||||||
|
assert result is not None
|
||||||
|
assert result.turns > 0
|
||||||
|
|
||||||
|
def test_result_has_winner_or_draw(self):
|
||||||
|
"""winner_seat must be 0, 1, or -1 (draw)."""
|
||||||
|
engine = GameEngine(seed=99)
|
||||||
|
deck = _make_deck(seed=5)
|
||||||
|
result = engine.run_game(
|
||||||
|
deck_a=[c.clone() for c in deck],
|
||||||
|
deck_b=[c.clone() for c in deck],
|
||||||
|
player_a_name="P1",
|
||||||
|
player_b_name="P2",
|
||||||
|
)
|
||||||
|
assert result.winner_seat in (-1, 0, 1)
|
||||||
|
|
||||||
|
def test_seeded_game_is_deterministic(self):
|
||||||
|
"""Same seed must produce identical results."""
|
||||||
|
deck_a = _make_deck(seed=7)
|
||||||
|
deck_b = _make_deck(seed=8)
|
||||||
|
|
||||||
|
engine1 = GameEngine(seed=42)
|
||||||
|
result1 = engine1.run_game(
|
||||||
|
deck_a=[c.clone() for c in deck_a],
|
||||||
|
deck_b=[c.clone() for c in deck_b],
|
||||||
|
player_a_name="Alice",
|
||||||
|
player_b_name="Bob",
|
||||||
|
)
|
||||||
|
|
||||||
|
engine2 = GameEngine(seed=42)
|
||||||
|
result2 = engine2.run_game(
|
||||||
|
deck_a=[c.clone() for c in deck_a],
|
||||||
|
deck_b=[c.clone() for c in deck_b],
|
||||||
|
player_a_name="Alice",
|
||||||
|
player_b_name="Bob",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result1.winner_seat == result2.winner_seat
|
||||||
|
assert result1.turns == result2.turns
|
||||||
|
|
||||||
|
def test_to_dict_serializable(self):
|
||||||
|
"""Game result must serialize to a dict with expected keys."""
|
||||||
|
import json
|
||||||
|
engine = GameEngine(seed=77)
|
||||||
|
deck = _make_deck(seed=3)
|
||||||
|
result = engine.run_game(
|
||||||
|
deck_a=[c.clone() for c in deck],
|
||||||
|
deck_b=[c.clone() for c in deck],
|
||||||
|
player_a_name="A",
|
||||||
|
player_b_name="B",
|
||||||
|
)
|
||||||
|
d = result.to_dict()
|
||||||
|
assert "winner" in d
|
||||||
|
assert "turns" in d
|
||||||
|
assert "playerResults" in d
|
||||||
|
# Ensure it's JSON-serializable
|
||||||
|
json.dumps(d)
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
"""Tests for commander_ai_lab.sim.models."""
|
||||||
|
import pytest
|
||||||
|
from commander_ai_lab.sim.models import (
|
||||||
|
Card, Player, PlayerStats, PlayerResult, GameResult, SimState
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Card ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class TestCardGetPower:
|
||||||
|
def test_from_power_field(self):
|
||||||
|
c = Card(name="Test", power="3", toughness="2", pt="3/2")
|
||||||
|
assert c.get_power() == 3
|
||||||
|
|
||||||
|
def test_from_pt_field(self):
|
||||||
|
c = Card(name="Test", pt="4/5")
|
||||||
|
assert c.get_power() == 4
|
||||||
|
|
||||||
|
def test_no_pt_returns_zero(self):
|
||||||
|
c = Card(name="Test")
|
||||||
|
assert c.get_power() == 0
|
||||||
|
|
||||||
|
def test_non_numeric_returns_zero(self):
|
||||||
|
c = Card(name="Test", power="*", pt="*/2")
|
||||||
|
assert c.get_power() == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestCardGetToughness:
|
||||||
|
def test_from_toughness_field(self):
|
||||||
|
c = Card(name="Test", power="2", toughness="5", pt="2/5")
|
||||||
|
assert c.get_toughness() == 5
|
||||||
|
|
||||||
|
def test_from_pt_field(self):
|
||||||
|
c = Card(name="Test", pt="2/7")
|
||||||
|
assert c.get_toughness() == 7
|
||||||
|
|
||||||
|
def test_no_pt_returns_zero(self):
|
||||||
|
c = Card(name="Test")
|
||||||
|
assert c.get_toughness() == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestCardHasKeyword:
|
||||||
|
def test_in_keywords_list(self):
|
||||||
|
c = Card(name="Test", keywords=["Flying", "Haste"])
|
||||||
|
assert c.has_keyword("flying") is True
|
||||||
|
assert c.has_keyword("haste") is True
|
||||||
|
|
||||||
|
def test_in_oracle_text(self):
|
||||||
|
c = Card(name="Test", oracle_text="This creature has flying and deathtouch.")
|
||||||
|
assert c.has_keyword("flying") is True
|
||||||
|
assert c.has_keyword("deathtouch") is True
|
||||||
|
|
||||||
|
def test_missing_keyword(self):
|
||||||
|
c = Card(name="Test", oracle_text="Draw a card.", keywords=[])
|
||||||
|
assert c.has_keyword("flying") is False
|
||||||
|
|
||||||
|
def test_case_insensitive(self):
|
||||||
|
c = Card(name="Test", keywords=["FLYING"])
|
||||||
|
assert c.has_keyword("Flying") is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestCardIsLand:
|
||||||
|
def test_basic_land(self):
|
||||||
|
c = Card(name="Forest", type_line="Basic Land — Forest")
|
||||||
|
assert c.is_land() is True
|
||||||
|
|
||||||
|
def test_nonbasic_land(self):
|
||||||
|
c = Card(name="Command Tower", type_line="Land")
|
||||||
|
assert c.is_land() is True
|
||||||
|
|
||||||
|
def test_creature_not_land(self):
|
||||||
|
c = Card(name="Grizzly Bears", type_line="Creature — Bear")
|
||||||
|
assert c.is_land() is False
|
||||||
|
|
||||||
|
def test_no_type_line(self):
|
||||||
|
c = Card(name="Unknown")
|
||||||
|
assert c.is_land() is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestCardIsCreature:
|
||||||
|
def test_creature(self):
|
||||||
|
c = Card(name="Grizzly Bears", type_line="Creature — Bear")
|
||||||
|
assert c.is_creature() is True
|
||||||
|
|
||||||
|
def test_legendary_creature(self):
|
||||||
|
c = Card(name="Korvold", type_line="Legendary Creature — Dragon Noble")
|
||||||
|
assert c.is_creature() is True
|
||||||
|
|
||||||
|
def test_instant_not_creature(self):
|
||||||
|
c = Card(name="Murder", type_line="Instant")
|
||||||
|
assert c.is_creature() is False
|
||||||
|
|
||||||
|
def test_land_not_creature(self):
|
||||||
|
c = Card(name="Forest", type_line="Basic Land — Forest")
|
||||||
|
assert c.is_creature() is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestCardClone:
|
||||||
|
def test_clone_is_equal(self, basic_creature):
|
||||||
|
clone = basic_creature.clone()
|
||||||
|
assert clone.name == basic_creature.name
|
||||||
|
assert clone.cmc == basic_creature.cmc
|
||||||
|
|
||||||
|
def test_clone_is_independent(self, basic_creature):
|
||||||
|
clone = basic_creature.clone()
|
||||||
|
clone.name = "Changed"
|
||||||
|
assert basic_creature.name == "Grizzly Bears"
|
||||||
|
|
||||||
|
|
||||||
|
# ── PlayerStats ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
class TestPlayerStats:
|
||||||
|
def test_to_dict_keys(self):
|
||||||
|
stats = PlayerStats(cards_drawn=7, damage_dealt=10)
|
||||||
|
d = stats.to_dict()
|
||||||
|
assert "cardsDrawn" in d
|
||||||
|
assert "damageDealt" in d
|
||||||
|
assert d["cardsDrawn"] == 7
|
||||||
|
assert d["damageDealt"] == 10
|
||||||
|
|
||||||
|
def test_default_values(self):
|
||||||
|
stats = PlayerStats()
|
||||||
|
assert stats.cards_drawn == 7
|
||||||
|
assert stats.lands_played == 0
|
||||||
|
|
||||||
|
|
||||||
|
# ── PlayerResult & GameResult ────────────────────────────────
|
||||||
|
|
||||||
|
class TestPlayerResult:
|
||||||
|
def test_to_dict_winner(self):
|
||||||
|
pr = PlayerResult(seat_index=0, name="Alice", life=40, finish_position=1)
|
||||||
|
d = pr.to_dict()
|
||||||
|
assert d["isWinner"] is True
|
||||||
|
assert d["seatIndex"] == 0
|
||||||
|
assert d["name"] == "Alice"
|
||||||
|
|
||||||
|
def test_to_dict_loser(self):
|
||||||
|
pr = PlayerResult(seat_index=1, name="Bob", life=0, eliminated=True, finish_position=2)
|
||||||
|
d = pr.to_dict()
|
||||||
|
assert d["isWinner"] is False
|
||||||
|
assert d["eliminated"] is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestGameResult:
|
||||||
|
def _make_result(self):
|
||||||
|
p0 = PlayerResult(seat_index=0, name="Alice", life=35, finish_position=1, stats=PlayerStats(damage_dealt=20))
|
||||||
|
p1 = PlayerResult(seat_index=1, name="Bob", life=0, eliminated=True, finish_position=2, stats=PlayerStats())
|
||||||
|
return GameResult(winner_seat=0, turns=12, players=[p0, p1])
|
||||||
|
|
||||||
|
def test_winner_property(self):
|
||||||
|
gr = self._make_result()
|
||||||
|
assert gr.winner == 0
|
||||||
|
|
||||||
|
def test_player_a_name(self):
|
||||||
|
gr = self._make_result()
|
||||||
|
assert gr.player_a_name == "Alice"
|
||||||
|
|
||||||
|
def test_player_b_name(self):
|
||||||
|
gr = self._make_result()
|
||||||
|
assert gr.player_b_name == "Bob"
|
||||||
|
|
||||||
|
def test_player_a_eliminated(self):
|
||||||
|
gr = self._make_result()
|
||||||
|
assert gr.player_a_eliminated is False
|
||||||
|
|
||||||
|
def test_player_b_eliminated(self):
|
||||||
|
gr = self._make_result()
|
||||||
|
assert gr.player_b_eliminated is True
|
||||||
|
|
||||||
|
def test_to_dict_has_legacy_keys(self):
|
||||||
|
gr = self._make_result()
|
||||||
|
d = gr.to_dict()
|
||||||
|
assert "winner" in d
|
||||||
|
assert "turns" in d
|
||||||
|
assert "playerResults" in d
|
||||||
|
assert len(d["playerResults"]) == 2
|
||||||
|
|
||||||
|
|
||||||
|
# ── SimState ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class TestSimState:
|
||||||
|
def test_init_battlefields(self):
|
||||||
|
state = SimState()
|
||||||
|
state.init_battlefields(4)
|
||||||
|
assert len(state.battlefields) == 4
|
||||||
|
|
||||||
|
def test_add_and_get_battlefield(self):
|
||||||
|
state = SimState()
|
||||||
|
c = Card(name="Test", id=1)
|
||||||
|
state.add_to_battlefield(0, c)
|
||||||
|
bf = state.get_battlefield(0)
|
||||||
|
assert len(bf) == 1
|
||||||
|
assert bf[0].name == "Test"
|
||||||
|
|
||||||
|
def test_remove_from_battlefield(self):
|
||||||
|
state = SimState()
|
||||||
|
c = Card(name="Test", id=42)
|
||||||
|
state.add_to_battlefield(0, c)
|
||||||
|
removed = state.remove_from_battlefield(42)
|
||||||
|
assert removed is not None
|
||||||
|
assert removed.name == "Test"
|
||||||
|
assert len(state.get_battlefield(0)) == 0
|
||||||
|
|
||||||
|
def test_remove_nonexistent_returns_none(self):
|
||||||
|
state = SimState()
|
||||||
|
assert state.remove_from_battlefield(999) is None
|
||||||
|
|
||||||
|
def test_all_battlefield_cards(self):
|
||||||
|
state = SimState()
|
||||||
|
state.add_to_battlefield(0, Card(name="A", id=1))
|
||||||
|
state.add_to_battlefield(1, Card(name="B", id=2))
|
||||||
|
state.add_to_battlefield(1, Card(name="C", id=3))
|
||||||
|
all_cards = state.all_battlefield_cards()
|
||||||
|
assert len(all_cards) == 3
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
"""Tests for commander_ai_lab.sim.rules."""
|
||||||
|
import pytest
|
||||||
|
from commander_ai_lab.sim.models import Card
|
||||||
|
from commander_ai_lab.sim.rules import enrich_card, score_card, parse_decklist, AI_DEFAULT_WEIGHTS
|
||||||
|
|
||||||
|
|
||||||
|
# ── enrich_card ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
class TestEnrichCardBasicLands:
|
||||||
|
@pytest.mark.parametrize("name,expected_color", [
|
||||||
|
("Forest", "G"),
|
||||||
|
("Island", "U"),
|
||||||
|
("Plains", "W"),
|
||||||
|
("Swamp", "B"),
|
||||||
|
("Mountain", "R"),
|
||||||
|
])
|
||||||
|
def test_basic_land_color(self, name, expected_color):
|
||||||
|
c = Card(name=name)
|
||||||
|
enrich_card(c)
|
||||||
|
assert c.is_land() is True
|
||||||
|
assert expected_color in c.oracle_text
|
||||||
|
assert c.cmc == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnrichCardKnownCards:
|
||||||
|
def test_sol_ring(self):
|
||||||
|
c = Card(name="Sol Ring")
|
||||||
|
enrich_card(c)
|
||||||
|
assert c.is_ramp is True
|
||||||
|
assert c.cmc == 1
|
||||||
|
|
||||||
|
def test_murder(self):
|
||||||
|
c = Card(name="Murder")
|
||||||
|
enrich_card(c)
|
||||||
|
assert c.is_removal is True
|
||||||
|
assert c.cmc == 3
|
||||||
|
|
||||||
|
def test_wrath_of_god(self):
|
||||||
|
c = Card(name="Wrath of God")
|
||||||
|
enrich_card(c)
|
||||||
|
assert c.is_board_wipe is True
|
||||||
|
|
||||||
|
def test_cultivate(self):
|
||||||
|
c = Card(name="Cultivate")
|
||||||
|
enrich_card(c)
|
||||||
|
assert c.is_ramp is True
|
||||||
|
assert c.cmc == 3
|
||||||
|
|
||||||
|
def test_counterspell(self):
|
||||||
|
c = Card(name="Counterspell")
|
||||||
|
enrich_card(c)
|
||||||
|
assert c.is_removal is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnrichCardDeterministic:
|
||||||
|
def test_same_unknown_card_same_cmc(self):
|
||||||
|
"""Unknown cards must get deterministic CMC (no random jitter)."""
|
||||||
|
c1 = Card(name="Unknown Foobar Card")
|
||||||
|
c2 = Card(name="Unknown Foobar Card")
|
||||||
|
enrich_card(c1)
|
||||||
|
enrich_card(c2)
|
||||||
|
assert c1.cmc == c2.cmc
|
||||||
|
assert c1.pt == c2.pt
|
||||||
|
|
||||||
|
def test_different_unknown_cards_may_differ(self):
|
||||||
|
c1 = Card(name="Aaaaa")
|
||||||
|
c2 = Card(name="Zzzzz")
|
||||||
|
enrich_card(c1)
|
||||||
|
enrich_card(c2)
|
||||||
|
# At minimum they are both valid creatures with cmc in [2,6]
|
||||||
|
assert 2 <= c1.cmc <= 6
|
||||||
|
assert 2 <= c2.cmc <= 6
|
||||||
|
|
||||||
|
def test_unknown_card_cmc_range(self):
|
||||||
|
for name in ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"]:
|
||||||
|
c = Card(name=name)
|
||||||
|
enrich_card(c)
|
||||||
|
assert 2 <= c.cmc <= 6
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnrichCardPreservesExistingData:
|
||||||
|
def test_real_data_not_overwritten(self):
|
||||||
|
c = Card(name="Custom Card", type_line="Legendary Creature", cmc=7, pt="6/6")
|
||||||
|
enrich_card(c)
|
||||||
|
assert c.cmc == 7
|
||||||
|
assert c.pt == "6/6"
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnrichCardOracleFlags:
|
||||||
|
def test_removal_flag_from_oracle(self):
|
||||||
|
c = Card(name="Custom Removal", type_line="Instant", cmc=2,
|
||||||
|
oracle_text="Destroy target creature.")
|
||||||
|
enrich_card(c)
|
||||||
|
assert c.is_removal is True
|
||||||
|
|
||||||
|
def test_board_wipe_flag_from_oracle(self):
|
||||||
|
c = Card(name="Custom Wipe", type_line="Sorcery", cmc=5,
|
||||||
|
oracle_text="Destroy all creatures.")
|
||||||
|
enrich_card(c)
|
||||||
|
assert c.is_board_wipe is True
|
||||||
|
|
||||||
|
def test_ramp_flag_from_oracle(self):
|
||||||
|
c = Card(name="Custom Ramp", type_line="Sorcery", cmc=2,
|
||||||
|
oracle_text="Search your library for a basic land and put it onto the battlefield.")
|
||||||
|
enrich_card(c)
|
||||||
|
assert c.is_ramp is True
|
||||||
|
|
||||||
|
|
||||||
|
# ── score_card ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
class TestScoreCard:
|
||||||
|
def test_land_scores_lower_than_creature(self):
|
||||||
|
land = Card(name="Forest", type_line="Basic Land — Forest", cmc=0)
|
||||||
|
creature = Card(name="Grizzly Bears", type_line="Creature — Bear", cmc=2, pt="2/2")
|
||||||
|
enrich_card(land)
|
||||||
|
enrich_card(creature)
|
||||||
|
# Creature with pt bonus should score higher than a plain land
|
||||||
|
assert score_card(creature) > score_card(land)
|
||||||
|
|
||||||
|
def test_flying_bonus(self):
|
||||||
|
base = Card(name="Vanilla", type_line="Creature", cmc=3, pt="3/3")
|
||||||
|
flyer = Card(name="Flyer", type_line="Creature", cmc=3, pt="3/3",
|
||||||
|
oracle_text="Flying", keywords=["Flying"])
|
||||||
|
assert score_card(flyer) > score_card(base)
|
||||||
|
|
||||||
|
def test_ramp_bonus(self):
|
||||||
|
ramp = Card(name="Sol Ring", type_line="Artifact", cmc=1, is_ramp=True)
|
||||||
|
plain = Card(name="Mox Nothing", type_line="Artifact", cmc=1)
|
||||||
|
assert score_card(ramp) > score_card(plain)
|
||||||
|
|
||||||
|
def test_removal_bonus(self):
|
||||||
|
removal = Card(name="Murder", type_line="Instant", cmc=3, is_removal=True,
|
||||||
|
oracle_text="Destroy target creature.")
|
||||||
|
plain = Card(name="Shock", type_line="Instant", cmc=1)
|
||||||
|
assert score_card(removal) > score_card(plain)
|
||||||
|
|
||||||
|
def test_multiplayer_scales_flying(self):
|
||||||
|
flyer = Card(name="Flyer", type_line="Creature", cmc=3, pt="3/3",
|
||||||
|
oracle_text="Flying", keywords=["Flying"])
|
||||||
|
score_1v1 = score_card(flyer, num_opponents=1)
|
||||||
|
score_4p = score_card(flyer, num_opponents=3)
|
||||||
|
assert score_4p > score_1v1
|
||||||
|
|
||||||
|
def test_defender_penalty(self):
|
||||||
|
defender = Card(name="Wall", type_line="Creature", cmc=2, pt="0/5",
|
||||||
|
oracle_text="Defender", keywords=["Defender"])
|
||||||
|
vanilla = Card(name="Vanilla", type_line="Creature", cmc=2, pt="2/2")
|
||||||
|
assert score_card(defender) < score_card(vanilla)
|
||||||
|
|
||||||
|
def test_high_cmc_penalized(self):
|
||||||
|
cheap = Card(name="Cheap", type_line="Creature", cmc=1, pt="1/1")
|
||||||
|
expensive = Card(name="Expensive", type_line="Creature", cmc=8, pt="1/1")
|
||||||
|
assert score_card(cheap) > score_card(expensive)
|
||||||
|
|
||||||
|
|
||||||
|
# ── parse_decklist ───────────────────────────────────────────
|
||||||
|
|
||||||
|
class TestParseDecklist:
|
||||||
|
def test_numbered_entries(self):
|
||||||
|
text = "4 Lightning Bolt\n2 Counterspell\n1 Black Lotus"
|
||||||
|
cards = parse_decklist(text)
|
||||||
|
assert len(cards) == 7
|
||||||
|
assert cards[0].name == "Lightning Bolt"
|
||||||
|
|
||||||
|
def test_unnumbered_entries(self):
|
||||||
|
cards = parse_decklist("Lightning Bolt")
|
||||||
|
assert len(cards) == 1
|
||||||
|
assert cards[0].name == "Lightning Bolt"
|
||||||
|
|
||||||
|
def test_skips_comments(self):
|
||||||
|
text = "// This is a comment\n1 Sol Ring\n# Also a comment\n1 Forest"
|
||||||
|
cards = parse_decklist(text)
|
||||||
|
assert len(cards) == 2
|
||||||
|
|
||||||
|
def test_skips_blank_lines(self):
|
||||||
|
cards = parse_decklist("1 Sol Ring\n\n\n1 Forest")
|
||||||
|
assert len(cards) == 2
|
||||||
|
|
||||||
|
def test_returns_card_objects(self):
|
||||||
|
cards = parse_decklist("1 Sol Ring")
|
||||||
|
from commander_ai_lab.sim.models import Card
|
||||||
|
assert isinstance(cards[0], Card)
|
||||||
|
|
||||||
|
def test_empty_input(self):
|
||||||
|
assert parse_decklist("") == []
|
||||||
|
|
||||||
|
def test_multi_word_card_names(self):
|
||||||
|
cards = parse_decklist("1 Kodama's Reach")
|
||||||
|
assert cards[0].name == "Kodama's Reach"
|
||||||
Reference in new issue
Block a user