Migrate all usages to the proper mode/value fields: - Weakness: mode=MULTIPLICATIVE, value=2 - Resistance: mode=ADDITIVE, value=-30 Remove backwards compatibility code and legacy test. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
980 lines
30 KiB
Python
980 lines
30 KiB
Python
"""Pytest fixtures for the core game engine tests.
|
|
|
|
This module provides reusable fixtures for testing the game engine:
|
|
- Sample card definitions (Pokemon, Trainer, Energy)
|
|
- Pre-configured game states
|
|
- Seeded RNG instances for deterministic testing
|
|
- Helper functions for creating test data
|
|
|
|
Usage:
|
|
def test_something(sample_pokemon, seeded_rng):
|
|
# sample_pokemon is a CardDefinition
|
|
# seeded_rng is a SeededRandom instance
|
|
pass
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from app.core.config import RulesConfig
|
|
from app.core.enums import (
|
|
CardType,
|
|
EnergyType,
|
|
ModifierMode,
|
|
PokemonStage,
|
|
PokemonVariant,
|
|
TrainerType,
|
|
TurnPhase,
|
|
)
|
|
from app.core.models.card import (
|
|
Ability,
|
|
Attack,
|
|
CardDefinition,
|
|
CardInstance,
|
|
WeaknessResistance,
|
|
)
|
|
from app.core.models.game_state import GameState, PlayerState
|
|
from app.core.rng import SeededRandom
|
|
|
|
# ============================================================================
|
|
# RNG Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def seeded_rng() -> SeededRandom:
|
|
"""Provide a SeededRandom instance with a fixed seed for deterministic tests.
|
|
|
|
The seed 42 is used consistently across tests for reproducibility.
|
|
"""
|
|
return SeededRandom(seed=42)
|
|
|
|
|
|
@pytest.fixture
|
|
def rng_factory():
|
|
"""Factory fixture to create SeededRandom instances with custom seeds.
|
|
|
|
Usage:
|
|
def test_something(rng_factory):
|
|
rng1 = rng_factory(seed=100)
|
|
rng2 = rng_factory(seed=200)
|
|
"""
|
|
|
|
def _create_rng(seed: int = 42) -> SeededRandom:
|
|
return SeededRandom(seed=seed)
|
|
|
|
return _create_rng
|
|
|
|
|
|
# ============================================================================
|
|
# Card Definition Fixtures - Pokemon
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def pikachu_def() -> CardDefinition:
|
|
"""Basic Lightning Pokemon - Pikachu.
|
|
|
|
A simple Basic Pokemon with one attack and standard stats.
|
|
Used as the canonical "basic Pokemon" in tests.
|
|
"""
|
|
return CardDefinition(
|
|
id="pikachu_base_001",
|
|
name="Pikachu",
|
|
card_type=CardType.POKEMON,
|
|
stage=PokemonStage.BASIC,
|
|
variant=PokemonVariant.NORMAL,
|
|
hp=60,
|
|
pokemon_type=EnergyType.LIGHTNING,
|
|
attacks=[
|
|
Attack(
|
|
name="Thunder Shock",
|
|
cost=[EnergyType.LIGHTNING],
|
|
damage=20,
|
|
effect_id="may_paralyze",
|
|
effect_params={"chance": 0.5},
|
|
effect_description="Flip a coin. If heads, the Defending Pokemon is now Paralyzed.",
|
|
),
|
|
],
|
|
weakness=WeaknessResistance(
|
|
energy_type=EnergyType.FIGHTING, mode=ModifierMode.MULTIPLICATIVE, value=2
|
|
),
|
|
retreat_cost=1,
|
|
rarity="common",
|
|
set_id="base",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def raichu_def() -> CardDefinition:
|
|
"""Stage 1 Lightning Pokemon - Raichu.
|
|
|
|
Evolves from Pikachu. Used for evolution tests.
|
|
"""
|
|
return CardDefinition(
|
|
id="raichu_base_001",
|
|
name="Raichu",
|
|
card_type=CardType.POKEMON,
|
|
stage=PokemonStage.STAGE_1,
|
|
variant=PokemonVariant.NORMAL,
|
|
evolves_from="Pikachu",
|
|
hp=90,
|
|
pokemon_type=EnergyType.LIGHTNING,
|
|
attacks=[
|
|
Attack(
|
|
name="Thunder",
|
|
cost=[EnergyType.LIGHTNING, EnergyType.LIGHTNING, EnergyType.COLORLESS],
|
|
damage=60,
|
|
),
|
|
],
|
|
weakness=WeaknessResistance(
|
|
energy_type=EnergyType.FIGHTING, mode=ModifierMode.MULTIPLICATIVE, value=2
|
|
),
|
|
retreat_cost=1,
|
|
rarity="rare",
|
|
set_id="base",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def charizard_def() -> CardDefinition:
|
|
"""Stage 2 Fire Pokemon - Charizard.
|
|
|
|
Classic high-HP Stage 2 Pokemon. Evolves from Charmeleon.
|
|
"""
|
|
return CardDefinition(
|
|
id="charizard_base_001",
|
|
name="Charizard",
|
|
card_type=CardType.POKEMON,
|
|
stage=PokemonStage.STAGE_2,
|
|
variant=PokemonVariant.NORMAL,
|
|
evolves_from="Charmeleon",
|
|
hp=120,
|
|
pokemon_type=EnergyType.FIRE,
|
|
attacks=[
|
|
Attack(
|
|
name="Fire Spin",
|
|
cost=[EnergyType.FIRE, EnergyType.FIRE, EnergyType.FIRE, EnergyType.FIRE],
|
|
damage=100,
|
|
effect_id="discard_energy",
|
|
effect_params={"count": 2, "type": "fire"},
|
|
),
|
|
],
|
|
weakness=WeaknessResistance(
|
|
energy_type=EnergyType.WATER, mode=ModifierMode.MULTIPLICATIVE, value=2
|
|
),
|
|
resistance=WeaknessResistance(
|
|
energy_type=EnergyType.FIGHTING, mode=ModifierMode.ADDITIVE, value=-30
|
|
),
|
|
retreat_cost=3,
|
|
rarity="rare_holo",
|
|
set_id="base",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mewtwo_ex_def() -> CardDefinition:
|
|
"""Basic EX Pokemon - Mewtwo EX.
|
|
|
|
High-HP Pokemon worth 2 knockout points.
|
|
"""
|
|
return CardDefinition(
|
|
id="mewtwo_ex_001",
|
|
name="Mewtwo EX",
|
|
card_type=CardType.POKEMON,
|
|
stage=PokemonStage.BASIC,
|
|
variant=PokemonVariant.EX,
|
|
hp=170,
|
|
pokemon_type=EnergyType.PSYCHIC,
|
|
attacks=[
|
|
Attack(
|
|
name="Psydrive",
|
|
cost=[EnergyType.PSYCHIC, EnergyType.COLORLESS],
|
|
damage=120,
|
|
effect_id="discard_energy",
|
|
effect_params={"count": 1, "type": "any"},
|
|
),
|
|
],
|
|
weakness=WeaknessResistance(
|
|
energy_type=EnergyType.PSYCHIC, mode=ModifierMode.MULTIPLICATIVE, value=2
|
|
),
|
|
retreat_cost=2,
|
|
rarity="ultra_rare",
|
|
set_id="ex_series",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def pikachu_v_def() -> CardDefinition:
|
|
"""Basic V Pokemon - Pikachu V.
|
|
|
|
V Pokemon worth 2 knockout points.
|
|
"""
|
|
return CardDefinition(
|
|
id="pikachu_v_001",
|
|
name="Pikachu V",
|
|
card_type=CardType.POKEMON,
|
|
stage=PokemonStage.BASIC,
|
|
variant=PokemonVariant.V,
|
|
hp=190,
|
|
pokemon_type=EnergyType.LIGHTNING,
|
|
attacks=[
|
|
Attack(
|
|
name="Volt Tackle",
|
|
cost=[EnergyType.LIGHTNING, EnergyType.LIGHTNING, EnergyType.COLORLESS],
|
|
damage=210,
|
|
effect_id="self_damage",
|
|
effect_params={"amount": 30},
|
|
),
|
|
],
|
|
weakness=WeaknessResistance(
|
|
energy_type=EnergyType.FIGHTING, mode=ModifierMode.MULTIPLICATIVE, value=2
|
|
),
|
|
retreat_cost=2,
|
|
rarity="ultra_rare",
|
|
set_id="v_series",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def pikachu_vmax_def() -> CardDefinition:
|
|
"""VMAX Pokemon - Pikachu VMAX.
|
|
|
|
Evolves from Pikachu V, worth 3 knockout points.
|
|
"""
|
|
return CardDefinition(
|
|
id="pikachu_vmax_001",
|
|
name="Pikachu VMAX",
|
|
card_type=CardType.POKEMON,
|
|
stage=PokemonStage.BASIC, # Stage is still Basic
|
|
variant=PokemonVariant.VMAX, # Variant indicates V evolution
|
|
evolves_from="Pikachu V",
|
|
hp=310,
|
|
pokemon_type=EnergyType.LIGHTNING,
|
|
attacks=[
|
|
Attack(
|
|
name="G-Max Volt Crash",
|
|
cost=[EnergyType.LIGHTNING, EnergyType.LIGHTNING, EnergyType.LIGHTNING],
|
|
damage=270,
|
|
),
|
|
],
|
|
weakness=WeaknessResistance(
|
|
energy_type=EnergyType.FIGHTING, mode=ModifierMode.MULTIPLICATIVE, value=2
|
|
),
|
|
retreat_cost=3,
|
|
rarity="secret_rare",
|
|
set_id="vmax_series",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def pokemon_with_ability_def() -> CardDefinition:
|
|
"""Pokemon with an Ability - Shaymin EX.
|
|
|
|
Used for ability testing.
|
|
"""
|
|
return CardDefinition(
|
|
id="shaymin_ex_001",
|
|
name="Shaymin EX",
|
|
card_type=CardType.POKEMON,
|
|
stage=PokemonStage.BASIC,
|
|
variant=PokemonVariant.EX,
|
|
hp=110,
|
|
pokemon_type=EnergyType.COLORLESS,
|
|
abilities=[
|
|
Ability(
|
|
name="Set Up",
|
|
effect_id="draw_until_hand_size",
|
|
effect_params={"count": 6},
|
|
effect_description="When you play this Pokemon from your hand to your Bench, "
|
|
"you may draw cards until you have 6 cards in your hand.",
|
|
once_per_turn=True,
|
|
),
|
|
],
|
|
attacks=[
|
|
Attack(
|
|
name="Sky Return",
|
|
cost=[EnergyType.COLORLESS, EnergyType.COLORLESS],
|
|
damage=30,
|
|
effect_id="return_to_hand",
|
|
),
|
|
],
|
|
retreat_cost=1,
|
|
rarity="ultra_rare",
|
|
set_id="ex_series",
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Card Definition Fixtures - Trainers
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def potion_def() -> CardDefinition:
|
|
"""Item card - Potion.
|
|
|
|
Basic healing item.
|
|
"""
|
|
return CardDefinition(
|
|
id="potion_base_001",
|
|
name="Potion",
|
|
card_type=CardType.TRAINER,
|
|
trainer_type=TrainerType.ITEM,
|
|
effect_id="heal",
|
|
effect_params={"amount": 30},
|
|
effect_description="Heal 30 damage from one of your Pokemon.",
|
|
rarity="common",
|
|
set_id="base",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def professor_oak_def() -> CardDefinition:
|
|
"""Supporter card - Professor Oak.
|
|
|
|
Classic draw supporter.
|
|
"""
|
|
return CardDefinition(
|
|
id="professor_oak_001",
|
|
name="Professor Oak",
|
|
card_type=CardType.TRAINER,
|
|
trainer_type=TrainerType.SUPPORTER,
|
|
effect_id="discard_hand_draw",
|
|
effect_params={"draw_count": 7},
|
|
effect_description="Discard your hand and draw 7 cards.",
|
|
rarity="uncommon",
|
|
set_id="base",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def pokemon_center_def() -> CardDefinition:
|
|
"""Stadium card - Pokemon Center.
|
|
|
|
Example stadium that stays in play.
|
|
"""
|
|
return CardDefinition(
|
|
id="pokemon_center_001",
|
|
name="Pokemon Center",
|
|
card_type=CardType.TRAINER,
|
|
trainer_type=TrainerType.STADIUM,
|
|
effect_id="stadium_heal_between_turns",
|
|
effect_params={"amount": 20},
|
|
effect_description="Between turns, heal 20 damage from each player's Active Pokemon.",
|
|
rarity="uncommon",
|
|
set_id="base",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def choice_band_def() -> CardDefinition:
|
|
"""Tool card - Choice Band.
|
|
|
|
Damage-boosting tool.
|
|
"""
|
|
return CardDefinition(
|
|
id="choice_band_001",
|
|
name="Choice Band",
|
|
card_type=CardType.TRAINER,
|
|
trainer_type=TrainerType.TOOL,
|
|
effect_id="damage_boost_vs_ex_gx",
|
|
effect_params={"amount": 30},
|
|
effect_description="The attacks of the Pokemon this is attached to do 30 more damage "
|
|
"to your opponent's Active Pokemon-EX or Pokemon-GX.",
|
|
rarity="uncommon",
|
|
set_id="modern",
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Card Definition Fixtures - Energy
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def lightning_energy_def() -> CardDefinition:
|
|
"""Basic Lightning Energy."""
|
|
return CardDefinition(
|
|
id="lightning_energy_001",
|
|
name="Lightning Energy",
|
|
card_type=CardType.ENERGY,
|
|
energy_type=EnergyType.LIGHTNING,
|
|
energy_provides=[EnergyType.LIGHTNING],
|
|
rarity="common",
|
|
set_id="base",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def fire_energy_def() -> CardDefinition:
|
|
"""Basic Fire Energy."""
|
|
return CardDefinition(
|
|
id="fire_energy_001",
|
|
name="Fire Energy",
|
|
card_type=CardType.ENERGY,
|
|
energy_type=EnergyType.FIRE,
|
|
energy_provides=[EnergyType.FIRE],
|
|
rarity="common",
|
|
set_id="base",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def double_colorless_energy_def() -> CardDefinition:
|
|
"""Special Energy - Double Colorless Energy.
|
|
|
|
Provides 2 Colorless energy.
|
|
"""
|
|
return CardDefinition(
|
|
id="dce_001",
|
|
name="Double Colorless Energy",
|
|
card_type=CardType.ENERGY,
|
|
energy_type=EnergyType.COLORLESS,
|
|
energy_provides=[EnergyType.COLORLESS, EnergyType.COLORLESS],
|
|
rarity="uncommon",
|
|
set_id="base",
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Card Registry Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def basic_card_registry(
|
|
pikachu_def,
|
|
raichu_def,
|
|
charizard_def,
|
|
potion_def,
|
|
professor_oak_def,
|
|
lightning_energy_def,
|
|
fire_energy_def,
|
|
) -> dict[str, CardDefinition]:
|
|
"""A basic card registry with common test cards.
|
|
|
|
Includes: Pikachu, Raichu, Charizard, Potion, Professor Oak, basic energy.
|
|
"""
|
|
cards = [
|
|
pikachu_def,
|
|
raichu_def,
|
|
charizard_def,
|
|
potion_def,
|
|
professor_oak_def,
|
|
lightning_energy_def,
|
|
fire_energy_def,
|
|
]
|
|
return {card.id: card for card in cards}
|
|
|
|
|
|
# ============================================================================
|
|
# Card Instance Factory
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def card_instance_factory():
|
|
"""Factory fixture to create CardInstance objects.
|
|
|
|
Usage:
|
|
def test_something(card_instance_factory):
|
|
card = card_instance_factory("pikachu_base_001")
|
|
card_with_damage = card_instance_factory("pikachu_base_001", damage=30)
|
|
card_evolved = card_instance_factory("raichu_base_001", turn_evolved=2)
|
|
"""
|
|
_counter = [0]
|
|
|
|
def _create_instance(
|
|
definition_id: str,
|
|
instance_id: str | None = None,
|
|
damage: int = 0,
|
|
turn_played: int | None = None,
|
|
turn_evolved: int | None = None,
|
|
) -> CardInstance:
|
|
if instance_id is None:
|
|
_counter[0] += 1
|
|
instance_id = f"inst_{definition_id}_{_counter[0]}"
|
|
|
|
card = CardInstance(
|
|
instance_id=instance_id,
|
|
definition_id=definition_id,
|
|
damage=damage,
|
|
turn_played=turn_played,
|
|
)
|
|
if turn_evolved is not None:
|
|
card.turn_evolved = turn_evolved
|
|
return card
|
|
|
|
return _create_instance
|
|
|
|
|
|
# ============================================================================
|
|
# Game State Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def empty_game_state(basic_card_registry) -> GameState:
|
|
"""An empty game state ready for setup.
|
|
|
|
Has two players with empty zones, in SETUP phase.
|
|
"""
|
|
return GameState(
|
|
game_id="test_game_001",
|
|
rules=RulesConfig(),
|
|
card_registry=basic_card_registry,
|
|
players={
|
|
"player1": PlayerState(player_id="player1"),
|
|
"player2": PlayerState(player_id="player2"),
|
|
},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=0,
|
|
phase=TurnPhase.SETUP,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mid_game_state(basic_card_registry, card_instance_factory) -> GameState:
|
|
"""A game state in the middle of play.
|
|
|
|
- Turn 3, player1's turn, MAIN phase
|
|
- Player1: Pikachu active, Raichu on bench, 3 cards in hand, score 1
|
|
- Player2: Charizard active, 4 cards in hand, score 0
|
|
- Both players have cards in deck and discard
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Player 1 setup
|
|
player1.active.add(card_instance_factory("pikachu_base_001", turn_played=1))
|
|
player1.bench.add(card_instance_factory("raichu_base_001", turn_played=2))
|
|
for _ in range(3):
|
|
player1.hand.add(card_instance_factory("lightning_energy_001"))
|
|
for _ in range(10):
|
|
player1.deck.add(card_instance_factory("pikachu_base_001"))
|
|
player1.discard.add(card_instance_factory("potion_base_001"))
|
|
player1.score = 1
|
|
|
|
# Player 2 setup
|
|
player2.active.add(card_instance_factory("charizard_base_001", turn_played=1, damage=40))
|
|
for _ in range(4):
|
|
player2.hand.add(card_instance_factory("fire_energy_001"))
|
|
for _ in range(8):
|
|
player2.deck.add(card_instance_factory("charizard_base_001"))
|
|
player2.discard.add(card_instance_factory("professor_oak_001"))
|
|
player2.score = 0
|
|
|
|
return GameState(
|
|
game_id="test_game_mid",
|
|
rules=RulesConfig(),
|
|
card_registry=basic_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=3,
|
|
phase=TurnPhase.MAIN,
|
|
first_turn_completed=True,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def game_near_win_state(basic_card_registry, card_instance_factory) -> GameState:
|
|
"""A game state where player1 is about to win (3/4 points).
|
|
|
|
Used for testing win condition detection.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Player 1 setup - one knockout away from winning
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player1.score = 3 # 4 points needed to win
|
|
|
|
# Player 2 setup - low HP Pokemon active
|
|
damaged_pikachu = card_instance_factory("pikachu_base_001", damage=50) # 10 HP remaining
|
|
player2.active.add(damaged_pikachu)
|
|
|
|
return GameState(
|
|
game_id="test_game_near_win",
|
|
rules=RulesConfig(), # Default: 4 points to win
|
|
card_registry=basic_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.ATTACK,
|
|
first_turn_completed=True,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Rules Config Fixtures
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def default_rules() -> RulesConfig:
|
|
"""Default Mantimon TCG rules.
|
|
|
|
40-card deck, 4 points to win, Pokemon Pocket-style energy.
|
|
"""
|
|
return RulesConfig()
|
|
|
|
|
|
@pytest.fixture
|
|
def standard_tcg_rules() -> RulesConfig:
|
|
"""Standard Pokemon TCG rules.
|
|
|
|
60-card deck, 6 prizes, no energy deck.
|
|
"""
|
|
return RulesConfig.standard_pokemon_tcg()
|
|
|
|
|
|
# ============================================================================
|
|
# Additional Card Definition Fixtures for Evolution Testing
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def charmander_def() -> CardDefinition:
|
|
"""Basic Fire Pokemon - Charmander.
|
|
|
|
Used for evolution chain testing (Charmander -> Charmeleon -> Charizard).
|
|
"""
|
|
return CardDefinition(
|
|
id="charmander_base_001",
|
|
name="Charmander",
|
|
card_type=CardType.POKEMON,
|
|
stage=PokemonStage.BASIC,
|
|
variant=PokemonVariant.NORMAL,
|
|
hp=50,
|
|
pokemon_type=EnergyType.FIRE,
|
|
attacks=[
|
|
Attack(
|
|
name="Scratch",
|
|
cost=[EnergyType.COLORLESS],
|
|
damage=10,
|
|
),
|
|
],
|
|
weakness=WeaknessResistance(
|
|
energy_type=EnergyType.WATER, mode=ModifierMode.MULTIPLICATIVE, value=2
|
|
),
|
|
retreat_cost=1,
|
|
rarity="common",
|
|
set_id="base",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def charmeleon_def() -> CardDefinition:
|
|
"""Stage 1 Fire Pokemon - Charmeleon.
|
|
|
|
Evolves from Charmander. Used for evolution chain testing.
|
|
"""
|
|
return CardDefinition(
|
|
id="charmeleon_base_001",
|
|
name="Charmeleon",
|
|
card_type=CardType.POKEMON,
|
|
stage=PokemonStage.STAGE_1,
|
|
variant=PokemonVariant.NORMAL,
|
|
evolves_from="Charmander",
|
|
hp=80,
|
|
pokemon_type=EnergyType.FIRE,
|
|
attacks=[
|
|
Attack(
|
|
name="Slash",
|
|
cost=[EnergyType.FIRE, EnergyType.COLORLESS],
|
|
damage=30,
|
|
),
|
|
],
|
|
weakness=WeaknessResistance(
|
|
energy_type=EnergyType.WATER, mode=ModifierMode.MULTIPLICATIVE, value=2
|
|
),
|
|
retreat_cost=1,
|
|
rarity="uncommon",
|
|
set_id="base",
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Extended Card Registry Fixture
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def extended_card_registry(
|
|
pikachu_def,
|
|
raichu_def,
|
|
charmander_def,
|
|
charmeleon_def,
|
|
charizard_def,
|
|
mewtwo_ex_def,
|
|
pikachu_v_def,
|
|
pikachu_vmax_def,
|
|
pokemon_with_ability_def,
|
|
potion_def,
|
|
professor_oak_def,
|
|
pokemon_center_def,
|
|
choice_band_def,
|
|
lightning_energy_def,
|
|
fire_energy_def,
|
|
double_colorless_energy_def,
|
|
) -> dict[str, CardDefinition]:
|
|
"""Extended card registry with all test cards.
|
|
|
|
Includes full evolution chains and all card types for comprehensive testing.
|
|
"""
|
|
cards = [
|
|
pikachu_def,
|
|
raichu_def,
|
|
charmander_def,
|
|
charmeleon_def,
|
|
charizard_def,
|
|
mewtwo_ex_def,
|
|
pikachu_v_def,
|
|
pikachu_vmax_def,
|
|
pokemon_with_ability_def,
|
|
potion_def,
|
|
professor_oak_def,
|
|
pokemon_center_def,
|
|
choice_band_def,
|
|
lightning_energy_def,
|
|
fire_energy_def,
|
|
double_colorless_energy_def,
|
|
]
|
|
return {card.id: card for card in cards}
|
|
|
|
|
|
# ============================================================================
|
|
# Game State Fixtures for Rules Validation
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.fixture
|
|
def game_in_main_phase(extended_card_registry, card_instance_factory) -> GameState:
|
|
"""Game state in MAIN phase for testing main phase actions.
|
|
|
|
- Turn 2, player1's turn, MAIN phase
|
|
- Player1: Pikachu active (with 1 lightning energy), Charmander on bench, cards in hand
|
|
- Player2: Raichu active
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Player 1 setup - active with energy, bench pokemon, cards in hand
|
|
pikachu = card_instance_factory("pikachu_base_001", turn_played=1)
|
|
# Attach energy as a CardInstance - energy is stored directly on the Pokemon
|
|
energy = card_instance_factory("lightning_energy_001", instance_id="energy_lightning_1")
|
|
pikachu.attach_energy(energy)
|
|
player1.active.add(pikachu)
|
|
player1.bench.add(card_instance_factory("charmander_base_001", turn_played=1))
|
|
|
|
# Cards in hand: evolution card, energy, trainer
|
|
player1.hand.add(card_instance_factory("raichu_base_001", instance_id="hand_raichu"))
|
|
player1.hand.add(card_instance_factory("charmeleon_base_001", instance_id="hand_charmeleon"))
|
|
player1.hand.add(card_instance_factory("lightning_energy_001", instance_id="hand_energy"))
|
|
player1.hand.add(card_instance_factory("potion_base_001", instance_id="hand_potion"))
|
|
player1.hand.add(card_instance_factory("professor_oak_001", instance_id="hand_supporter"))
|
|
player1.hand.add(card_instance_factory("pokemon_center_001", instance_id="hand_stadium"))
|
|
player1.hand.add(card_instance_factory("pikachu_base_001", instance_id="hand_basic"))
|
|
|
|
# Energy in energy zone (for Pokemon Pocket style)
|
|
player1.energy_zone.add(
|
|
card_instance_factory("lightning_energy_001", instance_id="zone_energy")
|
|
)
|
|
|
|
# Some deck cards
|
|
for i in range(10):
|
|
player1.deck.add(card_instance_factory("pikachu_base_001", instance_id=f"deck_{i}"))
|
|
|
|
# Player 2 setup
|
|
player2.active.add(card_instance_factory("raichu_base_001", turn_played=1))
|
|
for i in range(10):
|
|
player2.deck.add(card_instance_factory("pikachu_base_001", instance_id=f"p2_deck_{i}"))
|
|
|
|
return GameState(
|
|
game_id="test_main_phase",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=2,
|
|
phase=TurnPhase.MAIN,
|
|
first_turn_completed=True,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def game_in_attack_phase(extended_card_registry, card_instance_factory) -> GameState:
|
|
"""Game state in ATTACK phase for testing attack validation.
|
|
|
|
- Turn 2, player1's turn, ATTACK phase
|
|
- Player1: Pikachu active with enough energy for Thunder Shock
|
|
- Player2: Raichu active
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Player 1 - Pikachu with 1 lightning energy (enough for Thunder Shock)
|
|
pikachu = card_instance_factory("pikachu_base_001", turn_played=1)
|
|
# Attach energy as a CardInstance - energy is stored directly on the Pokemon
|
|
energy = card_instance_factory("lightning_energy_001", instance_id="energy_lightning_1")
|
|
pikachu.attach_energy(energy)
|
|
player1.active.add(pikachu)
|
|
player1.bench.add(card_instance_factory("charmander_base_001", turn_played=1))
|
|
|
|
for i in range(10):
|
|
player1.deck.add(card_instance_factory("pikachu_base_001", instance_id=f"deck_{i}"))
|
|
|
|
# Player 2
|
|
player2.active.add(card_instance_factory("raichu_base_001", turn_played=1))
|
|
|
|
return GameState(
|
|
game_id="test_attack_phase",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=2,
|
|
phase=TurnPhase.ATTACK,
|
|
first_turn_completed=True,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def game_in_setup_phase(extended_card_registry, card_instance_factory) -> GameState:
|
|
"""Game state in SETUP phase for testing setup actions.
|
|
|
|
- Turn 0, SETUP phase
|
|
- Both players have empty zones but basic pokemon in hand
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Player 1 - basic pokemon in hand for setup
|
|
player1.hand.add(card_instance_factory("pikachu_base_001", instance_id="p1_hand_basic1"))
|
|
player1.hand.add(card_instance_factory("charmander_base_001", instance_id="p1_hand_basic2"))
|
|
|
|
# Player 2 - basic pokemon in hand
|
|
player2.hand.add(card_instance_factory("pikachu_base_001", instance_id="p2_hand_basic1"))
|
|
|
|
return GameState(
|
|
game_id="test_setup",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=0,
|
|
phase=TurnPhase.SETUP,
|
|
first_turn_completed=False,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def game_first_turn(extended_card_registry, card_instance_factory) -> GameState:
|
|
"""Game state on the first turn for testing first-turn restrictions.
|
|
|
|
- Turn 1, player1's turn, MAIN phase
|
|
- First turn restrictions apply
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Player 1 - just placed basic, has cards in hand
|
|
player1.active.add(card_instance_factory("pikachu_base_001", turn_played=1))
|
|
player1.hand.add(card_instance_factory("lightning_energy_001", instance_id="hand_energy"))
|
|
player1.hand.add(card_instance_factory("raichu_base_001", instance_id="hand_raichu"))
|
|
player1.hand.add(card_instance_factory("professor_oak_001", instance_id="hand_supporter"))
|
|
|
|
for i in range(10):
|
|
player1.deck.add(card_instance_factory("pikachu_base_001", instance_id=f"deck_{i}"))
|
|
|
|
# Player 2
|
|
player2.active.add(card_instance_factory("raichu_base_001", turn_played=1))
|
|
|
|
return GameState(
|
|
game_id="test_first_turn",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=1,
|
|
phase=TurnPhase.MAIN,
|
|
first_turn_completed=False, # Still first turn
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def game_with_forced_action(extended_card_registry, card_instance_factory) -> GameState:
|
|
"""Game state with a forced action pending.
|
|
|
|
- Player2's active was knocked out, must select new active
|
|
- Player1 just attacked and knocked out player2's active
|
|
"""
|
|
from app.core.models.game_state import ForcedAction
|
|
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Player 1 - active pokemon
|
|
player1.active.add(card_instance_factory("pikachu_base_001", turn_played=1))
|
|
|
|
# Player 2 - no active (knocked out), but has bench
|
|
player2.bench.add(card_instance_factory("charmander_base_001", instance_id="p2_bench1"))
|
|
player2.bench.add(card_instance_factory("pikachu_base_001", instance_id="p2_bench2"))
|
|
|
|
game = GameState(
|
|
game_id="test_forced_action",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=3,
|
|
phase=TurnPhase.ATTACK,
|
|
first_turn_completed=True,
|
|
forced_actions=[
|
|
ForcedAction(
|
|
player_id="player2",
|
|
action_type="select_active",
|
|
reason="Active Pokemon was knocked out",
|
|
)
|
|
],
|
|
)
|
|
|
|
return game
|
|
|
|
|
|
@pytest.fixture
|
|
def game_over_state(extended_card_registry, card_instance_factory) -> GameState:
|
|
"""Game state where the game is over.
|
|
|
|
- Player1 has won
|
|
"""
|
|
from app.core.enums import GameEndReason
|
|
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player1.score = 4 # Won!
|
|
|
|
return GameState(
|
|
game_id="test_game_over",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=10,
|
|
phase=TurnPhase.END,
|
|
first_turn_completed=True,
|
|
winner_id="player1",
|
|
end_reason=GameEndReason.PRIZES_TAKEN,
|
|
)
|