mantimon-tcg/backend/tests/core/conftest.py
Cal Corum f93f5b617a Remove legacy modifier field from WeaknessResistance
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>
2026-01-29 13:16:51 -06:00

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,
)