mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-17 08:00:13 +01:00
feat: Add include/exclude card lists feature with web UI, validation, fuzzy matching, and JSON persistence (ALLOW_MUST_HAVES=1)
This commit is contained in:
parent
7ef45252f7
commit
0516260304
39 changed files with 3672 additions and 626 deletions
183
code/tests/test_include_exclude_engine_integration.py
Normal file
183
code/tests/test_include_exclude_engine_integration.py
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
"""
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue