Relocated SYSTEM_REVIEW.md and PROJECT_PLAN_ENERGY_EVOLUTION.md from backend/docs/legacy to project-level docs/legacy. Added docs/README.md indexing all documentation including ARCHITECTURE.md and GAME_RULES.md.
448 lines
19 KiB
Markdown
448 lines
19 KiB
Markdown
# Mantimon TCG Core Engine - System Review
|
|
|
|
**Date:** 2026-01-25
|
|
**Updated:** 2026-01-26
|
|
**Reviewed By:** Multi-agent system review
|
|
**Status:** 797 tests passing, 94% coverage
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
The core engine has a solid foundation with good separation of concerns, comprehensive documentation, and thorough test coverage. The review identified **8 critical issues** - **all 8 have been fixed**. The fixes span the Energy/Evolution Stack refactor, CardDefinition validation, and knockout processing verification.
|
|
|
|
**Fixed:** #1, #2, #3, #4, #5, #6, #7, #8
|
|
**Still Open:** None - all critical issues resolved!
|
|
|
|
---
|
|
|
|
## Critical Issues (Must Fix)
|
|
|
|
### 1. ~~Energy Zone Missing from Card Search~~ FIXED
|
|
**File:** `app/core/models/game_state.py:533-540`
|
|
**Severity:** CRITICAL
|
|
**Status:** FIXED in commit 2b8fac4
|
|
|
|
The `find_card_instance` method now searches all zones including `energy_zone`, plus:
|
|
- `attached_energy` - Energy cards attached to Pokemon in play
|
|
- `attached_tools` - Tool cards attached to Pokemon in play
|
|
- `cards_underneath` - Evolution stack (previous stages)
|
|
|
|
```python
|
|
# Fixed - includes energy_zone and attached cards
|
|
for zone_name in ["deck", "hand", "active", "bench", "discard", "prizes", "energy_deck", "energy_zone"]:
|
|
# Also searches attached_energy, attached_tools, cards_underneath on Pokemon in play
|
|
```
|
|
|
|
---
|
|
|
|
### 2. ~~No Validation That Pokemon Cards Have Required Fields~~ FIXED
|
|
**File:** `app/core/models/card.py:163-231`
|
|
**Severity:** CRITICAL
|
|
**Status:** FIXED in commit 7fae1c6
|
|
|
|
Added a Pydantic `model_validator` to `CardDefinition` that enforces:
|
|
- **Pokemon cards** require: `hp` (must be positive), `stage`, `pokemon_type`
|
|
- **Pokemon Stage 1/2** require: `evolves_from`
|
|
- **Pokemon VMAX/VSTAR** require: `evolves_from`
|
|
- **Trainer cards** require: `trainer_type`
|
|
- **Energy cards** require: `energy_type` (auto-fills `energy_provides` if empty)
|
|
|
|
This prevents invalid card definitions at construction time rather than runtime errors later.
|
|
|
|
---
|
|
|
|
### 3. ~~end_turn() Doesn't Process Knockouts~~ FIXED
|
|
**File:** `app/core/turn_manager.py:312-421`
|
|
**Severity:** CRITICAL
|
|
**Status:** FIXED - was already implemented correctly in commit eef857e
|
|
|
|
Investigation revealed this was a false positive. The `end_turn()` method at lines 409-413 **already calls** `process_knockout()` for each knockout detected from status damage. This processes:
|
|
- Moving the Pokemon to discard pile
|
|
- Discarding all attached energy and tools
|
|
- Discarding the evolution stack (cards underneath)
|
|
- Awarding points to the opponent
|
|
- Checking win conditions
|
|
- Setting up forced action for new active selection
|
|
|
|
**Verification:** Added 8 integration tests in `TestStatusKnockoutIntegration` to confirm the full flow works correctly.
|
|
|
|
---
|
|
|
|
### 4. ~~Win Condition Checked Before Knockout Processing~~ FIXED
|
|
**File:** `app/core/turn_manager.py:398-401`
|
|
**Severity:** CRITICAL
|
|
**Status:** FIXED - was already implemented correctly in commit eef857e
|
|
|
|
Investigation revealed this was a false positive. The win condition check happens **inside** `process_knockout()` (lines 483-500), which is called **after** the Pokemon is moved to discard (line 474). The sequence is:
|
|
1. Pokemon removed from active zone (line 453)
|
|
2. Attachments discarded (lines 458-471)
|
|
3. Pokemon added to discard (line 474)
|
|
4. Points awarded (lines 476-481)
|
|
5. Win by points checked (lines 483-491)
|
|
6. Win by no Pokemon checked (lines 493-500)
|
|
|
|
**Verification:** Tests `test_end_turn_status_knockout_triggers_win_by_points` and `test_end_turn_status_knockout_triggers_win_by_no_pokemon` confirm proper ordering.
|
|
|
|
---
|
|
|
|
### 5. ~~Energy Attachment Bug - Energy Card Disappears~~ FIXED
|
|
**File:** `app/core/engine.py:568`
|
|
**Severity:** CRITICAL
|
|
**Status:** FIXED in commit 2b8fac4
|
|
|
|
The data model was changed to store full `CardInstance` objects:
|
|
- `attached_energy: list[CardInstance]` (was `list[str]`)
|
|
- `attached_tools: list[CardInstance]` (was `list[str]`)
|
|
|
|
Energy cards are now stored directly on the Pokemon they're attached to, not just as IDs. The `find_card_instance` method searches these attached cards, so they're always findable.
|
|
|
|
When a Pokemon is knocked out or retreats, attached energy moves to the owner's discard pile.
|
|
|
|
---
|
|
|
|
### 6. ~~Status Knockouts Not Processed by Engine~~ FIXED
|
|
**File:** `app/core/engine.py:858-881`
|
|
**Severity:** CRITICAL
|
|
**Status:** FIXED - was already implemented correctly in commit eef857e
|
|
|
|
Investigation revealed this was a false positive. The `TurnManager.end_turn()` method **already processes knockouts internally** before returning the result. The `GameEngine.end_turn()` doesn't need to call `process_knockout()` again because `TurnManager` handles it.
|
|
|
|
The flow is:
|
|
1. `GameEngine.end_turn()` calls `turn_manager.end_turn()`
|
|
2. `TurnManager.end_turn()` applies status damage, detects knockouts
|
|
3. `TurnManager.end_turn()` calls `process_knockout()` for each knockout
|
|
4. `TurnManager.end_turn()` returns result with win_result if game ended
|
|
5. `GameEngine.end_turn()` applies win result if present
|
|
|
|
**Verification:** Added 3 tests in `TestEngineEndTurnKnockouts` confirming the full engine flow works correctly.
|
|
|
|
---
|
|
|
|
### 7. ~~Confusion Status Not Handled in Attack~~ FIXED
|
|
**File:** `app/core/engine.py:676-726`
|
|
**Severity:** CRITICAL
|
|
**Status:** FIXED in earlier commit
|
|
|
|
The `_execute_attack` method now handles Confusion status:
|
|
- Checks if attacker has `StatusCondition.CONFUSED`
|
|
- Flips a coin using the RNG
|
|
- On tails: attack fails, Pokemon damages itself (configurable via `rules.status.confusion_self_damage`)
|
|
- On heads: attack proceeds normally
|
|
|
|
The self-damage amount is configurable in `RulesConfig.status.confusion_self_damage` (default 30).
|
|
|
|
---
|
|
|
|
### 8. ~~Energy Discard Handler Doesn't Move Cards~~ FIXED
|
|
**File:** `app/core/effects/handlers.py:479-517`
|
|
**Severity:** CRITICAL
|
|
**Status:** FIXED in commit 2b8fac4
|
|
|
|
The `discard_energy` handler now:
|
|
1. Finds the owner of the target Pokemon
|
|
2. Calls `detach_energy()` which returns the `CardInstance`
|
|
3. Adds the detached energy to the owner's discard pile
|
|
|
|
Additionally, a new `devolve` effect handler was added for removing evolution stages.
|
|
|
|
---
|
|
|
|
## Medium Priority Issues
|
|
|
|
### 9. ~~Per-Ability Usage Tracking Flawed~~ FIXED
|
|
**File:** `app/core/models/card.py:390`
|
|
**Status:** FIXED in current session
|
|
|
|
Changed `ability_uses_this_turn` from `int` to `dict[int, int]` to track usage per ability:
|
|
- `ability_uses_this_turn: dict[int, int]` maps ability index to use count
|
|
- `can_use_ability(ability, ability_index)` now requires the ability index
|
|
- Added `get_ability_uses(ability_index)` helper method
|
|
- Added `increment_ability_uses(ability_index)` helper method
|
|
- `reset_turn_state()` now clears the dict instead of setting to 0
|
|
|
|
This ensures Pokemon with multiple abilities (e.g., one limited to 1/turn, another to 2/turn) track each ability independently.
|
|
|
|
---
|
|
|
|
### 10. ~~Double Knockout - Only One Forced Action~~ FIXED
|
|
**File:** `app/core/models/game_state.py`
|
|
**Status:** FIXED in current session
|
|
|
|
Changed `forced_action: ForcedAction | None` to `forced_actions: list[ForcedAction]` (FIFO queue):
|
|
- `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
|
|
|
|
When both players' active Pokemon are KO'd, both forced actions are queued and processed in order.
|
|
|
|
---
|
|
|
|
### 11. ~~No SelectPrizeAction Executor~~ FIXED
|
|
**File:** `app/core/engine.py:440-444`
|
|
**Status:** FIXED in current session
|
|
|
|
Added complete prize card system:
|
|
- `_execute_select_prize()` method in engine.py (lines ~887-949)
|
|
- Prize card mode support in `process_knockout()` via `_award_prize_cards()` helper
|
|
- Supports both random selection (auto-takes prizes) and player choice (sets `forced_action`)
|
|
- Multi-prize selection for EX/VMAX knockouts (worth 2-3 prizes)
|
|
- Win detection when all prizes taken
|
|
|
|
**Note:** The validator already allows `select_prize` during forced actions regardless of phase, so abilities/effects causing knockouts during main phase work correctly.
|
|
|
|
---
|
|
|
|
### 12. ~~Stadium Discard Goes to Wrong Player~~ FIXED
|
|
**File:** `app/core/engine.py:630-644`
|
|
**Status:** FIXED in current session
|
|
|
|
Added `stadium_owner_id: str | None` to GameState to track who played the current stadium:
|
|
- When a stadium is played, both `stadium_in_play` and `stadium_owner_id` are set
|
|
- When a stadium is replaced, the old stadium is discarded to its owner's discard pile
|
|
- Added to VisibleGameState for client visibility
|
|
|
|
Added 2 new tests for stadium ownership behavior.
|
|
|
|
---
|
|
|
|
### 13. ~~No Knockout Detection After Damage Effects~~ FIXED
|
|
**File:** `app/core/effects/handlers.py`
|
|
|
|
~~Neither `deal_damage` nor `attack_damage` check if the target is knocked out after applying damage.~~
|
|
|
|
**Resolution:** Both `deal_damage` and `attack_damage` now check if the target is knocked out after applying damage. If knocked out, they set `details["knockout"] = True` and `details["knockout_pokemon_id"]` in the EffectResult. The message also includes "knocked out!" notification.
|
|
|
|
The knockout check uses `target.is_knocked_out(card_def.hp)` which correctly accounts for HP modifiers via `effective_hp()`.
|
|
|
|
Added 9 new tests covering:
|
|
- Knockout detection for both handlers
|
|
- No false positives when target survives
|
|
- Knockout with existing damage
|
|
- HP modifier respect (both positive and negative)
|
|
- Knockout after weakness multiplier
|
|
- Survival when resistance prevents knockout
|
|
|
|
---
|
|
|
|
### 14. ~~Exception Swallowing Hides Errors~~ FIXED
|
|
**File:** `app/core/effects/registry.py:93-107`
|
|
|
|
~~This catches all exceptions including programming errors, making debugging difficult.~~
|
|
|
|
**Resolution:** Added proper logging with `logger.exception()` which captures the full traceback at ERROR level. The log message now includes:
|
|
- The effect_id that failed
|
|
- Source player ID
|
|
- Source and target card IDs
|
|
- Effect parameters
|
|
- Full exception traceback
|
|
|
|
The game still returns a safe `EffectResult.failure()` to prevent crashes, but debugging information is now preserved in the logs.
|
|
|
|
Added 1 test verifying that exceptions are logged with full context and traceback.
|
|
|
|
---
|
|
|
|
### 15. ~~Turn Limit Not Checked at Turn Start~~ FIXED
|
|
**File:** `app/core/engine.py:828-856`
|
|
**Status:** FIXED in current session
|
|
|
|
Added turn limit check at the start of `start_turn()` (lines ~968-980):
|
|
- Calls `turn_manager.check_turn_limit(game)` before processing turn
|
|
- Returns `ActionResult` with `win_result` if limit exceeded
|
|
- Winner determined by score (higher score wins)
|
|
- Equal scores result in a draw (`GameEndReason.DRAW`)
|
|
|
|
Also added `GameEndReason.TURN_LIMIT` enum value to distinguish from `TIMEOUT` (player ran out of clock time).
|
|
|
|
---
|
|
|
|
## Low Priority / Observations
|
|
|
|
### Models
|
|
- `ModifierMode` enum not exported in `__init__.py`
|
|
- `ActionType` enum exists but is unused (duplicates Literal values)
|
|
- No maximum damage validation (negative damage possible)
|
|
- Tools limit not validated at model level
|
|
- Stadium ownership not tracked
|
|
- No temporary effect system for "until end of turn" effects
|
|
|
|
### Game Logic
|
|
- Draw phase has no actions (may be intentional)
|
|
- `first_turn_of_game` config exists in two places (`FirstTurnConfig.can_evolve` and `EvolutionConfig.first_turn_of_game`)
|
|
- Prize card mode missing "taken" tracking vs "removed by effect"
|
|
- Evolution doesn't validate stage progression (Stage 1 could "evolve" into Stage 1)
|
|
- `turn_number` only increments on wraparound (could confuse turn limit calculations)
|
|
|
|
### Effects System
|
|
- No effect chaining for compound effects
|
|
- No duration/timing system for temporary modifiers
|
|
- No target selection callback for player-choice effects
|
|
- Missing common effects: `search_deck`, `switch_pokemon`, `force_switch`, `conditional_effect`
|
|
|
|
### Engine
|
|
- Hardcoded hand size `7` in mulligan (should use `rules.deck.starting_hand_size`)
|
|
- Async methods that don't need async (`_execute_play_trainer`, etc.)
|
|
- No setup phase state machine
|
|
- No `get_legal_actions()` method for UI/AI
|
|
- No weakness/resistance in damage calculation
|
|
- No undo/snapshot capability
|
|
- Mulligan doesn't award opponent extra cards
|
|
|
|
---
|
|
|
|
## Positive Observations
|
|
|
|
1. **Clean Architecture**: Excellent separation of CardDefinition (immutable template) vs CardInstance (mutable state)
|
|
|
|
2. **Comprehensive Configuration**: RulesConfig system supports multiple game variants (traditional, Pocket-style, custom)
|
|
|
|
3. **Well-Designed EffectContext**: Rich helper methods for parameter extraction, coin flips, player/card access
|
|
|
|
4. **Testability**: RandomProvider protocol with SeededRandom enables deterministic testing
|
|
|
|
5. **Zone Abstraction**: Clean `Zone` class with rich set of methods for deck/hand manipulation
|
|
|
|
6. **Good Documentation**: Comprehensive docstrings and type hints throughout
|
|
|
|
7. **Security Awareness**: Visibility filter properly hides opponent hand, deck order, prizes
|
|
|
|
---
|
|
|
|
## Recommended Fix Priority
|
|
|
|
### Phase 1: Critical Fixes (Before Any Testing) - 4/8 COMPLETE
|
|
1. ~~Fix `find_card_instance` to include `energy_zone`~~ DONE
|
|
2. Add CardDefinition field validation - STILL NEEDED
|
|
3. Fix knockout processing in `end_turn` - STILL NEEDED
|
|
4. Fix win condition check timing - STILL NEEDED
|
|
5. ~~Fix energy attachment to store cards properly~~ DONE (full refactor to CardInstance)
|
|
6. Fix engine to call `process_knockout()` for status KOs - STILL NEEDED
|
|
7. ~~Add confusion handling in attack execution~~ DONE
|
|
8. ~~Fix energy discard handler to move cards~~ DONE
|
|
|
|
### Phase 2: Functionality Gaps (Before Feature Complete)
|
|
6. ~~Add `SelectPrizeAction` executor~~ DONE (#11)
|
|
7. ~~Fix stadium discard ownership~~ DONE (#12)
|
|
8. ~~Add turn limit check~~ DONE (#15)
|
|
9. ~~Fix per-ability usage tracking~~ DONE (#9)
|
|
|
|
### Phase 3: Polish (Before Production)
|
|
10. ~~Handle double knockouts~~ DONE (#10)
|
|
11. Improve effect error handling
|
|
12. Add missing effect handlers
|
|
|
|
---
|
|
|
|
## Test Coverage Status
|
|
|
|
| Module | Coverage | Notes |
|
|
|--------|----------|-------|
|
|
| `config.py` | 100% | Complete |
|
|
| `models/enums.py` | 100% | Complete |
|
|
| `models/card.py` | 100% | Complete |
|
|
| `models/actions.py` | 100% | Complete |
|
|
| `models/game_state.py` | 99% | Near complete |
|
|
| `effects/base.py` | 98% | Near complete |
|
|
| `effects/registry.py` | 100% | Complete |
|
|
| `effects/handlers.py` | 99% | Near complete + devolve handler |
|
|
| `rules_validator.py` | 94% | Good |
|
|
| `turn_manager.py` | 93% | Good |
|
|
| `visibility.py` | 95% | Good |
|
|
| `win_conditions.py` | 99% | Near complete |
|
|
| `engine.py` | 81% | Gaps in error paths |
|
|
| `rng.py` | 93% | Good |
|
|
| **TOTAL** | **94%** | **797 tests** |
|
|
|
|
### New Tests Added
|
|
- `tests/core/test_evolution_stack.py` - 28 tests for evolution stack, devolve, knockout with attachments, find_card_instance
|
|
- `tests/core/test_engine.py::TestSelectPrizeAction` - 3 tests for SelectPrizeAction execution
|
|
- `tests/core/test_engine.py::TestTurnLimitCheck` - 5 tests for turn limit checking
|
|
- `tests/core/test_turn_manager.py::TestPrizeCardModeKnockout` - 4 tests for prize card mode in knockouts
|
|
- `tests/core/test_models/test_card.py::TestCardInstanceTurnState::test_can_use_ability_independent_tracking` - 1 test for per-ability usage tracking
|
|
- `tests/core/test_models/test_game_state.py::TestForcedActionQueue` - 8 tests for forced action queue management
|
|
- `tests/core/test_engine.py::TestPlayTrainerAction` - 2 new tests for stadium ownership
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. ~~Review this document and prioritize fixes~~ DONE
|
|
2. Create GitHub issues for remaining critical items (#2, #3, #4, #6)
|
|
3. ~~Address Phase 1 fixes before continuing development~~ 4/8 DONE
|
|
4. ~~Update tests as fixes are implemented~~ DONE (766 tests)
|
|
5. Re-run system review after remaining fixes
|
|
|
|
## Change Log
|
|
|
|
### 2026-01-26 - Stadium Ownership Tracking (Issue #12)
|
|
|
|
Added `stadium_owner_id` field to track who played the current stadium:
|
|
- When a stadium is played, `stadium_owner_id` is set to the player's ID
|
|
- When a stadium is replaced, old stadium discards to owner's pile (not current player)
|
|
- Added `stadium_owner_id` to VisibleGameState for client visibility
|
|
- Updated existing test and added 2 new tests for stadium ownership
|
|
|
|
**Total: 797 tests passing**
|
|
|
|
### 2026-01-26 - Forced Action Queue for Double Knockouts (Issue #10)
|
|
|
|
Changed `forced_action` from single item to FIFO queue:
|
|
- `forced_actions: list[ForcedAction]` replaces `forced_action: ForcedAction | None`
|
|
- Added queue management methods: `has_forced_action()`, `get_current_forced_action()`,
|
|
`add_forced_action()`, `pop_forced_action()`, `clear_forced_actions()`
|
|
- Updated engine, turn_manager, rules_validator, and visibility filter
|
|
- Added 8 new tests for forced action queue including double knockout scenario
|
|
|
|
**Total: 795 tests passing**
|
|
|
|
### 2026-01-26 - Per-Ability Usage Tracking (Issue #9)
|
|
|
|
Fixed issue #9 - ability usage now tracked per-ability instead of globally:
|
|
- Changed `ability_uses_this_turn: int` to `dict[int, int]`
|
|
- `can_use_ability()` now requires `ability_index` parameter
|
|
- Added `get_ability_uses()` and `increment_ability_uses()` helper methods
|
|
- Updated engine, rules_validator, and all affected tests
|
|
- Added new test `test_can_use_ability_independent_tracking` to verify fix
|
|
|
|
**Total: 789 tests passing**
|
|
|
|
### 2026-01-26 - SelectPrizeAction and Turn Limit Check (Issues #11, #15)
|
|
|
|
Fixed medium priority issues #11 and #15:
|
|
|
|
**Issue #11 - SelectPrizeAction Executor:**
|
|
- Added `_execute_select_prize()` method to GameEngine (lines ~887-949)
|
|
- Added prize card mode support to `process_knockout()` via `_award_prize_cards()` helper
|
|
- Supports random selection (auto-takes prizes) and player choice (sets forced_action)
|
|
- Multi-prize selection for EX/VMAX knockouts (worth 2-3 prizes)
|
|
- 7 new tests for prize card functionality
|
|
|
|
**Issue #15 - Turn Limit Check:**
|
|
- Added turn limit check at start of `start_turn()` (lines ~968-980)
|
|
- Added `GameEndReason.TURN_LIMIT` enum value (distinct from TIMEOUT)
|
|
- Winner determined by score, equal scores result in DRAW
|
|
- 5 new tests for turn limit functionality
|
|
|
|
**Total: 12 new tests, 788 tests passing**
|
|
|
|
### 2026-01-26 - Energy/Evolution Stack Refactor
|
|
**Commits:** 2b8fac4, dd2cadf
|
|
|
|
Fixed issues #1, #5, #7, #8 plus major enhancements:
|
|
|
|
Note: Issues #3 and #6 (knockout processing) were NOT fixed - we improved
|
|
`process_knockout()` to handle attached cards correctly, but the engine still
|
|
doesn't call it for status damage knockouts. That's a separate fix needed.
|
|
- `attached_energy` and `attached_tools` changed from `list[str]` to `list[CardInstance]`
|
|
- Added `cards_underneath` for evolution stack tracking
|
|
- Added `devolve` effect handler
|
|
- `find_card_instance` now searches attached cards and evolution stacks
|
|
- Knockout processing discards all attached cards and evolution stack
|
|
- Evolution clears status conditions (Pokemon TCG standard behavior)
|
|
- 28 new comprehensive tests in `test_evolution_stack.py`
|
|
|
|
See `PROJECT_PLAN_ENERGY_EVOLUTION.md` for full implementation details.
|