mtg_python_deckbuilder/code/tests/test_include_exclude_engine_integration.py

183 lines
8.1 KiB
Python

"""
Integration test demonstrating M2 include/exclude engine integration.
Shows the complete flow: lands → includes → creatures/spells with
proper exclusion and include injection.
"""
import unittest
from unittest.mock import Mock
import pandas as pd
from deck_builder.builder import DeckBuilder
class TestM2Integration(unittest.TestCase):
"""Integration test for M2 include/exclude engine integration."""
def setUp(self):
"""Set up test fixtures."""
self.mock_input = Mock(return_value="")
self.mock_output = Mock()
# Create comprehensive test card data
self.test_cards_df = pd.DataFrame([
# Lands
{'name': 'Forest', 'type': 'Basic Land — Forest', 'mana_cost': '', 'manaValue': 0, 'themeTags': [], 'colorIdentity': ['G']},
{'name': 'Command Tower', 'type': 'Land', 'mana_cost': '', 'manaValue': 0, 'themeTags': [], 'colorIdentity': []},
{'name': 'Sol Ring', 'type': 'Artifact', 'mana_cost': '{1}', 'manaValue': 1, 'themeTags': ['ramp'], 'colorIdentity': []},
# Creatures
{'name': 'Llanowar Elves', 'type': 'Creature — Elf Druid', 'mana_cost': '{G}', 'manaValue': 1, 'themeTags': ['ramp', 'elves'], 'colorIdentity': ['G']},
{'name': 'Elvish Mystic', 'type': 'Creature — Elf Druid', 'mana_cost': '{G}', 'manaValue': 1, 'themeTags': ['ramp', 'elves'], 'colorIdentity': ['G']},
{'name': 'Fyndhorn Elves', 'type': 'Creature — Elf Druid', 'mana_cost': '{G}', 'manaValue': 1, 'themeTags': ['ramp', 'elves'], 'colorIdentity': ['G']},
# Spells
{'name': 'Lightning Bolt', 'type': 'Instant', 'mana_cost': '{R}', 'manaValue': 1, 'themeTags': ['burn'], 'colorIdentity': ['R']},
{'name': 'Counterspell', 'type': 'Instant', 'mana_cost': '{U}{U}', 'manaValue': 2, 'themeTags': ['counterspell'], 'colorIdentity': ['U']},
{'name': 'Rampant Growth', 'type': 'Sorcery', 'mana_cost': '{1}{G}', 'manaValue': 2, 'themeTags': ['ramp'], 'colorIdentity': ['G']},
])
def test_complete_m2_workflow(self):
"""Test the complete M2 workflow with includes, excludes, and proper ordering."""
# Create builder with include/exclude configuration
builder = DeckBuilder(
input_func=self.mock_input,
output_func=self.mock_output,
log_outputs=False,
headless=True
)
# Configure include/exclude lists
builder.include_cards = ['Sol Ring', 'Lightning Bolt'] # Must include these
builder.exclude_cards = ['Counterspell', 'Fyndhorn Elves'] # Must exclude these
# Set up card pool
builder.color_identity = ['R', 'G', 'U']
builder._combined_cards_df = self.test_cards_df.copy()
builder._full_cards_df = self.test_cards_df.copy()
# Set small ideal counts for testing
builder.ideal_counts = {
'lands': 3,
'creatures': 2,
'spells': 2
}
# Track addition sequence
addition_sequence = []
original_add_card = builder.add_card
def track_additions(card_name, **kwargs):
addition_sequence.append({
'name': card_name,
'phase': kwargs.get('added_by', 'unknown'),
'role': kwargs.get('role', 'normal')
})
return original_add_card(card_name, **kwargs)
builder.add_card = track_additions
# Simulate deck building phases
# 1. Land phase
builder.add_card('Forest', card_type='Basic Land — Forest', added_by='lands')
builder.add_card('Command Tower', card_type='Land', added_by='lands')
# 2. Include injection (M2)
builder._inject_includes_after_lands()
# 3. Creature phase
builder.add_card('Llanowar Elves', card_type='Creature — Elf Druid', added_by='creatures')
# 4. Try to add excluded cards (should be prevented)
builder.add_card('Counterspell', card_type='Instant', added_by='spells') # Should be blocked
builder.add_card('Fyndhorn Elves', card_type='Creature — Elf Druid', added_by='creatures') # Should be blocked
# 5. Add allowed spell
builder.add_card('Rampant Growth', card_type='Sorcery', added_by='spells')
# Verify results
# Check that includes were added
self.assertIn('Sol Ring', builder.card_library)
self.assertIn('Lightning Bolt', builder.card_library)
# Check that includes have correct metadata
self.assertEqual(builder.card_library['Sol Ring']['Role'], 'include')
self.assertEqual(builder.card_library['Sol Ring']['AddedBy'], 'include_injection')
self.assertEqual(builder.card_library['Lightning Bolt']['Role'], 'include')
# Check that excludes were not added
self.assertNotIn('Counterspell', builder.card_library)
self.assertNotIn('Fyndhorn Elves', builder.card_library)
# Check that normal cards were added
self.assertIn('Forest', builder.card_library)
self.assertIn('Command Tower', builder.card_library)
self.assertIn('Llanowar Elves', builder.card_library)
self.assertIn('Rampant Growth', builder.card_library)
# Verify ordering: lands → includes → creatures/spells
# Get indices in sequence
land_indices = [i for i, entry in enumerate(addition_sequence) if entry['phase'] == 'lands']
include_indices = [i for i, entry in enumerate(addition_sequence) if entry['phase'] == 'include_injection']
creature_indices = [i for i, entry in enumerate(addition_sequence) if entry['phase'] == 'creatures']
# Verify ordering
if land_indices and include_indices:
self.assertLess(max(land_indices), min(include_indices), "Lands should come before includes")
if include_indices and creature_indices:
self.assertLess(max(include_indices), min(creature_indices), "Includes should come before creatures")
# Verify diagnostics
self.assertIsNotNone(builder.include_exclude_diagnostics)
include_added = builder.include_exclude_diagnostics.get('include_added', [])
self.assertEqual(set(include_added), {'Sol Ring', 'Lightning Bolt'})
# Verify final deck composition
expected_final_cards = {
'Forest', 'Command Tower', # lands
'Sol Ring', 'Lightning Bolt', # includes
'Llanowar Elves', # creatures
'Rampant Growth' # spells
}
self.assertEqual(set(builder.card_library.keys()), expected_final_cards)
def test_include_over_ideal_tracking(self):
"""Test that includes going over ideal counts are properly tracked."""
builder = DeckBuilder(
input_func=self.mock_input,
output_func=self.mock_output,
log_outputs=False,
headless=True
)
# Configure to force over-ideal situation
builder.include_cards = ['Sol Ring', 'Lightning Bolt'] # 2 includes
builder.exclude_cards = []
builder.color_identity = ['R', 'G']
builder._combined_cards_df = self.test_cards_df.copy()
builder._full_cards_df = self.test_cards_df.copy()
# Set very low ideal counts to trigger over-ideal
builder.ideal_counts = {
'spells': 1 # Only 1 spell allowed, but we're including 2
}
# Inject includes
builder._inject_includes_after_lands()
# Verify over-ideal tracking
self.assertIsNotNone(builder.include_exclude_diagnostics)
over_ideal = builder.include_exclude_diagnostics.get('include_over_ideal', {})
# Both Sol Ring and Lightning Bolt are categorized as 'spells'
self.assertIn('spells', over_ideal)
# At least one should be tracked as over-ideal
self.assertTrue(len(over_ideal['spells']) > 0)
if __name__ == '__main__':
unittest.main()