960 lines
32 KiB
Python
960 lines
32 KiB
Python
"""
|
|
MTG Card Interaction Rule Engine
|
|
|
|
Extracts card interactions using structured rules instead of NLP.
|
|
Designed for rolling updates when new MTGJSON data is loaded.
|
|
"""
|
|
import re
|
|
from typing import Dict, List, Tuple, Optional, Any
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
|
|
|
|
class InteractionType(Enum):
|
|
"""Types of card interactions."""
|
|
MECHANIC = "mechanic"
|
|
ARCHETYPE = "archetype"
|
|
SYNERGY = "synergy"
|
|
COUNTER = "counter"
|
|
EVOLUTION = "evolution"
|
|
MANA = "mana"
|
|
SET_THEME = "set_theme"
|
|
|
|
|
|
class SynergyType(Enum):
|
|
"""Types of synergies between cards."""
|
|
ARCHETYPE_SUPPORT = "archetype_support"
|
|
MECHANIC_SUPPORT = "mechanic_support"
|
|
MANA_BASE = "mana_base"
|
|
COMBO_PARTNER = "combo_partner"
|
|
COUNTER_PARTNER = "counter_partner"
|
|
EVOLUTION_CHAIN = "evolution_chain"
|
|
|
|
|
|
class CounterType(Enum):
|
|
"""Types of counter relationships."""
|
|
DIRECT_COUNTER = "direct_counter"
|
|
MANA_DISADVANTAGE = "mana_disadvantage"
|
|
OUTCLASS = "outclass"
|
|
COUNTER_ROLE = "counter_role"
|
|
|
|
|
|
class EvolutionType(Enum):
|
|
"""Types of evolution relationships."""
|
|
TRANSFORM = "transform"
|
|
EVOLVE = "evolve"
|
|
DOUBLE_SIDED = "double_sided"
|
|
MODAL_DFC = "modal_dfc"
|
|
REPRINTED = "reprinted"
|
|
|
|
|
|
@dataclass
|
|
class CardProfile:
|
|
"""Structured profile of a card for interaction extraction."""
|
|
name: str
|
|
mana_cost: Optional[str]
|
|
type_line: Optional[str]
|
|
oracle_text: Optional[str]
|
|
subtypes: Optional[str]
|
|
supertypes: Optional[str]
|
|
colors: Optional[str]
|
|
color_identity: Optional[str]
|
|
power: Optional[str]
|
|
toughness: Optional[str]
|
|
loyalty: Optional[str]
|
|
set_code: Optional[str]
|
|
set_id: int
|
|
card_id: int
|
|
|
|
# Extracted fields
|
|
mechanics: List[str] = None
|
|
archetypes: List[str] = None
|
|
targets: List[str] = None
|
|
triggers: List[str] = None
|
|
effects: List[str] = None
|
|
themes: List[str] = None
|
|
|
|
def __post_init__(self):
|
|
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 = []
|
|
|
|
|
|
class MTGRuleEngine:
|
|
"""
|
|
Extracts card interactions using structured rules.
|
|
|
|
This is NOT NLP. It uses:
|
|
- Regex patterns for known game language
|
|
- Curated dictionaries for mechanics/archetypes
|
|
- Game rule logic for determining interactions
|
|
"""
|
|
|
|
def __init__(self):
|
|
# Define mechanics and their extraction patterns
|
|
self.mechanics_patterns = {
|
|
'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',
|
|
'soulshift': r'Soulshift',
|
|
'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',
|
|
'banding': r'Band with',
|
|
'bestow': r'Bestow',
|
|
'burst': r'Burst',
|
|
'channel': r'Channel',
|
|
'clash': r'Clash',
|
|
'curse': r'Curse',
|
|
'day_night': r'Day|Night',
|
|
'decay': r'Decay',
|
|
'defiant': r'Defiant',
|
|
'demolish': r'Demolish',
|
|
'detain': r'Detain',
|
|
'detect': r'Detect',
|
|
'devour': r'Devour',
|
|
'disguise': r'Disguise',
|
|
'disturb': r'Disturb',
|
|
'dome': r'Dome',
|
|
'dredge': r'Dredge',
|
|
'emerge': r'Emerge',
|
|
'encore': r'Encore',
|
|
'endure': r'Endure',
|
|
'evoke': r'Evoke',
|
|
'evolve': r'Evolve',
|
|
'exalted': r'Exalted',
|
|
'exile': r'Exile',
|
|
'exploit': r'Exploit',
|
|
'extort': r'Extort',
|
|
'fairy': r'Fairy',
|
|
'fanatic': r'Fanatic',
|
|
'fathom': r'Fathom',
|
|
'fear': r'Fear',
|
|
'feline': r'Feline',
|
|
'flash': r'Flash',
|
|
'flight': r'Flight',
|
|
'foretell': r'Foretell',
|
|
'frenzy': r'Frenzy',
|
|
'fumble': r'Fumble',
|
|
'galvanize': r'Galvanize',
|
|
'gateway': r'Gateway',
|
|
'genesis': r'Genesis',
|
|
'graft': r'Graft',
|
|
'grave': r'Grave',
|
|
'grit': r'Grit',
|
|
'guardian': r'Guardian',
|
|
'harvest': r'Harvest',
|
|
'healer': r'Healer',
|
|
'heroic': r'Heroic',
|
|
'hideaway': r'Hideaway',
|
|
'hinterland': r'Hinterland',
|
|
'hoard': r'Hoard',
|
|
'hour': r'Hour',
|
|
'illusion': r'Illusion',
|
|
'immortal': r'Immortal',
|
|
'impulse': r'Impulse',
|
|
'inspiration': r'Inspiration',
|
|
'instill': r'Instill',
|
|
'iron': r'Iron',
|
|
'junk': r'Junk',
|
|
'kicker': r'Kicker',
|
|
'knight': r'Knight',
|
|
'land': r'Land',
|
|
'leech': r'Leech',
|
|
'lich': r'Lich',
|
|
'lifespan': r'Lifespan',
|
|
'lightning': r'Lightning',
|
|
'living': r'Living',
|
|
'lurk': r'Lurk',
|
|
'madness': r'Madness',
|
|
'manifest': r'Manifest',
|
|
'map': r'Map',
|
|
'meld': r'Meld',
|
|
'miracle': r'Miracle',
|
|
'mitosis': r'Mitosis',
|
|
'modular': r'Modular',
|
|
'moon': r'Moon',
|
|
'mother': r'Mother',
|
|
'morph': r'Morph',
|
|
'mutate': r'Mutate',
|
|
'ninja': r'Ninja',
|
|
'night': r'Night',
|
|
'nightmare': r'Nightmare',
|
|
'pact': r'Pact',
|
|
'paradox': r'Paradox',
|
|
'persist': r'Persist',
|
|
'pillage': r'Pillage',
|
|
'pivot': r'Pivot',
|
|
'planar': r'Planar',
|
|
'polar': r'Polar',
|
|
'pour': r'Pour',
|
|
'prey': r'Prey',
|
|
'priest': r'Priest',
|
|
'primer': r'Primer',
|
|
'probe': r'Probe',
|
|
'prosperity': r'Prosperity',
|
|
'psychic': r'Psychic',
|
|
'puppet': r'Puppet',
|
|
'quest': r'Quest',
|
|
'quote': r'Quote',
|
|
'rage': r'Rage',
|
|
'raid': r'Raid',
|
|
'raise': r'Raise',
|
|
'rally': r'Rally',
|
|
'rapid': r'Rapid',
|
|
'rat': r'Rat',
|
|
'rebound': r'Rebound',
|
|
'reckless': r'Reckless',
|
|
'recoup': r'Recoup',
|
|
'reflect': r'Reflect',
|
|
'refresh': r'Refresh',
|
|
'replicate': r'Replicate',
|
|
'reverberate': r'Reverberate',
|
|
'reviviant': r'Reviviant',
|
|
'rift': r'Rift',
|
|
'rip': r'Rip',
|
|
'ritual': r'Ritual',
|
|
'rite': r'Rite',
|
|
'rogue': r'Rogue',
|
|
'savant': r'Savant',
|
|
'scavenge': r'Scavenge',
|
|
'seek': r'Seek',
|
|
'shadow': r'Shadow',
|
|
'shards': r'Shards',
|
|
'skulk': r'Skulk',
|
|
'smelt': r'Smelt',
|
|
'snap': r'Snap',
|
|
'snow': r'Snow',
|
|
'spectacle': r'Spectacle',
|
|
'splice': r'Splice',
|
|
'spore': r'Spore',
|
|
'sprawl': r'Sprawl',
|
|
'stabilize': r'Stabilize',
|
|
'stasis': r'Stasis',
|
|
'storm': r'Storm',
|
|
'story': r'Story',
|
|
'substitute': r'Substitute',
|
|
'sunder': r'Sunder',
|
|
'surge': r'Surge',
|
|
'survive': r'Survive',
|
|
'swarm': r'Swarm',
|
|
'symbiosis': r'Symbiosis',
|
|
'synchronized': r'Synchronized',
|
|
'synth': r'Synth',
|
|
'table': r'Table',
|
|
'taint': r'Taint',
|
|
'tank': r'Tank',
|
|
'thorn': r'Thorn',
|
|
'thwart': r'Thwart',
|
|
'time': r'Time',
|
|
'tinker': r'Tinker',
|
|
'toxin': r'Toxin',
|
|
'trail': r'Trail',
|
|
'transfigure': r'Transfigure',
|
|
'transform': r'Transform',
|
|
'transport': r'Transport',
|
|
'trouble': r'Trouble',
|
|
'tunnel': r'Tunnel',
|
|
'unearth': r'Unearth',
|
|
'unleash': r'Unleash',
|
|
'unmask': r'Unmask',
|
|
'unstoppable': r'Unstoppable',
|
|
'urborg': r'Urborg',
|
|
'urgent': r'Urgent',
|
|
'utility': r'Utility',
|
|
'vengeful': r'Vengeful',
|
|
'vanish': r'Vanish',
|
|
'venom': r'Venom',
|
|
'victory': r'Victory',
|
|
'villainous': r'Villainous',
|
|
'vitalize': r'Vitalize',
|
|
'void': r'Void',
|
|
'voyage': r'Veoyage',
|
|
'ward': r'Ward',
|
|
'watch': r'Watch',
|
|
'weave': r'Weave',
|
|
'wed': r'Wed',
|
|
'whammy': r'Whammy',
|
|
'wild': r'Wild',
|
|
'will': r'Will',
|
|
'wisp': r'Wisp',
|
|
'witch': r'Witch',
|
|
'woe': r'Woe',
|
|
'wounded': r'Wounded',
|
|
'wrap': r'Wrap',
|
|
'wrought': r'Wrought',
|
|
'wurm': r'Wurm',
|
|
'wythe': r'Wythe',
|
|
}
|
|
|
|
# Define archetype patterns
|
|
self.archetype_patterns = {
|
|
'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',
|
|
'aether': r'Aether',
|
|
'pumpkin': r'Pumpkin',
|
|
'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',
|
|
'farmer': r'Farmer',
|
|
'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 for counter interactions
|
|
self.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',
|
|
}
|
|
|
|
# Trigger patterns
|
|
self.trigger_patterns = {
|
|
'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',
|
|
}
|
|
|
|
# Effect patterns
|
|
self.effect_patterns = {
|
|
'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',
|
|
}
|
|
|
|
def extract_mechanics(self, card: CardProfile) -> List[str]:
|
|
"""Extract mechanics from card type line and oracle text."""
|
|
mechanics = []
|
|
|
|
# Check type line for mechanics
|
|
if card.type_line:
|
|
for mechanic, pattern in self.mechanics_patterns.items():
|
|
if re.search(pattern, card.type_line, re.IGNORECASE):
|
|
mechanics.append(mechanic)
|
|
|
|
# Check oracle text for mechanics
|
|
if card.oracle_text:
|
|
for mechanic, pattern in self.mechanics_patterns.items():
|
|
if re.search(pattern, card.oracle_text, re.IGNORECASE):
|
|
if mechanic not in mechanics:
|
|
mechanics.append(mechanic)
|
|
|
|
return mechanics
|
|
|
|
def extract_archetypes(self, card: CardProfile) -> List[str]:
|
|
"""Extract archetypes from card subtypes."""
|
|
archetypes = []
|
|
|
|
if card.subtypes:
|
|
for archetype, pattern in self.archetype_patterns.items():
|
|
if re.search(pattern, card.subtypes, re.IGNORECASE):
|
|
archetypes.append(archetype)
|
|
|
|
return archetypes
|
|
|
|
def extract_targets(self, card: CardProfile) -> List[str]:
|
|
"""Extract target types from oracle text."""
|
|
targets = []
|
|
|
|
if card.oracle_text:
|
|
for target, pattern in self.target_types.items():
|
|
if re.search(pattern, card.oracle_text, re.IGNORECASE):
|
|
targets.append(target)
|
|
|
|
return targets
|
|
|
|
def extract_triggers(self, card: CardProfile) -> List[str]:
|
|
"""Extract trigger conditions from oracle text."""
|
|
triggers = []
|
|
|
|
if card.oracle_text:
|
|
for trigger, pattern in self.trigger_patterns.items():
|
|
if re.search(pattern, card.oracle_text, re.IGNORECASE):
|
|
triggers.append(trigger)
|
|
|
|
return triggers
|
|
|
|
def extract_effects(self, card: CardProfile) -> List[str]:
|
|
"""Extract game effects from oracle text."""
|
|
effects = []
|
|
|
|
if card.oracle_text:
|
|
for effect, pattern in self.effect_patterns.items():
|
|
if re.search(pattern, card.oracle_text, re.IGNORECASE):
|
|
effects.append(effect)
|
|
|
|
return effects
|
|
|
|
def extract_themes(self, card: CardProfile) -> List[str]:
|
|
"""Extract set themes based on card characteristics."""
|
|
themes = []
|
|
|
|
# Storm theme
|
|
if 'storm' in card.mechanics or 'storm' in card.oracle_text.lower():
|
|
themes.append('storm')
|
|
|
|
# Token theme
|
|
if any(e in card.effects for e in ['creates_token']):
|
|
themes.append('tokens')
|
|
|
|
# Mill theme
|
|
if any(t in card.triggers for t in ['draws_card']):
|
|
themes.append('draw')
|
|
|
|
# Life gain theme
|
|
if any(e in card.effects for e in ['gains_life']):
|
|
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 ['counters_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', 'untaps']):
|
|
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')
|
|
|
|
return themes
|
|
|
|
def profile_card(self, card_data: Dict[str, Any]) -> CardProfile:
|
|
"""Convert raw MTGJSON card data to CardProfile."""
|
|
# 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'])
|
|
|
|
# Parse colors
|
|
colors = None
|
|
if card_data.get('colors'):
|
|
colors = ', '.join(card_data['colors'])
|
|
|
|
# Parse color identity
|
|
color_identity = None
|
|
if card_data.get('colorIdentity'):
|
|
color_identity = ', '.join(card_data['colorIdentity'])
|
|
|
|
# Extract interactions
|
|
profile = CardProfile(
|
|
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,
|
|
colors=colors,
|
|
color_identity=color_identity,
|
|
power=card_data.get('power'),
|
|
toughness=card_data.get('toughness'),
|
|
loyalty=card_data.get('loyalty'),
|
|
set_code=card_data.get('set', {}).get('code') if card_data.get('set') else None,
|
|
set_id=card_data.get('setId', 0),
|
|
card_id=card_data.get('id', 0),
|
|
)
|
|
|
|
# Extract mechanics, archetypes, etc.
|
|
profile.mechanics = self.extract_mechanics(profile)
|
|
profile.archetypes = self.extract_archetypes(profile)
|
|
profile.targets = self.extract_targets(profile)
|
|
profile.triggers = self.extract_triggers(profile)
|
|
profile.effects = self.extract_effects(profile)
|
|
profile.themes = self.extract_themes(profile)
|
|
|
|
return profile
|
|
|
|
def find_synergies(self, card_a: CardProfile, card_b: CardProfile) -> List[Tuple[str, int, str]]:
|
|
"""
|
|
Find synergies between two cards.
|
|
|
|
Returns list of (synergy_type, strength, notes) tuples.
|
|
"""
|
|
synergies = []
|
|
|
|
# Same archetype synergy
|
|
if card_a.archetypes and card_b.archetypes:
|
|
common_archetypes = set(card_a.archetypes) & set(card_b.archetypes)
|
|
if common_archetypes:
|
|
synergies.append((
|
|
'archetype_support',
|
|
3,
|
|
f"Both are {', '.join(common_archetypes)}"
|
|
))
|
|
|
|
# Mechanic support
|
|
if card_a.mechanics and card_b.mechanics:
|
|
# If card_b has a mechanic that supports card_a's archetype
|
|
for mech in card_a.mechanics:
|
|
if mech in card_b.mechanics:
|
|
synergies.append((
|
|
'mechanic_support',
|
|
2,
|
|
f"Both have {mech}"
|
|
))
|
|
|
|
# Mana base synergy
|
|
if card_a.colors and card_b.colors:
|
|
# Check for color compatibility
|
|
colors_a = set(card_a.colors.split(','))
|
|
colors_b = set(card_b.colors.split(','))
|
|
|
|
if colors_a == colors_b:
|
|
synergies.append((
|
|
'mana_base',
|
|
4,
|
|
"Same color identity"
|
|
))
|
|
|
|
# Combo partner
|
|
if card_a.targets and card_b.triggers:
|
|
# If card_a targets creatures and card_b triggers on creatures
|
|
if 'creature' in card_a.targets and any(t in card_b.triggers for t in ['enters_battlefield', 'dies']):
|
|
synergies.append((
|
|
'combo_partner',
|
|
3,
|
|
"Card A targets creatures, Card B interacts with creature entry/death"
|
|
))
|
|
|
|
# Counter partner
|
|
if card_a.targets and card_b.targets:
|
|
# If they target different types, they complement each other
|
|
targets_a = set(card_a.targets)
|
|
targets_b = set(card_b.targets)
|
|
|
|
if targets_a != targets_b and targets_a & targets_b:
|
|
synergies.append((
|
|
'counter_partner',
|
|
2,
|
|
"Different target types provide coverage"
|
|
))
|
|
|
|
# Evolution chain
|
|
if card_a.name == card_b.name:
|
|
synergies.append((
|
|
'evolution_chain',
|
|
2,
|
|
"Same card name (reprint or different version)"
|
|
))
|
|
|
|
return synergies
|
|
|
|
def find_counters(self, card_a: CardProfile, card_b: CardProfile) -> List[Tuple[str, int, str]]:
|
|
"""
|
|
Find counter relationships between two cards.
|
|
|
|
Returns list of (counter_type, strength, notes) tuples.
|
|
"""
|
|
counters = []
|
|
|
|
# Different color identities
|
|
if card_a.color_identity and card_b.color_identity:
|
|
colors_a = set(card_a.color_identity.split(','))
|
|
colors_b = set(card_b.color_identity.split(','))
|
|
|
|
if colors_a != colors_b:
|
|
counters.append((
|
|
'mana_disadvantage',
|
|
2,
|
|
"Different color identities create strategic tension"
|
|
))
|
|
|
|
# Outclass
|
|
if card_a.power and card_b.power:
|
|
try:
|
|
power_a = int(card_a.power)
|
|
power_b = int(card_b.power)
|
|
|
|
if power_a > power_b + 1:
|
|
counters.append((
|
|
'outclass',
|
|
3,
|
|
f"Card A has higher power ({power_a} vs {power_b})"
|
|
))
|
|
elif power_b > power_a + 1:
|
|
counters.append((
|
|
'outclass',
|
|
3,
|
|
f"Card B has higher power ({power_b} vs {power_a})"
|
|
))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Counter role
|
|
if card_a.targets and 'creature' in card_a.targets:
|
|
if card_b.mechanics and any(m in card_b.mechanics for m in ['deathtouch', 'trample']):
|
|
counters.append((
|
|
'counter_role',
|
|
2,
|
|
"Card A targets creatures, Card B has combat keywords"
|
|
))
|
|
|
|
return counters
|
|
|
|
def find_evolution(self, card: CardProfile, all_cards: Dict[int, CardProfile]) -> List[Tuple[str, int, str]]:
|
|
"""
|
|
Find evolution relationships for a card.
|
|
|
|
Returns list of (evolution_type, strength, notes) tuples.
|
|
"""
|
|
evolutions = []
|
|
|
|
# Find reprints
|
|
for other_id, other_card in all_cards.items():
|
|
if other_id != card.card_id and card.name == other_card.name:
|
|
evolutions.append((
|
|
'reprinted',
|
|
2,
|
|
f"Reprint in {other_card.set_code} (set_id: {other_card.set_id})"
|
|
))
|
|
|
|
# Find transform pairs (same name, different face)
|
|
# This would require checking card_faces in the database
|
|
|
|
return evolutions
|
|
|
|
def build_interaction_graph(self, cards: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
"""
|
|
Build interaction graph for a batch of cards.
|
|
|
|
Returns dictionary with:
|
|
- mechanics: card_id -> mechanics list
|
|
- archetypes: card_id -> archetypes list
|
|
- synergies: (card_a, card_b) -> list of synergies
|
|
- counters: (card_a, card_b) -> list of counters
|
|
- evolutions: card_id -> list of evolutions
|
|
"""
|
|
# Profile all cards
|
|
profiles = {}
|
|
for card_data in cards:
|
|
if card_data.get('id'):
|
|
profile = self.profile_card(card_data)
|
|
profiles[profile.card_id] = profile
|
|
|
|
# Extract interactions
|
|
graph = {
|
|
'mechanics': {},
|
|
'archetypes': {},
|
|
'synergies': [],
|
|
'counters': [],
|
|
'evolutions': [],
|
|
}
|
|
|
|
# Extract mechanics and archetypes
|
|
for card_id, profile in profiles.items():
|
|
graph['mechanics'][card_id] = profile.mechanics
|
|
graph['archetypes'][card_id] = profile.archetypes
|
|
|
|
# Find synergies between all card pairs
|
|
card_ids = list(profiles.keys())
|
|
for i in range(len(card_ids)):
|
|
for j in range(i + 1, len(card_ids)):
|
|
card_a = profiles[card_ids[i]]
|
|
card_b = profiles[card_ids[j]]
|
|
|
|
synergies = self.find_synergies(card_a, card_b)
|
|
if synergies:
|
|
graph['synergies'].append({
|
|
'card_a': card_a.card_id,
|
|
'card_b': card_b.card_id,
|
|
'synergies': synergies,
|
|
})
|
|
|
|
# Find counters between all card pairs
|
|
for i in range(len(card_ids)):
|
|
for j in range(i + 1, len(card_ids)):
|
|
card_a = profiles[card_ids[i]]
|
|
card_b = profiles[card_ids[j]]
|
|
|
|
counters = self.find_counters(card_a, card_b)
|
|
if counters:
|
|
graph['counters'].append({
|
|
'card_a': card_a.card_id,
|
|
'card_b': card_b.card_id,
|
|
'counters': counters,
|
|
})
|
|
|
|
# Find evolutions for each card
|
|
for card_id, profile in profiles.items():
|
|
evolutions = self.find_evolution(profile, profiles)
|
|
if evolutions:
|
|
graph['evolutions'].append({
|
|
'card_id': card_id,
|
|
'evolutions': evolutions,
|
|
})
|
|
|
|
return graph
|
|
|
|
|
|
def main():
|
|
"""Test the rule engine with sample data."""
|
|
engine = MTGRuleEngine()
|
|
|
|
# Sample card data
|
|
sample_cards = [
|
|
{
|
|
'id': 1,
|
|
'name': 'Lightning Bolt',
|
|
'manaCost': '{R}',
|
|
'typeLine': 'Instant',
|
|
'oracleText': 'Lightning Bolt deals 3 damage to any target.',
|
|
'subtypes': [],
|
|
'supertypes': [],
|
|
'colors': ['R'],
|
|
'colorIdentity': ['R'],
|
|
'set': {'code': '2X2'},
|
|
'setId': 100,
|
|
},
|
|
{
|
|
'id': 2,
|
|
'name': 'Lightning Greaves',
|
|
'manaCost': '{1}{R}',
|
|
'typeLine': 'Artifact — Equipment',
|
|
'oracleText': 'Enchanted creature has hexproof and haste.\nEquip {1}',
|
|
'subtypes': ['Equipment'],
|
|
'supertypes': [],
|
|
'colors': ['R'],
|
|
'colorIdentity': ['R'],
|
|
'power': None,
|
|
'toughness': None,
|
|
'set': {'code': '10E'},
|
|
'setId': 200,
|
|
},
|
|
{
|
|
'id': 3,
|
|
'name': 'Elvish Archers',
|
|
'manaCost': '{G}',
|
|
'typeLine': 'Creature — Elf Ranger',
|
|
'oracleText': 'Elvish Archers can\'t be blocked by creatures with power 2 or less.\n{T}: Target creature gets -1/-1 until end of turn.',
|
|
'subtypes': ['Elf', 'Ranger'],
|
|
'supertypes': [],
|
|
'colors': ['G'],
|
|
'colorIdentity': ['G'],
|
|
'power': '1',
|
|
'toughness': '1',
|
|
'set': {'code': '5DN'},
|
|
'setId': 300,
|
|
},
|
|
{
|
|
'id': 4,
|
|
'name': 'Swords to Plowshares',
|
|
'manaCost': '{W}',
|
|
'typeLine': 'Enchantment',
|
|
'oracleText': 'Exile target creature. Its controller gains 1 life.',
|
|
'subtypes': [],
|
|
'supertypes': [],
|
|
'colors': ['W'],
|
|
'colorIdentity': ['W'],
|
|
'set': {'code': '2X2'},
|
|
'setId': 100,
|
|
},
|
|
]
|
|
|
|
# Build interaction graph
|
|
graph = engine.build_interaction_graph(sample_cards)
|
|
|
|
# Print results
|
|
print("=" * 60)
|
|
print("MTG Card Interaction Graph")
|
|
print("=" * 60)
|
|
|
|
print("\n📊 Mechanics:")
|
|
for card_id, mechanics in graph['mechanics'].items():
|
|
print(f" Card {card_id}: {mechanics}")
|
|
|
|
print("\n📊 Archetypes:")
|
|
for card_id, archetypes in graph['archetypes'].items():
|
|
print(f" Card {card_id}: {archetypes}")
|
|
|
|
print("\n🔗 Synergies:")
|
|
for synergy in graph['synergies']:
|
|
print(f" Cards {synergy['card_a']} ↔ {synergy['card_b']}:")
|
|
for syn_type, strength, notes in synergy['synergies']:
|
|
print(f" - {syn_type} (strength: {strength}): {notes}")
|
|
|
|
print("\n⚔️ Counters:")
|
|
for counter in graph['counters']:
|
|
print(f" Cards {counter['card_a']} ↔ {counter['card_b']}:")
|
|
for counter_type, strength, notes in counter['counters']:
|
|
print(f" - {counter_type} (strength: {strength}): {notes}")
|
|
|
|
print("\n🔄 Evolutions:")
|
|
for evolution in graph['evolutions']:
|
|
print(f" Card {evolution['card_id']}:")
|
|
for evol_type, strength, notes in evolution['evolutions']:
|
|
print(f" - {evol_type} (strength: {strength}): {notes}")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("✅ Interaction graph built successfully!")
|
|
print("=" * 60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|