"""Card models for the Mantimon TCG game engine. This module defines two key model types: - CardDefinition: Immutable template for a card (the "blueprint") - CardInstance: Mutable in-game state for a card in play The separation allows efficient memory usage (definitions are shared) while maintaining per-card state during gameplay (damage, attached energy, etc.). Usage: # Define a card template pikachu = CardDefinition( id="pikachu_base_001", name="Pikachu", card_type=CardType.POKEMON, stage=PokemonStage.BASIC, hp=60, pokemon_type=EnergyType.LIGHTNING, attacks=[Attack(name="Thunder Shock", cost=[EnergyType.LIGHTNING], damage=20)], ) # Create an instance for gameplay instance = CardInstance( instance_id="uuid-here", definition_id="pikachu_base_001", ) # Track damage during battle instance.damage = 30 """ from typing import Any from pydantic import BaseModel, Field from app.core.models.enums import ( CardType, EnergyType, ModifierMode, PokemonStage, PokemonVariant, StatusCondition, TrainerType, ) class Attack(BaseModel): """An attack that a Pokemon can use. Attacks have an energy cost, base damage, and optional effect. Effects are referenced by ID and resolved by the effect handler system. Attributes: name: Display name of the attack. cost: List of energy types required to use this attack. Colorless can be satisfied by any energy type. damage: Base damage dealt by this attack. May be modified by effects. effect_id: Optional reference to an effect handler for special effects. effect_params: Parameters passed to the effect handler. effect_description: Human-readable description of the effect. """ name: str cost: list[EnergyType] = Field(default_factory=list) damage: int = 0 effect_id: str | None = None effect_params: dict[str, Any] = Field(default_factory=dict) effect_description: str | None = None class Ability(BaseModel): """A special ability that a Pokemon has. Abilities can be activated during the main phase (unless specified otherwise). Like attacks, abilities reference effect handlers by ID. Attributes: name: Display name of the ability. effect_id: Reference to an effect handler. effect_params: Parameters passed to the effect handler. effect_description: Human-readable description of the ability. uses_per_turn: Maximum uses per turn. None means unlimited uses. Default is 1 (once per turn), which matches standard Pokemon TCG rules. """ name: str effect_id: str effect_params: dict[str, Any] = Field(default_factory=dict) effect_description: str | None = None uses_per_turn: int | None = 1 # None = unlimited, 1 = once per turn (default) class WeaknessResistance(BaseModel): """Weakness or resistance to a specific energy type. This model supports both the global defaults from RulesConfig.combat and per-card overrides. If mode/value are not specified, the handler will use the game's CombatConfig defaults. Attributes: energy_type: The energy type this applies to. mode: How to apply the modifier (multiplicative or additive). If None, uses the game's CombatConfig default for this modifier type. value: The modifier value to apply. - For multiplicative: damage * value (e.g., 2 for x2) - For additive: damage + value (e.g., +20 or -30) If None, uses the game's CombatConfig default. Examples: # Use game defaults (mode and value from CombatConfig) WeaknessResistance(energy_type=EnergyType.FIRE) # Override just the value (still uses default mode) WeaknessResistance(energy_type=EnergyType.FIRE, value=3) # x3 if default is multiplicative # Full override (card-specific behavior) WeaknessResistance( energy_type=EnergyType.FIRE, mode=ModifierMode.ADDITIVE, value=40, # +40 damage instead of x2 ) Legacy Support: The old 'modifier' field is deprecated but still works for backwards compatibility. If 'modifier' is provided without 'value', it will be used as the value. The mode will still default to CombatConfig. """ energy_type: EnergyType mode: ModifierMode | None = None value: int | None = None # Legacy field for backwards compatibility modifier: int | None = None def get_value(self, default: int) -> int: """Get the modifier value, falling back to default if not specified. Args: default: The default value from CombatConfig. Returns: The value to use for this modifier. """ if self.value is not None: return self.value if self.modifier is not None: return self.modifier return default def get_mode(self, default: ModifierMode) -> ModifierMode: """Get the modifier mode, falling back to default if not specified. Args: default: The default mode from CombatConfig. Returns: The mode to use for this modifier. """ return self.mode if self.mode is not None else default class CardDefinition(BaseModel): """Immutable template for a card. This represents the "blueprint" of a card - all the static information that doesn't change during gameplay. Multiple CardInstances can reference the same CardDefinition. The card_type field determines which other fields are relevant: - POKEMON: stage, variant, hp, pokemon_type, attacks, abilities, weakness, resistance, retreat_cost - TRAINER: trainer_type, effect_id, effect_params - ENERGY: energy_type, energy_provides Attributes: id: Unique identifier for this card definition (e.g., "pikachu_base_001"). name: Display name of the card. card_type: Whether this is a Pokemon, Trainer, or Energy card. stage: Evolution stage - BASIC, STAGE_1, or STAGE_2 (Pokemon only). variant: Special variant - NORMAL, EX, GX, V, VMAX, VSTAR (Pokemon only). Affects knockout points but not evolution mechanics. evolves_from: Name of the Pokemon this evolves from (Stage 1/2, VMAX, VSTAR only). hp: Hit points (Pokemon only). pokemon_type: Energy type of this Pokemon (for weakness/resistance). attacks: List of attacks this Pokemon can use. abilities: List of abilities this Pokemon has. weakness: Weakness to an energy type (Pokemon only). resistance: Resistance to an energy type (Pokemon only). retreat_cost: Number of energy to discard to retreat (Pokemon only). trainer_type: Subtype of trainer card (Trainer only). effect_id: Effect handler for this card (Trainer/special Energy). effect_params: Parameters for the effect handler. effect_description: Human-readable description of the card's effect. energy_type: Type of energy this card provides (Energy only). energy_provides: List of energy types this card provides. Basic energy provides one of its type. Special energy may provide multiple. rarity: Card rarity (common, uncommon, rare, etc.). set_id: Which card set this belongs to. image_url: URL to the card image. """ id: str name: str card_type: CardType # Pokemon-specific fields stage: PokemonStage | None = None variant: PokemonVariant = PokemonVariant.NORMAL evolves_from: str | None = None hp: int | None = None pokemon_type: EnergyType | None = None attacks: list[Attack] = Field(default_factory=list) abilities: list[Ability] = Field(default_factory=list) weakness: WeaknessResistance | None = None resistance: WeaknessResistance | None = None retreat_cost: int = 0 # Trainer-specific fields trainer_type: TrainerType | None = None effect_id: str | None = None effect_params: dict[str, Any] = Field(default_factory=dict) effect_description: str | None = None # Energy-specific fields energy_type: EnergyType | None = None energy_provides: list[EnergyType] = Field(default_factory=list) # Metadata rarity: str = "common" set_id: str = "" image_url: str | None = None def is_pokemon(self) -> bool: """Check if this is a Pokemon card.""" return self.card_type == CardType.POKEMON def is_trainer(self) -> bool: """Check if this is a Trainer card.""" return self.card_type == CardType.TRAINER def is_energy(self) -> bool: """Check if this is an Energy card.""" return self.card_type == CardType.ENERGY def is_basic_pokemon(self) -> bool: """Check if this is a Basic Pokemon (can be played directly to bench).""" if not self.is_pokemon(): return False return self.stage == PokemonStage.BASIC def is_evolution(self) -> bool: """Check if this is an evolution Pokemon (Stage 1 or Stage 2).""" if not self.is_pokemon(): return False return self.stage in (PokemonStage.STAGE_1, PokemonStage.STAGE_2) def requires_evolution_from_variant(self) -> bool: """Check if this variant requires evolving from another variant. VMAX and VSTAR must evolve from a V Pokemon. """ return self.variant in (PokemonVariant.VMAX, PokemonVariant.VSTAR) def knockout_points(self) -> int: """Get the default knockout points for this Pokemon based on variant. Note: Actual points should come from RulesConfig.prizes.points_for_knockout() for configurability. This is a convenience method for the common case. """ if not self.is_pokemon(): return 0 points_map = { PokemonVariant.NORMAL: 1, PokemonVariant.EX: 2, PokemonVariant.GX: 2, PokemonVariant.V: 2, PokemonVariant.VMAX: 3, PokemonVariant.VSTAR: 3, } return points_map.get(self.variant, 1) class CardInstance(BaseModel): """A card instance in play with mutable state. While CardDefinition is the template, CardInstance represents a specific card during gameplay. It tracks damage, attached energy, status conditions, and other per-card state. The definition_id links back to the CardDefinition in the game's card registry. Stat Modifiers: Card effects can modify base stats using the modifier fields. These are additive: effective_stat = base_stat + modifier. For example, a Tool card that grants +20 HP would set hp_modifier=20. Attributes: instance_id: Unique identifier for this specific instance (UUID). definition_id: Reference to the CardDefinition.id. damage: Current damage on this card (Pokemon only). attached_energy: List of CardInstance IDs for attached energy cards. attached_tools: List of CardInstance IDs for attached tool cards. status_conditions: Active status conditions on this Pokemon. ability_uses_this_turn: Number of times abilities have been used this turn. Compared against Ability.uses_per_turn to determine if more uses allowed. hp_modifier: Additive modifier to the Pokemon's base HP. Can be positive (e.g., from Tool cards) or negative (e.g., from effects). retreat_cost_modifier: Additive modifier to retreat cost. Negative values reduce the cost (e.g., Float Stone sets this to -99 to effectively make retreat free). Effective retreat cost is clamped to minimum 0. damage_modifier: Additive modifier to damage dealt by this Pokemon's attacks. Positive values increase damage, negative reduce it. attack_cost_overrides: Override the energy cost for specific attacks. Maps attack index (0-based) to a new list of EnergyType requirements. If an attack index is not in this dict, the base cost from CardDefinition is used. Example: {0: [EnergyType.COLORLESS, EnergyType.COLORLESS]} makes the first attack cost 2 colorless instead of its normal cost. evolved_from_instance_id: The CardInstance this evolved from. turn_played: The turn number when this card was played/evolved. Used for evolution timing rules. turn_evolved: The turn number when this card last evolved. Used to prevent same-turn evolution chains. """ instance_id: str definition_id: str # Battle state (Pokemon only) damage: int = 0 attached_energy: list[str] = Field(default_factory=list) attached_tools: list[str] = Field(default_factory=list) status_conditions: list[StatusCondition] = Field(default_factory=list) ability_uses_this_turn: int = 0 # Stat modifiers (applied by card effects, tools, abilities, etc.) hp_modifier: int = 0 retreat_cost_modifier: int = 0 damage_modifier: int = 0 attack_cost_overrides: dict[int, list[EnergyType]] = Field(default_factory=dict) # Evolution tracking evolved_from_instance_id: str | None = None turn_played: int | None = None turn_evolved: int | None = None def effective_hp(self, base_hp: int) -> int: """Calculate effective max HP including modifiers. Args: base_hp: The Pokemon's base HP (from CardDefinition.hp). Returns: Effective max HP (minimum 1 to prevent instant KO from negative modifiers). """ return max(1, base_hp + self.hp_modifier) def effective_retreat_cost(self, base_cost: int) -> int: """Calculate effective retreat cost including modifiers. Args: base_cost: The Pokemon's base retreat cost (from CardDefinition.retreat_cost). Returns: Effective retreat cost (minimum 0, can't be negative). """ return max(0, base_cost + self.retreat_cost_modifier) def effective_attack_cost( self, attack_index: int, base_cost: list[EnergyType] ) -> list[EnergyType]: """Get the effective energy cost for an attack, including any overrides. Args: attack_index: The index of the attack in CardDefinition.attacks (0-based). base_cost: The attack's base energy cost (from Attack.cost). Returns: The effective energy cost - either the override if set, or the base cost. Example: # An opponent's effect makes your first attack cost 2 more colorless instance.attack_cost_overrides[0] = base_cost + [EnergyType.COLORLESS] * 2 # A control effect completely changes the cost instance.attack_cost_overrides[1] = [EnergyType.PSYCHIC, EnergyType.PSYCHIC] """ if attack_index in self.attack_cost_overrides: return self.attack_cost_overrides[attack_index] return base_cost def is_knocked_out(self, base_hp: int) -> bool: """Check if this Pokemon is knocked out. Args: base_hp: The Pokemon's base HP (from CardDefinition.hp). Returns: True if damage >= effective HP. """ return self.damage >= self.effective_hp(base_hp) def remaining_hp(self, base_hp: int) -> int: """Calculate remaining HP. Args: base_hp: The Pokemon's base HP (from CardDefinition.hp). Returns: Remaining HP (minimum 0). """ return max(0, self.effective_hp(base_hp) - self.damage) def has_status(self, status: StatusCondition) -> bool: """Check if this Pokemon has a specific status condition.""" return status in self.status_conditions def add_status(self, status: StatusCondition) -> None: """Add a status condition, handling overrides. ASLEEP, PARALYZED, and CONFUSED override each other. POISONED and BURNED stack with everything. """ # These three override each other overriding = {StatusCondition.ASLEEP, StatusCondition.PARALYZED, StatusCondition.CONFUSED} if status in overriding: # Remove any existing overriding conditions self.status_conditions = [s for s in self.status_conditions if s not in overriding] if status not in self.status_conditions: self.status_conditions.append(status) def remove_status(self, status: StatusCondition) -> None: """Remove a status condition.""" if status in self.status_conditions: self.status_conditions.remove(status) def clear_all_status(self) -> None: """Remove all status conditions (e.g., when Pokemon retreats or evolves).""" self.status_conditions.clear() def reset_turn_state(self) -> None: """Reset per-turn state counters. Called at the start of each turn.""" self.ability_uses_this_turn = 0 def can_use_ability(self, ability: Ability) -> bool: """Check if this Pokemon can use the given ability. Compares ability_uses_this_turn against the ability's uses_per_turn limit. If uses_per_turn is None, the ability has no usage limit. Args: ability: The Ability to check. Returns: True if the ability can be used (hasn't hit its per-turn limit). """ if ability.uses_per_turn is None: return True # Unlimited uses return self.ability_uses_this_turn < ability.uses_per_turn def can_evolve_this_turn(self, current_turn: int) -> bool: """Check if this Pokemon can evolve this turn. Pokemon cannot evolve: - The same turn they were played - The same turn they already evolved Args: current_turn: The current turn number. Returns: True if this Pokemon can evolve. """ # Can't evolve same turn as played if self.turn_played == current_turn: return False # Can't evolve same turn as previous evolution return self.turn_evolved != current_turn def energy_count(self) -> int: """Get the number of attached energy cards.""" return len(self.attached_energy) def attach_energy(self, energy_instance_id: str) -> None: """Attach an energy card to this Pokemon. Args: energy_instance_id: The CardInstance.instance_id of the energy card. """ self.attached_energy.append(energy_instance_id) def detach_energy(self, energy_instance_id: str) -> bool: """Detach an energy card from this Pokemon. Args: energy_instance_id: The CardInstance.instance_id of the energy card. Returns: True if the energy was found and removed, False otherwise. """ if energy_instance_id in self.attached_energy: self.attached_energy.remove(energy_instance_id) return True return False