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:
Cal Corum 2026-01-31 15:43:07 -06:00
parent 7b79f02124
commit 1123d61067
4 changed files with 87 additions and 4 deletions

View File

@ -110,6 +110,8 @@ class PrizeConfig(BaseModel):
per_knockout_v: Points scored for knocking out a V Pokemon. per_knockout_v: Points scored for knocking out a V Pokemon.
per_knockout_vmax: Points scored for knocking out a VMAX 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_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. use_prize_cards: If True, use classic prize card mechanic instead of points.
prize_selection_random: If True, prize cards are taken randomly (classic). prize_selection_random: If True, prize cards are taken randomly (classic).
If False, player chooses which prize to take. If False, player chooses which prize to take.
@ -122,6 +124,8 @@ class PrizeConfig(BaseModel):
per_knockout_v: int = 2 per_knockout_v: int = 2
per_knockout_vmax: int = 3 per_knockout_vmax: int = 3
per_knockout_vstar: int = 3 per_knockout_vstar: int = 3
per_knockout_radiant: int = 2
per_knockout_prism_star: int = 2
use_prize_cards: bool = False use_prize_cards: bool = False
prize_selection_random: bool = True prize_selection_random: bool = True
@ -141,6 +145,8 @@ class PrizeConfig(BaseModel):
PokemonVariant.V: self.per_knockout_v, PokemonVariant.V: self.per_knockout_v,
PokemonVariant.VMAX: self.per_knockout_vmax, PokemonVariant.VMAX: self.per_knockout_vmax,
PokemonVariant.VSTAR: self.per_knockout_vstar, 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) return variant_map.get(variant, self.per_knockout_normal)

View File

