mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-22 04:50:46 +02:00
247 lines
10 KiB
Python
247 lines
10 KiB
Python
"""
|
|
Tests for exclude re-entry prevention (M2).
|
|
|
|
Tests that excluded cards cannot re-enter the deck through downstream
|
|
heuristics or additional card addition calls.
|
|
"""
|
|
|
|
import unittest
|
|
from unittest.mock import Mock
|
|
import pandas as pd
|
|
from typing import List
|
|
|
|
from deck_builder.builder import DeckBuilder
|
|
|
|
|
|
class TestExcludeReentryPrevention(unittest.TestCase):
|
|
"""Test that excluded cards cannot re-enter the deck."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures."""
|
|
# Mock input/output functions to avoid interactive prompts
|
|
self.mock_input = Mock(return_value="")
|
|
self.mock_output = Mock()
|
|
|
|
# Create test card data
|
|
self.test_cards_df = pd.DataFrame([
|
|
{
|
|
'name': 'Lightning Bolt',
|
|
'type': 'Instant',
|
|
'mana_cost': '{R}',
|
|
'manaValue': 1,
|
|
'themeTags': ['burn'],
|
|
'colorIdentity': ['R']
|
|
},
|
|
{
|
|
'name': 'Sol Ring',
|
|
'type': 'Artifact',
|
|
'mana_cost': '{1}',
|
|
'manaValue': 1,
|
|
'themeTags': ['ramp'],
|
|
'colorIdentity': []
|
|
},
|
|
{
|
|
'name': 'Counterspell',
|
|
'type': 'Instant',
|
|
'mana_cost': '{U}{U}',
|
|
'manaValue': 2,
|
|
'themeTags': ['counterspell'],
|
|
'colorIdentity': ['U']
|
|
},
|
|
{
|
|
'name': 'Llanowar Elves',
|
|
'type': 'Creature — Elf Druid',
|
|
'mana_cost': '{G}',
|
|
'manaValue': 1,
|
|
'themeTags': ['ramp', 'elves'],
|
|
'colorIdentity': ['G'],
|
|
'creatureTypes': ['Elf', 'Druid']
|
|
}
|
|
])
|
|
|
|
def _create_test_builder(self, exclude_cards: List[str] = None) -> DeckBuilder:
|
|
"""Create a DeckBuilder instance for testing."""
|
|
builder = DeckBuilder(
|
|
input_func=self.mock_input,
|
|
output_func=self.mock_output,
|
|
log_outputs=False,
|
|
headless=True
|
|
)
|
|
|
|
# Set up basic configuration
|
|
builder.color_identity = ['R', 'G', 'U']
|
|
builder.color_identity_key = 'R, G, U'
|
|
builder._combined_cards_df = self.test_cards_df.copy()
|
|
builder._full_cards_df = self.test_cards_df.copy()
|
|
|
|
# Set exclude cards
|
|
builder.exclude_cards = exclude_cards or []
|
|
|
|
return builder
|
|
|
|
def test_exclude_prevents_direct_add_card(self):
|
|
"""Test that excluded cards are prevented from being added directly."""
|
|
builder = self._create_test_builder(exclude_cards=['Lightning Bolt', 'Sol Ring'])
|
|
|
|
# Try to add excluded cards directly
|
|
builder.add_card('Lightning Bolt', card_type='Instant')
|
|
builder.add_card('Sol Ring', card_type='Artifact')
|
|
|
|
# Verify excluded cards were not added
|
|
self.assertNotIn('Lightning Bolt', builder.card_library)
|
|
self.assertNotIn('Sol Ring', builder.card_library)
|
|
|
|
def test_exclude_allows_non_excluded_cards(self):
|
|
"""Test that non-excluded cards can still be added normally."""
|
|
builder = self._create_test_builder(exclude_cards=['Lightning Bolt'])
|
|
|
|
# Add a non-excluded card
|
|
builder.add_card('Sol Ring', card_type='Artifact')
|
|
builder.add_card('Counterspell', card_type='Instant')
|
|
|
|
# Verify non-excluded cards were added
|
|
self.assertIn('Sol Ring', builder.card_library)
|
|
self.assertIn('Counterspell', builder.card_library)
|
|
|
|
def test_exclude_prevention_with_fuzzy_matching(self):
|
|
"""Test that exclude prevention works with normalized card names."""
|
|
# Test variations in card name formatting
|
|
builder = self._create_test_builder(exclude_cards=['lightning bolt']) # lowercase
|
|
|
|
# Try to add with different casing/formatting
|
|
builder.add_card('Lightning Bolt', card_type='Instant') # proper case
|
|
builder.add_card('LIGHTNING BOLT', card_type='Instant') # uppercase
|
|
|
|
# All should be prevented
|
|
self.assertNotIn('Lightning Bolt', builder.card_library)
|
|
self.assertNotIn('LIGHTNING BOLT', builder.card_library)
|
|
|
|
def test_exclude_prevention_with_punctuation_variations(self):
|
|
"""Test exclude prevention with punctuation variations."""
|
|
# Create test data with punctuation
|
|
test_df = pd.DataFrame([
|
|
{
|
|
'name': 'Krenko, Mob Boss',
|
|
'type': 'Legendary Creature — Goblin Warrior',
|
|
'mana_cost': '{2}{R}{R}',
|
|
'manaValue': 4,
|
|
'themeTags': ['goblins'],
|
|
'colorIdentity': ['R']
|
|
}
|
|
])
|
|
|
|
builder = self._create_test_builder(exclude_cards=['Krenko Mob Boss']) # no comma
|
|
builder._combined_cards_df = test_df
|
|
builder._full_cards_df = test_df
|
|
|
|
# Try to add with comma (should be prevented due to normalization)
|
|
builder.add_card('Krenko, Mob Boss', card_type='Legendary Creature — Goblin Warrior')
|
|
|
|
# Should be prevented
|
|
self.assertNotIn('Krenko, Mob Boss', builder.card_library)
|
|
|
|
def test_commander_exemption_from_exclude_prevention(self):
|
|
"""Test that commanders are exempted from exclude prevention."""
|
|
builder = self._create_test_builder(exclude_cards=['Lightning Bolt'])
|
|
|
|
# Add Lightning Bolt as commander (should be allowed)
|
|
builder.add_card('Lightning Bolt', card_type='Instant', is_commander=True)
|
|
|
|
# Should be added despite being in exclude list
|
|
self.assertIn('Lightning Bolt', builder.card_library)
|
|
self.assertTrue(builder.card_library['Lightning Bolt']['Commander'])
|
|
|
|
def test_exclude_reentry_prevention_during_phases(self):
|
|
"""Test that excluded cards cannot re-enter during creature/spell phases."""
|
|
builder = self._create_test_builder(exclude_cards=['Llanowar Elves'])
|
|
|
|
# Simulate a creature addition phase trying to add excluded creature
|
|
# This would typically happen through automated heuristics
|
|
builder.add_card('Llanowar Elves', card_type='Creature — Elf Druid', added_by='creature_phase')
|
|
|
|
# Should be prevented
|
|
self.assertNotIn('Llanowar Elves', builder.card_library)
|
|
|
|
def test_exclude_prevention_with_empty_exclude_list(self):
|
|
"""Test that exclude prevention handles empty exclude lists gracefully."""
|
|
builder = self._create_test_builder(exclude_cards=[])
|
|
|
|
# Should allow normal addition
|
|
builder.add_card('Lightning Bolt', card_type='Instant')
|
|
|
|
# Should be added normally
|
|
self.assertIn('Lightning Bolt', builder.card_library)
|
|
|
|
def test_exclude_prevention_with_none_exclude_list(self):
|
|
"""Test that exclude prevention handles None exclude lists gracefully."""
|
|
builder = self._create_test_builder()
|
|
builder.exclude_cards = None # Explicitly set to None
|
|
|
|
# Should allow normal addition
|
|
builder.add_card('Lightning Bolt', card_type='Instant')
|
|
|
|
# Should be added normally
|
|
self.assertIn('Lightning Bolt', builder.card_library)
|
|
|
|
def test_multiple_exclude_attempts_logged(self):
|
|
"""Test that multiple attempts to add excluded cards are properly logged."""
|
|
builder = self._create_test_builder(exclude_cards=['Sol Ring'])
|
|
|
|
# Track log calls by mocking the logger
|
|
with self.assertLogs('deck_builder.builder', level='INFO') as log_context:
|
|
# Try to add excluded card multiple times
|
|
builder.add_card('Sol Ring', card_type='Artifact', added_by='test1')
|
|
builder.add_card('Sol Ring', card_type='Artifact', added_by='test2')
|
|
builder.add_card('Sol Ring', card_type='Artifact', added_by='test3')
|
|
|
|
# Verify card was not added
|
|
self.assertNotIn('Sol Ring', builder.card_library)
|
|
|
|
# Verify logging occurred
|
|
log_messages = [record.message for record in log_context.records]
|
|
prevent_logs = [msg for msg in log_messages if 'EXCLUDE_REENTRY_PREVENTED' in msg]
|
|
self.assertEqual(len(prevent_logs), 3) # Should log each prevention
|
|
|
|
def test_exclude_prevention_maintains_deck_integrity(self):
|
|
"""Test that exclude prevention doesn't interfere with normal deck building."""
|
|
builder = self._create_test_builder(exclude_cards=['Lightning Bolt'])
|
|
|
|
# Add a mix of cards, some excluded, some not
|
|
cards_to_add = [
|
|
('Lightning Bolt', 'Instant'), # excluded
|
|
('Sol Ring', 'Artifact'), # allowed
|
|
('Counterspell', 'Instant'), # allowed
|
|
('Lightning Bolt', 'Instant'), # excluded (retry)
|
|
('Llanowar Elves', 'Creature — Elf Druid') # allowed
|
|
]
|
|
|
|
for name, card_type in cards_to_add:
|
|
builder.add_card(name, card_type=card_type)
|
|
|
|
# Verify only non-excluded cards were added
|
|
expected_cards = {'Sol Ring', 'Counterspell', 'Llanowar Elves'}
|
|
actual_cards = set(builder.card_library.keys())
|
|
|
|
self.assertEqual(actual_cards, expected_cards)
|
|
self.assertNotIn('Lightning Bolt', actual_cards)
|
|
|
|
def test_exclude_prevention_works_after_pool_filtering(self):
|
|
"""Test that exclude prevention works even after pool filtering removes cards."""
|
|
builder = self._create_test_builder(exclude_cards=['Lightning Bolt'])
|
|
|
|
# Simulate setup_dataframes filtering (M0.5 implementation)
|
|
# The card should already be filtered from the pool, but prevention should still work
|
|
original_df = builder._combined_cards_df.copy()
|
|
|
|
# Remove Lightning Bolt from pool (simulating M0.5 filtering)
|
|
builder._combined_cards_df = original_df[original_df['name'] != 'Lightning Bolt']
|
|
|
|
# Try to add it anyway (simulating downstream heuristic attempting to add)
|
|
builder.add_card('Lightning Bolt', card_type='Instant')
|
|
|
|
# Should still be prevented
|
|
self.assertNotIn('Lightning Bolt', builder.card_library)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|