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