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_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)
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user