Final project commit: MTGJSON data integration and backend API

This commit is contained in:
wall-o
2026-07-20 01:09:45 +00:00
parent b170dfd577
commit db01e29a54
37 changed files with 9674 additions and 198 deletions
+314
View File
@@ -0,0 +1,314 @@
"""
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,
}