# 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.
