Final project commit: MTGJSON data integration and backend API
This commit is contained in:
@@ -0,0 +1,959 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user