@ -55,6 +55,8 @@ class PokemonVariant(StrEnum):
- V: Worth 2 knockout points - V: Worth 2 knockout points
- VMAX: Evolves from V variant, worth 3 knockout points - VMAX: Evolves from V variant, worth 3 knockout points
- VSTAR: Evolves from V variant, worth 3 knockout points, has VSTAR power - 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" NORMAL = "normal"
@ -63,6 +65,26 @@ class PokemonVariant(StrEnum):
V = "v" V = "v"
VMAX = "vmax" VMAX = "vmax"
VSTAR = "vstar" 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): class EnergyType(StrEnum):
@ -210,6 +232,7 @@ __all__ = [
"EnergyType", "EnergyType",
"GameEndReason", "GameEndReason",
"ModifierMode", "ModifierMode",
"PokemonPowerType",
"PokemonStage", "PokemonStage",
"PokemonVariant", "PokemonVariant",
"StatusCondition", "StatusCondition",

View File

@ -37,6 +37,7 @@ from app.core.enums import (
CardType, CardType,
EnergyType, EnergyType,
ModifierMode, ModifierMode,
PokemonPowerType,
PokemonStage, PokemonStage,
PokemonVariant, PokemonVariant,
StatusCondition, StatusCondition,
@ -60,6 +61,8 @@ class Attack(BaseModel):
effect_id: Optional reference to an effect handler for special effects. effect_id: Optional reference to an effect handler for special effects.
effect_params: Parameters passed to the effect handler. effect_params: Parameters passed to the effect handler.
effect_description: Human-readable description of the effect. 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 name: str
@ -69,6 +72,7 @@ class Attack(BaseModel):
effect_id: str | None = None effect_id: str | None = None
effect_params: dict[str, Any] = Field(default_factory=dict) effect_params: dict[str, Any] = Field(default_factory=dict)
effect_description: str | None = None effect_description: str | None = None
is_gx_attack: bool = False
class Ability(BaseModel): class Ability(BaseModel):
@ -84,6 +88,11 @@ class Ability(BaseModel):
effect_description: Human-readable description of the ability. effect_description: Human-readable description of the ability.
uses_per_turn: Maximum uses per turn. None means unlimited uses. uses_per_turn: Maximum uses per turn. None means unlimited uses.
Default is 1 (once per turn), which matches standard Pokemon TCG rules. 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 name: str
@ -91,6 +100,8 @@ class Ability(BaseModel):
effect_params: dict[str, Any] = Field(default_factory=dict) effect_params: dict[str, Any] = Field(default_factory=dict)
effect_description: str | None = None effect_description: str | None = None
uses_per_turn: int | None = 1 # None = unlimited, 1 = once per turn (default) 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): class WeaknessResistance(BaseModel):
@ -170,8 +181,8 @@ class CardDefinition(BaseModel):
name: Display name of the card. name: Display name of the card.
card_type: Whether this is a Pokemon, Trainer, or Energy card. card_type: Whether this is a Pokemon, Trainer, or Energy card.
stage: Evolution stage - BASIC, STAGE_1, or STAGE_2 (Pokemon only). stage: Evolution stage - BASIC, STAGE_1, or STAGE_2 (Pokemon only).
variant: Special variant - NORMAL, EX, GX, V, VMAX, VSTAR (Pokemon only). variant: Special variant - NORMAL, EX, GX, V, VMAX, VSTAR, RADIANT, PRISM_STAR
Affects knockout points but not evolution mechanics. (Pokemon only). Affects knockout points but not evolution mechanics.
evolves_from: Name of the Pokemon this evolves from (Stage 1/2, VMAX, VSTAR only). evolves_from: Name of the Pokemon this evolves from (Stage 1/2, VMAX, VSTAR only).
hp: Hit points (Pokemon only). hp: Hit points (Pokemon only).
pokemon_type: Energy type of this Pokemon (for weakness/resistance). 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). weakness: Weakness to an energy type (Pokemon only).
resistance: Resistance 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). 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). trainer_type: Subtype of trainer card (Trainer only).
effect_id: Effect handler for this card (Trainer/special Energy). effect_id: Effect handler for this card (Trainer/special Energy).
effect_params: Parameters for the effect handler. effect_params: Parameters for the effect handler.
effect_description: Human-readable description of the card's effect. 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_type: Type of energy this card provides (Energy only).
energy_provides: List of energy types this card provides. energy_provides: List of energy types this card provides.
Basic energy provides one of its type. Special energy may provide multiple. 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.). rarity: Card rarity (common, uncommon, rare, etc.).
set_id: Which card set this belongs to. set_id: Which card set this belongs to.
image_url: URL to the card image (CDN). image_url: URL to the card image (CDN).
image_path: Local path to the card image (e.g., "pokemon/a1/001-bulbasaur.webp"). image_path: Local path to the card image (e.g., "pokemon/a1/001-bulbasaur.webp").
illustrator: Artist who illustrated this card. illustrator: Artist who illustrated this card.
flavor_text: Flavor text on the card (Pokemon only, typically). 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 id: str
@ -210,16 +229,20 @@ class CardDefinition(BaseModel):
weakness: WeaknessResistance | None = None weakness: WeaknessResistance | None = None
resistance: WeaknessResistance | None = None resistance: WeaknessResistance | None = None
retreat_cost: int = 0 retreat_cost: int = 0
baby_rule: bool = False # Classic era: flip coin to attack Baby Pokemon
# Trainer-specific fields # Trainer-specific fields
trainer_type: TrainerType | None = None trainer_type: TrainerType | None = None
effect_id: str | None = None effect_id: str | None = None
effect_params: dict[str, Any] = Field(default_factory=dict) effect_params: dict[str, Any] = Field(default_factory=dict)
effect_description: str | None = None effect_description: str | None = None
is_ace_spec: bool = False # Modern: only 1 ACE SPEC card allowed per deck
# Energy-specific fields # Energy-specific fields
energy_type: EnergyType | None = None energy_type: EnergyType | None = None
energy_provides: list[EnergyType] = Field(default_factory=list) energy_provides: list[EnergyType] = Field(default_factory=list)
is_special_energy: bool = False
special_energy_effect_id: str | None = None
# Metadata # Metadata
rarity: str = "common" rarity: str = "common"
@ -228,6 +251,7 @@ class CardDefinition(BaseModel):
image_path: str | None = None # Local path: "pokemon/a1/001-bulbasaur.webp" image_path: str | None = None # Local path: "pokemon/a1/001-bulbasaur.webp"
illustrator: str | None = None illustrator: str | None = None
flavor_text: 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") @model_validator(mode="after")
def validate_card_type_fields(self) -> "CardDefinition": def validate_card_type_fields(self) -> "CardDefinition":
@ -324,9 +348,35 @@ class CardDefinition(BaseModel):
PokemonVariant.V: 2, PokemonVariant.V: 2,
PokemonVariant.VMAX: 3, PokemonVariant.VMAX: 3,
PokemonVariant.VSTAR: 3, PokemonVariant.VSTAR: 3,
PokemonVariant.RADIANT: 2,
PokemonVariant.PRISM_STAR: 2,
} }
return points_map.get(self.variant, 1) 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): class CardInstance(BaseModel):
"""A card instance in play with mutable state. """A card instance in play with mutable state.

View File

@ -119,7 +119,9 @@ class TestPokemonVariant:
assert PokemonVariant.V == "v" assert PokemonVariant.V == "v"
assert PokemonVariant.VMAX == "vmax" assert PokemonVariant.VMAX == "vmax"
assert PokemonVariant.VSTAR == "vstar" 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: def test_pokemon_variant_membership(self) -> None:
""" """
@ -136,7 +138,7 @@ class TestPokemonVariant:
Document which variants are worth more knockout points. Document which variants are worth more knockout points.
- NORMAL: 1 point (standard Pokemon) - NORMAL: 1 point (standard Pokemon)
- EX, GX, V: 2 points - EX, GX, V, RADIANT, PRISM_STAR: 2 points
- VMAX, VSTAR: 3 points - VMAX, VSTAR: 3 points
""" """
one_point_variants = {PokemonVariant.NORMAL} one_point_variants = {PokemonVariant.NORMAL}
@ -144,6 +146,8 @@ class TestPokemonVariant:
PokemonVariant.EX, PokemonVariant.EX,
PokemonVariant.GX, PokemonVariant.GX,
PokemonVariant.V, PokemonVariant.V,
PokemonVariant.RADIANT,
PokemonVariant.PRISM_STAR,
} }
three_point_variants = {PokemonVariant.VMAX, PokemonVariant.VSTAR} three_point_variants = {PokemonVariant.VMAX, PokemonVariant.VSTAR}