Add forced action handling, turn boundary detection, and DB persistence:
- Check for pending forced actions before allowing regular actions
- Only specified player can act during forced action (except resign)
- Only specified action type allowed during forced action
- Detect turn boundaries (turn number OR current player change)
- Persist to Postgres at turn boundaries for durability
- Include pending_forced_action in GameActionResult for client
New exceptions: ForcedActionRequiredError
Tests: 11 new tests covering forced actions, turn boundaries, and
pending action reporting. Total 47 tests for GameService.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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 forced_action from single item to FIFO queue to support
scenarios where multiple forced actions are needed simultaneously:
- forced_actions: list[ForcedAction] replaces forced_action: ForcedAction | None
- Added queue management methods:
- has_forced_action() - check if queue has pending actions
- get_current_forced_action() - get first action without removing
- add_forced_action(action) - add to end of queue
- pop_forced_action() - remove and return first action
- clear_forced_actions() - clear all pending actions
- Updated engine, turn_manager, rules_validator, and visibility filter
- Added 8 new tests for forced action queue including double knockout scenario
This fixes the bug where simultaneous knockouts (e.g., mutual poison damage)
would lose one player's select_active action due to overwriting.
795 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.