Implemented with Repository Protocol pattern for offline fork support: - CollectionService with PostgresCollectionRepository - DeckService with PostgresDeckRepository - DeckValidator with DeckConfig + CardService injection - Starter deck definitions (5 types: grass, fire, water, psychic, lightning) - Pydantic schemas for collection and deck APIs - Unit tests for DeckValidator (32 tests passing) Architecture follows pure dependency injection - no service locator patterns. Added CLAUDE.md documenting DI requirements and patterns. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
297 lines
9.2 KiB
Python
297 lines
9.2 KiB
Python
"""Starter deck definitions for Mantimon TCG.
|
|
|
|
This module defines the 5 starter decks available to new players:
|
|
- Grass: Bulbasaur, Caterpie, and Bellsprout evolution lines
|
|
- Fire: Charmander, Growlithe, and Ponyta lines
|
|
- Water: Squirtle, Poliwag, and Horsea lines
|
|
- Psychic: Abra, Gastly, and Drowzee lines
|
|
- Lightning: Pikachu, Magnemite, and Voltorb lines
|
|
|
|
Each deck contains exactly 40 Pokemon/Trainer cards + 20 energy cards,
|
|
following Mantimon TCG house rules.
|
|
|
|
Deck composition philosophy:
|
|
- 3 evolution lines (4 cards each for basics, 3-4 for evolutions)
|
|
- Basic support trainers for card draw and healing
|
|
- Type-specific energy (14) + colorless (6)
|
|
|
|
Usage:
|
|
from app.data.starter_decks import get_starter_deck, STARTER_TYPES
|
|
|
|
deck = get_starter_deck("grass")
|
|
cards = deck["cards"] # {card_id: quantity}
|
|
energy = deck["energy_cards"] # {type: quantity}
|
|
"""
|
|
|
|
from typing import Protocol, TypedDict
|
|
|
|
|
|
class DeckSizeConfig(Protocol):
|
|
"""Protocol for deck size configuration.
|
|
|
|
Defines the minimal interface needed for deck validation.
|
|
Any object with these attributes (like DeckConfig) satisfies this protocol.
|
|
"""
|
|
|
|
min_size: int
|
|
energy_deck_size: int
|
|
|
|
|
|
class StarterDeckDefinition(TypedDict):
|
|
"""Type definition for starter deck structure."""
|
|
|
|
name: str
|
|
description: str
|
|
cards: dict[str, int]
|
|
energy_cards: dict[str, int]
|
|
|
|
|
|
# Available starter deck types
|
|
STARTER_TYPES: list[str] = ["grass", "fire", "water", "psychic", "lightning"]
|
|
|
|
# Classic starter types (always available)
|
|
CLASSIC_STARTERS: list[str] = ["grass", "fire", "water"]
|
|
|
|
# Rotating starter types (may change seasonally)
|
|
ROTATING_STARTERS: list[str] = ["psychic", "lightning"]
|
|
|
|
|
|
STARTER_DECKS: dict[str, StarterDeckDefinition] = {
|
|
"grass": {
|
|
"name": "Forest Guardian",
|
|
"description": "A balanced Grass deck featuring Bulbasaur, Caterpie, and Bellsprout evolution lines.",
|
|
"cards": {
|
|
# Bulbasaur line (11 cards)
|
|
"a1-001-bulbasaur": 4,
|
|
"a1-002-ivysaur": 3,
|
|
"a1-003-venusaur": 2,
|
|
# Caterpie line (9 cards)
|
|
"a1-005-caterpie": 4,
|
|
"a1-006-metapod": 3,
|
|
"a1-007-butterfree": 2,
|
|
# Bellsprout line (9 cards)
|
|
"a1-018-bellsprout": 4,
|
|
"a1-019-weepinbell": 3,
|
|
"a1-020-victreebel": 2,
|
|
# Tangela (2 cards) - extra basics
|
|
"a1-024-tangela": 2,
|
|
# Trainers (11 cards)
|
|
"a1-219-erika": 2,
|
|
"a1-216-helix-fossil": 2,
|
|
"a1-217-dome-fossil": 2,
|
|
"a1-218-old-amber": 2,
|
|
"a1-224-brock": 3,
|
|
},
|
|
"energy_cards": {
|
|
"grass": 14,
|
|
"colorless": 6,
|
|
},
|
|
},
|
|
"fire": {
|
|
"name": "Inferno Blaze",
|
|
"description": "An aggressive Fire deck featuring Charmander, Growlithe, and Ponyta evolution lines.",
|
|
"cards": {
|
|
# Charmander line (9 cards)
|
|
"a1-033-charmander": 4,
|
|
"a1-034-charmeleon": 3,
|
|
"a1-035-charizard": 2,
|
|
# Growlithe line (6 cards)
|
|
"a1-039-growlithe": 4,
|
|
"a1-040-arcanine": 2,
|
|
# Ponyta line (7 cards)
|
|
"a1-042-ponyta": 4,
|
|
"a1-043-rapidash": 3,
|
|
# Vulpix (3 cards) - extra basics
|
|
"a1-037-vulpix": 3,
|
|
# Magmar (4 cards) - extra basics
|
|
"a1-044-magmar": 4,
|
|
# Trainers (11 cards)
|
|
"a1-221-blaine": 3,
|
|
"a1-216-helix-fossil": 2,
|
|
"a1-217-dome-fossil": 2,
|
|
"a1-218-old-amber": 2,
|
|
"a1-224-brock": 2,
|
|
},
|
|
"energy_cards": {
|
|
"fire": 14,
|
|
"colorless": 6,
|
|
},
|
|
},
|
|
"water": {
|
|
"name": "Tidal Wave",
|
|
"description": "A versatile Water deck featuring Squirtle, Poliwag, and Horsea evolution lines.",
|
|
"cards": {
|
|
# Squirtle line (9 cards)
|
|
"a1-053-squirtle": 4,
|
|
"a1-054-wartortle": 3,
|
|
"a1-055-blastoise": 2,
|
|
# Poliwag line (9 cards)
|
|
"a1-059-poliwag": 4,
|
|
"a1-060-poliwhirl": 3,
|
|
"a1-061-poliwrath": 2,
|
|
# Horsea line (6 cards)
|
|
"a1-070-horsea": 4,
|
|
"a1-071-seadra": 2,
|
|
# Seel line (5 cards) - extra
|
|
"a1-064-seel": 3,
|
|
"a1-065-dewgong": 2,
|
|
# Trainers (11 cards)
|
|
"a1-220-misty": 3,
|
|
"a1-216-helix-fossil": 2,
|
|
"a1-217-dome-fossil": 2,
|
|
"a1-218-old-amber": 2,
|
|
"a1-224-brock": 2,
|
|
},
|
|
"energy_cards": {
|
|
"water": 14,
|
|
"colorless": 6,
|
|
},
|
|
},
|
|
"psychic": {
|
|
"name": "Mind Over Matter",
|
|
"description": "A control-focused Psychic deck featuring Abra, Gastly, and Drowzee evolution lines.",
|
|
"cards": {
|
|
# Abra line (9 cards)
|
|
"a1-115-abra": 4,
|
|
"a1-116-kadabra": 3,
|
|
"a1-117-alakazam": 2,
|
|
# Gastly line (9 cards)
|
|
"a1-120-gastly": 4,
|
|
"a1-121-haunter": 3,
|
|
"a1-122-gengar": 2,
|
|
# Drowzee line (6 cards)
|
|
"a1-124-drowzee": 4,
|
|
"a1-125-hypno": 2,
|
|
# Slowpoke line (5 cards) - extra
|
|
"a1-118-slowpoke": 3,
|
|
"a1-119-slowbro": 2,
|
|
# Trainers (11 cards)
|
|
"a1-225-sabrina": 3,
|
|
"a1-216-helix-fossil": 2,
|
|
"a1-217-dome-fossil": 2,
|
|
"a1-218-old-amber": 2,
|
|
"a1-224-brock": 2,
|
|
},
|
|
"energy_cards": {
|
|
"psychic": 14,
|
|
"colorless": 6,
|
|
},
|
|
},
|
|
"lightning": {
|
|
"name": "Thunder Strike",
|
|
"description": "A fast Lightning deck featuring Pikachu, Magnemite, and Voltorb evolution lines.",
|
|
"cards": {
|
|
# Pikachu line (7 cards)
|
|
"a1-094-pikachu": 4,
|
|
"a1-095-raichu": 3,
|
|
# Magnemite line (7 cards)
|
|
"a1-097-magnemite": 4,
|
|
"a1-098-magneton": 3,
|
|
# Voltorb line (7 cards)
|
|
"a1-099-voltorb": 4,
|
|
"a1-100-electrode": 3,
|
|
# Electabuzz (4 cards) - extra basics
|
|
"a1-101-electabuzz": 4,
|
|
# Blitzle line (4 cards)
|
|
"a1-105-blitzle": 2,
|
|
"a1-106-zebstrika": 2,
|
|
# Trainers (11 cards)
|
|
"a1-226-lt-surge": 3,
|
|
"a1-216-helix-fossil": 2,
|
|
"a1-217-dome-fossil": 2,
|
|
"a1-218-old-amber": 2,
|
|
"a1-224-brock": 2,
|
|
},
|
|
"energy_cards": {
|
|
"lightning": 14,
|
|
"colorless": 6,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def get_starter_deck(starter_type: str) -> StarterDeckDefinition:
|
|
"""Get a starter deck definition by type.
|
|
|
|
Args:
|
|
starter_type: One of grass, fire, water, psychic, lightning.
|
|
|
|
Returns:
|
|
StarterDeckDefinition with name, description, cards, and energy_cards.
|
|
|
|
Raises:
|
|
ValueError: If starter_type is not valid.
|
|
|
|
Example:
|
|
deck = get_starter_deck("grass")
|
|
print(f"Deck: {deck['name']}")
|
|
print(f"Total cards: {sum(deck['cards'].values())}")
|
|
"""
|
|
if starter_type not in STARTER_DECKS:
|
|
raise ValueError(
|
|
f"Invalid starter type: {starter_type}. " f"Must be one of: {', '.join(STARTER_TYPES)}"
|
|
)
|
|
return STARTER_DECKS[starter_type]
|
|
|
|
|
|
def validate_starter_decks(config: DeckSizeConfig) -> dict[str, list[str]]:
|
|
"""Validate all starter deck definitions against config rules.
|
|
|
|
Checks that each deck has the correct number of cards and energy
|
|
as defined by the provided configuration.
|
|
|
|
Args:
|
|
config: Configuration providing min_size and energy_deck_size.
|
|
Typically a DeckConfig instance, but any object satisfying
|
|
the DeckSizeConfig protocol works.
|
|
|
|
Returns:
|
|
Dictionary mapping deck type to list of validation errors.
|
|
Empty dict if all decks are valid.
|
|
|
|
Example:
|
|
from app.core.config import DeckConfig
|
|
|
|
config = DeckConfig()
|
|
errors = validate_starter_decks(config)
|
|
if errors:
|
|
for deck_type, deck_errors in errors.items():
|
|
print(f"{deck_type}: {deck_errors}")
|
|
"""
|
|
errors: dict[str, list[str]] = {}
|
|
|
|
for deck_type, deck in STARTER_DECKS.items():
|
|
deck_errors: list[str] = []
|
|
|
|
# Check card count against config
|
|
total_cards = sum(deck["cards"].values())
|
|
if total_cards != config.min_size:
|
|
deck_errors.append(f"Expected {config.min_size} cards, got {total_cards}")
|
|
|
|
# Check energy count against config
|
|
total_energy = sum(deck["energy_cards"].values())
|
|
if total_energy != config.energy_deck_size:
|
|
deck_errors.append(f"Expected {config.energy_deck_size} energy, got {total_energy}")
|
|
|
|
if deck_errors:
|
|
errors[deck_type] = deck_errors
|
|
|
|
return errors
|
|
|
|
|
|
def get_starter_card_ids(starter_type: str) -> list[str]:
|
|
"""Get list of all card IDs in a starter deck.
|
|
|
|
Args:
|
|
starter_type: One of grass, fire, water, psychic, lightning.
|
|
|
|
Returns:
|
|
List of card IDs (without quantities).
|
|
|
|
Example:
|
|
card_ids = get_starter_card_ids("grass")
|
|
# ["a1-001-bulbasaur", "a1-002-ivysaur", ...]
|
|
"""
|
|
deck = get_starter_deck(starter_type)
|
|
return list(deck["cards"].keys())
|