- 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.
Architectural refactor to eliminate circular imports and establish clean
module boundaries:
- Move enums from app/core/models/enums.py to app/core/enums.py
(foundational module with zero dependencies)
- Update all imports across 30 files to use new enum location
- Set up clean export structure:
- app.core.enums: canonical source for all enums
- app.core: convenience exports for full public API
- app.core.models: exports models only (not enums)
- Add module exports to app/core/__init__.py and app/core/effects/__init__.py
- Remove circular import workarounds from game_state.py
This enables app.core.models to export GameState without circular import
issues, since enums no longer depend on the models package.
All 826 tests passing.
Changed ability_uses_this_turn from int to dict[int, int] to track each
ability's usage independently:
- ability_uses_this_turn: dict[int, int] maps ability index to use count
- can_use_ability() now requires ability_index parameter
- Added get_ability_uses() and increment_ability_uses() helper methods
- reset_turn_state() clears the dict instead of setting to 0
This fixes the bug where using one ability could incorrectly block
another ability on the same Pokemon from being used.
789 tests passing.
Major refactor to properly track attached cards and evolution history:
Model Changes (app/core/models/card.py):
- Change attached_energy from list[str] to list[CardInstance]
- Change attached_tools from list[str] to list[CardInstance]
- Add cards_underneath field for evolution stack tracking
- Update attach_energy/detach_energy to work with CardInstance
- Add attach_tool/detach_tool methods
- Add get_all_attached_cards helper
Engine Changes (app/core/engine.py):
- _execute_attach_energy: Pass full CardInstance to attach_energy
- _execute_evolve: Build evolution stack, transfer attachments, clear status
- _execute_retreat: Detached energy goes to discard pile
- Fix: Evolution now clears status conditions (Pokemon TCG standard)
Game State (app/core/models/game_state.py):
- find_card_instance now searches attached_energy, attached_tools, cards_underneath
Turn Manager (app/core/turn_manager.py):
- process_knockout: Discard all attached energy, tools, and evolution stack
Effects (app/core/effects/handlers.py):
- discard_energy: Find owner's discard pile and move detached energy there
- NEW devolve effect: Remove evolution stages with configurable destination
- Fix: Use EffectType.SPECIAL instead of non-existent EffectType.ZONE
Rules Validator (app/core/rules_validator.py):
- Update energy type checking to iterate CardInstance objects
Tests:
- Update existing tests for new CardInstance-based energy attachment
- NEW test_evolution_stack.py with 28 comprehensive tests covering:
- Evolution stack building (Basic -> Stage 1 -> Stage 2)
- Energy/tool transfer and damage carryover on evolution
- Devolve effect (single/multi stage, hand/discard destination, KO check)
- Knockout processing with all attachments going to discard
- find_card_instance for attached cards and evolution stack
All 765 tests pass.
- Add CardDefinition and CardInstance models for card templates and in-game state
- Add Attack, Ability, and WeaknessResistance models for Pokemon card components
- Add 11 action types as discriminated union (PlayPokemon, Evolve, Attack, etc.)
- Split PokemonStage (BASIC, STAGE_1, STAGE_2) from PokemonVariant (NORMAL, EX, GX, V, VMAX, VSTAR)
- Stage determines evolution mechanics, variant determines knockout points
- Update PrizeConfig to use variant for knockout point calculation
- VSTAR and VMAX both worth 3 points; EX, GX, V worth 2 points; NORMAL worth 1 point
Tests: 204 passing, all linting clean