mantimon-tcg/backend/app/core/models
Cal Corum 934aa4c443 Add CardService and card data conversion pipeline
- Rename data/cards/ to data/raw/ for scraped data
- Add data/definitions/ as authoritative card data source
- Add convert_cards.py script to transform raw -> definitions
- Generate 378 card definitions (344 Pokemon, 24 Trainers, 10 Energy)
- Add CardService for loading and querying card definitions
  - In-memory indexes for fast lookups by type, set, pokemon_type
  - search() with multiple filter criteria
  - get_all_cards() for GameEngine integration
- Add SetInfo model for set metadata
- Update Attack model with damage_display field for variable damage
- Update CardDefinition with image_path, illustrator, flavor_text
- Add 45 tests (21 converter + 24 CardService)
- Update scraper output path to data/raw/

Card data is JSON-authoritative (no database) to support offline fork goal.
2026-01-27 14:16:40 -06:00
..
__init__.py Move enums to app/core/enums.py and set up clean module exports 2026-01-26 14:45:26 -06:00
actions.py Add rules validator, win conditions checker, and coverage gap tests 2026-01-25 12:57:06 -06:00
card.py Add CardService and card data conversion pipeline 2026-01-27 14:16:40 -06:00
game_state.py Move enums to app/core/enums.py and set up clean module exports 2026-01-26 14:45:26 -06:00
README.md Add Active Effects system design and module README files 2026-01-26 22:39:02 -06:00

Models Module

Pydantic models for game state, card definitions, and player actions.


Files

File Description
__init__.py Module exports
card.py Card-related models
game_state.py Game state and player state
actions.py Player action types

Card Models (card.py)

CardDefinition

Immutable template defining a card's properties. Stored in the card registry.

CardDefinition(
    id="pikachu-001",
    name="Pikachu",
    card_type=CardType.POKEMON,
    stage=PokemonStage.BASIC,
    hp=60,
    pokemon_type=EnergyType.LIGHTNING,
    attacks=[
        Attack(name="Thunder Shock", damage=20, cost=[EnergyType.LIGHTNING]),
    ],
    weakness=WeaknessResistance(energy_type=EnergyType.FIGHTING, value=20),
    retreat_cost=1,
)

CardInstance

A specific card in play, referencing a CardDefinition. Tracks runtime state.

CardInstance(
    instance_id="p1-pikachu-0",      # Unique per-game
    definition_id="pikachu-001",     # References CardDefinition
    damage=30,                        # Current damage
    attached_energy=[...],            # Energy cards attached
    status_conditions=[StatusCondition.PARALYZED],
)

Attack

A Pokemon's attack with cost, damage, and optional effect.

Attack(
    name="Crimson Storm",
    cost=[EnergyType.FIRE, EnergyType.FIRE, EnergyType.COLORLESS],
    damage=200,
    effect_id="discard_energy",
    effect_params={"count": 2},
    effect_description="Discard 2 Energy from this Pokemon.",
)

Ability

A Pokemon's special ability.

Ability(
    name="Psy Shadow",
    effect_id="energy_acceleration",
    effect_params={"energy_type": "psychic", "target": "active"},
    effect_description="Attach Psychic Energy from Energy Zone to Active.",
    uses_per_turn=1,
)

WeaknessResistance

Weakness or resistance to an energy type.

# Additive weakness (+20)
WeaknessResistance(energy_type=EnergyType.FIRE, mode=ModifierMode.ADDITIVE, value=20)

# Multiplicative weakness (x2)
WeaknessResistance(energy_type=EnergyType.FIRE, mode=ModifierMode.MULTIPLICATIVE, value=2)

# Resistance (-30)
WeaknessResistance(energy_type=EnergyType.WATER, mode=ModifierMode.ADDITIVE, value=-30)

Game State Models (game_state.py)

GameState

Complete state of a game. Self-contained with embedded card registry.

