Final project commit: MTGJSON data integration and backend API

This commit is contained in:
wall-o
2026-07-20 01:09:45 +00:00
parent b170dfd577
commit db01e29a54
37 changed files with 9674 additions and 198 deletions
@@ -0,0 +1,927 @@
"""
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()