928 lines
38 KiB
Python
928 lines
38 KiB
Python
"""
|
||
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()
|