Add MTG rules engine source files
This commit is contained in:
@@ -0,0 +1,845 @@
|
||||
"""
|
||||
MTG Rules Engine - Main Rules Engine Module
|
||||
|
||||
Core rules engine for an online Magic: The Gathering application.
|
||||
Enforces rule compliance during gameplay by validating:
|
||||
- Card casting (type, mana cost, timing)
|
||||
- Creature combat (attackers, blockers, damage assignment)
|
||||
- Spell resolution (targets, costs, effects)
|
||||
- Zone transitions (enters/leaves battlefield, stack)
|
||||
- Keyword ability interactions
|
||||
- State-based actions (lethal damage, legend rules)
|
||||
|
||||
Built on the hardcoded keyword database from keywords_db.py
|
||||
and the validator from keyword_validator.py.
|
||||
"""
|
||||
|
||||
from .keywords_db import (
|
||||
KEYWORD_ABILITIES,
|
||||
KEYWORD_ACTIONS,
|
||||
KEYWORD_VARIANTS,
|
||||
KEYWORD_TYPES,
|
||||
RULES_INDEX,
|
||||
get_keyword_definition,
|
||||
get_all_keywords,
|
||||
)
|
||||
from .keyword_validator import KeywordValidator, KeywordValidationError
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DATA MODELS
|
||||
# =============================================================================
|
||||
|
||||
class CardType:
|
||||
"""Card types in Magic: The Gathering."""
|
||||
CREATURE = "creature"
|
||||
SPELL = "spell"
|
||||
LAND = "land"
|
||||
ENCHANTMENT = "enchantment"
|
||||
ARTIFACT = "artifact"
|
||||
PLANEWALKER = "planeswalker"
|
||||
INSTANT = "instant"
|
||||
SORCERY = "sorcery"
|
||||
BATTLE = "battle"
|
||||
CONSPIRACY = "conspiracy"
|
||||
SCHEME = "scheme"
|
||||
PLANE = "plane"
|
||||
PHENOMENON = "phenomenon"
|
||||
VANGUARD = "vanguard"
|
||||
SAGA = "saga"
|
||||
OMEGA = "omega"
|
||||
CASE = "case"
|
||||
DUNGEON = "dungeon"
|
||||
ROOM = "room"
|
||||
DOOR = "door"
|
||||
|
||||
|
||||
class CardTypeGroup:
|
||||
"""Groups of card types."""
|
||||
CREATURES = {CardType.CREATURE}
|
||||
PERMANENTS = {
|
||||
CardType.CREATURE,
|
||||
CardType.LAND,
|
||||
CardType.ENCHANTMENT,
|
||||
CardType.ARTIFACT,
|
||||
CardType.PLANEWALKER,
|
||||
}
|
||||
SPELLS = {CardType.INSTANT, CardType.SORCERY}
|
||||
PERMANENT_SPELLS = {CardType.LAND, CardType.ENCHANTMENT, CardType.ARTIFACT, CardType.PLANEWALKER}
|
||||
|
||||
|
||||
class Zone:
|
||||
"""Game zones."""
|
||||
HAND = "hand"
|
||||
LIBRARY = "library"
|
||||
EXILE = "exile"
|
||||
GRAVEYARD = "graveyard"
|
||||
COMMAND_ZONE = "command_zone"
|
||||
BATTLEFIELD = "battlefield"
|
||||
STACK = "stack"
|
||||
PLANAR_DECK = "planar_deck"
|
||||
SCHEME_DECK = "scheme_deck"
|
||||
DUNGEON = "dungeon"
|
||||
|
||||
|
||||
class Phase:
|
||||
"""Turn phases."""
|
||||
UNTAP = "untap"
|
||||
DRAW = "draw"
|
||||
MAIN_PHASE = "main_phase"
|
||||
PRECOMBAT = "precombat"
|
||||
COMBAT = "combat"
|
||||
BEGINNING_COMBAT = "beginning_combat"
|
||||
DECLARE_ATTACKERS = "declare_attackers"
|
||||
DECLARE_BLOCKERS = "declare_blockers"
|
||||
COMBAT_DAMAGE = "combat_damage"
|
||||
END_COMBAT = "end_combat"
|
||||
END_PHASE = "end_phase"
|
||||
END_STEP = "end_step"
|
||||
CLEANUP = "cleanup"
|
||||
|
||||
|
||||
class ZoneTransitionEvent:
|
||||
"""Represents an object moving between zones."""
|
||||
def __init__(self, object_id: str, from_zone: str, to_zone: str, object_type: str = None):
|
||||
self.object_id = object_id
|
||||
self.from_zone = from_zone
|
||||
self.to_zone = to_zone
|
||||
self.object_type = object_type
|
||||
|
||||
def is_enters_battlefield(self) -> bool:
|
||||
return self.to_zone == Zone.BATTLEFIELD
|
||||
|
||||
def is_leaves_battlefield(self) -> bool:
|
||||
return self.from_zone == Zone.BATTLEFIELD
|
||||
|
||||
|
||||
class GameAction:
|
||||
"""Represents a game action that may need validation."""
|
||||
def __init__(self, action_type: str, details: dict = None):
|
||||
self.action_type = action_type
|
||||
self.details = details or {}
|
||||
|
||||
def __repr__(self):
|
||||
return f"GameAction({self.action_type}, {self.details})"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ZONE MANAGEMENT
|
||||
# =============================================================================
|
||||
|
||||
class ZoneManager:
|
||||
"""Manages object positions in game zones."""
|
||||
|
||||
def __init__(self):
|
||||
self.zones = {zone: [] for zone in Zone.__dict__.values() if not zone.startswith("_")}
|
||||
|
||||
def add_object(self, object_id: str, zone: str, object_type: str = None):
|
||||
"""Add an object to a zone."""
|
||||
if zone not in self.zones:
|
||||
raise ValueError(f"Invalid zone: {zone}")
|
||||
self.zones[zone].append({
|
||||
"id": object_id,
|
||||
"type": object_type,
|
||||
"added_at": len(self.zones[zone]),
|
||||
})
|
||||
|
||||
def remove_object(self, object_id: str, zone: str) -> dict | None:
|
||||
"""Remove an object from a zone. Returns the object data or None."""
|
||||
if zone not in self.zones:
|
||||
return None
|
||||
for i, obj in enumerate(self.zones[zone]):
|
||||
if obj["id"] == object_id:
|
||||
return self.zones[zone].pop(i)
|
||||
return None
|
||||
|
||||
def get_objects_in_zone(self, zone: str) -> list[dict]:
|
||||
"""Get all objects in a zone."""
|
||||
return self.zones.get(zone, [])
|
||||
|
||||
def get_object(self, object_id: str) -> dict | None:
|
||||
"""Find an object by ID across all zones."""
|
||||
for zone_objs in self.zones.values():
|
||||
for obj in zone_objs:
|
||||
if obj["id"] == object_id:
|
||||
return obj, zone
|
||||
return None
|
||||
|
||||
def transition(self, object_id: str, from_zone: str, to_zone: str, object_type: str = None) -> ZoneTransitionEvent:
|
||||
"""
|
||||
Transition an object between zones.
|
||||
|
||||
Returns:
|
||||
ZoneTransitionEvent with the transition details.
|
||||
"""
|
||||
obj = self.remove_object(object_id, from_zone)
|
||||
if obj is None:
|
||||
raise ValueError(f"Object {object_id} not found in zone {from_zone}")
|
||||
|
||||
self.add_object(object_id, to_zone, object_type)
|
||||
return ZoneTransitionEvent(object_id, from_zone, to_zone, object_type)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CARD PARSE AND VALIDATE
|
||||
# =============================================================================
|
||||
|
||||
class CardParser:
|
||||
"""
|
||||
Parse and validate card data.
|
||||
|
||||
Extracts card type, mana cost, keywords, and abilities from card text.
|
||||
Validates the card against MTG rules.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.validator = KeywordValidator()
|
||||
|
||||
def parse_card(self, card_data: dict) -> dict:
|
||||
"""
|
||||
Parse and validate card data.
|
||||
|
||||
Args:
|
||||
card_data: Dictionary with card information (name, type, text, mana_cost, etc.)
|
||||
|
||||
Returns:
|
||||
Validated card dictionary.
|
||||
|
||||
Raises:
|
||||
KeywordValidationError: If the card has invalid keyword usage.
|
||||
"""
|
||||
validated = {
|
||||
"id": card_data.get("id", "unknown"),
|
||||
"name": card_data.get("name", "Unknown"),
|
||||
"type": card_data.get("type", "unknown").lower(),
|
||||
"mana_cost": card_data.get("mana_cost", ""),
|
||||
"text": card_data.get("text", ""),
|
||||
"keywords": self._extract_keywords(card_data.get("text", "")),
|
||||
"abilities": self._extract_abilities(card_data.get("text", "")),
|
||||
"is_valid": True,
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
# Validate card type
|
||||
if validated["type"] not in CardTypeGroup.CREATURES and validated["type"] not in CardTypeGroup.SPELLS:
|
||||
validated["is_valid"] = False
|
||||
validated["errors"].append(f"Invalid card type: {validated['type']}")
|
||||
|
||||
# Validate keywords
|
||||
if validated["text"]:
|
||||
validation_results = self.validator.validate_card_text(validated["text"])
|
||||
for result in validation_results:
|
||||
if not result["valid"]:
|
||||
validated["is_valid"] = False
|
||||
validated["errors"].append(
|
||||
f"Invalid keyword usage: {result['keyword']}"
|
||||
)
|
||||
|
||||
return validated
|
||||
|
||||
def _extract_keywords(self, text: str) -> list[str]:
|
||||
"""Extract keywords from card text."""
|
||||
keywords = set()
|
||||
for kw in get_all_keywords():
|
||||
if kw.lower() in text.lower():
|
||||
keywords.add(kw)
|
||||
return sorted(keywords)
|
||||
|
||||
def _extract_abilities(self, text: str) -> list[dict]:
|
||||
"""Extract abilities from card text."""
|
||||
abilities = []
|
||||
# Simple extraction - in production, use NLP/parsing
|
||||
for line in text.split("\n"):
|
||||
line = line.strip()
|
||||
if line:
|
||||
# Check for keyword abilities
|
||||
for kw, data in KEYWORD_ABILITIES.items():
|
||||
if kw in line.lower():
|
||||
abilities.append({
|
||||
"keyword": kw,
|
||||
"text": line,
|
||||
"definition": data["definition"],
|
||||
"rule": data["rule"],
|
||||
"type": data["type"],
|
||||
})
|
||||
return abilities
|
||||
|
||||
def validate_casting(self, card: dict, player_state: dict) -> dict:
|
||||
"""
|
||||
Validate if a card can be cast.
|
||||
|
||||
Args:
|
||||
card: The validated card dictionary.
|
||||
player_state: Dictionary with player's current state (mana, life, etc.)
|
||||
|
||||
Returns:
|
||||
Validation result with errors and warnings.
|
||||
"""
|
||||
result = {
|
||||
"can_cast": True,
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
}
|
||||
|
||||
# Validate card type for casting
|
||||
if card["type"] not in CardTypeGroup.SPELLS and card["type"] not in CardTypeGroup.PERMANENT_SPELLS:
|
||||
result["can_cast"] = False
|
||||
result["errors"].append(f"Card type '{card['type']}' cannot be cast")
|
||||
return result
|
||||
|
||||
# Validate timing (instant vs sorcery)
|
||||
if card["type"] == CardType.SORCERY:
|
||||
if "main_phase" not in player_state.get("current_phase", ""):
|
||||
result["can_cast"] = False
|
||||
result["errors"].append("Sorceries can only be cast during the main phase")
|
||||
|
||||
# Validate mana cost
|
||||
mana_cost = card.get("mana_cost", "")
|
||||
if mana_cost:
|
||||
required_mana = self._parse_mana_cost(mana_cost)
|
||||
available_mana = player_state.get("available_mana", {})
|
||||
|
||||
if required_mana and not self._can_pay_cost(required_mana, available_mana):
|
||||
result["can_cast"] = False
|
||||
result["errors"].append(
|
||||
f"Insufficient mana. Required: {required_mana}, Available: {available_mana}"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _parse_mana_cost(self, cost: str) -> dict:
|
||||
"""Parse a mana cost string into a structured format."""
|
||||
# Simple parser - production would use regex
|
||||
costs = {"W": 0, "U": 0, "B": 0, "R": 0, "G": 0, "C": 0, "E": 0, "X": 0}
|
||||
import re
|
||||
for match in re.finditer(r"\{([WUBRGCEEXS]|[\d]+)\}", cost):
|
||||
sym = match.group(1)
|
||||
if sym in costs:
|
||||
costs[sym] += 1
|
||||
elif sym.isdigit():
|
||||
costs["C"] += int(sym)
|
||||
elif sym == "E":
|
||||
costs["E"] += 1
|
||||
return {k: v for k, v in costs.items() if v > 0}
|
||||
|
||||
def _can_pay_cost(self, required: dict, available: dict) -> bool:
|
||||
"""Check if a mana cost can be paid."""
|
||||
total_required = sum(required.values())
|
||||
total_available = sum(available.values())
|
||||
return total_available >= total_required
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# COMBAT RESOLVER
|
||||
# =============================================================================
|
||||
|
||||
class CombatResolver:
|
||||
"""
|
||||
Resolves combat phase according to MTG rules.
|
||||
|
||||
Handles:
|
||||
- Attacker declaration
|
||||
- Blocker declaration
|
||||
- Combat damage assignment
|
||||
- Death resolution (deathtouch, lethal damage)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.validator = KeywordValidator()
|
||||
self.zone_manager = ZoneManager()
|
||||
|
||||
def resolve_turn(self, game_state: dict) -> dict:
|
||||
"""
|
||||
Resolve a full turn.
|
||||
|
||||
Args:
|
||||
game_state: Dictionary with game state information.
|
||||
|
||||
Returns:
|
||||
Turn resolution results.
|
||||
"""
|
||||
results = {
|
||||
"phases_resolved": [],
|
||||
"actions_taken": [],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
# Untap phase
|
||||
results["phases_resolved"].append("untap")
|
||||
results["actions_taken"].append(self._untap_phase(game_state))
|
||||
|
||||
# Draw phase
|
||||
results["phases_resolved"].append("draw")
|
||||
results["actions_taken"].append(self._draw_phase(game_state))
|
||||
|
||||
# Main phase
|
||||
results["phases_resolved"].append("main_phase")
|
||||
results["actions_taken"].append(self._main_phase(game_state))
|
||||
|
||||
# Pre-combat
|
||||
results["phases_resolved"].append("precombat")
|
||||
|
||||
# Combat phase
|
||||
if game_state.get("has_combat"):
|
||||
results["phases_resolved"].append("combat")
|
||||
combat_result = self._resolve_combat(game_state)
|
||||
results["actions_taken"].append(combat_result)
|
||||
|
||||
# End phase
|
||||
results["phases_resolved"].append("end_phase")
|
||||
results["actions_taken"].append(self._end_phase(game_state))
|
||||
|
||||
return results
|
||||
|
||||
def _untap_phase(self, game_state: dict) -> dict:
|
||||
"""Resolve the untap phase."""
|
||||
return {"action": "untap", "untapped": game_state.get("untapped_permanents", [])}
|
||||
|
||||
def _draw_phase(self, game_state: dict) -> dict:
|
||||
"""Resolve the draw phase."""
|
||||
draw_amount = game_state.get("draw_amount", 1)
|
||||
return {"action": "draw", "drawn": draw_amount}
|
||||
|
||||
def _main_phase(self, game_state: dict) -> dict:
|
||||
"""Resolve the main phase (player actions)."""
|
||||
# Player takes actions here - simplified
|
||||
return {"action": "main_phase", "actions": game_state.get("player_actions", [])}
|
||||
|
||||
def _resolve_combat(self, game_state: dict) -> dict:
|
||||
"""Resolve the combat phase."""
|
||||
result = {
|
||||
"action": "combat",
|
||||
"attackers": [],
|
||||
"blockers": [],
|
||||
"damage_assigned": [],
|
||||
"deaths": [],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
# Get attackers and blockers
|
||||
attackers = game_state.get("attackers", [])
|
||||
blockers = game_state.get("blockers", [])
|
||||
|
||||
# Validate attackers
|
||||
for attacker in attackers:
|
||||
validation = self._validate_attacker(attacker, game_state)
|
||||
if validation["valid"]:
|
||||
result["attackers"].append(attacker)
|
||||
else:
|
||||
result["errors"].extend(validation["errors"])
|
||||
|
||||
# Validate blockers
|
||||
for blocker in blockers:
|
||||
validation = self._validate_blocker(blocker, game_state)
|
||||
if validation["valid"]:
|
||||
result["blockers"].append(blocker)
|
||||
else:
|
||||
result["errors"].extend(validation["errors"])
|
||||
|
||||
# Assign combat damage
|
||||
result["damage_assigned"] = self._assign_damage(attackers, blockers, game_state)
|
||||
|
||||
# Process deaths
|
||||
result["deaths"] = self._process_deaths(game_state)
|
||||
|
||||
return result
|
||||
|
||||
def _validate_attacker(self, attacker: dict, game_state: dict) -> dict:
|
||||
"""Validate an attacker."""
|
||||
result = {"valid": True, "errors": []}
|
||||
|
||||
# Check for haste (summoning sickness)
|
||||
if not attacker.get("haste") and not attacker.get("untapped"):
|
||||
result["valid"] = False
|
||||
result["errors"].append(f"Attacker {attacker.get('id')} has summoning sickness")
|
||||
|
||||
# Check if attacker is a creature
|
||||
if attacker.get("type") != "creature":
|
||||
result["valid"] = False
|
||||
result["errors"].append(f"Attacker {attacker.get('id')} is not a creature")
|
||||
|
||||
return result
|
||||
|
||||
def _validate_blocker(self, blocker: dict, game_state: dict) -> dict:
|
||||
"""Validate a blocker."""
|
||||
result = {"valid": True, "errors": []}
|
||||
|
||||
# Check if blocker is a creature
|
||||
if blocker.get("type") != "creature":
|
||||
result["valid"] = False
|
||||
result["errors"].append(f"Blocker {blocker.get('id')} is not a creature")
|
||||
return result
|
||||
|
||||
# Check evasion abilities
|
||||
attackers = game_state.get("attackers", [])
|
||||
blocker_evasion = blocker.get("evasion", [])
|
||||
|
||||
for attacker in attackers:
|
||||
attacker_evasion = attacker.get("evasion", [])
|
||||
|
||||
if not self._blocker_can_block(blocker_evasion, attacker_evasion):
|
||||
result["valid"] = False
|
||||
result["errors"].append(
|
||||
f"Blocker {blocker.get('id')} can't block attacker {attacker.get('id')} "
|
||||
f"due to evasion abilities"
|
||||
)
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def _blocker_can_block(self, blocker_evasion: list[str], attacker_evasion: list[str]) -> bool:
|
||||
"""Check if a blocker can block an attacker based on evasion abilities."""
|
||||
if not attacker_evasion:
|
||||
return True
|
||||
|
||||
for ev in attacker_evasion:
|
||||
if ev == "flying":
|
||||
if "flying" not in blocker_evasion and "reach" not in blocker_evasion:
|
||||
return False
|
||||
elif ev == "shadow":
|
||||
if "shadow" not in blocker_evasion:
|
||||
return False
|
||||
elif ev == "horsemanship":
|
||||
if "horsemanship" not in blocker_evasion:
|
||||
return False
|
||||
elif ev == "intimidate":
|
||||
if "intimidate" not in blocker_evasion:
|
||||
return False
|
||||
elif ev == "flanking":
|
||||
if "flanking" not in blocker_evasion:
|
||||
return False
|
||||
elif ev == "skulk":
|
||||
# Skulk: can't be blocked by creatures with greater power
|
||||
# We don't have power info here, so we assume it can block
|
||||
pass
|
||||
elif ev == "menace":
|
||||
if len(blocker_evasion) < 2:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _assign_damage(self, attackers: list[dict], blockers: list[dict], game_state: dict) -> list[dict]:
|
||||
"""Assign combat damage."""
|
||||
damage_assigned = []
|
||||
|
||||
for attacker in attackers:
|
||||
attacker_power = attacker.get("power", 0)
|
||||
attacker_id = attacker.get("id")
|
||||
|
||||
# Check for trample
|
||||
has_trample = "trample" in attacker.get("keywords", [])
|
||||
trample_over_pws = "trample_over_planeswalkers" in attacker.get("keywords", [])
|
||||
|
||||
# Assign damage to blockers first
|
||||
for blocker in blockers:
|
||||
blocker_id = blocker.get("id")
|
||||
blocker_toughness = blocker.get("toughness", 0)
|
||||
damage_to_blocker = min(attacker_power, blocker_toughness)
|
||||
|
||||
damage_assigned.append({
|
||||
"attacker_id": attacker_id,
|
||||
"target_id": blocker_id,
|
||||
"damage": damage_to_blocker,
|
||||
"type": "combat",
|
||||
})
|
||||
|
||||
attacker_power -= damage_to_blocker
|
||||
if attacker_power <= 0:
|
||||
break
|
||||
|
||||
# Assign remaining damage to defender (trample)
|
||||
if attacker_power > 0 and (has_trample or trample_over_pws):
|
||||
defender_life = game_state.get("defender_life", 20)
|
||||
damage_to_defender = min(attacker_power, defender_life)
|
||||
damage_assigned.append({
|
||||
"attacker_id": attacker_id,
|
||||
"target_id": "defender",
|
||||
"damage": damage_to_defender,
|
||||
"type": "combat",
|
||||
})
|
||||
|
||||
return damage_assigned
|
||||
|
||||
def _process_deaths(self, game_state: dict) -> list[str]:
|
||||
"""Process state-based death resolution."""
|
||||
deaths = []
|
||||
|
||||
# Check for lethal damage
|
||||
for damage in game_state.get("damage_assigned", []):
|
||||
target = damage.get("target_id")
|
||||
amount = damage.get("damage", 0)
|
||||
|
||||
if target == "defender":
|
||||
# Player takes damage
|
||||
game_state["defender_life"] -= amount
|
||||
if game_state["defender_life"] <= 0:
|
||||
deaths.append("defender")
|
||||
else:
|
||||
# Creature takes damage
|
||||
target_info = game_state.get("targets", {}).get(target)
|
||||
if target_info:
|
||||
toughness = target_info.get("toughness", 0)
|
||||
damage_marked = target_info.get("damage_marked", 0) + amount
|
||||
|
||||
# Check for deathtouch
|
||||
attacker_id = damage.get("attacker_id")
|
||||
attacker_has_deathtouch = any(
|
||||
k in game_state.get("attackers", {}).get(attacker_id, {}).get("keywords", [])
|
||||
for k in ["deathtouch"]
|
||||
)
|
||||
|
||||
if attacker_has_deathtouch:
|
||||
# Deathtouch: any nonzero damage is lethal
|
||||
if amount > 0:
|
||||
deaths.append(target)
|
||||
else:
|
||||
# Normal damage: check for lethal
|
||||
if damage_marked >= toughness:
|
||||
deaths.append(target)
|
||||
|
||||
return deaths
|
||||
|
||||
def _end_phase(self, game_state: dict) -> dict:
|
||||
"""Resolve the end phase."""
|
||||
return {"action": "end_phase", "life": game_state.get("defender_life", 20)}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN RULES ENGINE
|
||||
# =============================================================================
|
||||
|
||||
class RulesEngine:
|
||||
"""
|
||||
Main rules engine for Magic: The Gathering.
|
||||
|
||||
Provides high-level rule enforcement by composing:
|
||||
- CardParser for card validation
|
||||
- ZoneManager for zone tracking
|
||||
- CombatResolver for combat resolution
|
||||
- KeywordValidator for keyword validation
|
||||
|
||||
Usage:
|
||||
engine = RulesEngine()
|
||||
result = engine.validate_action(card, player_state, action)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.validator = KeywordValidator()
|
||||
self.card_parser = CardParser()
|
||||
self.zone_manager = ZoneManager()
|
||||
self.combat_resolver = CombatResolver()
|
||||
|
||||
def validate_action(self, card: dict, player_state: dict, action: GameAction) -> dict:
|
||||
"""
|
||||
Validate a game action.
|
||||
|
||||
Args:
|
||||
card: The validated card dictionary.
|
||||
player_state: Dictionary with player's current state.
|
||||
action: The GameAction to validate.
|
||||
|
||||
Returns:
|
||||
Validation result dictionary.
|
||||
"""
|
||||
result = {
|
||||
"valid": True,
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"action": action.action_type,
|
||||
}
|
||||
|
||||
# Validate based on action type
|
||||
if action.action_type == "cast":
|
||||
result = self._validate_cast(card, player_state, result)
|
||||
elif action.action_type == "attack":
|
||||
result = self._validate_attack(card, player_state, result)
|
||||
elif action.action_type == "block":
|
||||
result = self._validate_block(card, player_state, result)
|
||||
elif action.action_type == "resolve_combat":
|
||||
result = self._validate_combat_resolution(player_state, result)
|
||||
elif action.action_type == "zone_transition":
|
||||
result = self._validate_zone_transition(card, player_state, result)
|
||||
else:
|
||||
result["valid"] = False
|
||||
result["errors"].append(f"Unknown action type: {action.action_type}")
|
||||
|
||||
return result
|
||||
|
||||
def _validate_cast(self, card: dict, player_state: dict, result: dict) -> dict:
|
||||
"""Validate a spell casting action."""
|
||||
cast_result = self.card_parser.validate_casting(card, player_state)
|
||||
|
||||
if not cast_result["can_cast"]:
|
||||
result["valid"] = False
|
||||
result["errors"].extend(cast_result["errors"])
|
||||
result["warnings"].extend(cast_result.get("warnings", []))
|
||||
|
||||
# Validate targets
|
||||
if card.get("targets"):
|
||||
for target in card["targets"]:
|
||||
if not self.validator.is_valid_keyword(target):
|
||||
result["valid"] = False
|
||||
result["errors"].append(f"Invalid target keyword: {target}")
|
||||
|
||||
return result
|
||||
|
||||
def _validate_attack(self, card: dict, player_state: dict, result: dict) -> dict:
|
||||
"""Validate an attack action."""
|
||||
# Check if card is a creature
|
||||
if card["type"] != "creature":
|
||||
result["valid"] = False
|
||||
result["errors"].append("Only creatures can attack")
|
||||
return result
|
||||
|
||||
# Check summoning sickness
|
||||
if not card.get("haste") and not card.get("untapped"):
|
||||
result["valid"] = False
|
||||
result["errors"].append("Creature has summoning sickness")
|
||||
|
||||
return result
|
||||
|
||||
def _validate_block(self, card: dict, player_state: dict, result: dict) -> dict:
|
||||
"""Validate a block action."""
|
||||
if card["type"] != "creature":
|
||||
result["valid"] = False
|
||||
result["errors"].append("Only creatures can block")
|
||||
return result
|
||||
|
||||
# Check evasion abilities
|
||||
attacker = player_state.get("attacker", {})
|
||||
attacker_evasion = attacker.get("evasion", [])
|
||||
blocker_evasion = card.get("evasion", [])
|
||||
|
||||
if not self.combat_resolver._blocker_can_block(blocker_evasion, attacker_evasion):
|
||||
result["valid"] = False
|
||||
result["errors"].append("Blocker can't block attacker due to evasion")
|
||||
|
||||
return result
|
||||
|
||||
def _validate_combat_resolution(self, player_state: dict, result: dict) -> dict:
|
||||
"""Validate combat resolution."""
|
||||
combat_result = self.combat_resolver.resolve_turn(player_state)
|
||||
|
||||
if combat_result.get("errors"):
|
||||
result["valid"] = False
|
||||
result["errors"].extend(combat_result["errors"])
|
||||
|
||||
return result
|
||||
|
||||
def _validate_zone_transition(self, card: dict, player_state: dict, result: dict) -> dict:
|
||||
"""Validate a zone transition action."""
|
||||
from_zone = player_state.get("from_zone")
|
||||
to_zone = player_state.get("to_zone")
|
||||
|
||||
if not from_zone or not to_zone:
|
||||
result["valid"] = False
|
||||
result["errors"].append("Zone transition requires from_zone and to_zone")
|
||||
return result
|
||||
|
||||
# Validate transition
|
||||
valid_transitions = {
|
||||
(Zone.HAND, Zone.BATTLEFIELD): CardTypeGroup.PERMANENT_SPELLS,
|
||||
(Zone.LIBRARY, Zone.HAND): set(),
|
||||
(Zone.HAND, Zone.LIBRARY): set(),
|
||||
(Zone.BATTLEFIELD, Zone.GRAVEYARD): set(),
|
||||
(Zone.GRAVEYARD, Zone.HAND): set(),
|
||||
(Zone.BATTLEFIELD, Zone.EXILE): set(),
|
||||
(Zone.EXILE, Zone.BATTLEFIELD): set(),
|
||||
}
|
||||
|
||||
transition_key = (from_zone, to_zone)
|
||||
if transition_key not in valid_transitions:
|
||||
result["valid"] = False
|
||||
result["errors"].append(f"Invalid zone transition: {from_zone} -> {to_zone}")
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
def get_rules_index(self) -> dict:
|
||||
"""Get the rules index for reference."""
|
||||
return RULES_INDEX
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GAME STATE MANAGER
|
||||
# =============================================================================
|
||||
|
||||
class GameState:
|
||||
"""
|
||||
Manages the state of a Magic: The Gathering game.
|
||||
|
||||
Tracks:
|
||||
- Players and their resources
|
||||
- Card zones (hand, battlefield, graveyard, etc.)
|
||||
- Stack of pending effects
|
||||
- Current phase and turn
|
||||
- Combat state
|
||||
"""
|
||||
|
||||
def __init__(self, player_count: int = 2):
|
||||
self.player_count = player_count
|
||||
self.current_player = 0
|
||||
self.current_phase = Phase.UNTAP
|
||||
self.stack = []
|
||||
self.zone_manager = ZoneManager()
|
||||
self.combat_state = {
|
||||
"attackers": [],
|
||||
"blockers": [],
|
||||
"damage_assigned": [],
|
||||
"phase": None,
|
||||
}
|
||||
self.game_state = {
|
||||
"players": self._init_players(),
|
||||
"has_combat": False,
|
||||
"defender_life": 20,
|
||||
"targets": {},
|
||||
}
|
||||
|
||||
def _init_players(self) -> list[dict]:
|
||||
"""Initialize player states."""
|
||||
players = []
|
||||
for i in range(self.player_count):
|
||||
players.append({
|
||||
"id": i,
|
||||
"life": 20,
|
||||
"mana": 0,
|
||||
"mana_available": 0,
|
||||
"hand": [],
|
||||
"zone": Zone.HAND,
|
||||
})
|
||||
return players
|
||||
|
||||
def advance_phase(self) -> str:
|
||||
"""Advance to the next phase."""
|
||||
phase_order = [
|
||||
Phase.UNTAP,
|
||||
Phase.DRAW,
|
||||
Phase.MAIN_PHASE,
|
||||
Phase.PRECOMBAT,
|
||||
Phase.BEGINNING_COMBAT,
|
||||
Phase.DECLARE_ATTACKERS,
|
||||
Phase.DECLARE_BLOCKERS,
|
||||
Phase.COMBAT_DAMAGE,
|
||||
Phase.END_COMBAT,
|
||||
Phase.END_PHASE,
|
||||
Phase.END_STEP,
|
||||
Phase.CLEANUP,
|
||||
]
|
||||
|
||||
current_idx = phase_order.index(self.current_phase)
|
||||
next_idx = (current_idx + 1) % len(phase_order)
|
||||
self.current_phase = phase_order[next_idx]
|
||||
return self.current_phase
|
||||
|
||||
def get_player_state(self, player_id: int) -> dict:
|
||||
"""Get a player's state."""
|
||||
return self.game_state["players"][player_id]
|
||||
|
||||
def set_combat_state(self, state: dict):
|
||||
"""Set the combat phase state."""
|
||||
self.combat_state.update(state)
|
||||
self.game_state["has_combat"] = True
|
||||
|
||||
def get_engine(self) -> RulesEngine:
|
||||
"""Get the rules engine for this game state."""
|
||||
return RulesEngine()
|
||||
Reference in New Issue
Block a user