Files
mtgonline/backend/scripts/interaction_determinator.py
T

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,
}