""" Commander deck validation service. Enforces Commander/EDH format rules: - 100-card minimum (excluding basic lands) - Color identity rules - Commander uniqueness - Ban list enforcement """ import logging from typing import List, Dict, Set, Optional, Tuple from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.models.models import MtgonlineCard logger = logging.getLogger(__name__) # Commander ban list (subset - would be loaded from MTGJSON in production) COMMANDER_BAN_LIST = { "Balance", "Bolas's Citadel", "Cabal Coffers", "Chalice of the Void", "Chemister's Insight", "Commander's Sphere", "Demonic Consultation", "Demonic Tutor", "Diabolic Edict", "Dueling Coach", "Enlightened Tutor", "Ezuri's Renegade", "Fierce Empath", "Field of the Dead", "Gamble", "Golos, Tireless Pilgrim", "Graveyard Shift", "Hullbreacher", "Imperial Seal", "Jeska's Will", "Karn, the Great Creator", "Kethis, the Hidden Hand", "Library of Alexandria", "Lotho, Corrupt Shirriff", "Mox Opal", "Mox Petal", "Mox Diamond", "Narset, Parter of Veils", "Oko, Thief of Crowns", "Power Sink", "Rafiq of the Many", "Reanimate", "Rings of Brighthearth", "Rishadan Port", "Swords to Plowshares", "Teferi, Time Raveler", "Thoughtseize", "Tolarian Academy", "Underworld Connections", "Vampiric Tutor", "Windfall", "Yawgmoth's Will", } # Basic land names (excluded from 100-card count) BASIC_LANDS = { "Plains", "Island", "Swamp", "Mountain", "Forest", } class CommanderValidationError(Exception): """Raised when a Commander deck fails validation.""" pass class CommanderValidator: """Validates Commander/EDH decks against format rules.""" def __init__(self): self.ban_list = COMMANDER_BAN_LIST.copy() async def validate_deck( self, db: AsyncSession, deck_cards: List[Dict], commander_card_id: Optional[int] = None, ) -> Tuple[bool, List[str]]: """ Validate a Commander deck. Args: db: Database session deck_cards: List of card dicts with 'card_id' and 'quantity' commander_card_id: ID of the commander card Returns: Tuple of (is_valid, list_of_errors) """ errors = [] # Load all cards in the deck card_ids = [c["card_id"] for c in deck_cards] if not card_ids: errors.append("Deck is empty") return False, errors stmt = select(MtgonlineCard).where(MtgonlineCard.id.in_(card_ids)) result = await db.execute(stmt) cards = result.scalars().all() card_map = {c.id: c for c in cards} # Count non-basic-land cards total_cards = 0 for card_info in deck_cards: card_id = card_info["card_id"] quantity = card_info.get("quantity", 1) card = card_map.get(card_id) if not card: errors.append(f"Card ID {card_id} not found") continue # Skip basic lands from count if card.name in BASIC_LANDS: continue total_cards += quantity # Check 100-card minimum if total_cards < 100: errors.append( f"Deck has {total_cards} non-basic-land cards, " f"minimum is 100" ) # Check Commander uniqueness if commander_card_id: commander = card_map.get(commander_card_id) if not commander: errors.append(f"Commander card ID {commander_card_id} not found") elif not self._is_commander(commander): errors.append("Commander must be a legendary creature or planeswalker") # Check ban list for card_info in deck_cards: card_id = card_info["card_id"] card = card_map.get(card_id) if card and card.name in self.ban_list: errors.append(f"Card '{card.name}' is banned in Commander") # Check color identity (simplified - would need full implementation) # This requires checking the commander's color identity # and ensuring all cards match is_valid = len(errors) == 0 return is_valid, errors def _is_commander(self, card) -> bool: """Check if a card can be a commander.""" # Check if legendary is_legendary = "Legendary" in (card.type_line or "") # Check if has Commander ability (for some cards) has_commander_ability = "Commander" in (card.oracle_text or "") # Planeswalkers can be commanders if legendary is_planeswalker = "Planeswalker" in (card.type_line or "") return (is_legendary and is_planeswalker) or has_commander_ability def get_color_identity(self, commander_card) -> Set[str]: """ Get the color identity of a commander card. Color identity includes all colors in the card's mana cost, type line, and rules text. """ colors = set() # Parse mana cost mana_cost = commander_card.mana_cost or "" if "W" in mana_cost: colors.add("W") if "U" in mana_cost: colors.add("U") if "B" in mana_cost: colors.add("B") if "R" in mana_cost: colors.add("R") if "G" in mana_cost: colors.add("G") # Check type line for color indicators type_line = commander_card.type_line or "" if "White" in type_line: colors.add("W") if "Blue" in type_line: colors.add("U") if "Black" in type_line: colors.add("B") if "Red" in type_line: colors.add("R") if "Green" in type_line: colors.add("G") # Check oracle text for color indicators oracle_text = commander_card.oracle_text or "" if "White" in oracle_text: colors.add("W") if "Blue" in oracle_text: colors.add("U") if "Black" in oracle_text: colors.add("B") if "Red" in oracle_text: colors.add("R") if "Green" in oracle_text: colors.add("G") return colors def validate_color_identity( self, deck_cards: List[Dict], commander_card, card_map: Dict[int, MtgonlineCard], ) -> List[str]: """ Validate that all cards in the deck match the commander's color identity. """ errors = [] commander_identity = self.get_color_identity(commander_card) if not commander_identity: # Colorless commander allows only colorless cards pass for card_info in deck_cards: card_id = card_info["card_id"] card = card_map.get(card_id) if not card: continue # Skip basic lands if card.name in BASIC_LANDS: continue # Check card colors against commander identity card_colors = self._get_card_colors(card) if card_colors and not card_colors.issubset(commander_identity): errors.append( f"Card '{card.name}' has colors {card_colors} " f"not in commander identity {commander_identity}" ) return errors def _get_card_colors(self, card) -> Set[str]: """Get the colors of a card.""" colors = set() mana_cost = card.mana_cost or "" if "W" in mana_cost: colors.add("W") if "U" in mana_cost: colors.add("U") if "B" in mana_cost: colors.add("B") if "R" in mana_cost: colors.add("R") if "G" in mana_cost: colors.add("G") return colors # Singleton instance commander_validator = CommanderValidator()