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