""" MTG Card Profile Extractor Extracts structured profiles from MTGJSON card data. Identifies mechanics, archetypes, mana costs, targets, and other game-relevant attributes. """ import re from typing import List, Dict, Optional, Set from dataclasses import dataclass @dataclass class CardProfile: """ Structured profile of a card for interaction analysis. Contains all relevant game attributes extracted from MTGJSON data. """ # Basic info id: int name: str mana_cost: Optional[str] type_line: Optional[str] oracle_text: Optional[str] subtypes: Optional[str] supertypes: Optional[str] set_code: Optional[str] # Extracted attributes colors: List[str] = None # ['W', 'U', 'B', 'R', 'G'] color_identity: List[str] = None mechanics: List[str] = None archetypes: List[str] = None targets: List[str] = None # ['creature', 'artifact', 'player', etc.] triggers: List[str] = None effects: List[str] = None themes: List[str] = None # ['storm', 'tokens', 'draw', etc.] def __post_init__(self): """Initialize lists if None.""" if self.colors is None: self.colors = [] if self.color_identity is None: self.color_identity = [] if self.mechanics is None: self.mechanics = [] if self.archetypes is None: self.archetypes = [] if self.targets is None: self.targets = [] if self.triggers is None: self.triggers = [] if self.effects is None: self.effects = [] if self.themes is None: self.themes = [] def to_dict(self) -> Dict: """Convert profile to dictionary.""" return { 'id': self.id, 'name': self.name, 'mana_cost': self.mana_cost, 'type_line': self.type_line, 'oracle_text': self.oracle_text, 'subtypes': self.subtypes, 'supertypes': self.supertypes, 'set_code': self.set_code, 'colors': self.colors, 'color_identity': self.color_identity, 'mechanics': self.mechanics, 'archetypes': self.archetypes, 'targets': self.targets, 'triggers': self.triggers, 'effects': self.effects, 'themes': self.themes, } class CardProfileExtractor: """ Extracts card profiles from MTGJSON data. Uses regex patterns and curated dictionaries to identify: - Mana costs and color identity - Game mechanics (flying, first strike, etc.) - Archetypes (goblin, elf, vampire, etc.) - Targets (creature, artifact, player, etc.) - Triggers (enters battlefield, dies, attacks, etc.) - Effects (gain flying, draw card, etc.) - Themes (storm, tokens, mill, etc.) """ # Color symbols in mana costs COLOR_SYMBOLS = { '{W}': 'W', '{U}': 'U', '{B}': 'B', '{R}': 'R', '{G}': 'G', } # Known mechanics and their patterns MECHANICS = { 'flying': r'Flying', 'first_strike': r'First strike', 'double_strike': r'Double strike', 'deathtouch': r'Death touch', 'lifelink': r'Lifelink', 'haste': r'Haste', 'trample': r'Trample', 'menace': r'Menace', 'vigilance': r'Vegilance', 'reach': r'Reach', 'indestructible': r'Indestructible', 'hexproof': r'Hexproof', 'shroud': r'Shroud', 'defender': r'Defender', 'landfall': r'Landfall', 'delve': r'Delve', 'suspend': r'Suspend', 'convoke': r'Convoke', 'rampage': r'Rampage', 'toxic': r'Toxic', 'crew': r'Crew', 'equip': r'Equip', 'annihilator': r'Annihilator', 'spectacle': r'Spectacle', 'prowess': r'Prowess', 'aftermath': r'Aftermath', 'adapt': r'Adapt', 'amplify': r'Amplify', 'awaken': r'Awaken', 'kicker': r'Kicker', 'morph': r'Morph', 'evolve': r'Evolve', 'exalted': r'Exalted', 'storm': r'Storm', 'madness': r'Madness', 'manifest': r'Manifest', 'modular': r'Modular', 'mutate': r'Mutate', 'transform': r'Transform', 'unearth': r'Unearth', 'persist': r'Persist', 'rebound': r'Rebound', 'replicate': r'Replicate', 'soulshift': r'Soulshift', 'dredge': r'Dredge', 'devour': r'Devour', 'banding': r'Band with', 'bestow': r'Bestow', 'channel': r'Channel', 'clash': r'Clash', 'curse': r'Curse', 'dwell': r'Dwell', 'evoke': r'Evoke', 'exploit': r'Exploit', 'extort': r'Extort', 'flash': r'Flash', 'foretell': r'Foretell', 'frenzy': r'Frenzy', 'grudge': r'Grudge', 'heroic': r'Heroic', 'hideaway': r'Hideaway', 'horrify': r'Horrify', 'impetus': r'Impetus', 'infect': r'Infect', 'journey': r'Journey', 'kicker': r'Kicker', 'landfall': r'Landfall', 'meld': r'Meld', 'miracle': r'Miracle', 'monstrosity': r'Monstrosity', 'morph': r'Morph', 'mutate': r'Mutate', 'ninja': r'Ninja', 'pact': r'Pact', 'persist': r'Persist', 'provoke': r'Provoke', 'quest': r'Quest', 'raid': r'Raid', 'rebound': r'Rebound', 'replicate': r'Replicate', 'revolt': r'Revolt', 'shroud': r'Shroud', 'skulk': r'Skulk', 'snow': r'Snow', 'splice': r'Splice', 'staunch': r'Staunch', 'storm': r'Storm', 'suspend': r'Suspend', 'surge': r'Surge', 'swarm': r'Swarm', 'thorn': r'Thorn', 'toxic': r'Toxic', 'transfigure': r'Transfigure', 'transform': r'Transform', 'unearth': r'Unearth', 'unleash': r'Unleash', 'vampiric': r'Vampiric', 'ward': r'Ward', 'willow': r'Willow', 'winter': r'Winter', 'wither': r'Wither', 'wurm': r'Wurm', } # Known archetypes and their patterns ARCHETYPES = { 'goblin': r'Goblin', 'elf': r'Elf', 'vampire': r'Veampire', 'angel': r'Angel', 'dragon': r'Dragon', 'human': r'Human', 'zombie': r'Zombie', 'soldier': r'Soldier', 'knight': r'Knight', 'wizard': r'Wizard', 'spirit': r'Spirit', 'demon': r'Demon', 'snake': r'Snake', 'cat': r'Cat', 'wolf': r'Wolf', 'bear': r'Bear', 'bird': r'Bird', 'insect': r'Insect', 'horror': r'Horror', 'goat': r'Goat', 'ox': r'Ox', 'elephant': r'Elephant', 'whale': r'Whale', 'shark': r'Shark', 'fish': r'Fish', 'serpent': r'Serpent', 'lizard': r'Lizard', 'scorpion': r'Scorpion', 'spider': r'Spider', 'rat': r'Rat', 'drake': r'Drake', 'wyvern': r'Wyvern', 'phoenix': r'Phoenix', 'lynx': r'Lynx', 'jaguar': r'Jaguar', 'hydra': r'Hydra', 'leviathan': r'Leviathan', 'kraken': r'Kraken', 'cyclops': r'Cyclops', 'golem': r'Golem', 'homunculus': r'Homunculus', 'clay': r'Clay', 'construct': r'Construct', 'myr': r'Myr', 'pirate': r'Pirate', 'pegasus': r'Pegasus', 'unicorn': r'Unicorn', 'centaur': r'Centaur', 'merfolk': r'Merfolk', 'mermaid': r'Mermaid', 'naga': r'Naga', 'satyr': r'Satyr', 'dryad': r'Dryad', 'treant': r'Treant', 'elemental': r'Elemental', 'fiend': r'Fiend', 'imp': r'Imp', 'faerie': r'Faerie', 'minion': r'Minion', 'abomination': r'Abomination', 'beast': r'Beast', 'demigod': r'Demigod', 'god': r'God', 'avatar': r'Avatar', 'guardian': r'Guardian', 'warrior': r'Warrior', 'rogue': r'Rogue', 'artificer': r'Artificer', 'bard': r'Bard', 'monk': r'Monk', 'ninja': r'Ninja', 'samurai': r'Samurai', 'assassin': r'Assassin', 'thief': r'Thief', 'acrobat': r'Acrobat', 'explorer': r'Explorer', 'myth': r'Myth', 'illusion': r'Illusion', 'mirror': r'Mirror', 'phantom': r'Phantom', 'shapeshifter': r'Shapeshifter', 'shaman': r'Shaman', 'skeleton': r'Skeleton', 'slime': r'Slime', 'squirrel': r'Squirrel', 'troll': r'Troll', 'tyrannosaur': r'Tyrannosaur', 'wraith': r'Wraith', 'wurm': r'Wurm', } # Target types and their patterns TARGET_TYPES = { 'creature': r'creature', 'artifact': r'artifact', 'enchantment': r'enchantment', 'instant': r'instant', 'sorcery': r'sorcery', 'planeswalker': r'planeswalker', 'land': r'land', 'player': r'player', 'spell': r'spell', 'permanent': r'permanent', 'creature card': r'creature [Cc]ard', } # Trigger conditions and their patterns TRIGGERS = { 'enters_battlefield': r'when [~|this] enters the battlefield', 'leaves_battlefield': r'when [~|this] leaves the battlefield', 'attacks': r'whenever [~|this] attacks', 'blocks': r'whenever [~|this] blocks', 'dies': r'when [~|this] dies', 'damage': r'deals [0-9]+ damage', 'draws_card': r'draw a card|draw two cards', 'gains_life': r'gain [0-9]+ life', 'creates_token': r'create a token', 'taps': r'tap: add', 'untaps': r'untap: add', 'destroys': r'destroy target', 'exiles': r'exile target', 'counters_spell': r'counter target spell', } # Game effects and their patterns EFFECTS = { 'gain_flying': r'gain flying', 'gain_first_strike': r'gain first strike', 'gain_double_strike': r'gain double strike', 'gain_deathtouch': r'gain deathtouch', 'gain_lifelink': r'gain lifelink', 'gain_haste': r'gain haste', 'gain_trample': r'gain trample', 'gain_vigilance': r'gain vigilance', 'gain_indestructible': r'gain indestructible', 'gain_hexproof': r'gain hexproof', 'until_end_of_turn': r'until end of turn', 'until_next_turn': r'until your next turn', 'deal_damage': r'deal [0-9]+ damage', 'gain_life': r'gain [0-9]+ life', 'draw_card': r'draw [0-9]+ card', 'create_token': r'create [0-9]+ token', 'destroy': r'destroy target', 'exile': r'exile target', 'counter_spell': r'counter target spell', } # Theme keywords and their patterns THEMES = { 'storm': r'Storm', 'tokens': r'create a token', 'draw': r'draw a card', 'life_gain': r'gain life', 'board_wipe': r'destroy all', 'reanimate': r'put from grave', 'countermagic': r'counter target spell', 'card_advantage': r'draw', 'mana_acceleration': r'tap: add', 'combat_tricks': r'gain [A-Za-z]+ until end of turn', 'etb_effects': r'enters the battlefield', 'ltb_effects': r'leaves the battlefield', 'mill': r'put on bottom of library', 'draw_go': r'draw a card', 'aggro': r'deal [0-9]+ damage', 'control': r'counter target spell', 'midrange': r'creature', } def __init__(self): """Initialize the profile extractor.""" pass def extract_colors(self, mana_cost: Optional[str]) -> List[str]: """ Extract colors from mana cost. Args: mana_cost: Mana cost string (e.g., '{1}{R}') Returns: List of color symbols (e.g., ['R']) """ if not mana_cost: return [] colors = [] for symbol, color in self.COLOR_SYMBOLS.items(): if symbol in mana_cost: if color not in colors: colors.append(color) return colors def extract_mechanics(self, card_type_line: Optional[str], card_oracle: Optional[str]) -> List[str]: """ Extract game mechanics from card text. Args: card_type_line: Card type line (e.g., 'Creature - Goblin Warrior') card_oracle: Card oracle text Returns: List of mechanic names (e.g., ['haste', 'trample']) """ mechanics = [] # Combine type line and oracle text for checking text_to_check = f"{card_type_line or ''} {card_oracle or ''}".upper() for mechanic, pattern in self.MECHANICS.items(): if re.search(pattern, text_to_check, re.IGNORECASE): if mechanic not in mechanics: mechanics.append(mechanic) return mechanics def extract_archetypes(self, card_subtypes: Optional[str]) -> List[str]: """ Extract archetypes from card subtypes. Args: card_subtypes: Card subtypes (e.g., 'Goblin, Warrior') Returns: List of archetype names (e.g., ['goblin']) """ if not card_subtypes: return [] archetypes = [] for archetype, pattern in self.ARCHETYPES.items(): if re.search(pattern, card_subtypes, re.IGNORECASE): if archetype not in archetypes: archetypes.append(archetype) return archetypes def extract_targets(self, card_oracle: Optional[str]) -> List[str]: """ Extract target types from oracle text. Args: card_oracle: Card oracle text Returns: List of target types (e.g., ['creature', 'player']) """ if not card_oracle: return [] targets = [] for target, pattern in self.TARGET_TYPES.items(): if re.search(pattern, card_oracle, re.IGNORECASE): if target not in targets: targets.append(target) return targets def extract_triggers(self, card_oracle: Optional[str]) -> List[str]: """ Extract trigger conditions from oracle text. Args: card_oracle: Card oracle text Returns: List of trigger names (e.g., ['enters_battlefield', 'dies']) """ if not card_oracle: return [] triggers = [] for trigger, pattern in self.TRIGGERS.items(): if re.search(pattern, card_oracle, re.IGNORECASE): if trigger not in triggers: triggers.append(trigger) return triggers def extract_effects(self, card_oracle: Optional[str]) -> List[str]: """ Extract game effects from oracle text. Args: card_oracle: Card oracle text Returns: List of effect names (e.g., ['gain_flying', 'draw_card']) """ if not card_oracle: return [] effects = [] for effect, pattern in self.EFFECTS.items(): if re.search(pattern, card_oracle, re.IGNORECASE): if effect not in effects: effects.append(effect) return effects def extract_themes(self, card_mechanics: List[str], card_triggers: List[str], card_effects: List[str], card_targets: List[str]) -> List[str]: """ Extract card themes based on characteristics. Args: card_mechanics: List of mechanics card_triggers: List of triggers card_effects: List of effects card_targets: List of targets Returns: List of theme names (e.g., ['storm', 'tokens']) """ themes = [] # Storm theme if 'storm' in card_mechanics or 'storm' in card_targets: themes.append('storm') # Token theme if any(e in card_effects for e in ['create_token', 'draw_card']): themes.append('tokens') # Mill theme if any(t in card_triggers for t in ['draws_card']): themes.append('mill') # Life gain theme if any(e in card_effects for e in ['gain_life', 'draw_card']): themes.append('life_gain') # Board wipe theme if any(t in card_triggers for t in ['dies']): themes.append('board_wipe') # Reanimate theme if any(t in card_triggers for t in ['leaves_battlefield']): themes.append('reanimate') # Countermagic theme if any(e in card_effects for e in ['counter_spell']): themes.append('countermagic') # Card advantage theme if any(t in card_triggers for t in ['draws_card']): themes.append('card_advantage') # Mana acceleration theme if any(t in card_triggers for t in ['taps']): themes.append('mana_acceleration') # Combat tricks theme if any(e in card_effects for e in ['gain_flying', 'gain_first_strike', 'gain_double_strike', 'gain_deathtouch', 'gain_lifelink', 'gain_vigilance']): themes.append('combat_tricks') # ETB effects theme if any(t in card_triggers for t in ['enters_battlefield']): themes.append('etb_effects') # LTB effects theme if any(t in card_triggers for t in ['leaves_battlefield']): themes.append('ltb_effects') # Aggro theme if any(e in card_effects for e in ['deal_damage']): themes.append('aggro') # Control theme if any(e in card_effects for e in ['counter_spell']): themes.append('control') # Midrange theme if any(t in card_targets for t in ['creature']): themes.append('midrange') return themes def extract_profile(self, card_data: Dict) -> CardProfile: """ Extract a complete card profile from MTGJSON data. Args: card_data: MTGJSON card dictionary Returns: CardProfile object with all extracted attributes """ # Parse subtypes subtypes = None if card_data.get('subtypes'): subtypes = ', '.join(card_data['subtypes']) # Parse supertypes supertypes = None if card_data.get('supertypes'): supertypes = ', '.join(card_data['supertypes']) # Extract colors from mana cost colors = self.extract_colors(card_data.get('manaCost')) # Extract mechanics mechanics = self.extract_mechanics( card_data.get('typeLine'), card_data.get('oracleText') ) # Extract archetypes archetypes = self.extract_archetypes(subtypes) # Extract targets targets = self.extract_targets(card_data.get('oracleText')) # Extract triggers triggers = self.extract_triggers(card_data.get('oracleText')) # Extract effects effects = self.extract_effects(card_data.get('oracleText')) # Extract themes themes = self.extract_themes(mechanics, triggers, effects, targets) # Create profile profile = CardProfile( id=card_data.get('id', 0), name=card_data.get('name', ''), mana_cost=card_data.get('manaCost'), type_line=card_data.get('typeLine'), oracle_text=card_data.get('oracleText'), subtypes=subtypes, supertypes=supertypes, set_code=card_data.get('set', {}).get('code') if card_data.get('set') else None, colors=colors, color_identity=card_data.get('colorIdentity'), mechanics=mechanics, archetypes=archetypes, targets=targets, triggers=triggers, effects=effects, themes=themes, ) return profile def extract_profiles_batch(self, cards: List[Dict]) -> List[CardProfile]: """ Extract profiles for a batch of cards. Args: cards: List of MTGJSON card dictionaries Returns: List of CardProfile objects """ return [self.extract_profile(card) for card in cards]