Add test coverage for validation and confusion, document effect knockout handling
Issue #2 gap: Added 14 CardDefinition validation tests covering all required field checks (hp, stage, pokemon_type, evolves_from, trainer_type, energy_type) with both negative and positive test cases. Issue #7 gap: Added 4 confusion attack engine tests covering heads/tails outcomes, self-damage, self-KO with opponent scoring, and configurable damage from RulesConfig. Issue #13 documentation: Added TODO comments in engine.py and handlers.py documenting the expected pattern for knockout detection when effect execution is implemented. Effect handlers set knockout flags; engine should process knockouts after all effects resolve. 825 tests passing (+17 new tests)
This commit is contained in:
parent
939ae421aa
commit
5f1eb11344
@ -425,10 +425,33 @@ def handle_remove_status(ctx: EffectContext) -> EffectResult:
|
||||
)
|
||||
|
||||
|
||||
# TODO: KNOCKOUT DETECTION FOR EFFECT-BASED DAMAGE
|
||||
#
|
||||
# The handlers below (coin_flip_damage, bench_damage) apply damage but do NOT
|
||||
# currently detect knockouts. This is intentional for now because:
|
||||
#
|
||||
# 1. The engine doesn't yet execute attack effects - only base damage is applied
|
||||
# 2. When effect execution is added, the engine should handle knockout detection
|
||||
# AFTER all effects resolve, not during each effect
|
||||
# 3. The deal_damage/attack_damage handlers set knockout flags as informational
|
||||
# markers that the engine can use to identify which Pokemon to check
|
||||
#
|
||||
# When implementing effect execution in engine.py:
|
||||
# - Execute all effects and collect EffectResults
|
||||
# - Check details["knockout"] flag in each result
|
||||
# - Call process_knockout() for each unique knockout_pokemon_id
|
||||
# - Handle both active and bench knockouts (including multi-knockout scenarios)
|
||||
#
|
||||
# See: engine.py _execute_attack() for the TODO on integrating this
|
||||
|
||||
|
||||
@effect_handler("coin_flip_damage")
|
||||
def handle_coin_flip_damage(ctx: EffectContext) -> EffectResult:
|
||||
"""Deal damage based on coin flip results.
|
||||
|
||||
Note: This handler does NOT check for knockouts. The engine is responsible
|
||||
for knockout detection after all effects resolve. See module-level TODO.
|
||||
|
||||
Params:
|
||||
damage_per_heads (int): Damage dealt per heads result. Required.
|
||||
flip_count (int): Number of coins to flip. Default 1.
|
||||
@ -772,6 +795,9 @@ def handle_shuffle_deck(ctx: EffectContext) -> EffectResult:
|
||||
def handle_bench_damage(ctx: EffectContext) -> EffectResult:
|
||||
"""Deal damage to benched Pokemon.
|
||||
|
||||
Note: This handler does NOT check for knockouts. The engine is responsible
|
||||
for knockout detection after all effects resolve. See module-level TODO.
|
||||
|
||||
Params:
|
||||
amount (int): Damage to deal to each target. Required.
|
||||
target_opponent (bool): Target opponent's bench. Default True.
|
||||
|
||||
@ -765,6 +765,18 @@ class GameEngine:
|
||||
return ActionResult(success=False, message="Opponent has no active Pokemon")
|
||||
|
||||
# Calculate and apply damage (simplified)
|
||||
# TODO: EFFECT EXECUTION - When attack effects are implemented, they should be
|
||||
# executed here BEFORE knockout detection. Effects like coin_flip_damage and
|
||||
# bench_damage will deal additional damage. After ALL effects resolve, iterate
|
||||
# through all damaged Pokemon (defender, benched Pokemon, even attacker from
|
||||
# recoil) and call process_knockout() for each KO'd Pokemon.
|
||||
#
|
||||
# The damage handlers (deal_damage, attack_damage) set details["knockout"]=True
|
||||
# when damage KOs a target - use this to identify which Pokemon need knockout
|
||||
# processing without re-checking every Pokemon.
|
||||
#
|
||||
# See: app/core/effects/handlers.py for knockout flag pattern
|
||||
# See: SYSTEM_REVIEW.md Issue #13 for context
|
||||
base_damage = attack.damage or 0
|
||||
defender.damage += base_damage
|
||||
|
||||
|
||||
@ -2393,6 +2393,257 @@ class TestDeckOutAndEdgeCases:
|
||||
assert not result.success
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Confusion Status - Attack Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestConfusionAttack:
|
||||
"""Tests for confusion status during attack execution.
|
||||
|
||||
When a confused Pokemon attacks, it must flip a coin:
|
||||
- Heads: attack proceeds normally
|
||||
- Tails: attack fails and Pokemon damages itself
|
||||
|
||||
These tests verify the engine correctly handles both outcomes,
|
||||
including edge cases like self-KO from confusion damage.
|
||||
"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confused_attack_heads_proceeds_normally(
|
||||
self,
|
||||
player1_deck: list[CardInstance],
|
||||
player2_deck: list[CardInstance],
|
||||
card_registry: dict[str, CardDefinition],
|
||||
basic_pokemon_def: CardDefinition,
|
||||
energy_def: CardDefinition,
|
||||
):
|
||||
"""
|
||||
Test that a confused Pokemon's attack proceeds normally on heads.
|
||||
|
||||
When the confusion coin flip is heads, the attack should execute
|
||||
as if the Pokemon was not confused, dealing normal damage to defender.
|
||||
"""
|
||||
from app.core.rng import SeededRandom
|
||||
|
||||
# Create engine - we'll replace RNG after game creation
|
||||
engine = GameEngine()
|
||||
|
||||
result = engine.create_game(
|
||||
player_ids=["player1", "player2"],
|
||||
decks={"player1": player1_deck, "player2": player2_deck},
|
||||
card_registry=card_registry,
|
||||
)
|
||||
game = result.game
|
||||
|
||||
# Replace RNG with one that will flip heads (seed=1 produces heads)
|
||||
engine.rng = SeededRandom(seed=1)
|
||||
|
||||
p1 = game.players["player1"]
|
||||
p2 = game.players["player2"]
|
||||
|
||||
attacker = CardInstance(instance_id="p1-active", definition_id=basic_pokemon_def.id)
|
||||
energy = CardInstance(instance_id="attacker-energy", definition_id=energy_def.id)
|
||||
attacker.attach_energy(energy)
|
||||
p1.active.add(attacker)
|
||||
|
||||
defender = CardInstance(instance_id="p2-active", definition_id=basic_pokemon_def.id)
|
||||
p2.active.add(defender)
|
||||
|
||||
attacker.add_status(StatusCondition.CONFUSED)
|
||||
game.current_player_id = "player1" # Set player1's turn
|
||||
game.phase = TurnPhase.ATTACK
|
||||
game.turn_number = 2
|
||||
|
||||
action = AttackAction(attack_index=0)
|
||||
result = await engine.execute_action(game, "player1", action)
|
||||
|
||||
assert result.success
|
||||
assert "heads" in result.message.lower()
|
||||
# Defender should have taken damage
|
||||
assert defender.damage > 0
|
||||
# Attacker should not have self-damage
|
||||
assert attacker.damage == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confused_attack_tails_fails_with_self_damage(
|
||||
self,
|
||||
player1_deck: list[CardInstance],
|
||||
player2_deck: list[CardInstance],
|
||||
card_registry: dict[str, CardDefinition],
|
||||
basic_pokemon_def: CardDefinition,
|
||||
energy_def: CardDefinition,
|
||||
):
|
||||
"""
|
||||
Test that a confused Pokemon damages itself on tails.
|
||||
|
||||
When the confusion coin flip is tails, the attack should fail
|
||||
and the Pokemon should deal self-damage (default 30).
|
||||
"""
|
||||
from app.core.rng import SeededRandom
|
||||
|
||||
# Create engine - we'll replace RNG after game creation
|
||||
engine = GameEngine()
|
||||
|
||||
result = engine.create_game(
|
||||
player_ids=["player1", "player2"],
|
||||
decks={"player1": player1_deck, "player2": player2_deck},
|
||||
card_registry=card_registry,
|
||||
)
|
||||
game = result.game
|
||||
|
||||
# Replace RNG with one that will flip tails (seed=0 produces tails)
|
||||
engine.rng = SeededRandom(seed=0)
|
||||
|
||||
p1 = game.players["player1"]
|
||||
p2 = game.players["player2"]
|
||||
|
||||
attacker = CardInstance(instance_id="p1-active", definition_id=basic_pokemon_def.id)
|
||||
energy = CardInstance(instance_id="attacker-energy", definition_id=energy_def.id)
|
||||
attacker.attach_energy(energy)
|
||||
p1.active.add(attacker)
|
||||
|
||||
defender = CardInstance(instance_id="p2-active", definition_id=basic_pokemon_def.id)
|
||||
p2.active.add(defender)
|
||||
|
||||
attacker.add_status(StatusCondition.CONFUSED)
|
||||
game.current_player_id = "player1" # Set player1's turn
|
||||
game.phase = TurnPhase.ATTACK
|
||||
game.turn_number = 2
|
||||
|
||||
action = AttackAction(attack_index=0)
|
||||
result = await engine.execute_action(game, "player1", action)
|
||||
|
||||
# Action still succeeds (coin was flipped), but attack failed
|
||||
assert result.success
|
||||
assert "tails" in result.message.lower()
|
||||
assert "self-damage" in result.message.lower() or "30" in result.message
|
||||
# Attacker should have self-damage (default 30)
|
||||
assert attacker.damage == 30
|
||||
# Defender should NOT have taken damage
|
||||
assert defender.damage == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confused_attack_tails_self_ko(
|
||||
self,
|
||||
player1_deck: list[CardInstance],
|
||||
player2_deck: list[CardInstance],
|
||||
card_registry: dict[str, CardDefinition],
|
||||
basic_pokemon_def: CardDefinition,
|
||||
energy_def: CardDefinition,
|
||||
):
|
||||
"""
|
||||
Test that confusion self-damage can knock out the attacker.
|
||||
|
||||
If the attacker has low HP remaining, the 30 self-damage from
|
||||
confusion can knock it out. The opponent should receive points/prizes.
|
||||
"""
|
||||
from app.core.rng import SeededRandom
|
||||
|
||||
# Create engine - we'll replace RNG after game creation
|
||||
engine = GameEngine()
|
||||
|
||||
result = engine.create_game(
|
||||
player_ids=["player1", "player2"],
|
||||
decks={"player1": player1_deck, "player2": player2_deck},
|
||||
card_registry=card_registry,
|
||||
)
|
||||
game = result.game
|
||||
|
||||
# Replace RNG with one that will flip tails (seed=0 produces tails)
|
||||
engine.rng = SeededRandom(seed=0)
|
||||
|
||||
p1 = game.players["player1"]
|
||||
p2 = game.players["player2"]
|
||||
|
||||
attacker = CardInstance(instance_id="p1-active", definition_id=basic_pokemon_def.id)
|
||||
energy = CardInstance(instance_id="attacker-energy", definition_id=energy_def.id)
|
||||
attacker.attach_energy(energy)
|
||||
# Give attacker damage so self-damage will KO it (60 HP Pokemon)
|
||||
attacker.damage = 40 # 40 + 30 self-damage = 70 > 60 HP
|
||||
p1.active.add(attacker)
|
||||
|
||||
# Add bench Pokemon so game doesn't end immediately
|
||||
bench = CardInstance(instance_id="p1-bench", definition_id=basic_pokemon_def.id)
|
||||
p1.bench.add(bench)
|
||||
|
||||
defender = CardInstance(instance_id="p2-active", definition_id=basic_pokemon_def.id)
|
||||
p2.active.add(defender)
|
||||
|
||||
attacker.add_status(StatusCondition.CONFUSED)
|
||||
game.current_player_id = "player1" # Set player1's turn
|
||||
game.phase = TurnPhase.ATTACK
|
||||
game.turn_number = 2
|
||||
|
||||
initial_p2_score = p2.score
|
||||
|
||||
action = AttackAction(attack_index=0)
|
||||
result = await engine.execute_action(game, "player1", action)
|
||||
|
||||
assert result.success
|
||||
# Attacker should be knocked out (in discard, not in active)
|
||||
assert p1.active.get(attacker.instance_id) is None
|
||||
assert p1.discard.get(attacker.instance_id) is not None
|
||||
# Opponent should have scored
|
||||
assert p2.score > initial_p2_score
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confused_attack_uses_config_self_damage(
|
||||
self,
|
||||
player1_deck: list[CardInstance],
|
||||
player2_deck: list[CardInstance],
|
||||
card_registry: dict[str, CardDefinition],
|
||||
basic_pokemon_def: CardDefinition,
|
||||
energy_def: CardDefinition,
|
||||
):
|
||||
"""
|
||||
Test that confusion self-damage is configurable via RulesConfig.
|
||||
|
||||
The self-damage amount should come from rules.status.confusion_self_damage.
|
||||
"""
|
||||
from app.core.config import RulesConfig, StatusConfig
|
||||
from app.core.rng import SeededRandom
|
||||
|
||||
# Create engine - we'll replace RNG after game creation
|
||||
engine = GameEngine()
|
||||
|
||||
result = engine.create_game(
|
||||
player_ids=["player1", "player2"],
|
||||
decks={"player1": player1_deck, "player2": player2_deck},
|
||||
card_registry=card_registry,
|
||||
)
|
||||
game = result.game
|
||||
|
||||
# Replace RNG with one that will flip tails (seed=0 produces tails)
|
||||
engine.rng = SeededRandom(seed=0)
|
||||
|
||||
# Custom rules with different confusion damage
|
||||
game.rules = RulesConfig(status=StatusConfig(confusion_self_damage=50))
|
||||
|
||||
p1 = game.players["player1"]
|
||||
p2 = game.players["player2"]
|
||||
|
||||
attacker = CardInstance(instance_id="p1-active", definition_id=basic_pokemon_def.id)
|
||||
energy = CardInstance(instance_id="attacker-energy", definition_id=energy_def.id)
|
||||
attacker.attach_energy(energy)
|
||||
p1.active.add(attacker)
|
||||
|
||||
defender = CardInstance(instance_id="p2-active", definition_id=basic_pokemon_def.id)
|
||||
p2.active.add(defender)
|
||||
|
||||
attacker.add_status(StatusCondition.CONFUSED)
|
||||
game.current_player_id = "player1" # Set player1's turn
|
||||
game.phase = TurnPhase.ATTACK
|
||||
game.turn_number = 2
|
||||
|
||||
action = AttackAction(attack_index=0)
|
||||
result = await engine.execute_action(game, "player1", action)
|
||||
|
||||
assert result.success
|
||||
# Attacker should have custom self-damage amount
|
||||
assert attacker.damage == 50
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Attach Energy - Energy Zone Tests
|
||||
# =============================================================================
|
||||
|
||||
@ -677,6 +677,274 @@ class TestCardDefinitionEnergy:
|
||||
assert rainbow.effect_id == "rainbow_damage"
|
||||
|
||||
|
||||
class TestCardDefinitionValidation:
|
||||
"""Tests for CardDefinition field validation.
|
||||
|
||||
These tests verify that the model_validator correctly enforces required
|
||||
fields based on card type. Each test uses pytest.raises to confirm that
|
||||
invalid card definitions are rejected at construction time.
|
||||
|
||||
This explicit negative testing ensures validation rules are documented
|
||||
and protected against regression.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
def test_pokemon_without_hp_raises(self) -> None:
|
||||
"""
|
||||
Verify Pokemon cards must have hp field set.
|
||||
|
||||
Pokemon without HP would break damage calculations and knockout detection.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="hp"):
|
||||
CardDefinition(
|
||||
id="invalid-pokemon",
|
||||
name="Invalid",
|
||||
card_type=CardType.POKEMON,
|
||||
stage=PokemonStage.BASIC,
|
||||
pokemon_type=EnergyType.FIRE,
|
||||
# hp is missing
|
||||
)
|
||||
|
||||
def test_pokemon_with_zero_hp_raises(self) -> None:
|
||||
"""
|
||||
Verify Pokemon cards must have positive hp.
|
||||
|
||||
Zero HP Pokemon would be instantly knocked out, which is invalid.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
CardDefinition(
|
||||
id="invalid-pokemon",
|
||||
name="Invalid",
|
||||
card_type=CardType.POKEMON,
|
||||
stage=PokemonStage.BASIC,
|
||||
pokemon_type=EnergyType.FIRE,
|
||||
hp=0,
|
||||
)
|
||||
|
||||
def test_pokemon_with_negative_hp_raises(self) -> None:
|
||||
"""
|
||||
Verify Pokemon cards reject negative hp values.
|
||||
|
||||
Negative HP is nonsensical and would break game logic.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
CardDefinition(
|
||||
id="invalid-pokemon",
|
||||
name="Invalid",
|
||||
card_type=CardType.POKEMON,
|
||||
stage=PokemonStage.BASIC,
|
||||
pokemon_type=EnergyType.FIRE,
|
||||
hp=-10,
|
||||
)
|
||||
|
||||
def test_pokemon_without_stage_raises(self) -> None:
|
||||
"""
|
||||
Verify Pokemon cards must have stage field set.
|
||||
|
||||
Stage is required for evolution chain validation and gameplay rules.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="stage"):
|
||||
CardDefinition(
|
||||
id="invalid-pokemon",
|
||||
name="Invalid",
|
||||
card_type=CardType.POKEMON,
|
||||
hp=60,
|
||||
pokemon_type=EnergyType.FIRE,
|
||||
# stage is missing
|
||||
)
|
||||
|
||||
def test_pokemon_without_type_raises(self) -> None:
|
||||
"""
|
||||
Verify Pokemon cards must have pokemon_type field set.
|
||||
|
||||
Type is required for weakness/resistance calculations.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="pokemon_type"):
|
||||
CardDefinition(
|
||||
id="invalid-pokemon",
|
||||
name="Invalid",
|
||||
card_type=CardType.POKEMON,
|
||||
hp=60,
|
||||
stage=PokemonStage.BASIC,
|
||||
# pokemon_type is missing
|
||||
)
|
||||
|
||||
def test_stage_1_without_evolves_from_raises(self) -> None:
|
||||
"""
|
||||
Verify Stage 1 Pokemon must specify what they evolve from.
|
||||
|
||||
Stage 1 Pokemon must have evolves_from to validate evolution chains.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="evolves_from"):
|
||||
CardDefinition(
|
||||
id="invalid-stage1",
|
||||
name="Invalid Stage 1",
|
||||
card_type=CardType.POKEMON,
|
||||
hp=90,
|
||||
stage=PokemonStage.STAGE_1,
|
||||
pokemon_type=EnergyType.FIRE,
|
||||
# evolves_from is missing
|
||||
)
|
||||
|
||||
def test_stage_2_without_evolves_from_raises(self) -> None:
|
||||
"""
|
||||
Verify Stage 2 Pokemon must specify what they evolve from.
|
||||
|
||||
Stage 2 Pokemon must have evolves_from to validate evolution chains.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="evolves_from"):
|
||||
CardDefinition(
|
||||
id="invalid-stage2",
|
||||
name="Invalid Stage 2",
|
||||
card_type=CardType.POKEMON,
|
||||
hp=150,
|
||||
stage=PokemonStage.STAGE_2,
|
||||
pokemon_type=EnergyType.FIRE,
|
||||
# evolves_from is missing
|
||||
)
|
||||
|
||||
def test_vmax_without_evolves_from_raises(self) -> None:
|
||||
"""
|
||||
Verify VMAX Pokemon must specify what they evolve from.
|
||||
|
||||
VMAX are evolution variants that require a base V Pokemon.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="evolves_from"):
|
||||
CardDefinition(
|
||||
id="invalid-vmax",
|
||||
name="Invalid VMAX",
|
||||
card_type=CardType.POKEMON,
|
||||
hp=320,
|
||||
stage=PokemonStage.BASIC, # VMAX can be BASIC stage but still evolves
|
||||
variant=PokemonVariant.VMAX,
|
||||
pokemon_type=EnergyType.FIRE,
|
||||
# evolves_from is missing
|
||||
)
|
||||
|
||||
def test_vstar_without_evolves_from_raises(self) -> None:
|
||||
"""
|
||||
Verify VSTAR Pokemon must specify what they evolve from.
|
||||
|
||||
VSTAR are evolution variants that require a base V Pokemon.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="evolves_from"):
|
||||
CardDefinition(
|
||||
id="invalid-vstar",
|
||||
name="Invalid VSTAR",
|
||||
card_type=CardType.POKEMON,
|
||||
hp=280,
|
||||
stage=PokemonStage.BASIC,
|
||||
variant=PokemonVariant.VSTAR,
|
||||
pokemon_type=EnergyType.FIRE,
|
||||
# evolves_from is missing
|
||||
)
|
||||
|
||||
def test_trainer_without_trainer_type_raises(self) -> None:
|
||||
"""
|
||||
Verify Trainer cards must have trainer_type field set.
|
||||
|
||||
Trainer type (Item, Supporter, Stadium) determines gameplay rules.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="trainer_type"):
|
||||
CardDefinition(
|
||||
id="invalid-trainer",
|
||||
name="Invalid Trainer",
|
||||
card_type=CardType.TRAINER,
|
||||
# trainer_type is missing
|
||||
)
|
||||
|
||||
def test_energy_without_energy_type_raises(self) -> None:
|
||||
"""
|
||||
Verify Energy cards must have energy_type field set.
|
||||
|
||||
Energy type determines what attack costs the energy can satisfy.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="energy_type"):
|
||||
CardDefinition(
|
||||
id="invalid-energy",
|
||||
name="Invalid Energy",
|
||||
card_type=CardType.ENERGY,
|
||||
# energy_type is missing
|
||||
)
|
||||
|
||||
def test_valid_basic_pokemon_passes(self) -> None:
|
||||
"""
|
||||
Verify valid Basic Pokemon passes validation (sanity check).
|
||||
|
||||
Ensures validation doesn't reject valid cards.
|
||||
"""
|
||||
pokemon = CardDefinition(
|
||||
id="valid-pokemon",
|
||||
name="Valid Pokemon",
|
||||
card_type=CardType.POKEMON,
|
||||
hp=60,
|
||||
stage=PokemonStage.BASIC,
|
||||
pokemon_type=EnergyType.FIRE,
|
||||
)
|
||||
|
||||
assert pokemon.hp == 60
|
||||
assert pokemon.stage == PokemonStage.BASIC
|
||||
assert pokemon.pokemon_type == EnergyType.FIRE
|
||||
|
||||
def test_valid_stage_1_with_evolves_from_passes(self) -> None:
|
||||
"""
|
||||
Verify valid Stage 1 Pokemon with evolves_from passes validation.
|
||||
"""
|
||||
pokemon = CardDefinition(
|
||||
id="valid-stage1",
|
||||
name="Valid Stage 1",
|
||||
card_type=CardType.POKEMON,
|
||||
hp=90,
|
||||
stage=PokemonStage.STAGE_1,
|
||||
pokemon_type=EnergyType.FIRE,
|
||||
evolves_from="charmander",
|
||||
)
|
||||
|
||||
assert pokemon.stage == PokemonStage.STAGE_1
|
||||
assert pokemon.evolves_from == "charmander"
|
||||
|
||||
def test_valid_vmax_with_evolves_from_passes(self) -> None:
|
||||
"""
|
||||
Verify valid VMAX Pokemon with evolves_from passes validation.
|
||||
"""
|
||||
pokemon = CardDefinition(
|
||||
id="valid-vmax",
|
||||
name="Valid VMAX",
|
||||
card_type=CardType.POKEMON,
|
||||
hp=320,
|
||||
stage=PokemonStage.BASIC,
|
||||
variant=PokemonVariant.VMAX,
|
||||
pokemon_type=EnergyType.FIRE,
|
||||
evolves_from="charizard-v",
|
||||
)
|
||||
|
||||
assert pokemon.variant == PokemonVariant.VMAX
|
||||
assert pokemon.evolves_from == "charizard-v"
|
||||
|
||||
|
||||
class TestCardDefinitionHelperMethodsOnNonPokemon:
|
||||
"""Tests for Pokemon-specific helper methods when called on non-Pokemon cards.
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user