Files
mtgonline/backend/scripts/card_interaction_rule_engine.py
T

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()