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.
258 lines
8.8 KiB
Markdown
258 lines
8.8 KiB
Markdown
# 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
|