Files
mtgonline/backend/scripts/create_card_interaction_graph.py
T

928 lines
38 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
MTG Card Interaction Graph Schema
Creates tables for categorizing cards based on their interactions with each other.
This creates a knowledge graph of card relationships including:
- Synergies (cards that work well together)
- Combos (cards that create powerful combinations)
- Counters (cards that counter each other)
- Evolution chains (cards that transform/evolve)
- Partners (commander partnerships, etc.)
- Archetypes (goblins, vampires, elves, etc.)
- Mechanics (first strike, trample, flying, etc.)
- Themes (storm, tokens, mill, etc.)
- Mana relationships (land support)
- Set themes (cards that share set-specific themes)
"""
from sqlalchemy import create_engine, text
DB_URL = "postgresql+psycopg2://mtgonline:mtgonline_pass@172.18.0.2:5432/mtgdata"
class CardInteractionGraph:
"""Creates and manages the card interaction knowledge graph."""
def __init__(self):
self.engine = create_engine(DB_URL)
self.conn = None
def connect(self):
"""Connect to database."""
self.conn = self.engine.connect()
print("✓ Connected to database")
def disconnect(self):
"""Disconnect from database."""
if self.conn:
self.conn.close()
self.engine.dispose()
print("✓ Disconnected from database")
def column_exists(self, table_name: str, column_name: str) -> bool:
"""Check if a column exists in a table."""
result = self.conn.execute(text("""
SELECT column_name
FROM information_schema.columns
WHERE table_name = :table AND column_name = :column
"""), {"table": table_name, "column": column_name})
return result.fetchone() is not None
def add_column(self, table_name: str, column_name: str, column_type: str):
"""Add a column to a table if it doesn't exist."""
if not self.column_exists(table_name, column_name):
self.conn.execute(text(f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type}"))
print(f" ✓ Added: {table_name}.{column_name} ({column_type})")
def create_table(self, table_sql: str):
"""Create a table if it doesn't exist."""
self.conn.execute(text(table_sql))
print(f" ✓ Created table: {table_sql.split('CREATE TABLE')[1].split('(')[0].strip()}")
def create_unique_constraint(self, constraint_sql: str):
"""Create a unique constraint if it doesn't exist."""
try:
self.conn.execute(text(constraint_sql))
print(f" ✓ Created constraint: {constraint_sql.split('ADD')[1].split('CONSTRAINT')[1].split('(')[0].strip()}")
except Exception as e:
# Constraint might already exist
pass
def create_index(self, index_sql: str):
"""Create an index if it doesn't exist."""
self.conn.execute(text(f"CREATE INDEX IF NOT EXISTS {index_sql}"))
print(f" ✓ Created index: {index_sql.split('ON ')[1].split(' ')[0]}")
def create_card_mechanics_table(self):
"""Create table for card mechanics (first strike, trample, flying, etc.)."""
print("\n📊 Creating mtg_card_mechanics table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_mechanics (
id SERIAL PRIMARY KEY,
card_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
mechanic VARCHAR(100) NOT NULL,
strength INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_id, mechanic)
)
""")
# Add indexes for frequently queried mechanics
indexes = [
"idx_mechanics_card_id ON mtg_card_mechanics(card_id)",
"idx_mechanics_mechanic ON mtg_card_mechanics(mechanic)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card mechanics table created")
def create_card_archetypes_table(self):
"""Create table for card archetypes (goblins, vampires, elves, etc.)."""
print("\n📊 Creating mtg_card_archetypes table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_archetypes (
id SERIAL PRIMARY KEY,
card_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
archetype VARCHAR(100) NOT NULL,
strength INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_id, archetype)
)
""")
indexes = [
"idx_archetypes_card_id ON mtg_card_archetypes(card_id)",
"idx_archetypes_archetype ON mtg_card_archetypes(archetype)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card archetypes table created")
def create_card_themes_table(self):
"""Create table for card themes (storm, tokens, mill, etc.)."""
print("\n📊 Creating mtg_card_themes table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_themes (
id SERIAL PRIMARY KEY,
card_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
theme VARCHAR(100) NOT NULL,
strength INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_id, theme)
)
""")
indexes = [
"idx_themes_card_id ON mtg_card_themes(card_id)",
"idx_themes_theme ON mtg_card_themes(theme)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card themes table created")
def create_card_relationships_table(self):
"""Create table for general card relationships."""
print("\n📊 Creating mtg_card_relationships table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_relationships (
id SERIAL PRIMARY KEY,
card_a_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
card_b_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
relationship_type VARCHAR(50) NOT NULL,
-- Types: synergy, combo, counter, evolution, partner, support, rival
strength INTEGER DEFAULT 1,
-- Strength: 1-5 (how strong the relationship is)
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_a_id, card_b_id, relationship_type)
)
""")
indexes = [
"idx_relationships_card_a ON mtg_card_relationships(card_a_id)",
"idx_relationships_card_b ON mtg_card_relationships(card_b_id)",
"idx_relationships_type ON mtg_card_relationships(relationship_type)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card relationships table created")
def create_card_synergies_table(self):
"""Create table for card synergies with detailed scoring."""
print("\n📊 Creating mtg_card_synergies table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_synergies (
id SERIAL PRIMARY KEY,
card_a_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
card_b_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
synergy_type VARCHAR(50) NOT NULL,
-- Types: mana_base, mechanic_support, archetype_support,
-- combo_partner, counter_partner, evolution_chain
strength INTEGER NOT NULL CHECK (strength BETWEEN 1 AND 5),
-- 1: Weak synergy, 5: Essential synergy
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_a_id, card_b_id, synergy_type)
)
""")
indexes = [
"idx_synergies_card_a ON mtg_card_synergies(card_a_id)",
"idx_synergies_card_b ON mtg_card_synergies(card_b_id)",
"idx_synergies_type ON mtg_card_synergies(synergy_type)",
"idx_synergies_strength ON mtg_card_synergies(strength)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card synergies table created")
def create_card_counters_table(self):
"""Create table for cards that counter each other."""
print("\n📊 Creating mtg_card_counters table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_counters (
id SERIAL PRIMARY KEY,
card_a_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
card_b_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
counter_type VARCHAR(50) NOT NULL,
-- Types: direct_counter, disadvantage, outclass, counter_role
strength INTEGER DEFAULT 1,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_a_id, card_b_id, counter_type)
)
""")
indexes = [
"idx_counters_card_a ON mtg_card_counters(card_a_id)",
"idx_counters_card_b ON mtg_card_counters(card_b_id)",
"idx_counters_type ON mtg_card_counters(counter_type)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card counters table created")
def create_card_evolution_table(self):
"""Create table for evolution chains (cards that transform/evolve)."""
print("\n📊 Creating mtg_card_evolution table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_evolution (
id SERIAL PRIMARY KEY,
card_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
evolved_card_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
evolution_type VARCHAR(50) NOT NULL,
-- Types: transform, evolve, double_sided, modal_dfc
strength INTEGER DEFAULT 1,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_id, evolved_card_id, evolution_type)
)
""")
indexes = [
"idx_evolution_card_id ON mtg_card_evolution(card_id)",
"idx_evolution_evolved_id ON mtg_card_evolution(evolved_card_id)",
"idx_evolution_type ON mtg_card_evolution(evolution_type)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card evolution table created")
def create_card_partners_table(self):
"""Create table for card partnerships (commander partners, etc.)."""
print("\n📊 Creating mtg_card_partners table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_partners (
id SERIAL PRIMARY KEY,
card_a_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
card_b_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
partnership_type VARCHAR(50) NOT NULL,
-- Types: commander_partner, double_faced, companion, partner_commander
strength INTEGER DEFAULT 1,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_a_id, card_b_id, partnership_type)
)
""")
indexes = [
"idx_partners_card_a ON mtg_card_partners(card_a_id)",
"idx_partners_card_b ON mtg_card_partners(card_b_id)",
"idx_partners_type ON mtg_card_partners(partnership_type)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card partners table created")
def create_card_mana_relations_table(self):
"""Create table for land/mana relationships."""
print("\n📊 Creating mtg_card_mana_relations table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_mana_relations (
id SERIAL PRIMARY KEY,
card_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
land_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
mana_type VARCHAR(10) NOT NULL,
-- Types: produces, taps_for, fetches, searches, enters_tapped
strength INTEGER DEFAULT 1,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_id, land_id, mana_type)
)
""")
indexes = [
"idx_mana_card_id ON mtg_card_mana_relations(card_id)",
"idx_mana_land_id ON mtg_card_mana_relations(land_id)",
"idx_mana_type ON mtg_card_mana_relations(mana_type)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card mana relations table created")
def create_card_set_relations_table(self):
"""Create table for set/theme relationships."""
print("\n📊 Creating mtg_card_set_relations table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_set_relations (
id SERIAL PRIMARY KEY,
card_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
set_id INTEGER REFERENCES mtg_sets(id) ON DELETE CASCADE,
theme VARCHAR(100) NOT NULL,
strength INTEGER DEFAULT 1,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_id, set_id, theme)
)
""")
indexes = [
"idx_setrel_card_id ON mtg_card_set_relations(card_id)",
"idx_setrel_set_id ON mtg_card_set_relations(set_id)",
"idx_setrel_theme ON mtg_card_set_relations(theme)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card set relations table created")
def create_card_power_relations_table(self):
"""Create table for power/toughness relationships."""
print("\n📊 Creating mtg_card_power_relations table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_power_relations (
id SERIAL PRIMARY KEY,
card_a_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
card_b_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
relation_type VARCHAR(50) NOT NULL,
-- Types: outclasses, matches, underclasses, counters_power
strength INTEGER DEFAULT 1,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_a_id, card_b_id, relation_type)
)
""")
indexes = [
"idx_power_card_a ON mtg_card_power_relations(card_a_id)",
"idx_power_card_b ON mtg_card_power_relations(card_b_id)",
"idx_power_type ON mtg_card_power_relations(relation_type)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card power relations table created")
def create_card_history_table(self):
"""Create table for card history and legacy relationships."""
print("\n📊 Creating mtg_card_history table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_history (
id SERIAL PRIMARY KEY,
card_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
related_card_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
history_type VARCHAR(50) NOT NULL,
-- Types: reprinted_in, previous_version, alternative_art,
-- superseded_by, predecessor
strength INTEGER DEFAULT 1,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_id, related_card_id, history_type)
)
""")
indexes = [
"idx_history_card_id ON mtg_card_history(card_id)",
"idx_history_related_id ON mtg_card_history(related_card_id)",
"idx_history_type ON mtg_card_history(history_type)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card history table created")
def create_card_interaction_stats_table(self):
"""Create summary statistics table for card interactions."""
print("\n📊 Creating mtg_card_interaction_stats table...")
self.create_table("""
CREATE TABLE IF NOT EXISTS mtg_card_interaction_stats (
id SERIAL PRIMARY KEY,
card_id INTEGER REFERENCES mtg_cards(id) ON DELETE CASCADE,
total_synergies INTEGER DEFAULT 0,
total_counters INTEGER DEFAULT 0,
total_evolution INTEGER DEFAULT 0,
total_partners INTEGER DEFAULT 0,
total_mechanics INTEGER DEFAULT 0,
total_archetypes INTEGER DEFAULT 0,
total_themes INTEGER DEFAULT 0,
avg_synergy_strength DECIMAL(3,2) DEFAULT 0.00,
max_synergy_strength INTEGER DEFAULT 0,
primary_archetype VARCHAR(100),
primary_theme VARCHAR(100),
computed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(card_id)
)
""")
indexes = [
"idx_stats_card_id ON mtg_card_interaction_stats(card_id)",
"idx_stats_total_synergies ON mtg_card_interaction_stats(total_synergies)",
"idx_stats_primary_archetype ON mtg_card_interaction_stats(primary_archetype)",
]
for idx in indexes:
self.create_index(idx)
print(" ✓ Card interaction stats table created")
def populate_mechanics_from_type_line(self):
"""Populate mechanics from card type lines and oracle text."""
print("\n🔄 Populating mechanics from type lines...")
# Define mechanics to look for in type lines
mechanics_map = {
'Flying': 'flying',
'Flying feet': 'flying',
'First strike': 'first_strike',
'Double strike': 'double_strike',
'Deathtouch': 'deathtouch',
'Lifelink': 'lifelink',
'Haste': 'haste',
'Trample': 'trample',
'Menace': 'menace',
'Vigilance': 'vigilance',
'Reach': 'reach',
'Indestructible': 'indestructible',
'Hexproof': 'hexproof',
'Shroud': 'shroud',
'Defender': 'defender',
'Etrata, the Silencer': 'first_strike', # Just as example
'Landfall': 'landfall',
'Delve': 'delve',
'Soulshift': 'soulshift',
'Suspend': 'suspend',
'Convoke': 'convoke',
'Rampage': 'rampage',
'Toxic': 'toxic',
'Crew': 'crew',
'Equip': 'equip',
'Annihilator': 'annihilator',
'Boltwall': 'boltwall',
'Boltwing': 'boltwing',
'Spectacle': 'spectacle',
'Prowess': 'prowess',
'Aftermath': 'aftermath',
'Adapt': 'adapt',
'Archon': 'archon',
'Amplify': 'amplify',
'Arrest': 'arrest',
'Awaken': 'awaken',
'Band with': 'banding',
'Bestow': 'bestow',
'Borrow': 'borrow',
'Burst': 'burst',
'Channel': 'channel',
'Clash': 'clash',
'Codex': 'codex',
'Crawl': 'crawl',
'Crew': 'crew',
'Curse': 'curse',
'Day': 'day_night',
'Decay': 'decay',
'Defiant': 'defiant',
'Demolish': 'demolish',
'Detain': 'detain',
'Detect': 'detect',
'Devour': 'devour',
'Disguise': 'disguise',
'Disturb': 'disturb',
'Dome': 'dome',
'Double strike': 'double_strike',
'Dredge': 'dredge',
'Emerge': 'emerge',
'Encore': 'encore',
'Endure': 'endure',
'Evoke': 'evoke',
'Evolve': 'evolve',
'Exalted': 'exalted',
'Exile': 'exile',
'Exploit': 'exploit',
'Extort': 'extort',
'Fairy': 'fairy',
'Fanatic': 'fanatic',
'Fathom': 'fathom',
'Fear': 'fear',
'Feline': 'feline',
'Flash': 'flash',
'Flight': 'flight',
'Foretell': 'foretell',
'Frenzy': 'frenzy',
'Fumble': 'fumble',
'Galvanize': 'galvanize',
'Gateway': 'gateway',
'Genesis': 'genesis',
'Graft': 'graft',
'Grave': 'grave',
'Grit': 'grit',
'Guardian': 'guardian',
'Harvest': 'harvest',
'Healer': 'healer',
'Heroic': 'heroic',
'Hideaway': 'hideaway',
'Hinterland': 'hinterland',
'Hoard': 'hoard',
'Hour': 'hour',
'Illusion': 'illusion',
'Immortal': 'immortal',
'Impulse': 'impulse',
'Inspiration': 'inspiration',
'Instill': 'instill',
'Iron': 'iron',
'Junk': 'junk',
'Kicker': 'kicker',
'Knight': 'knight',
'Land': 'land',
'Leech': 'leech',
'Lich': 'lich',
'Lifespan': 'lifespan',
'Lightning': 'lightning',
'Living': 'living',
'Lurk': 'lurk',
'Madness': 'madness',
'Manifest': 'manifest',
'Map': 'map',
'Meld': 'meld',
'Miracle': 'miracle',
'Mitosis': 'mitosis',
'Modular': 'modular',
'Moon': 'moon',
'Mother': 'mother',
'Morph': 'morph',
'Mutate': 'mutate',
'Ninja': 'ninja',
'Night': 'night',
'Nightmare': 'nightmare',
'Pact': 'pact',
'Paradox': 'paradox',
'Persist': 'persist',
'Pillage': 'pillage',
'Pivot': 'pivot',
'Planar': 'planar',
'Polar': 'polar',
'Pour': 'pour',
'Prey': 'prey',
'Prey': 'prey',
'Priest': 'priest',
'Primer': 'primer',
'Probe': 'probe',
'Prosperity': 'prosperity',
'Psychic': 'psychic',
'Puppet': 'puppet',
'Quest': 'quest',
'Quote': 'quote',
'Rage': 'rage',
'Raid': 'raid',
'Raise': 'raise',
'Rally': 'rally',
'Rapid': 'rapid',
'Rat': 'rat',
'Rebound': 'rebound',
'Reckless': 'reckless',
'Recoup': 'recoup',
'Reflect': 'reflect',
'Refresh': 'refresh',
'Replicate': 'replicate',
'Reverberate': 'reverberate',
'Reveillant': 'reviviant',
'Rift': 'rift',
'Rip': 'rip',
'Ritual': 'ritual',
'Rite': 'rite',
'Rogue': 'rogue',
'Savant': 'savant',
'Scavenge': 'scavenge',
'Seek': 'seek',
'Shadow': 'shadow',
'Shards': 'shards',
'Skulk': 'skulk',
'Smelt': 'smelt',
'Snap': 'snap',
'Snow': 'snow',
'Spectacle': 'spectacle',
'Splice': 'splice',
'Spore': 'spore',
'Sprawl': 'sprawl',
'Stabilize': 'stabilize',
'Stasis': 'stasis',
'Storm': 'storm',
'Story': 'story',
'Substitute': 'substitute',
'Sunder': 'sunder',
'Surge': 'surge',
'Survive': 'survive',
'Swarm': 'swarm',
'Symbiosis': 'symbiosis',
'Synchronized': 'synchronized',
'Synth': 'synth',
'Table': 'table',
'Taint': 'taint',
'Tank': 'tank',
'Thorn': 'thorn',
'Thwart': 'thwart',
'Time': 'time',
'Tinker': 'tinker',
'Toxin': 'toxin',
'Trail': 'trail',
'Transfigure': 'transfigure',
'Transform': 'transform',
'Transport': 'transport',
'Trouble': 'trouble',
'Tunnel': 'tunnel',
'Unearth': 'unearth',
'Unleash': 'unleash',
'Unmask': 'unmask',
'Unstoppable': 'unstoppable',
'Urborg': 'urborg',
'Urgent': 'urgent',
'Utility': 'utility',
'Vengeful': 'vengeful',
'Vanish': 'vanish',
'Vanish': 'vanish',
'Venom': 'venom',
'Victory': 'victory',
'Villainous': 'villainous',
'Vitalize': 'vitalize',
'Void': 'void',
'Voyage': 'voyage',
'Ward': 'ward',
'Watch': 'watch',
'Weave': 'weave',
'Wed': 'wed',
'Whammy': 'whammy',
'Wild': 'wild',
'Will': 'will',
'Wisp': 'wisp',
'Witch': 'witch',
'Woe': 'woe',
'Wounded': 'wounded',
'Wrap': 'wrap',
'Wrought': 'wrought',
'Wurm': 'wurm',
'Wythe': 'wythe',
}
# Insert mechanics from type lines
self.conn.execute(text("""
INSERT INTO mtg_card_mechanics (card_id, mechanic)
SELECT DISTINCT c.id, LOWER(UNNEST(string_to_array(c.subtypes, ',')))
FROM mtg_cards c
WHERE c.subtypes IS NOT NULL
AND c.subtypes != ''
AND c.subtypes != 'null'
AND LOWER(UNNEST(string_to_array(c.subtypes, ','))) IN (
'flying', 'first_strike', 'double_strike', 'deathtouch', 'lifelink',
'haste', 'trample', 'menace', 'vigilance', 'reach', 'indestructible',
'hexproof', 'shroud', 'defender', 'landfall', 'delve', 'soulshift',
'suspend', 'convoke', 'rampage', 'toxic', 'crew', 'equip', 'annihilator',
'spectacle', 'prowess', 'aftermath', 'adapt', 'amplify', 'awaken',
'banding', 'bestow', 'burst', 'channel', 'clash', 'crawl', 'curse',
'day_night', 'decay', 'defiant', 'demolish', 'detain', 'detect',
'devour', 'disguise', 'disturb', 'dome', 'double_strike', 'dredge',
'emerge', 'encore', 'endure', 'evoke', 'evolve', 'exalted', 'exile',
'exploit', 'extort', 'fairy', 'fanatic', 'fathom', 'fear', 'feline',
'flash', 'flight', 'foretell', 'frenzy', 'fumble', 'galvanize',
'gateway', 'genesis', 'graft', 'grave', 'grit', 'guardian', 'harvest',
'healer', 'heroic', 'hideaway', 'hinterland', 'hoard', 'hour', 'illusion',
'immortal', 'impulse', 'inspiration', 'instill', 'iron', 'junk', 'kicker',
'knight', 'land', 'leech', 'lich', 'lifespan', 'lightning', 'living',
'lurk', 'madness', 'manifest', 'map', 'meld', 'miracle', 'mitosis',
'modular', 'moon', 'mother', 'morph', 'mutate', 'ninja', 'night',
'nightmare', 'pact', 'paradox', 'persist', 'pillage', 'pivot', 'planar',
'polar', 'pour', 'prey', 'priest', 'primer', 'probe', 'prosperity',
'psychic', 'puppet', 'quest', 'quote', 'rage', 'raid', 'raise', 'rally',
'rapid', 'rat', 'rebound', 'reckless', 'recoup', 'reflect', 'refresh',
'replicate', 'reverberate', 'reviviant', 'rift', 'rip', 'ritual', 'rite',
'rogue', 'savant', 'scavenge', 'seek', 'shadow', 'shards', 'skulk',
'smelt', 'snap', 'snow', 'spectacle', 'splice', 'spore', 'sprawl',
'stabilize', 'stasis', 'storm', 'story', 'substitute', 'sunder', 'surge',
'survive', 'swarm', 'symbiosis', 'synchronized', 'synth', 'table', 'taint',
'tank', 'thorn', 'thwart', 'time', 'tinker', 'toxin', 'trail', 'transfigure',
'transform', 'transport', 'trouble', 'tunnel', 'unearth', 'unleash', 'unmask',
'unstoppable', 'urborg', 'urgent', 'utility', 'vengeful', 'vanish', 'venom',
'victory', 'villainous', 'vitalize', 'void', 'voyage', 'ward', 'watch', 'weave',
'wed', 'whammy', 'wild', 'will', 'wisp', 'witch', 'woe', 'wounded', 'wrap',
'wrought', 'wurm', 'wythe'
)
ON CONFLICT DO NOTHING
"""))
print(" ✓ Populated mechanics from type lines")
def populate_archetypes_from_subtypes(self):
"""Populate archetypes from card subtypes."""
print("\n🔄 Populating archetypes from subtypes...")
# Define archetype mappings
archetype_map = {
'Goblin': 'goblins',
'Elf': 'elves',
'Vampire': 'vampires',
'Angel': 'angels',
'Dragon': 'dragons',
'Human': 'humans',
'Zombie': 'zombies',
'Soldier': 'soldiers',
'Knight': 'knights',
'Wizard': 'wizards',
'Spirit': 'spirits',
'Demon': 'demons',
'Snake': 'snakes',
'Cat': 'cats',
'Wolf': 'wolves',
'Bear': 'bears',
'Bird': 'birds',
'Insect': 'insects',
'Horror': 'horrors',
'Goat': 'goats',
'Ox': 'oxen',
'Elephant': 'elephants',
'Whale': 'whales',
'Shark': 'sharks',
'Fish': 'fish',
'Serpent': 'serpents',
'Lizard': 'lizards',
'Scorpion': 'scorpions',
'Spider': 'spiders',
'Rat': 'rats',
'Snake': 'snakes',
'Drake': 'drakes',
'Wyvern': 'wyverns',
'Phoenix': 'phoenixes',
'Lynx': 'lynxes',
'Jaguar': 'jaguars',
'Hydra': 'hydrae',
'Leviathan': 'leviathans',
'Kraken': 'krakens',
'Cyclops': 'cyclopes',
'Golem': 'golems',
'Homunculus': 'homunculi',
'Clay': 'clay',
'Construct': 'constructs',
'Myr': 'myr',
'Aether': 'aether',
'Pumpkin': 'pumpkins',
'Pirate': 'pirates',
'Pegasus': 'pegasuses',
'Unicorn': 'unicorns',
'Centaur': 'centaurs',
'Merfolk': 'merfolk',
'Mermaid': 'mermaids',
'Naga': 'nagas',
'Satyr': 'satyrs',
'Dryad': 'dryads',
'Treant': 'treants',
'Elemental': 'elementals',
'Fiend': 'fiends',
'Imp': 'imps',
'Faerie': 'faeries',
'Minion': 'minions',
'Abomination': 'abominations',
'Beast': 'beasts',
'Demigod': 'demigods',
'God': 'gods',
'Avatar': 'avatars',
'Guardian': 'guardians',
'Warrior': 'warriors',
'Rogue': 'rogues',
'Artificer': 'artificers',
'Bard': 'bards',
'Monk': 'monks',
'Ninja': 'ninjas',
'Samurai': 'samurai',
'Assassin': 'assassins',
'Thief': 'thieves',
'Acrobat': 'acrobats',
'Explorer': 'explorers',
'Farmer': 'farmers',
'Myth': 'myths',
'Illusion': 'illusions',
'Mirror': 'mirrors',
'Phantom': 'phantoms',
'Shapeshifter': 'shapeshifters',
'Shaman': 'shamans',
'Shark': 'sharks',
'Skeleton': 'skeletons',
'Slime': 'slimes',
'Squirrel': 'squirrels',
'Troll': 'trolls',
'Tyrannosaur': 'tyrannosaurs',
'Utility': 'utilities',
'Warrior': 'warriors',
'Wraith': 'wraiths',
'Wurm': 'wurms',
}
# Insert archetypes
self.conn.execute(text("""
INSERT INTO mtg_card_archetypes (card_id, archetype)
SELECT DISTINCT c.id, LOWER(UNNEST(string_to_array(c.subtypes, ',')))
FROM mtg_cards c
WHERE c.subtypes IS NOT NULL
AND c.subtypes != ''
AND c.subtypes != 'null'
AND LOWER(UNNEST(string_to_array(c.subtypes, ','))) IN (
'goblin', 'elf', 'vampire', 'angel', 'dragon', 'human', 'zombie',
'soldier', 'knight', 'wizard', 'spirit', 'demon', 'snake', 'cat',
'wolf', 'bear', 'bird', 'insect', 'horror', 'goat', 'ox', 'elephant',
'whale', 'shark', 'fish', 'serpent', 'lizard', 'scorpion', 'spider',
'rat', 'drake', 'wyvern', 'phoenix', 'lynx', 'jaguar', 'hydra',
'leviathan', 'kraken', 'cyclops', 'golem', 'homunculus', 'clay',
'construct', 'myr', 'aether', 'pumpkin', 'pirate', 'pegasus',
'unicorn', 'centaur', 'merfolk', 'mermaid', 'naga', 'satyr', 'dryad',
'treant', 'elemental', 'fiend', 'imp', 'faerie', 'minion', 'abomination',
'beast', 'demigod', 'god', 'avatar', 'guardian', 'warrior', 'rogue',
'artificer', 'bard', 'monk', 'ninja', 'samurai', 'assassin', 'thief',
'acrobat', 'explorer', 'farmer', 'myth', 'illusion', 'mirror', 'phantom',
'shapeshifter', 'shaman', 'skeleton', 'slime', 'squirrel', 'troll',
'tyrannosaur', 'wraith', 'wurm'
)
ON CONFLICT DO NOTHING
"""))
print(" ✓ Populated archetypes from subtypes")
def populate_themes_from_oracle_text(self):
"""Populate themes from oracle text patterns."""
print("\n🔄 Populating themes from oracle text...")
# Define theme patterns to search for
theme_patterns = [
('storm', 'oracle_text LIKE \'%cast %spell%\' OR oracle_text LIKE \'%copy spell%\' OR oracle_text LIKE \'%cast additional spell%\''),
('tokens', 'oracle_text LIKE \'%create %token%\' OR oracle_text LIKE \'%put %token%\' OR oracle_text LIKE \'%you get %token%\''),
('mill', 'oracle_text LIKE \'%mill%\' OR oracle_text LIKE \'%put cards from top of your library into your graveyard%\''),
('flicker', 'oracle_text LIKE \'%exile %and return%\' OR oracle_text LIKE \'%unmark%\' OR oracle_text LIKE \'%bounce%\''),
('draw', 'oracle_text LIKE \'%draw %cards%\' OR oracle_text LIKE \'%you may draw%\''),
('life_gain', 'oracle_text LIKE \'%gain life%\' OR oracle_text LIKE \'%you gain % life%\''),
('board_wipe', 'oracle_text LIKE \'%all creatures get -%\' OR oracle_text LIKE \'%destroy all creatures%\''),
('deck_out', 'oracle_text LIKE \'%lose the game%\' OR oracle_text LIKE \'%you lose the game%\''),
('reanimate', 'oracle_text LIKE \'%put card from graveyard%\' OR oracle_text LIKE \'%return card from graveyard%\''),
('countermagic', 'oracle_text LIKE \'%counter target spell%\' OR oracle_text LIKE \'%counter target spell%\''),
('card_advantage', 'oracle_text LIKE \'%draw %card%\' OR oracle_text LIKE \'%draw two cards%\''),
('mana_acceleration', 'oracle_text LIKE \'%add %mana%\' OR oracle_text LIKE \'%add {C}%\' OR oracle_text LIKE \'%add {R}%\' OR oracle_text LIKE \'%add {U}%\' OR oracle_text LIKE \'%add {B}%\' OR oracle_text LIKE \'%add {G}%\' OR oracle_text LIKE \'%add {W}%\''),
('combat_tricks', 'oracle_text LIKE \'%gain first strike%\' OR oracle_text LIKE \'%gain trample%\' OR oracle_text LIKE \'%gain deathtouch%\' OR oracle_text LIKE \'%gain lifelink%\' OR oracle_text LIKE \'%gain vigilance%\' OR oracle_text LIKE \'%until end of turn%\''),
('etb_effects', 'oracle_text LIKE \'%when %enters the battlefield%\' OR oracle_text LIKE \'%enters the battlefield with%\' OR oracle_text LIKE \'%enters the battlefield tapped%\''),
('ltb_effects', 'oracle_text LIKE \'%when %leaves the battlefield%\' OR oracle_text LIKE \'%leaves the battlefield, exile%\'\'' ),
('synergy', 'oracle_text LIKE \'%copy %spell%\' OR oracle_text LIKE \'%create %token%\' OR oracle_text LIKE \'%gain % life%\'' ),
]
# This is a complex query, let's simplify for demonstration
# In production, you'd want to use more sophisticated NLP or pattern matching
print(" ️ Theme population requires complex pattern matching")
print(" ️ Skipping for now - can be added as a separate step")
def run_migration(self):
"""Run the full migration."""
print("=" * 60)
print("🚀 Creating Card Interaction Graph Schema")
print("=" * 60)
self.connect()
# Create all interaction tables
self.create_card_mechanics_table()
self.create_card_archetypes_table()
self.create_card_themes_table()
self.create_card_relationships_table()
self.create_card_synergies_table()
self.create_card_counters_table()
self.create_card_evolution_table()
self.create_card_partners_table()
self.create_card_mana_relations_table()
self.create_card_set_relations_table()
self.create_card_power_relations_table()
self.create_card_history_table()
self.create_card_interaction_stats_table()
# Populate some data
self.populate_mechanics_from_type_line()
self.populate_archetypes_from_subtypes()
# Skip themes for now (complex pattern matching)
# self.populate_themes_from_oracle_text()
self.disconnect()
print("\n" + "=" * 60)
print("✅ Card Interaction Graph created successfully!")
print("=" * 60)
def main():
"""Main entry point."""
graph = CardInteractionGraph()
graph.run_migration()
if __name__ == "__main__":
main()