315 lines
12 KiB
Python
315 lines
12 KiB
Python
"""
|
|
MTG Card Interaction Determinator
|
|
|
|
Determines specific card interactions (synergies, counters, evolutions)
|
|
using game rules and card profiles.
|
|
"""
|
|
from typing import List, Tuple, Optional
|
|
from dataclasses import dataclass
|
|
from card_profile_extractor import CardProfile
|
|
|
|
|
|
@dataclass
|
|
class InteractionResult:
|
|
"""Result of an interaction determination."""
|
|
card_a_id: int
|
|
card_b_id: int
|
|
interaction_type: str # 'synergy', 'counter', 'evolution'
|
|
strength: int # 1-5
|
|
confidence: float # 0.0-1.0
|
|
notes: str
|
|
metadata: dict = None
|
|
|
|
def __post_init__(self):
|
|
if self.metadata is None:
|
|
self.metadata = {}
|
|
|
|
|
|
class InteractionDeterminator:
|
|
"""
|
|
Determines card interactions using game rules.
|
|
|
|
Uses deterministic rules based on:
|
|
- Shared archetypes (e.g., both are goblins)
|
|
- Supporting mechanics (e.g., one has haste, the other has trample)
|
|
- Mana compatibility (same colors work well together)
|
|
- Target/trigger relationships (one targets, the other interacts)
|
|
- Evolution chains (same card, different versions)
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the determinator."""
|
|
pass
|
|
|
|
def determine_synergies(
|
|
self, profile_a: CardProfile, profile_b: CardProfile
|
|
) -> List[InteractionResult]:
|
|
"""
|
|
Determine synergies between two cards.
|
|
|
|
Synergies are positive interactions where cards work well together.
|
|
|
|
Examples:
|
|
- Both are goblins (archetype synergy)
|
|
- One has haste, the other has trample (mechanic synergy)
|
|
- Same color identity (mana synergy)
|
|
- One targets creatures, the other buffs creatures (combo synergy)
|
|
|
|
Args:
|
|
profile_a: First card profile
|
|
profile_b: Second card profile
|
|
|
|
Returns:
|
|
List of synergy results
|
|
"""
|
|
synergies = []
|
|
|
|
# 1. Archetype synergy: both share an archetype
|
|
if profile_a.archetypes and profile_b.archetypes:
|
|
common_archetypes = set(profile_a.archetypes) & set(profile_b.archetypes)
|
|
if common_archetypes:
|
|
synergies.append(InteractionResult(
|
|
card_a_id=profile_a.id,
|
|
card_b_id=profile_b.id,
|
|
interaction_type='synergy',
|
|
strength=3,
|
|
confidence=0.95,
|
|
notes=f"Both are {', '.join(common_archetypes)}",
|
|
metadata={'common_archetypes': list(common_archetypes)}
|
|
))
|
|
|
|
# 2. Mana synergy: same color identity
|
|
if profile_a.colors and profile_b.colors:
|
|
if set(profile_a.colors) == set(profile_b.colors):
|
|
synergies.append(InteractionResult(
|
|
card_a_id=profile_a.id,
|
|
card_b_id=profile_b.id,
|
|
interaction_type='synergy',
|
|
strength=4,
|
|
confidence=0.9,
|
|
notes="Same color identity",
|
|
metadata={'colors': profile_a.colors}
|
|
))
|
|
|
|
# 3. Mechanic synergy: complementary mechanics
|
|
if profile_a.mechanics and profile_b.mechanics:
|
|
# Haste + trample = aggressive combo
|
|
if ('haste' in profile_a.mechanics and 'trample' in profile_b.mechanics) or \
|
|
('haste' in profile_b.mechanics and 'trample' in profile_a.mechanics):
|
|
synergies.append(InteractionResult(
|
|
card_a_id=profile_a.id,
|
|
card_b_id=profile_b.id,
|
|
interaction_type='synergy',
|
|
strength=4,
|
|
confidence=0.85,
|
|
notes="Haste + Trample combo",
|
|
metadata={'mechanics': ['haste', 'trample']}
|
|
))
|
|
|
|
# Lifelink + combat keywords = combat combo
|
|
combat_keywords = ['first_strike', 'double_strike', 'deathtouch', 'trample']
|
|
if ('lifelink' in profile_a.mechanics and any(k in profile_b.mechanics for k in combat_keywords)) or \
|
|
('lifelink' in profile_b.mechanics and any(k in profile_a.mechanics for k in combat_keywords)):
|
|
synergies.append(InteractionResult(
|
|
card_a_id=profile_a.id,
|
|
card_b_id=profile_b.id,
|
|
interaction_type='synergy',
|
|
strength=3,
|
|
confidence=0.8,
|
|
notes="Lifelink + combat keywords combo",
|
|
metadata={'mechanics': ['lifelink', 'combat']}
|
|
))
|
|
|
|
# 4. Combo synergy: one targets, the other interacts with targets
|
|
if profile_a.targets and profile_b.triggers:
|
|
# Card A targets creatures, Card B interacts with creature actions
|
|
if 'creature' in profile_a.targets:
|
|
creature_actions = ['enters_battlefield', 'dies', 'attacks', 'blocks']
|
|
if any(t in creature_actions for t in profile_b.triggers):
|
|
synergies.append(InteractionResult(
|
|
card_a_id=profile_a.id,
|
|
card_b_id=profile_b.id,
|
|
interaction_type='synergy',
|
|
strength=3,
|
|
confidence=0.85,
|
|
notes="Card A targets creatures, Card B interacts with creature actions",
|
|
metadata={'card_a_targets': 'creature', 'card_b_interacts': 'creature_actions'}
|
|
))
|
|
|
|
# 5. Support synergy: one has a mechanic, the other supports it
|
|
if profile_a.mechanics and profile_b.effects:
|
|
# If card B has an effect that supports card A's mechanic
|
|
if 'haste' in profile_a.mechanics and 'gain_haste' in profile_b.effects:
|
|
synergies.append(InteractionResult(
|
|
card_a_id=profile_a.id,
|
|
card_b_id=profile_b.id,
|
|
interaction_type='synergy',
|
|
strength=3,
|
|
confidence=0.8,
|
|
notes="Card B grants haste to Card A",
|
|
metadata={'mechanic': 'haste', 'effect': 'gain_haste'}
|
|
))
|
|
|
|
return synergies
|
|
|
|
def determine_counters(
|
|
self, profile_a: CardProfile, profile_b: CardProfile
|
|
) -> List[InteractionResult]:
|
|
"""
|
|
Determine counter relationships between two cards.
|
|
|
|
Counters are negative interactions where one card is disadvantaged by another.
|
|
|
|
Examples:
|
|
- Different color identities (strategic tension)
|
|
- One has higher power (stat disadvantage)
|
|
- One counters the other's strategy (counter role)
|
|
|
|
Args:
|
|
profile_a: First card profile
|
|
profile_b: Second card profile
|
|
|
|
Returns:
|
|
List of counter results
|
|
"""
|
|
counters = []
|
|
|
|
# 1. Color counter: different color identities
|
|
if profile_a.colors and profile_b.colors:
|
|
if set(profile_a.colors) != set(profile_b.colors):
|
|
counters.append(InteractionResult(
|
|
card_a_id=profile_a.id,
|
|
card_b_id=profile_b.id,
|
|
interaction_type='counter',
|
|
strength=2,
|
|
confidence=0.8,
|
|
notes="Different color identities",
|
|
metadata={'colors_a': profile_a.colors, 'colors_b': profile_b.colors}
|
|
))
|
|
|
|
# 2. Stat counter: one has significantly higher power
|
|
if profile_a.power and profile_b.power:
|
|
try:
|
|
power_a = int(profile_a.power)
|
|
power_b = int(profile_b.power)
|
|
|
|
if power_a > power_b + 1:
|
|
counters.append(InteractionResult(
|
|
card_a_id=profile_a.id,
|
|
card_b_id=profile_b.id,
|
|
interaction_type='counter',
|
|
strength=3,
|
|
confidence=0.75,
|
|
notes=f"Card A has higher power ({power_a} vs {power_b})",
|
|
metadata={'power_a': power_a, 'power_b': power_b}
|
|
))
|
|
elif power_b > power_a + 1:
|
|
counters.append(InteractionResult(
|
|
card_a_id=profile_a.id,
|
|
card_b_id=profile_b.id,
|
|
interaction_type='counter',
|
|
strength=3,
|
|
confidence=0.75,
|
|
notes=f"Card B has higher power ({power_b} vs {power_a})",
|
|
metadata={'power_a': power_a, 'power_b': power_b}
|
|
))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# 3. Counter role: one targets creatures, the other has combat keywords
|
|
if profile_a.targets and profile_b.mechanics:
|
|
if 'creature' in profile_a.targets:
|
|
combat_keywords = ['deathtouch', 'trample', 'first_strike', 'double_strike']
|
|
if any(m in combat_keywords for m in profile_b.mechanics):
|
|
counters.append(InteractionResult(
|
|
card_a_id=profile_a.id,
|
|
card_b_id=profile_b.id,
|
|
interaction_type='counter',
|
|
strength=2,
|
|
confidence=0.6,
|
|
notes="Card A targets creatures, Card B has combat keywords",
|
|
metadata={'target': 'creature', 'mechanics': profile_b.mechanics}
|
|
))
|
|
|
|
return counters
|
|
|
|
def determine_evolutions(
|
|
self, profile_a: CardProfile, profile_b: CardProfile
|
|
) -> List[InteractionResult]:
|
|
"""
|
|
Determine evolution relationships between two cards.
|
|
|
|
Evolutions track when a card has been reprinted, transformed, or evolved.
|
|
|
|
Examples:
|
|
- Same name in different sets (reprint)
|
|
- Transform pairs (different faces of same card)
|
|
- Double-sided cards
|
|
|
|
Args:
|
|
profile_a: First card profile
|
|
profile_b: Second card profile
|
|
|
|
Returns:
|
|
List of evolution results
|
|
"""
|
|
evolutions = []
|
|
|
|
# 1. Same name = reprint
|
|
if profile_a.name == profile_b.name:
|
|
evolutions.append(InteractionResult(
|
|
card_a_id=profile_a.id,
|
|
card_b_id=profile_b.id,
|
|
interaction_type='evolution',
|
|
strength=2,
|
|
confidence=0.9,
|
|
notes=f"Reprint of {profile_a.name}",
|
|
metadata={'card_name': profile_a.name}
|
|
))
|
|
|
|
# 2. Transform pairs would require checking card_faces in the database
|
|
# This is handled separately in the pipeline
|
|
|
|
return evolutions
|
|
|
|
def determine_all_interactions(
|
|
self,
|
|
profiles: List[CardProfile]
|
|
) -> dict:
|
|
"""
|
|
Determine all interactions for a batch of cards.
|
|
|
|
Args:
|
|
profiles: List of card profiles
|
|
|
|
Returns:
|
|
Dictionary with:
|
|
- synergies: list of synergy results
|
|
- counters: list of counter results
|
|
- evolutions: list of evolution results
|
|
"""
|
|
synergies = []
|
|
counters = []
|
|
evolutions = []
|
|
|
|
# Compare all pairs
|
|
for i in range(len(profiles)):
|
|
for j in range(i + 1, len(profiles)):
|
|
profile_a = profiles[i]
|
|
profile_b = profiles[j]
|
|
|
|
# Determine synergies
|
|
synergies.extend(self.determine_synergies(profile_a, profile_b))
|
|
|
|
# Determine counters
|
|
counters.extend(self.determine_counters(profile_a, profile_b))
|
|
|
|
# Determine evolutions
|
|
evolutions.extend(self.determine_evolutions(profile_a, profile_b))
|
|
|
|
return {
|
|
'synergies': synergies,
|
|
'counters': counters,
|
|
'evolutions': evolutions,
|
|
}
|