Files
mtgonline/backend/scripts/interaction_recommender.py
T

388 lines
14 KiB
Python

"""
MTG Card Interaction Recommender
Generates card recommendations based on interaction data.
Provides synergy suggestions, archetype cards, and card-like-this recommendations.
"""
from typing import List, Dict, Optional
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from card_profile_extractor import CardProfileExtractor
from interaction_determinator import InteractionDeterminator
class InteractionRecommender:
"""
Generates card recommendations based on interaction data.
Provides:
- Synergy recommendations for a specific card
- Archetype cards for a given archetype
- Similar cards based on profiles
- Deck building suggestions
"""
def __init__(self, db_url: str):
"""
Initialize the recommender.
Args:
db_url: PostgreSQL database URL
"""
self.db_url = db_url
self.engine = create_engine(db_url)
self.SessionLocal = sessionmaker(bind=self.engine)
self.profile_extractor = CardProfileExtractor()
self.determinator = InteractionDeterminator()
def get_card_profile(self, card_id: int) -> Optional[CardProfile]:
"""
Get a card profile from the database.
Args:
card_id: Card ID
Returns:
CardProfile or None if not found
"""
db = self.SessionLocal()
try:
query = text("""
SELECT c.*, s.code as set_code
FROM mtg_cards c
JOIN mtg_sets s ON c.set_id = s.id
WHERE c.id = :card_id
""")
result = db.execute(query, {"card_id": card_id}).fetchone()
if result:
card_data = dict(result._mapping)
# Extract profile from raw data
profile = self.profile_extractor.extract_profile(card_data)
return profile
return None
finally:
db.close()
def get_card_by_id(self, card_id: int) -> Optional[Dict]:
"""
Get raw card data from the database.
Args:
card_id: Card ID
Returns:
Card dictionary or None if not found
"""
db = self.SessionLocal()
try:
query = text("""
SELECT c.*, s.code as set_code, s.name as set_name
FROM mtg_cards c
JOIN mtg_sets s ON c.set_id = s.id
WHERE c.id = :card_id
""")
result = db.execute(query, {"card_id": card_id}).fetchone()
if result:
return dict(result._mapping)
return None
finally:
db.close()
def get_synergies_for_card(self, card_id: int) -> List[Dict]:
"""
Get synergy data for a card from the database.
Args:
card_id: Card ID
Returns:
List of synergy dictionaries
"""
db = self.SessionLocal()
try:
query = text("""
SELECT cs.*,
ca.name as card_a_name, ca.type_line as card_a_type_line,
cb.name as card_b_name, cb.type_line as card_b_type_line
FROM mtg_card_synergies cs
JOIN mtg_cards ca ON cs.card_a_id = ca.id
JOIN mtg_cards cb ON cs.card_b_id = cb.id
WHERE cs.card_a_id = :card_id OR cs.card_b_id = :card_id
ORDER BY cs.strength DESC, cs.confidence DESC
LIMIT :limit
""")
results = db.execute(query, {
"card_id": card_id,
"limit": 100
}).fetchall()
return [dict(row._mapping) for row in results]
finally:
db.close()
def get_counters_for_card(self, card_id: int) -> List[Dict]:
"""
Get counter data for a card from the database.
Args:
card_id: Card ID
Returns:
List of counter dictionaries
"""
db = self.SessionLocal()
try:
query = text("""
SELECT cc.*,
ca.name as card_a_name, ca.type_line as card_a_type_line,
cb.name as card_b_name, cb.type_line as card_b_type_line
FROM mtg_card_counters cc
JOIN mtg_cards ca ON cc.card_a_id = ca.id
JOIN mtg_cards cb ON cc.card_b_id = cb.id
WHERE cc.card_a_id = :card_id OR cc.card_b_id = :card_id
ORDER BY cc.strength DESC, cc.confidence DESC
LIMIT :limit
""")
results = db.execute(query, {
"card_id": card_id,
"limit": 100
}).fetchall()
return [dict(row._mapping) for row in results]
finally:
db.close()
def recommend_synergies(self, card_id: int, max_results: int = 20) -> List[Dict]:
"""
Recommend cards that synergize with a given card.
Args:
card_id: Card ID to find synergies for
max_results: Maximum number of recommendations
Returns:
List of recommendation dictionaries
"""
synergies = self.get_synergies_for_card(card_id)
recommendations = []
seen_cards = set()
for synergy in synergies:
# Determine which card is the "other" card
if synergy['card_a_id'] == card_id:
other_card_id = synergy['card_b_id']
other_card_name = synergy['card_b_name']
other_card_type = synergy['card_b_type_line']
else:
other_card_id = synergy['card_a_id']
other_card_name = synergy['card_a_name']
other_card_type = synergy['card_a_type_line']
# Skip if already seen
if other_card_id in seen_cards:
continue
seen_cards.add(other_card_id)
recommendations.append({
'card_id': other_card_id,
'card_name': other_card_name,
'card_type_line': other_card_type,
'synergy_type': synergy['synergy_type'],
'strength': synergy['strength'],
'confidence': synergy['confidence'],
'notes': synergy['notes'],
'recommendation_type': 'synergy',
})
# Sort by strength and confidence
recommendations.sort(key=lambda r: (r['strength'], r['confidence']), reverse=True)
return recommendations[:max_results]
def recommend_archetype_cards(self, archetype: str, max_results: int = 20) -> List[Dict]:
"""
Recommend cards that fit a specific archetype.
Args:
archetype: Archetype name (e.g., 'goblin', 'elf')
max_results: Maximum number of recommendations
Returns:
List of recommendation dictionaries
"""
db = self.SessionLocal()
try:
query = text("""
SELECT c.*, s.code as set_code, s.name as set_name
FROM mtg_cards c
JOIN mtg_sets s ON c.set_id = s.id
WHERE c.subtypes LIKE :archetype
ORDER BY c.id
LIMIT :limit
""")
results = db.execute(query, {
"archetype": f"%{archetype}%",
"limit": max_results
}).fetchall()
recommendations = []
for result in results:
card_data = dict(result._mapping)
recommendations.append({
'card_id': card_data['id'],
'card_name': card_data['name'],
'card_type_line': card_data['type_line'],
'set_code': card_data['set_code'],
'set_name': card_data['set_name'],
'recommendation_type': 'archetype',
'archetype': archetype,
'confidence': 0.8,
'notes': f"Matches {archetype} archetype",
})
return recommendations
finally:
db.close()
def recommend_similar_cards(self, card_id: int, max_results: int = 20) -> List[Dict]:
"""
Recommend cards similar to a given card.
Args:
card_id: Card ID to find similar cards for
max_results: Maximum number of recommendations
Returns:
List of recommendation dictionaries
"""
card_data = self.get_card_by_id(card_id)
if not card_data:
return []
profile = self.profile_extractor.extract_profile(card_data)
# Get similar cards based on archetype and mechanics
db = self.SessionLocal()
try:
recommendations = []
seen_cards = set()
# Get cards with matching archetypes
if profile.archetypes:
for archetype in profile.archetypes:
query = text("""
SELECT c.*, s.code as set_code
FROM mtg_cards c
JOIN mtg_sets s ON c.set_id = s.id
WHERE c.subtypes LIKE :archetype
AND c.id != :card_id
LIMIT :limit
""")
results = db.execute(query, {
"archetype": f"%{archetype}%",
"card_id": card_id,
"limit": max_results * 2
}).fetchall()
for result in results:
card = dict(result._mapping)
if card['id'] not in seen_cards:
seen_cards.add(card['id'])
recommendations.append({
'card_id': card['id'],
'card_name': card['name'],
'card_type_line': card['type_line'],
'set_code': card['set_code'],
'recommendation_type': 'similar',
'reason': f"Same archetype: {archetype}",
'confidence': 0.7,
})
# Get cards with matching mechanics
if profile.mechanics:
for mechanic in profile.mechanics[:3]: # Limit to top 3 mechanics
query = text("""
SELECT c.*, s.code as set_code
FROM mtg_cards c
JOIN mtg_sets s ON c.set_id = s.id
WHERE c.oracle_text LIKE :mechanic
AND c.id != :card_id
LIMIT :limit
""")
results = db.execute(query, {
"mechanic": f"%{mechanic}%",
"card_id": card_id,
"limit": max_results
}).fetchall()
for result in results:
card = dict(result._mapping)
if card['id'] not in seen_cards:
seen_cards.add(card['id'])
recommendations.append({
'card_id': card['id'],
'card_name': card['name'],
'card_type_line': card['type_line'],
'set_code': card['set_code'],
'recommendation_type': 'similar',
'reason': f"Has mechanic: {mechanic}",
'confidence': 0.6,
})
# Sort by confidence
recommendations.sort(key=lambda r: r['confidence'], reverse=True)
return recommendations[:max_results]
finally:
db.close()
def get_deck_recommendations(self, card_id: int, max_results: int = 10) -> Dict:
"""
Get deck building recommendations for a card.
Args:
card_id: Card ID
max_results: Maximum number of recommendations
Returns:
Dictionary with synergy cards, archetype cards, and similar cards
"""
# Get synergy cards
synergy_cards = self.recommend_synergies(card_id, max_results)
# Get archetype cards
card_data = self.get_card_by_id(card_id)
archetype_cards = []
if card_data and card_data.get('subtypes'):
# Extract first archetype
archetypes = [a.strip() for a in card_data['subtypes'].split(',')]
if archetypes:
archetype_cards = self.recommend_archetype_cards(archetypes[0], max_results)
# Get similar cards
similar_cards = self.recommend_similar_cards(card_id, max_results)
return {
'synergy_cards': synergy_cards,
'archetype_cards': archetype_cards,
'similar_cards': similar_cards,
'total_recommendations': len(synergy_cards) + len(archetype_cards) + len(similar_cards),
}
def close(self):
"""Close database connection."""
self.engine.dispose()