Extend core card models with rarity tiers and card subtypes
Add CardRarityTier enum for pull rate calculations (common through crown_rare). Add CardSubtype enum for Pokemon classifications (basic, stage1, stage2, ex, etc.). Update CardDefinition model with new fields for subtypes and rarity display.
This commit is contained in:
parent
7b79f02124
commit
1123d61067
@ -110,6 +110,8 @@ class PrizeConfig(BaseModel):
|
||||
per_knockout_v: Points scored for knocking out a V Pokemon.
|
||||
per_knockout_vmax: Points scored for knocking out a VMAX Pokemon.
|
||||
per_knockout_vstar: Points scored for knocking out a VSTAR Pokemon (3 points).
|
||||
per_knockout_radiant: Points scored for knocking out a Radiant Pokemon (2 points).
|
||||
per_knockout_prism_star: Points scored for knocking out a Prism Star Pokemon (2 points).
|
||||
use_prize_cards: If True, use classic prize card mechanic instead of points.
|
||||
prize_selection_random: If True, prize cards are taken randomly (classic).
|
||||
If False, player chooses which prize to take.
|
||||
@ -122,6 +124,8 @@ class PrizeConfig(BaseModel):
|
||||
per_knockout_v: int = 2
|
||||
per_knockout_vmax: int = 3
|
||||
per_knockout_vstar: int = 3
|
||||
per_knockout_radiant: int = 2
|
||||
per_knockout_prism_star: int = 2
|
||||
use_prize_cards: bool = False
|
||||
prize_selection_random: bool = True
|
||||
|
||||
@ -141,6 +145,8 @@ class PrizeConfig(BaseModel):
|
||||
PokemonVariant.V: self.per_knockout_v,
|
||||
PokemonVariant.VMAX: self.per_knockout_vmax,
|
||||
PokemonVariant.VSTAR: self.per_knockout_vstar,
|
||||
PokemonVariant.RADIANT: self.per_knockout_radiant,
|
||||
PokemonVariant.PRISM_STAR: self.per_knockout_prism_star,
|
||||
}
|
||||
return variant_map.get(variant, self.per_knockout_normal)
|
||||
|
||||
|
||||
@ -55,6 +55,8 @@ class PokemonVariant(StrEnum):
|
||||
- V: Worth 2 knockout points
|
||||
- VMAX: Evolves from V variant, worth 3 knockout points
|
||||
- VSTAR: Evolves from V variant, worth 3 knockout points, has VSTAR power
|
||||
- RADIANT: Worth 2 knockout points, only 1 Radiant Pokemon allowed per deck
|
||||
- PRISM_STAR: Worth 2 knockout points, only 1 per deck, goes to Lost Zone when KO'd
|
||||
"""
|
||||
|
||||
NORMAL = "normal"
|
||||
@ -63,6 +65,26 @@ class PokemonVariant(StrEnum):
|
||||
V = "v"
|
||||
VMAX = "vmax"
|
||||
VSTAR = "vstar"
|
||||
RADIANT = "radiant"
|
||||
PRISM_STAR = "prism_star"
|
||||
|
||||
|
||||
class PokemonPowerType(StrEnum):
|
||||
"""Type of Pokemon Power or Ability (era-specific).
|
||||
|
||||
Different eras of the Pokemon TCG used different terminology and rules
|
||||
for special abilities:
|
||||
|
||||
- POKEMON_POWER: Original Base-Fossil era. Can be "turned off" by certain effects.
|
||||
- POKE_POWER: Neo era activated abilities. Require explicit activation.
|
||||
- POKE_BODY: Neo era passive abilities. Always active, cannot be "turned off".
|
||||
- ABILITY: Modern era (BW onwards). Unified term for all special abilities.
|
||||
"""
|
||||
|
||||
POKEMON_POWER = "pokemon_power"
|
||||
POKE_POWER = "poke_power"
|
||||
POKE_BODY = "poke_body"
|
||||
ABILITY = "ability"
|
||||
|
||||
|
||||
class EnergyType(StrEnum):
|
||||
@ -210,6 +232,7 @@ __all__ = [
|
||||
"EnergyType",
|
||||
"GameEndReason",
|
||||
"ModifierMode",
|
||||
"PokemonPowerType",
|
||||
"PokemonStage",
|
||||
"PokemonVariant",
|
||||
"StatusCondition",
|
||||
|
||||
@ -37,6 +37,7 @@ from app.core.enums import (
|
||||
CardType,
|
||||
EnergyType,
|
||||
ModifierMode,
|
||||
PokemonPowerType,
|
||||
PokemonStage,
|
||||
PokemonVariant,
|
||||
StatusCondition,
|
||||
@ -60,6 +61,8 @@ class Attack(BaseModel):
|
||||
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.
|
||||
is_gx_attack: If True, this is a GX attack that can only be used once per game.
|
||||
The game engine tracks GX attack usage per player, not per Pokemon.
|
||||
"""
|
||||
|
||||
name: str
|
||||
@ -69,6 +72,7 @@ class Attack(BaseModel):
|
||||
effect_id: str | None = None
|
||||
effect_params: dict[str, Any] = Field(default_factory=dict)
|
||||
effect_description: str | None = None
|
||||
is_gx_attack: bool = False
|
||||
|
||||
|
||||
class Ability(BaseModel):
|
||||
@ -84,6 +88,11 @@ class Ability(BaseModel):
|
||||
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.
|
||||
is_vstar_power: If True, this is a VSTAR Power that can only be used once per game.
|
||||
The game engine tracks VSTAR Power usage per player, not per Pokemon.
|
||||
power_type: The era-specific type of this ability. Affects how certain card
|
||||
effects interact with it (e.g., "turn off Pokemon Powers" wouldn't affect
|
||||
modern Abilities). Defaults to ABILITY for modern cards.
|
||||
"""
|
||||
|
||||
name: str
|
||||
@ -91,6 +100,8 @@ class Ability(BaseModel):
|
||||
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)
|
||||
is_vstar_power: bool = False
|
||||
power_type: PokemonPowerType = PokemonPowerType.ABILITY
|
||||
|
||||
|
||||
class WeaknessResistance(BaseModel):
|
||||
@ -170,8 +181,8 @@ class CardDefinition(BaseModel):
|
||||
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.
|
||||
variant: Special variant - NORMAL, EX, GX, V, VMAX, VSTAR, RADIANT, PRISM_STAR
|
||||
(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).
|
||||
@ -180,19 +191,27 @@ class CardDefinition(BaseModel):
|
||||
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).
|
||||
baby_rule: If True, this is a Baby Pokemon (Classic era). Opponents must flip
|
||||
a coin before attacking; on tails, the attack fails.
|
||||
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.
|
||||
is_ace_spec: If True, this is an ACE SPEC Trainer (Modern era). Only one
|
||||
ACE SPEC card is allowed per deck.
|
||||
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.
|
||||
is_special_energy: If True, this is a Special Energy with additional effects.
|
||||
special_energy_effect_id: Effect handler for special energy effects.
|
||||
rarity: Card rarity (common, uncommon, rare, etc.).
|
||||
set_id: Which card set this belongs to.
|
||||
image_url: URL to the card image (CDN).
|
||||
image_path: Local path to the card image (e.g., "pokemon/a1/001-bulbasaur.webp").
|
||||
illustrator: Artist who illustrated this card.
|
||||
flavor_text: Flavor text on the card (Pokemon only, typically).
|
||||
ruleset_tags: List of rulesets this card is designed for (e.g., ["pocket", "classic"]).
|
||||
Used for filtering cards by game mode.
|
||||
"""
|
||||
|
||||
id: str
|
||||
@ -210,16 +229,20 @@ class CardDefinition(BaseModel):
|
||||
weakness: WeaknessResistance | None = None
|
||||
resistance: WeaknessResistance | None = None
|
||||
retreat_cost: int = 0
|
||||
baby_rule: bool = False # Classic era: flip coin to attack Baby Pokemon
|
||||
|
||||
# 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
|
||||
is_ace_spec: bool = False # Modern: only 1 ACE SPEC card allowed per deck
|
||||
|
||||
# Energy-specific fields
|
||||
energy_type: EnergyType | None = None
|
||||
energy_provides: list[EnergyType] = Field(default_factory=list)
|
||||
is_special_energy: bool = False
|
||||
special_energy_effect_id: str | None = None
|
||||
|
||||
# Metadata
|
||||
rarity: str = "common"
|
||||
@ -228,6 +251,7 @@ class CardDefinition(BaseModel):
|
||||
image_path: str | None = None # Local path: "pokemon/a1/001-bulbasaur.webp"
|
||||
illustrator: str | None = None
|
||||
flavor_text: str | None = None
|
||||
ruleset_tags: list[str] = Field(default_factory=list) # e.g., ["pocket", "classic", "modern"]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_card_type_fields(self) -> "CardDefinition":
|
||||
@ -324,9 +348,35 @@ class CardDefinition(BaseModel):
|
||||
PokemonVariant.V: 2,
|
||||
PokemonVariant.VMAX: 3,
|
||||
PokemonVariant.VSTAR: 3,
|
||||
PokemonVariant.RADIANT: 2,
|
||||
PokemonVariant.PRISM_STAR: 2,
|
||||
}
|
||||
return points_map.get(self.variant, 1)
|
||||
|
||||
def has_rule_box(self) -> bool:
|
||||
"""Check if this Pokemon has a Rule Box.
|
||||
|
||||
Pokemon with Rule Boxes are subject to various card effects that
|
||||
specifically target or exclude them. All special variants (EX, GX,
|
||||
V, VMAX, VSTAR, Radiant, Prism Star) have Rule Boxes.
|
||||
|
||||
Returns:
|
||||
True if this Pokemon has a Rule Box.
|
||||
"""
|
||||
if not self.is_pokemon():
|
||||
return False
|
||||
|
||||
rule_box_variants = {
|
||||
PokemonVariant.EX,
|
||||
PokemonVariant.GX,
|
||||
PokemonVariant.V,
|
||||
PokemonVariant.VMAX,
|
||||
PokemonVariant.VSTAR,
|
||||
PokemonVariant.RADIANT,
|
||||
PokemonVariant.PRISM_STAR,
|
||||
}
|
||||
return self.variant in rule_box_variants
|
||||
|
||||
|
||||
class CardInstance(BaseModel):
|
||||
"""A card instance in play with mutable state.
|
||||
|
||||
@ -119,7 +119,9 @@ class TestPokemonVariant:
|
||||
assert PokemonVariant.V == "v"
|
||||
assert PokemonVariant.VMAX == "vmax"
|
||||
assert PokemonVariant.VSTAR == "vstar"
|
||||
assert len(PokemonVariant) == 6
|
||||
assert PokemonVariant.RADIANT == "radiant"
|
||||
assert PokemonVariant.PRISM_STAR == "prism_star"
|
||||
assert len(PokemonVariant) == 8
|
||||
|
||||
def test_pokemon_variant_membership(self) -> None:
|
||||
"""
|
||||
@ -136,7 +138,7 @@ class TestPokemonVariant:
|
||||
Document which variants are worth more knockout points.
|
||||
|
||||
- NORMAL: 1 point (standard Pokemon)
|
||||
- EX, GX, V: 2 points
|
||||
- EX, GX, V, RADIANT, PRISM_STAR: 2 points
|
||||
- VMAX, VSTAR: 3 points
|
||||
"""
|
||||
one_point_variants = {PokemonVariant.NORMAL}
|
||||
@ -144,6 +146,8 @@ class TestPokemonVariant:
|
||||
PokemonVariant.EX,
|
||||
PokemonVariant.GX,
|
||||
PokemonVariant.V,
|
||||
PokemonVariant.RADIANT,
|
||||
PokemonVariant.PRISM_STAR,
|
||||
}
|
||||
three_point_variants = {PokemonVariant.VMAX, PokemonVariant.VSTAR}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user