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.
19 KiB
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 playattached_tools- Tool cards attached to Pokemon in playcards_underneath- Evolution stack (previous stages)
# 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-fillsenergy_providesif 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:
- Pokemon removed from active zone (line 453)
- Attachments discarded (lines 458-471)
- Pokemon added to discard (line 474)
- Points awarded (lines 476-481)
- Win by points checked (lines 483-491)
- 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](waslist[str])attached_tools: list[CardInstance](waslist[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:
GameEngine.end_turn()callsturn_manager.end_turn()TurnManager.end_turn()applies status damage, detects knockoutsTurnManager.end_turn()callsprocess_knockout()for each knockoutTurnManager.end_turn()returns result with win_result if game endedGameEngine.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:
- Finds the owner of the target Pokemon
- Calls
detach_energy()which returns theCardInstance - 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 countcan_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 actionsget_current_forced_action()- Get first action without removingadd_forced_action(action)- Add to end of queuepop_forced_action()- Remove and return first actionclear_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_playandstadium_owner_idare 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
ActionResultwithwin_resultif 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
ModifierModeenum not exported in__init__.pyActionTypeenum 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_gameconfig exists in two places (FirstTurnConfig.can_evolveandEvolutionConfig.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_numberonly 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
7in mulligan (should userules.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
-
Clean Architecture: Excellent separation of CardDefinition (immutable template) vs CardInstance (mutable state)
-
Comprehensive Configuration: RulesConfig system supports multiple game variants (traditional, Pocket-style, custom)
-
Well-Designed EffectContext: Rich helper methods for parameter extraction, coin flips, player/card access
-
Testability: RandomProvider protocol with SeededRandom enables deterministic testing
-
Zone Abstraction: Clean
Zoneclass with rich set of methods for deck/hand manipulation -
Good Documentation: Comprehensive docstrings and type hints throughout
-
Security Awareness: Visibility filter properly hides opponent hand, deck order, prizes
Recommended Fix Priority
Phase 1: Critical Fixes (Before Any Testing) - 4/8 COMPLETE
FixDONEfind_card_instanceto includeenergy_zone- Add CardDefinition field validation - STILL NEEDED
- Fix knockout processing in
end_turn- STILL NEEDED - Fix win condition check timing - STILL NEEDED
Fix energy attachment to store cards properlyDONE (full refactor to CardInstance)- Fix engine to call
process_knockout()for status KOs - STILL NEEDED Add confusion handling in attack executionDONEFix energy discard handler to move cardsDONE
Phase 2: Functionality Gaps (Before Feature Complete)
AddDONE (#11)SelectPrizeActionexecutorFix stadium discard ownershipDONE (#12)Add turn limit checkDONE (#15)Fix per-ability usage trackingDONE (#9)
Phase 3: Polish (Before Production)
Handle double knockoutsDONE (#10)- Improve effect error handling
- 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_instancetests/core/test_engine.py::TestSelectPrizeAction- 3 tests for SelectPrizeAction executiontests/core/test_engine.py::TestTurnLimitCheck- 5 tests for turn limit checkingtests/core/test_turn_manager.py::TestPrizeCardModeKnockout- 4 tests for prize card mode in knockoutstests/core/test_models/test_card.py::TestCardInstanceTurnState::test_can_use_ability_independent_tracking- 1 test for per-ability usage trackingtests/core/test_models/test_game_state.py::TestForcedActionQueue- 8 tests for forced action queue managementtests/core/test_engine.py::TestPlayTrainerAction- 2 new tests for stadium ownership
Next Steps
Review this document and prioritize fixesDONE- Create GitHub issues for remaining critical items (#2, #3, #4, #6)
Address Phase 1 fixes before continuing development4/8 DONEUpdate tests as fixes are implementedDONE (766 tests)- 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_idis set to the player's ID - When a stadium is replaced, old stadium discards to owner's pile (not current player)
- Added
stadium_owner_idto 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]replacesforced_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: inttodict[int, int] can_use_ability()now requiresability_indexparameter- Added
get_ability_uses()andincrement_ability_uses()helper methods - Updated engine, rules_validator, and all affected tests
- Added new test
test_can_use_ability_independent_trackingto 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_LIMITenum 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
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_energyandattached_toolschanged fromlist[str]tolist[CardInstance]- Added
cards_underneathfor evolution stack tracking - Added
devolveeffect handler find_card_instancenow 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.