# Project Plan: Energy, Tools, and Evolution Stack Refactor **Created:** 2026-01-25 **Updated:** 2026-01-25 **Status:** COMPLETE (All phases done, 765 tests passing) **Priority:** Critical (Phase 1 fixes from SYSTEM_REVIEW.md) --- ## Overview Refactor the card attachment and evolution system to properly track: 1. **Energy cards** as `CardInstance` objects attached to Pokemon (not just IDs) 2. **Tool cards** as `CardInstance` objects attached to Pokemon (not just IDs) 3. **Evolution stack** - previous evolution stages stored underneath the current Pokemon This fixes critical bugs where energy cards "disappeared" and enables proper devolve mechanics. --- ## Key Design Decisions | Aspect | Decision | |--------|----------| | `attached_energy` type | `list[CardInstance]` (was `list[str]`) | | `attached_tools` type | `list[CardInstance]` (was `list[str]`) | | Evolution behavior | Previous stages stored in `cards_underneath` (was discarded) | | Energy on KO | Moves to owner's discard pile | | Tools on KO | Moves to owner's discard pile | | Evolution stack on KO | All cards in stack move to owner's discard | | Devolve mechanic | Effect only (not player action) | | Devolve stages | Configurable per effect (default 1) | | Devolve destination | Configurable: "hand" (default) or "discard" | | Devolve KO check | Immediate - if damage >= new HP, Pokemon is KO'd | | Energy/tools on devolve | Stay attached to the devolved Pokemon | | Damage on evolve/devolve | Carries over (enables sneaky KO strategies) | --- ## Implementation Phases ### Phase 1: Update CardInstance Model **File:** `app/core/models/card.py` **Status:** [x] COMPLETE - [x] Change `attached_energy: list[str]` to `list["CardInstance"]` - [x] Change `attached_tools: list[str]` to `list["CardInstance"]` - [x] Add `cards_underneath: list["CardInstance"]` field - [x] Update `attach_energy()` method signature to accept `CardInstance` - [x] Update `detach_energy()` to return `CardInstance | None` - [x] Add `attach_tool()` method - [x] Add `detach_tool()` method returning `CardInstance | None` - [x] Update docstrings for all affected fields/methods - [x] Add `CardInstance.model_rebuild()` for self-referential types --- ### Phase 2: Revert Incorrect Fix **File:** `app/core/engine.py` **Status:** [x] COMPLETE - [x] Remove `player.discard.add(energy_card)` from `_execute_attach_energy` --- ### Phase 3: Update Energy Attachment **File:** `app/core/engine.py` **Status:** [x] COMPLETE - [x] Change `target.attach_energy(energy_card.instance_id)` to `target.attach_energy(energy_card)` --- ### Phase 4: Update Evolution Execution **File:** `app/core/engine.py` **Status:** [x] COMPLETE - [x] Transfer `attached_energy` list (not copy IDs) - [x] Transfer `attached_tools` list - [x] Transfer `damage` and `status_conditions` - [x] Build `cards_underneath` stack (copy existing + append target) - [x] Clear target's attached lists after transfer - [x] Remove `player.discard.add(target)` - target stays in stack - [x] Update `turn_played` tracking on evo_card --- ### Phase 5: Update Retreat Execution **File:** `app/core/engine.py` **Status:** [x] COMPLETE - [x] Call `detach_energy()` which returns `CardInstance` - [x] Add returned energy to `player.discard` --- ### Phase 6: Update find_card_instance **File:** `app/core/models/game_state.py` **Status:** [x] COMPLETE - [x] Search `pokemon.attached_energy` for each Pokemon in play - [x] Search `pokemon.attached_tools` for each Pokemon in play - [x] Search `pokemon.cards_underneath` for each Pokemon in play - [x] Return appropriate zone names: `"attached_energy"`, `"attached_tools"`, `"cards_underneath"` --- ### Phase 7: Update Knockout Processing **File:** `app/core/turn_manager.py` **Status:** [x] COMPLETE - [x] Add TODO comment for future hook point (pre_knockout_discard event) - [x] Discard all `attached_energy` to owner's discard - [x] Discard all `attached_tools` to owner's discard - [x] Discard all `cards_underneath` to owner's discard - [x] Clear lists before discarding Pokemon - [x] Apply to both active and bench knockout sections --- ### Phase 8: Update discard_energy Effect Handler **File:** `app/core/effects/handlers.py` **Status:** [x] COMPLETE - [x] Find owner of target Pokemon (for discard pile access) - [x] Update to work with `CardInstance` objects - [x] Call `detach_energy()` and add result to owner's discard - [x] Update docstring --- ### Phase 9: Add devolve Effect Handler **File:** `app/core/effects/handlers.py` **Status:** [x] COMPLETE - [x] Create `@effect_handler("devolve")` decorator - [x] Accept `stages` param (int, default 1) - [x] Accept `destination` param ("hand" default, or "discard") - [x] Validate target is evolved (has cards_underneath) - [x] Find owner and zone of target Pokemon - [x] Implement stage removal loop: - Pop previous from cards_underneath - Transfer all state (energy, tools, remaining stack, damage, status, modifiers) - Create CardInstance for removed evolution - Send to destination (hand or discard) - Swap in zone - [x] Check for KO after devolve (damage >= new HP) - [x] Return result with knockout flag if applicable --- ### Phase 10: Update Rules Validator **File:** `app/core/rules_validator.py` **Status:** [x] COMPLETE - [x] Update `_get_attached_energy_types()` to iterate CardInstance objects - [x] Update retreat energy validation to check instance_id in CardInstance list --- ### Phase 11: Update Existing Tests **Status:** [x] COMPLETE Files updated: - [x] `tests/core/test_models/test_card.py` - attach/detach signatures, serialization - [x] `tests/core/test_engine.py` - energy attachment, evolution assertions - [x] `tests/core/test_effects/test_handlers.py` - discard_energy tests - [x] `tests/core/test_rules_validator.py` - energy requirement checks - [x] `tests/core/conftest.py` - game_in_main_phase and game_in_attack_phase fixtures --- ### Phase 12: Add New Tests **Status:** [x] COMPLETE **Test File:** `tests/core/test_evolution_stack.py` (28 tests) Evolution Stack Tests: - [x] Basic → Stage 1 creates correct stack - [x] Stage 1 → Stage 2 preserves full stack - [x] Energy/tools transfer on evolution - [x] Damage carries over on evolution - [x] Status conditions clear on evolution (Pokemon TCG standard) Devolve Effect Tests: - [x] Devolve Stage 2 → Stage 1 (stages=1) - [x] Devolve Stage 2 → Basic (stages=2) - [x] Devolve to hand (default destination) - [x] Devolve to discard - [x] Devolve triggers KO when damage >= new HP - [x] Cannot devolve Basic (failure) - [x] Energy/tools remain attached after devolve Knockout Tests: - [x] Attached energy goes to discard on KO - [x] Attached tools go to discard on KO - [x] Evolution stack goes to discard on KO - [x] All attachments discard together on KO find_card_instance Tests: - [x] Find attached energy by ID - [x] Find attached tool by ID - [x] Find card in evolution stack - [x] Find attached card on bench Pokemon - [x] Returns None for nonexistent card CardInstance Method Tests: - [x] attach_energy adds CardInstance - [x] detach_energy removes and returns CardInstance - [x] detach_energy returns None for not attached - [x] attach_tool adds CardInstance - [x] detach_tool removes and returns CardInstance - [x] get_all_attached_cards returns energy and tools - [x] Multiple energy attachment works --- ### Phase 13: Final Verification **Status:** [x] COMPLETE - [x] Run full test suite: `uv run pytest` - 765 tests pass (including 28 new Phase 12 tests) - [x] Run linter: `uv run ruff check .` - Clean (only pre-existing issues in references/) - [ ] Run type checker: `uv run mypy app` - Not run (pre-existing type issues exist) - [x] Review all changes --- ## Files Modified | File | Change Type | |------|-------------| | `app/core/models/card.py` | Model fields + methods | | `app/core/engine.py` | Attach/evolve/retreat execution + status clear fix | | `app/core/models/game_state.py` | find_card_instance search | | `app/core/turn_manager.py` | Knockout processing | | `app/core/effects/handlers.py` | discard_energy update + new devolve + EffectType fix | | `app/core/rules_validator.py` | Energy type checking | | `tests/core/test_models/test_card.py` | Test updates | | `tests/core/test_engine.py` | Test updates | | `tests/core/test_effects/test_handlers.py` | Test updates + new tests | | `tests/core/test_rules_validator.py` | Test updates | | `tests/core/conftest.py` | Fixture updates (energy attachment) | | `tests/core/test_evolution_stack.py` | NEW: 28 comprehensive tests for Phase 12 | --- ## Related Issues - SYSTEM_REVIEW.md Issue #5: Energy Attachment Bug - Energy Card Disappears - SYSTEM_REVIEW.md Issue #8: Energy Discard Handler Doesn't Move Cards --- ## Notes - Future hook point needed for effects that redirect energy/tools on knockout - Event system design should be planned holistically for the entire engine - Devolve is effect-only; no DevolveAction needed