GameState(
    game_id="match-123",
    rules=RulesConfig(),
    card_registry={...},
    players={"p1": PlayerState(...), "p2": PlayerState(...)},
    current_player_id="p1",
    turn_number=3,
    phase=TurnPhase.MAIN,
)

Key Methods:

  • get_current_player() - Current player's state
  • get_opponent(player_id) - Opponent's state
  • get_card_definition(card_id) - Lookup card definition
  • advance_turn() - Move to next player's turn

PlayerState

All zones and state for a single player.

player = game.players["p1"]

# Zones (all are Zone objects)
player.deck        # Draw pile
player.hand        # Cards in hand
player.active      # Active Pokemon (0-1 cards)
player.bench       # Benched Pokemon
player.discard     # Discard pile
player.prizes      # Prize cards
player.energy_deck # Energy deck (Pocket-style)
player.energy_zone # Available energy this turn

# State tracking
player.score                       # Points scored
player.energy_attachments_this_turn
player.supporters_played_this_turn

Key Methods:

  • get_active_pokemon() - Returns active Pokemon or None
  • get_all_pokemon_in_play() - Active + bench
  • can_attach_energy(rules) - Check turn limits
  • reset_turn_state() - Clear per-turn counters

Zone

A collection of cards (deck, hand, bench, etc.).

zone = player.hand

len(zone)                    # Card count
"card-id" in zone            # Check if card present
zone.add(card)               # Add to end
zone.add_to_top(card)        # Add to top (for deck)
zone.remove("card-id")       # Remove and return card
zone.draw()                  # Remove and return from top
zone.shuffle(rng)            # Randomize order
zone.get("card-id")          # Get without removing
zone.clear()                 # Remove all cards

ForcedAction

Represents a required action (e.g., "select new active after KO").

ForcedAction(
    player_id="p1",
    action_type="select_active",
    reason="Your active Pokemon was knocked out",
    params={"from_zones": ["bench"]},
)

Action Models (actions.py)

All actions implement a common interface with type discriminator field.

Attack Actions

AttackAction(attack_index=0)  # Use first attack

Energy Actions

AttachEnergyAction(
    energy_card_id="p1-lightning-0",
    target_pokemon_id="p1-pikachu-0",
    from_energy_zone=True,
)

Pokemon Actions

PlayPokemonAction(card_id="p1-bulbasaur-0", to_active=False)
EvolvePokemonAction(card_id="p1-ivysaur-0", target_pokemon_id="p1-bulbasaur-0")

Trainer Actions

PlayTrainerAction(card_instance_id="p1-potion-0", targets=["p1-pikachu-0"])

Ability Actions

UseAbilityAction(pokemon_id="p1-greninja-0", ability_index=0, targets=["p2-charmander-0"])

Movement Actions

RetreatAction(new_active_id="p1-squirtle-0", energy_to_discard=["p1-water-0"])
SelectActiveAction(new_active_id="p1-squirtle-0")  # After KO

Turn Control

PassAction()    # End turn without attacking
ResignAction()  # Forfeit the game

Action Union Type

Action = (
    AttackAction | AttachEnergyAction | PlayPokemonAction | 
    EvolvePokemonAction | PlayTrainerAction | UseAbilityAction |
    RetreatAction | SelectActiveAction | PassAction | ResignAction |
    SelectPrizeAction
)

Model Relationships

GameState
├── card_registry: dict[str, CardDefinition]
├── players: dict[str, PlayerState]
│   └── PlayerState
│       ├── deck: Zone[CardInstance]
│       ├── hand: Zone[CardInstance]
│       ├── active: Zone[CardInstance]
│       │   └── CardInstance
│       │       ├── definition_id -> CardDefinition
│       │       ├── attached_energy: list[CardInstance]
│       │       └── evolution_stage: list[CardInstance]
│       ├── bench: Zone[CardInstance]
│       ├── discard: Zone[CardInstance]
│       └── ...
├── stadium_in_play: CardInstance | None
└── forced_actions: list[ForcedAction]

See Also