388 lines
14 KiB
Python
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()
|