- Add CardDefinition and CardInstance models for card templates and in-game state - Add Attack, Ability, and WeaknessResistance models for Pokemon card components - Add 11 action types as discriminated union (PlayPokemon, Evolve, Attack, etc.) - Split PokemonStage (BASIC, STAGE_1, STAGE_2) from PokemonVariant (NORMAL, EX, GX, V, VMAX, VSTAR) - Stage determines evolution mechanics, variant determines knockout points - Update PrizeConfig to use variant for knockout point calculation - VSTAR and VMAX both worth 3 points; EX, GX, V worth 2 points; NORMAL worth 1 point Tests: 204 passing, all linting clean
277 lines
8.7 KiB
Python
277 lines
8.7 KiB
Python
"""Player action models for the Mantimon TCG game engine.
|
|
|
|
This module defines all actions a player can take during their turn.
|
|
Actions are modeled as a discriminated union using Pydantic's Literal types,
|
|
enabling type-safe action handling and automatic JSON serialization.
|
|
|
|
The union type allows code like:
|
|
match action:
|
|
case PlayPokemonAction():
|
|
handle_play_pokemon(action)
|
|
case AttackAction():
|
|
handle_attack(action)
|
|
|
|
Usage:
|
|
# Create an action
|
|
action = AttackAction(attack_index=0)
|
|
|
|
# Serialize to JSON for WebSocket
|
|
json_str = action.model_dump_json()
|
|
|
|
# Parse incoming action from client
|
|
data = {"type": "attack", "attack_index": 0}
|
|
action = parse_action(data) # Returns AttackAction
|
|
"""
|
|
|
|
from typing import Annotated, Any, Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class PlayPokemonAction(BaseModel):
|
|
"""Play a Basic Pokemon from hand to the bench (or active during setup).
|
|
|
|
This action places a Basic Pokemon card from the player's hand onto their
|
|
bench. During the setup phase, it can optionally be placed as the active
|
|
Pokemon.
|
|
|
|
Attributes:
|
|
type: Discriminator field, always "play_pokemon".
|
|
card_instance_id: The CardInstance.instance_id of the Pokemon to play.
|
|
to_active: If True and during setup, place as active Pokemon.
|
|
Ignored during normal gameplay.
|
|
"""
|
|
|
|
type: Literal["play_pokemon"] = "play_pokemon"
|
|
card_instance_id: str
|
|
to_active: bool = False
|
|
|
|
|
|
class EvolvePokemonAction(BaseModel):
|
|
"""Evolve a Pokemon in play.
|
|
|
|
Places an evolution card from hand onto a Pokemon in play, provided
|
|
evolution rules are satisfied (not same turn as played/evolved, etc.).
|
|
|
|
Attributes:
|
|
type: Discriminator field, always "evolve".
|
|
evolution_card_id: The CardInstance.instance_id of the evolution card.
|
|
target_pokemon_id: The CardInstance.instance_id of the Pokemon to evolve.
|
|
"""
|
|
|
|
type: Literal["evolve"] = "evolve"
|
|
evolution_card_id: str
|
|
target_pokemon_id: str
|
|
|
|
|
|
class AttachEnergyAction(BaseModel):
|
|
"""Attach an energy card from hand (or energy deck) to a Pokemon.
|
|
|
|
Energy can be attached to the active Pokemon or any benched Pokemon.
|
|
Limited to once per turn by default (configurable via RulesConfig).
|
|
|
|
Attributes:
|
|
type: Discriminator field, always "attach_energy".
|
|
energy_card_id: The CardInstance.instance_id of the energy card.
|
|
target_pokemon_id: The CardInstance.instance_id of the Pokemon to attach to.
|
|
from_energy_deck: If True, the energy comes from the energy deck
|
|
(Pokemon Pocket style) rather than hand.
|
|
"""
|
|
|
|
type: Literal["attach_energy"] = "attach_energy"
|
|
energy_card_id: str
|
|
target_pokemon_id: str
|
|
from_energy_deck: bool = False
|
|
|
|
|
|
class PlayTrainerAction(BaseModel):
|
|
"""Play a Trainer card from hand.
|
|
|
|
The effect of the trainer card is resolved by the effect handler system.
|
|
Some trainer cards require targets (specified in the targets list).
|
|
|
|
Attributes:
|
|
type: Discriminator field, always "play_trainer".
|
|
card_instance_id: The CardInstance.instance_id of the trainer card.
|
|
targets: List of target CardInstance IDs for cards that require targeting.
|
|
The specific meaning depends on the card's effect.
|
|
additional_params: Extra parameters for complex trainer effects.
|
|
"""
|
|
|
|
type: Literal["play_trainer"] = "play_trainer"
|
|
card_instance_id: str
|
|
targets: list[str] = Field(default_factory=list)
|
|
additional_params: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class UseAbilityAction(BaseModel):
|
|
"""Use an ability on a Pokemon in play.
|
|
|
|
Abilities are typically limited to once per turn per Pokemon
|
|
(configurable per ability).
|
|
|
|
Attributes:
|
|
type: Discriminator field, always "use_ability".
|
|
pokemon_id: The CardInstance.instance_id of the Pokemon with the ability.
|
|
ability_index: Index of the ability in the Pokemon's abilities list.
|
|
targets: List of target CardInstance IDs if the ability requires targeting.
|
|
additional_params: Extra parameters for complex ability effects.
|
|
"""
|
|
|
|
type: Literal["use_ability"] = "use_ability"
|
|
pokemon_id: str
|
|
ability_index: int = 0
|
|
targets: list[str] = Field(default_factory=list)
|
|
additional_params: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class AttackAction(BaseModel):
|
|
"""Declare an attack with the active Pokemon.
|
|
|
|
This action can only be performed during the attack phase and ends the turn.
|
|
The attack's effect is resolved by the effect handler system.
|
|
|
|
Attributes:
|
|
type: Discriminator field, always "attack".
|
|
attack_index: Index of the attack in the active Pokemon's attacks list.
|
|
targets: List of target CardInstance IDs for attacks that allow targeting.
|
|
Most attacks target the defending Pokemon automatically.
|
|
additional_params: Extra parameters for complex attack effects.
|
|
"""
|
|
|
|
type: Literal["attack"] = "attack"
|
|
attack_index: int = 0
|
|
targets: list[str] = Field(default_factory=list)
|
|
additional_params: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class RetreatAction(BaseModel):
|
|
"""Retreat the active Pokemon, switching with a benched Pokemon.
|
|
|
|
Requires discarding energy equal to the retreat cost (unless modified
|
|
by effects). Limited to once per turn by default.
|
|
|
|
Attributes:
|
|
type: Discriminator field, always "retreat".
|
|
new_active_id: The CardInstance.instance_id of the benched Pokemon
|
|
to become the new active.
|
|
energy_to_discard: List of CardInstance IDs of energy cards to discard
|
|
as the retreat cost.
|
|
"""
|
|
|
|
type: Literal["retreat"] = "retreat"
|
|
new_active_id: str
|
|
energy_to_discard: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class PassAction(BaseModel):
|
|
"""Pass without taking an action, ending the current phase.
|
|
|
|
During the main phase, this advances to the attack phase.
|
|
During the attack phase, this ends the turn without attacking.
|
|
|
|
Attributes:
|
|
type: Discriminator field, always "pass".
|
|
"""
|
|
|
|
type: Literal["pass"] = "pass"
|
|
|
|
|
|
class SelectPrizeAction(BaseModel):
|
|
"""Select a prize card to take after a knockout.
|
|
|
|
Only used when using classic prize card rules (not the default point system).
|
|
|
|
Attributes:
|
|
type: Discriminator field, always "select_prize".
|
|
prize_index: Index of the prize card to take (0-5 for 6 prizes).
|
|
"""
|
|
|
|
type: Literal["select_prize"] = "select_prize"
|
|
prize_index: int
|
|
|
|
|
|
class SelectActiveAction(BaseModel):
|
|
"""Select a new active Pokemon after the current active is knocked out.
|
|
|
|
Attributes:
|
|
type: Discriminator field, always "select_active".
|
|
pokemon_id: The CardInstance.instance_id of the benched Pokemon
|
|
to become the new active.
|
|
"""
|
|
|
|
type: Literal["select_active"] = "select_active"
|
|
pokemon_id: str
|
|
|
|
|
|
class ResignAction(BaseModel):
|
|
"""Resign from the match, conceding victory to the opponent.
|
|
|
|
Attributes:
|
|
type: Discriminator field, always "resign".
|
|
"""
|
|
|
|
type: Literal["resign"] = "resign"
|
|
|
|
|
|
# Union type for all player actions
|
|
# Using Annotated with Field(discriminator) for automatic type resolution
|
|
Action = Annotated[
|
|
PlayPokemonAction
|
|
| EvolvePokemonAction
|
|
| AttachEnergyAction
|
|
| PlayTrainerAction
|
|
| UseAbilityAction
|
|
| AttackAction
|
|
| RetreatAction
|
|
| PassAction
|
|
| SelectPrizeAction
|
|
| SelectActiveAction
|
|
| ResignAction,
|
|
Field(discriminator="type"),
|
|
]
|
|
|
|
|
|
def parse_action(data: dict[str, Any]) -> Action:
|
|
"""Parse an action from a dictionary (e.g., from JSON).
|
|
|
|
Uses the 'type' field to determine which action model to use.
|
|
|
|
Args:
|
|
data: Dictionary containing action data with a 'type' field.
|
|
|
|
Returns:
|
|
The appropriate Action subtype.
|
|
|
|
Raises:
|
|
ValueError: If the action type is unknown.
|
|
ValidationError: If the data doesn't match the action schema.
|
|
|
|
Example:
|
|
data = {"type": "attack", "attack_index": 0}
|
|
action = parse_action(data)
|
|
assert isinstance(action, AttackAction)
|
|
"""
|
|
from pydantic import TypeAdapter
|
|
|
|
adapter = TypeAdapter(Action)
|
|
return adapter.validate_python(data)
|
|
|
|
|
|
# Mapping of action types to their valid phases
|
|
# This is used by the rules validator to check if an action is valid
|
|
# in the current phase
|
|
VALID_PHASES_FOR_ACTION: dict[str, list[str]] = {
|
|
"play_pokemon": ["setup", "main"],
|
|
"evolve": ["main"],
|
|
"attach_energy": ["main"],
|
|
"play_trainer": ["main"],
|
|
"use_ability": ["main"],
|
|
"attack": ["attack"],
|
|
"retreat": ["main"],
|
|
"pass": ["main", "attack"],
|
|
"select_prize": ["end"],
|
|
"select_active": ["main", "end"], # After knockout
|
|
"resign": ["setup", "draw", "main", "attack", "end"], # Any time
|
|
}
|