mantimon-tcg/docs/legacy/SYSTEM_REVIEW.md
Cal Corum 11e244d7e9 Move legacy docs to project root docs/legacy
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.
2026-01-26 14:19:19 -06:00

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 play
  • attached_tools - Tool cards attached to Pokemon in play
  • cards_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-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


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)

  1. Add SelectPrizeAction executor DONE (#11)
  2. Fix stadium discard ownership DONE (#12)
  3. Add turn limit check DONE (#15)
  4. Fix per-ability usage tracking DONE (#9)

Phase 3: Polish (Before Production)

  1. Handle double knockouts DONE (#10)
  2. Improve effect error handling
  3. 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.