Add centralized deal_damage() method to GameEngine that handles life reduction, stat tracking, commander damage (both per-seat and per-card), and elimination checks. Route both burn spells and combat damage through this unified path, building on the dual-dict commander damage model merged in PR #89. - Add is_direct_damage / direct_damage_amount fields to Card - Add burn spell detection via known-card table + oracle regex fallback - Route burn spells in _play_spells() through deal_damage() - Refactor _resolve_combat() to route per-attacker damage through deal_damage() - Add 38 regression tests covering deal_damage(), burn detection, spell routing, combat routing, and full-game integration Co-authored-by: Claude Code <noreply@anthropic.com>
540 lines
22 KiB
Python
540 lines
22 KiB
Python
"""Regression tests for direct-damage spell routing (Issue #86).
|
|
|
|
Tests that burn spells and similar direct-damage effects are routed
|
|
through the centralized deal_damage() path, correctly update
|
|
life totals / stats, and integrate with commander damage tracking.
|
|
|
|
Adapted to work with the dual-dict commander damage model from PR #89:
|
|
- commander_damage_received[seat] = per-seat aggregate
|
|
- commander_damage_by_card[(seat, card_name)] = per-card breakdown
|
|
"""
|
|
import pytest
|
|
from commander_ai_lab.sim.models import Card, Player, PlayerStats, SimState
|
|
from commander_ai_lab.sim.rules import enrich_card
|
|
from commander_ai_lab.sim.engine import GameEngine
|
|
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────
|
|
|
|
def _make_creature(name="Bear", pt="2/2", cmc=2, oracle_text="", **kwargs) -> Card:
|
|
"""Build a creature Card with optional oracle text."""
|
|
c = Card(
|
|
name=name,
|
|
type_line="Creature",
|
|
cmc=cmc,
|
|
pt=pt,
|
|
power=pt.split("/")[0],
|
|
toughness=pt.split("/")[1],
|
|
oracle_text=oracle_text,
|
|
**kwargs,
|
|
)
|
|
enrich_card(c)
|
|
return c
|
|
|
|
|
|
def _make_land(name="Forest") -> Card:
|
|
c = Card(name=name)
|
|
enrich_card(c)
|
|
return c
|
|
|
|
|
|
def _make_burn(name="Lightning Bolt", cmc=1, oracle_text="Lightning Bolt deals 3 damage to any target.") -> Card:
|
|
c = Card(name=name, type_line="Instant", cmc=cmc, oracle_text=oracle_text)
|
|
enrich_card(c)
|
|
return c
|
|
|
|
|
|
def _setup_two_player(
|
|
p0_hand=None, p1_hand=None, p0_life=40, p1_life=40,
|
|
p0_battlefield=None, p1_battlefield=None,
|
|
) -> tuple[GameEngine, SimState]:
|
|
"""Set up a 2-player SimState with controlled hands/battlefields."""
|
|
engine = GameEngine(max_turns=5)
|
|
sim = SimState(max_turns=5)
|
|
for i, (hand, life, bf) in enumerate([
|
|
(p0_hand or [], p0_life, p0_battlefield or []),
|
|
(p1_hand or [], p1_life, p1_battlefield or []),
|
|
]):
|
|
p = Player(
|
|
name=f"Player {i}",
|
|
life=life,
|
|
owner_id=i,
|
|
hand=list(hand),
|
|
library=[_make_land() for _ in range(30)],
|
|
stats=PlayerStats(cards_drawn=7),
|
|
)
|
|
sim.players.append(p)
|
|
sim.init_battlefields(2)
|
|
for seat, bf_cards in enumerate([p0_battlefield or [], p1_battlefield or []]):
|
|
for c in bf_cards:
|
|
c.owner_id = seat
|
|
c.id = sim.next_card_id
|
|
sim.next_card_id += 1
|
|
c.turn_played = -1
|
|
sim.add_to_battlefield(seat, c)
|
|
return engine, sim
|
|
|
|
|
|
def _setup_four_player(
|
|
hands=None, lives=None, battlefields=None,
|
|
) -> tuple[GameEngine, SimState]:
|
|
"""Set up a 4-player SimState."""
|
|
engine = GameEngine(max_turns=5)
|
|
sim = SimState(max_turns=5)
|
|
hands = hands or [[] for _ in range(4)]
|
|
lives = lives or [40, 40, 40, 40]
|
|
battlefields = battlefields or [[] for _ in range(4)]
|
|
for i in range(4):
|
|
p = Player(
|
|
name=f"Player {i}",
|
|
life=lives[i],
|
|
owner_id=i,
|
|
hand=list(hands[i]),
|
|
library=[_make_land() for _ in range(30)],
|
|
stats=PlayerStats(cards_drawn=7),
|
|
)
|
|
sim.players.append(p)
|
|
sim.init_battlefields(4)
|
|
for seat, bf_cards in enumerate(battlefields):
|
|
for c in bf_cards:
|
|
c.owner_id = seat
|
|
c.id = sim.next_card_id
|
|
sim.next_card_id += 1
|
|
c.turn_played = -1
|
|
sim.add_to_battlefield(seat, c)
|
|
return engine, sim
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# deal_damage() centralized method tests
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
class TestDealDamage:
|
|
"""Tests for GameEngine.deal_damage() static method."""
|
|
|
|
def test_basic_damage_reduces_life(self):
|
|
engine, sim = _setup_two_player(p1_life=20)
|
|
dealt = engine.deal_damage(sim, 5, target_seat=1, source_seat=0)
|
|
assert dealt == 5
|
|
assert sim.players[1].life == 15
|
|
|
|
def test_zero_damage_is_noop(self):
|
|
engine, sim = _setup_two_player(p1_life=20)
|
|
dealt = engine.deal_damage(sim, 0, target_seat=1)
|
|
assert dealt == 0
|
|
assert sim.players[1].life == 20
|
|
|
|
def test_negative_damage_is_noop(self):
|
|
engine, sim = _setup_two_player(p1_life=20)
|
|
dealt = engine.deal_damage(sim, -3, target_seat=1)
|
|
assert dealt == 0
|
|
assert sim.players[1].life == 20
|
|
|
|
def test_damage_updates_stats(self):
|
|
engine, sim = _setup_two_player()
|
|
engine.deal_damage(sim, 7, target_seat=1, source_seat=0)
|
|
assert sim.players[0].stats.damage_dealt == 7
|
|
assert sim.players[1].stats.damage_received == 7
|
|
|
|
def test_damage_eliminates_at_zero(self):
|
|
engine, sim = _setup_two_player(p1_life=3)
|
|
engine.deal_damage(sim, 3, target_seat=1, source_seat=0)
|
|
assert sim.players[1].life == 0
|
|
assert sim.players[1].eliminated is True
|
|
|
|
def test_damage_eliminates_below_zero(self):
|
|
engine, sim = _setup_two_player(p1_life=2)
|
|
engine.deal_damage(sim, 5, target_seat=1, source_seat=0)
|
|
assert sim.players[1].life == -3
|
|
assert sim.players[1].eliminated is True
|
|
|
|
def test_damage_to_eliminated_player_is_noop(self):
|
|
engine, sim = _setup_two_player(p1_life=0)
|
|
sim.players[1].eliminated = True
|
|
dealt = engine.deal_damage(sim, 10, target_seat=1)
|
|
assert dealt == 0
|
|
assert sim.players[1].life == 0
|
|
|
|
def test_events_logged(self):
|
|
engine, sim = _setup_two_player(p1_life=20)
|
|
events: list[str] = []
|
|
engine.deal_damage(sim, 3, target_seat=1, source_seat=0,
|
|
events=events, label="Bolt deals 3")
|
|
assert len(events) == 1
|
|
assert "Bolt deals 3" in events[0]
|
|
assert "Player 1 now at 17 life" in events[0]
|
|
|
|
def test_no_label_means_no_event(self):
|
|
engine, sim = _setup_two_player(p1_life=20)
|
|
events: list[str] = []
|
|
engine.deal_damage(sim, 3, target_seat=1, events=events)
|
|
assert len(events) == 0
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# Commander damage tracking through deal_damage()
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
class TestCommanderDamageTracking:
|
|
"""Tests for commander damage tracked by deal_damage() using
|
|
the dual-dict model: per-seat aggregate + per-card breakdown."""
|
|
|
|
def test_commander_damage_tracked_both_dicts(self):
|
|
engine, sim = _setup_two_player()
|
|
cmd = _make_creature("Atraxa", pt="4/4", is_commander=True)
|
|
engine.deal_damage(sim, 4, target_seat=1, source_card=cmd, source_seat=0, is_combat=True)
|
|
# Per-seat aggregate
|
|
assert sim.players[1].commander_damage_received[0] == 4
|
|
# Per-card breakdown
|
|
assert sim.players[1].commander_damage_by_card[(0, "Atraxa")] == 4
|
|
|
|
def test_commander_damage_accumulates(self):
|
|
engine, sim = _setup_two_player()
|
|
cmd = _make_creature("Atraxa", pt="4/4", is_commander=True)
|
|
engine.deal_damage(sim, 4, target_seat=1, source_card=cmd, source_seat=0)
|
|
engine.deal_damage(sim, 4, target_seat=1, source_card=cmd, source_seat=0)
|
|
assert sim.players[1].commander_damage_received[0] == 8
|
|
assert sim.players[1].commander_damage_by_card[(0, "Atraxa")] == 8
|
|
|
|
def test_commander_21_damage_eliminates(self):
|
|
engine, sim = _setup_two_player()
|
|
cmd = _make_creature("Atraxa", pt="7/7", is_commander=True)
|
|
for _ in range(3):
|
|
engine.deal_damage(sim, 7, target_seat=1, source_card=cmd, source_seat=0)
|
|
assert sim.players[1].commander_damage_by_card[(0, "Atraxa")] == 21
|
|
assert sim.players[1].eliminated is True
|
|
|
|
def test_partner_commanders_tracked_separately(self):
|
|
engine, sim = _setup_two_player()
|
|
cmd_a = _make_creature("Thrasios", pt="1/3", is_commander=True)
|
|
cmd_b = _make_creature("Tymna", pt="2/2", is_commander=True)
|
|
# 15 from Thrasios, 15 from Tymna — neither hits 21 alone
|
|
for _ in range(15):
|
|
engine.deal_damage(sim, 1, target_seat=1, source_card=cmd_a, source_seat=0)
|
|
for _ in range(15):
|
|
engine.deal_damage(sim, 1, target_seat=1, source_card=cmd_b, source_seat=0)
|
|
# Per-card: each under 21
|
|
assert sim.players[1].commander_damage_by_card[(0, "Thrasios")] == 15
|
|
assert sim.players[1].commander_damage_by_card[(0, "Tymna")] == 15
|
|
# Per-seat aggregate: 30 total
|
|
assert sim.players[1].commander_damage_received[0] == 30
|
|
# Life is 10, but not eliminated by commander damage (no single source >= 21)
|
|
assert sim.players[1].life == 10
|
|
assert sim.players[1].is_dead_to_commander_damage() is False
|
|
|
|
def test_non_commander_damage_not_tracked(self):
|
|
engine, sim = _setup_two_player()
|
|
regular = _make_creature("Bear", pt="2/2")
|
|
engine.deal_damage(sim, 2, target_seat=1, source_card=regular, source_seat=0)
|
|
assert sim.players[1].commander_damage_received == {}
|
|
assert sim.players[1].commander_damage_by_card == {}
|
|
|
|
def test_is_dead_to_commander_damage(self):
|
|
p = Player(name="Test", life=40)
|
|
assert p.is_dead_to_commander_damage() is False
|
|
p.commander_damage_by_card[(0, "Kozilek")] = 20
|
|
p.commander_damage_received[0] = 20
|
|
assert p.is_dead_to_commander_damage() is False
|
|
p.commander_damage_by_card[(0, "Kozilek")] = 21
|
|
p.commander_damage_received[0] = 21
|
|
assert p.is_dead_to_commander_damage() is True
|
|
|
|
def test_commander_damage_eliminates_despite_high_life(self):
|
|
"""A player at 100 life should still die to 21 commander damage."""
|
|
engine, sim = _setup_two_player(p1_life=100)
|
|
cmd = _make_creature("Voltron", pt="21/21", is_commander=True)
|
|
engine.deal_damage(sim, 21, target_seat=1, source_card=cmd, source_seat=0)
|
|
assert sim.players[1].life == 79
|
|
assert sim.players[1].eliminated is True
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# Direct-damage spell detection (rules.py)
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
class TestDirectDamageDetection:
|
|
"""Tests for is_direct_damage detection in enrich_card/rules."""
|
|
|
|
def test_lightning_bolt_detected(self):
|
|
c = Card(name="Lightning Bolt", type_line="Instant", cmc=1,
|
|
oracle_text="Lightning Bolt deals 3 damage to any target.")
|
|
enrich_card(c)
|
|
assert c.is_direct_damage is True
|
|
assert c.direct_damage_amount == 3
|
|
|
|
def test_shock_detected(self):
|
|
c = Card(name="Shock", type_line="Instant", cmc=1,
|
|
oracle_text="Shock deals 2 damage to any target.")
|
|
enrich_card(c)
|
|
assert c.is_direct_damage is True
|
|
assert c.direct_damage_amount == 2
|
|
|
|
def test_known_burn_by_name(self):
|
|
"""Known burn spells should be detected even without oracle text."""
|
|
c = Card(name="Lightning Bolt")
|
|
enrich_card(c)
|
|
assert c.is_direct_damage is True
|
|
assert c.direct_damage_amount == 3
|
|
|
|
def test_chain_lightning_detected(self):
|
|
c = Card(name="Chain Lightning")
|
|
enrich_card(c)
|
|
assert c.is_direct_damage is True
|
|
assert c.direct_damage_amount == 3
|
|
|
|
def test_boros_charm_detected(self):
|
|
c = Card(name="Boros Charm")
|
|
enrich_card(c)
|
|
assert c.is_direct_damage is True
|
|
assert c.direct_damage_amount == 4
|
|
|
|
def test_oracle_text_detection(self):
|
|
"""An unknown card with direct-damage oracle text should be detected."""
|
|
c = Card(
|
|
name="Custom Burn Spell",
|
|
type_line="Instant",
|
|
cmc=2,
|
|
oracle_text="Custom Burn Spell deals 5 damage to target creature or player.",
|
|
)
|
|
enrich_card(c)
|
|
assert c.is_direct_damage is True
|
|
assert c.direct_damage_amount == 5
|
|
|
|
def test_board_wipe_not_flagged(self):
|
|
"""Board wipes should not be flagged as direct damage."""
|
|
c = Card(name="Blasphemous Act", type_line="Sorcery", cmc=9,
|
|
oracle_text="Blasphemous Act deals 13 damage to each creature.")
|
|
enrich_card(c)
|
|
assert c.is_board_wipe is True
|
|
assert c.is_direct_damage is False
|
|
|
|
def test_creature_not_flagged(self):
|
|
"""A vanilla creature should not be flagged as direct damage."""
|
|
c = _make_creature("Grizzly Bears", pt="2/2")
|
|
assert c.is_direct_damage is False
|
|
|
|
def test_removal_spell_not_flagged_as_direct_damage(self):
|
|
"""Pure removal (destroy target) should not become direct damage."""
|
|
c = Card(name="Murder", type_line="Instant", cmc=3,
|
|
oracle_text="Destroy target creature.")
|
|
enrich_card(c)
|
|
assert c.is_removal is True
|
|
assert c.is_direct_damage is False
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# Direct-damage routing through _play_spells()
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
class TestDirectDamageSpellRouting:
|
|
"""Tests that burn spells resolve through deal_damage() during gameplay."""
|
|
|
|
def test_burn_spell_reduces_opponent_life(self):
|
|
bolt = _make_burn("Lightning Bolt")
|
|
lands = [_make_land() for _ in range(3)]
|
|
engine, sim = _setup_two_player(
|
|
p0_hand=[bolt],
|
|
p0_battlefield=lands,
|
|
p1_life=20,
|
|
)
|
|
events: list[str] = []
|
|
engine._play_spells(sim, 0, available_mana=3, events=events)
|
|
# Lightning Bolt: 3 damage
|
|
assert sim.players[1].life == 17
|
|
assert sim.players[0].stats.damage_dealt == 3
|
|
assert sim.players[1].stats.damage_received == 3
|
|
|
|
def test_burn_spell_goes_to_graveyard(self):
|
|
bolt = _make_burn("Lightning Bolt")
|
|
lands = [_make_land() for _ in range(3)]
|
|
engine, sim = _setup_two_player(
|
|
p0_hand=[bolt],
|
|
p0_battlefield=lands,
|
|
)
|
|
engine._play_spells(sim, 0, available_mana=3)
|
|
# Bolt should be in graveyard
|
|
assert any(c.name == "Lightning Bolt" for c in sim.players[0].graveyard)
|
|
|
|
def test_burn_spell_eliminates_low_life_opponent(self):
|
|
bolt = _make_burn("Lightning Bolt")
|
|
lands = [_make_land() for _ in range(3)]
|
|
engine, sim = _setup_two_player(
|
|
p0_hand=[bolt],
|
|
p0_battlefield=lands,
|
|
p1_life=2,
|
|
)
|
|
engine._play_spells(sim, 0, available_mana=3)
|
|
assert sim.players[1].life == -1
|
|
assert sim.players[1].eliminated is True
|
|
|
|
def test_burn_targets_weakest_opponent(self):
|
|
"""In multiplayer, burn should target the weakest opponent."""
|
|
bolt = _make_burn("Lightning Bolt")
|
|
lands = [_make_land() for _ in range(3)]
|
|
engine, sim = _setup_four_player(
|
|
hands=[[bolt], [], [], []],
|
|
lives=[40, 30, 10, 20],
|
|
battlefields=[lands, [], [], []],
|
|
)
|
|
engine._play_spells(sim, 0, available_mana=3)
|
|
# Player 2 (life=10) is weakest, should be targeted
|
|
assert sim.players[2].life == 7
|
|
|
|
def test_burn_events_logged(self):
|
|
bolt = _make_burn("Lightning Bolt")
|
|
lands = [_make_land() for _ in range(3)]
|
|
engine, sim = _setup_two_player(
|
|
p0_hand=[bolt],
|
|
p0_battlefield=lands,
|
|
p1_life=20,
|
|
)
|
|
events: list[str] = []
|
|
engine._play_spells(sim, 0, available_mana=3, events=events)
|
|
burn_events = [e for e in events if "Lightning Bolt" in e and "damage" in e]
|
|
assert len(burn_events) >= 1
|
|
|
|
def test_burn_spell_taps_lands(self):
|
|
bolt = _make_burn("Lightning Bolt")
|
|
lands = [_make_land() for _ in range(3)]
|
|
engine, sim = _setup_two_player(
|
|
p0_hand=[bolt],
|
|
p0_battlefield=lands,
|
|
)
|
|
engine._play_spells(sim, 0, available_mana=3)
|
|
tapped_lands = [c for c in sim.get_battlefield(0) if c.is_land() and c.tapped]
|
|
assert len(tapped_lands) == 1 # CMC 1
|
|
|
|
def test_shock_deals_2(self):
|
|
shock = _make_burn("Shock", cmc=1, oracle_text="Shock deals 2 damage to any target.")
|
|
lands = [_make_land() for _ in range(3)]
|
|
engine, sim = _setup_two_player(
|
|
p0_hand=[shock],
|
|
p0_battlefield=lands,
|
|
p1_life=20,
|
|
)
|
|
engine._play_spells(sim, 0, available_mana=3)
|
|
assert sim.players[1].life == 18
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# Combat damage routing through deal_damage()
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
class TestCombatDamageRouting:
|
|
"""Tests that combat damage routes through deal_damage()."""
|
|
|
|
def test_unblocked_creature_deals_damage(self):
|
|
bear = _make_creature("Bear", pt="2/2")
|
|
lands = [_make_land() for _ in range(5)]
|
|
engine, sim = _setup_two_player(
|
|
p0_battlefield=[bear] + lands,
|
|
p1_life=20,
|
|
)
|
|
engine._resolve_combat(sim, 0, turn=1)
|
|
assert sim.players[1].life == 18
|
|
assert sim.players[0].stats.damage_dealt == 2
|
|
assert sim.players[1].stats.damage_received == 2
|
|
|
|
def test_commander_combat_damage_tracked(self):
|
|
cmd = _make_creature("Atraxa", pt="4/4", is_commander=True)
|
|
lands = [_make_land() for _ in range(5)]
|
|
engine, sim = _setup_two_player(
|
|
p0_battlefield=[cmd] + lands,
|
|
p1_life=40,
|
|
)
|
|
engine._resolve_combat(sim, 0, turn=1)
|
|
assert sim.players[1].life == 36
|
|
# Per-seat aggregate
|
|
assert sim.players[1].commander_damage_received[0] == 4
|
|
# Per-card breakdown
|
|
assert sim.players[1].commander_damage_by_card[(0, "Atraxa")] == 4
|
|
|
|
def test_non_commander_combat_damage_not_tracked_as_commander(self):
|
|
bear = _make_creature("Bear", pt="3/3")
|
|
lands = [_make_land() for _ in range(5)]
|
|
engine, sim = _setup_two_player(
|
|
p0_battlefield=[bear] + lands,
|
|
p1_life=40,
|
|
)
|
|
engine._resolve_combat(sim, 0, turn=1)
|
|
assert sim.players[1].life == 37
|
|
assert sim.players[1].commander_damage_received == {}
|
|
assert sim.players[1].commander_damage_by_card == {}
|
|
|
|
def test_commander_lethal_at_21(self):
|
|
"""A commander dealing 21 damage should eliminate even at high life."""
|
|
cmd = _make_creature("Voltron", pt="7/7", is_commander=True)
|
|
lands = [_make_land() for _ in range(5)]
|
|
engine, sim = _setup_two_player(
|
|
p0_battlefield=[cmd] + lands,
|
|
p1_life=100,
|
|
)
|
|
# Simulate 3 combat rounds
|
|
for t in range(3):
|
|
if sim.players[1].eliminated:
|
|
break
|
|
# Reset attacker for next combat
|
|
cmd.tapped = False
|
|
cmd.turn_played = -1
|
|
engine._resolve_combat(sim, 0, turn=t + 1)
|
|
assert sim.players[1].commander_damage_by_card[(0, "Voltron")] == 21
|
|
assert sim.players[1].eliminated is True
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# Full-game integration with burn spells
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
class TestFullGameWithBurn:
|
|
"""Integration tests running full games with burn spells in decks."""
|
|
|
|
def test_game_with_burn_completes(self):
|
|
"""A game with burn spells in decks should complete without error."""
|
|
deck_a = (
|
|
[Card(name="Lightning Bolt") for _ in range(8)]
|
|
+ [Card(name="Shock") for _ in range(8)]
|
|
+ [Card(name="Forest") for _ in range(24)]
|
|
+ [Card(name="Grizzly Bears") for _ in range(20)]
|
|
)
|
|
deck_b = (
|
|
[Card(name="Forest") for _ in range(24)]
|
|
+ [Card(name="Serra Angel") for _ in range(10)]
|
|
+ [Card(name="Grizzly Bears") for _ in range(20)]
|
|
+ [Card(name="Murder") for _ in range(6)]
|
|
)
|
|
for c in deck_a + deck_b:
|
|
enrich_card(c)
|
|
|
|
engine = GameEngine(max_turns=25, record_log=True)
|
|
result = engine.run(
|
|
deck_a=[c.clone() for c in deck_a],
|
|
deck_b=[c.clone() for c in deck_b],
|
|
name_a="Burn",
|
|
name_b="Midrange",
|
|
)
|
|
assert result is not None
|
|
assert result.turns > 0
|
|
assert result.winner_seat in (-1, 0, 1)
|
|
|
|
def test_burn_damage_appears_in_stats(self):
|
|
"""Burn spell damage should be reflected in player stats."""
|
|
deck_a = (
|
|
[Card(name="Lightning Bolt") for _ in range(15)]
|
|
+ [Card(name="Mountain") for _ in range(45)]
|
|
)
|
|
deck_b = [Card(name="Forest") for _ in range(60)]
|
|
for c in deck_a + deck_b:
|
|
enrich_card(c)
|
|
|
|
engine = GameEngine(max_turns=25)
|
|
result = engine.run(
|
|
deck_a=[c.clone() for c in deck_a],
|
|
deck_b=[c.clone() for c in deck_b],
|
|
name_a="Burn",
|
|
name_b="Lands",
|
|
)
|
|
# Player A should have dealt some damage from burn spells
|
|
pa_stats = result.player_a_stats
|
|
assert pa_stats is not None
|
|
assert pa_stats.damage_dealt > 0
|