Relocated SYSTEM_REVIEW.md and PROJECT_PLAN_ENERGY_EVOLUTION.md to docs/legacy/ now that all issues are resolved. Added docs/README.md with index of documentation files.
8.8 KiB
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:
- Energy cards as
CardInstanceobjects attached to Pokemon (not just IDs) - Tool cards as
CardInstanceobjects attached to Pokemon (not just IDs) - 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
- Change
attached_energy: list[str]tolist["CardInstance"] - Change
attached_tools: list[str]tolist["CardInstance"] - Add
cards_underneath: list["CardInstance"]field - Update
attach_energy()method signature to acceptCardInstance - Update
detach_energy()to returnCardInstance | None - Add
attach_tool()method - Add
detach_tool()method returningCardInstance | None - Update docstrings for all affected fields/methods
- Add
CardInstance.model_rebuild()for self-referential types
Phase 2: Revert Incorrect Fix
File: app/core/engine.py
Status: [x] COMPLETE
- Remove
player.discard.add(energy_card)from_execute_attach_energy
Phase 3: Update Energy Attachment
File: app/core/engine.py
Status: [x] COMPLETE
- Change
target.attach_energy(energy_card.instance_id)totarget.attach_energy(energy_card)
Phase 4: Update Evolution Execution
File: app/core/engine.py
Status: [x] COMPLETE
- Transfer
attached_energylist (not copy IDs) - Transfer
attached_toolslist - Transfer
damageandstatus_conditions - Build
cards_underneathstack (copy existing + append target) - Clear target's attached lists after transfer
- Remove
player.discard.add(target)- target stays in stack - Update
turn_playedtracking on evo_card
Phase 5: Update Retreat Execution
File: app/core/engine.py
Status: [x] COMPLETE
- Call
detach_energy()which returnsCardInstance - Add returned energy to
player.discard
Phase 6: Update find_card_instance
File: app/core/models/game_state.py
Status: [x] COMPLETE
- Search
pokemon.attached_energyfor each Pokemon in play - Search
pokemon.attached_toolsfor each Pokemon in play - Search
pokemon.cards_underneathfor each Pokemon in play - Return appropriate zone names:
"attached_energy","attached_tools","cards_underneath"
Phase 7: Update Knockout Processing
File: app/core/turn_manager.py
Status: [x] COMPLETE
- Add TODO comment for future hook point (pre_knockout_discard event)
- Discard all
attached_energyto owner's discard - Discard all
attached_toolsto owner's discard - Discard all
cards_underneathto owner's discard - Clear lists before discarding Pokemon
- Apply to both active and bench knockout sections
Phase 8: Update discard_energy Effect Handler
File: app/core/effects/handlers.py
Status: [x] COMPLETE
- Find owner of target Pokemon (for discard pile access)
- Update to work with
CardInstanceobjects - Call
detach_energy()and add result to owner's discard - Update docstring
Phase 9: Add devolve Effect Handler
File: app/core/effects/handlers.py
Status: [x] COMPLETE
- Create
@effect_handler("devolve")decorator - Accept
stagesparam (int, default 1) - Accept
destinationparam ("hand" default, or "discard") - Validate target is evolved (has cards_underneath)
- Find owner and zone of target Pokemon
- 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
- Check for KO after devolve (damage >= new HP)
- Return result with knockout flag if applicable
Phase 10: Update Rules Validator
File: app/core/rules_validator.py
Status: [x] COMPLETE
- Update
_get_attached_energy_types()to iterate CardInstance objects - Update retreat energy validation to check instance_id in CardInstance list
Phase 11: Update Existing Tests
Status: [x] COMPLETE
Files updated:
tests/core/test_models/test_card.py- attach/detach signatures, serializationtests/core/test_engine.py- energy attachment, evolution assertionstests/core/test_effects/test_handlers.py- discard_energy teststests/core/test_rules_validator.py- energy requirement checkstests/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:
- Basic → Stage 1 creates correct stack
- Stage 1 → Stage 2 preserves full stack
- Energy/tools transfer on evolution
- Damage carries over on evolution
- Status conditions clear on evolution (Pokemon TCG standard)
Devolve Effect Tests:
- Devolve Stage 2 → Stage 1 (stages=1)
- Devolve Stage 2 → Basic (stages=2)
- Devolve to hand (default destination)
- Devolve to discard
- Devolve triggers KO when damage >= new HP
- Cannot devolve Basic (failure)
- Energy/tools remain attached after devolve
Knockout Tests:
- Attached energy goes to discard on KO
- Attached tools go to discard on KO
- Evolution stack goes to discard on KO
- All attachments discard together on KO
find_card_instance Tests:
- Find attached energy by ID
- Find attached tool by ID
- Find card in evolution stack
- Find attached card on bench Pokemon
- Returns None for nonexistent card
CardInstance Method Tests:
- attach_energy adds CardInstance
- detach_energy removes and returns CardInstance
- detach_energy returns None for not attached
- attach_tool adds CardInstance
- detach_tool removes and returns CardInstance
- get_all_attached_cards returns energy and tools
- Multiple energy attachment works
Phase 13: Final Verification
Status: [x] COMPLETE
- Run full test suite:
uv run pytest- 765 tests pass (including 28 new Phase 12 tests) - 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) - 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