# Core Game Engine - Agent Guidelines > `app/core/` - The heart of Mantimon TCG This document provides guidelines for AI agents working with the core game engine module. The engine is designed to be **standalone and database-agnostic**, enabling future extraction as an offline game. ## Quick Reference | Module | Purpose | Key Exports | |--------|---------|-------------| | `enums.py` | All enumeration types (foundational) | `CardType`, `EnergyType`, `TurnPhase`, `StatusCondition`, etc. | | `config.py` | Rules configuration system | `RulesConfig`, `DeckConfig`, `PrizeConfig`, etc. | | `rng.py` | Deterministic & secure randomness | `SeededRandom`, `SecureRandom`, `create_rng()` | | `models/card.py` | Card templates and instances | `CardDefinition`, `CardInstance`, `Attack`, `Ability` | | `models/actions.py` | Player action types | `Action` union, `AttackAction`, `PlayPokemonAction`, etc. | | `models/game_state.py` | Game state hierarchy | `GameState`, `PlayerState`, `Zone`, `ForcedAction` | | `rules_validator.py` | Action legality checking | `validate_action()`, `ValidationResult` | | `turn_manager.py` | Turn/phase state machine | `TurnManager`, `TurnStartResult` | | `win_conditions.py` | Win/loss detection | `check_win_conditions()`, `WinResult` | | `visibility.py` | Hidden information filtering | `get_visible_state()`, `VisibleGameState` | | `effects/` | Effect handler system | `@effect_handler`, `resolve_effect()`, `EffectContext` | | `engine.py` | Main orchestrator | `GameEngine`, `ActionResult` | --- ## Architecture Overview ``` ┌─────────────────────────────────────────┐ │ GameEngine │ │ (Main public API - use this!) │ └───────────────┬─────────────────────────┘ │ ┌─────────────────────────┼─────────────────────────┐ │ │ │ v v v ┌─────────────────┐ ┌────────────────────┐ ┌─────────────────┐ │ RulesValidator │ │ TurnManager │ │ WinConditions │ │ validate_action │ │ start/end_turn │ │ check_win │ └─────────────────┘ │ phase transitions │ └─────────────────┘ └────────────────────┘ │ ┌─────────────────────────┼─────────────────────────┐ │ │ │ v v v ┌─────────────────┐ ┌────────────────────┐ ┌─────────────────┐ │ GameState │ │ EffectRegistry │ │ Visibility │ │ PlayerState │ │ @effect_handler │ │ get_visible_ │ │ Zone │ │ resolve_effect │ │ state() │ └─────────────────┘ └────────────────────┘ └─────────────────┘ ``` --- ## Import Patterns ### Standard Import (Recommended) ```python from app.core import ( GameEngine, RulesConfig, create_rng, CardType, EnergyType, TurnPhase, ) from app.core.models import ( CardDefinition, CardInstance, Action, AttackAction, GameState, ) from app.core.effects import ( effect_handler, resolve_effect, EffectContext, ) ``` ### Enum Import Enums live in `app/core/enums.py` (the foundational module with zero dependencies). They're re-exported from both `app.core` and NOT from `app.core.models` (to avoid circular imports): ```python # CORRECT - import enums from core or enums module from app.core import CardType, EnergyType, TurnPhase from app.core.enums import StatusCondition, PokemonStage # WRONG - models does NOT export enums # from app.core.models import CardType # This won't work! ``` --- ## Core Patterns ### 1. Game Creation Flow ```python from app.core import GameEngine, RulesConfig, create_rng # 1. Create engine with rules and RNG engine = GameEngine( rules=RulesConfig(), # Default Mantimon rules rng=create_rng(seed=42), # Seeded for tests, None for production ) # 2. Prepare card registry and decks card_registry: dict[str, CardDefinition] = {...} player1_deck: list[CardInstance] = [...] player2_deck: list[CardInstance] = [...] # 3. Create game result = engine.create_game( player_ids=["player1", "player2"], decks={"player1": player1_deck, "player2": player2_deck}, energy_decks={"player1": p1_energy, "player2": p2_energy}, # Optional card_registry=card_registry, ) if result.success: game = result.game ``` ### 2. Action Execution Flow ```python # All actions go through engine.execute_action() action = AttackAction(attack_index=0) result = await engine.execute_action(game, "player1", action) if result.success: # Check for win if result.win_result: print(f"Game over! Winner: {result.win_result.winner_id}") else: print(f"Action failed: {result.message}") ``` ### 3. Effect Handler Pattern Cards reference effects by ID. Handlers are registered with `@effect_handler`: ```python from app.core.effects import effect_handler, EffectContext, EffectResult @effect_handler("coin_flip_damage") async def handle_coin_flip_damage(ctx: EffectContext) -> EffectResult: """Deal bonus damage on heads.""" base = ctx.get_param("base_damage", 0) bonus = ctx.get_param("bonus_on_heads", 0) if ctx.flip_coin(): # Uses RNG from context ctx.target_card.damage += base + bonus return EffectResult.success(f"Heads! Dealt {base + bonus} damage") else: ctx.target_card.damage += base return EffectResult.success(f"Tails. Dealt {base} damage") ``` Card definition references the effect: ```python Attack( name="Thunder Shock", cost=[EnergyType.LIGHTNING], damage=30, effect_id="coin_flip_damage", effect_params={"base_damage": 30, "bonus_on_heads": 10}, ) ``` ### 4. Configuration System All game rules are driven by `RulesConfig`: ```python from app.core import RulesConfig # Default Mantimon rules rules = RulesConfig() # Customize specific settings rules = RulesConfig( deck=DeckConfig(min_size=60, max_size=60), # Standard Pokemon prizes=PrizeConfig(count=6, use_prize_cards=True), first_turn=FirstTurnConfig(can_attack=False), # No first-turn attacks ) # Use standard Pokemon TCG preset rules = RulesConfig.standard_pokemon_tcg() ``` ### 5. Visibility Filtering **CRITICAL**: Never send raw `GameState` to clients. Always filter: ```python # For players - sees own hand, opponent hand COUNT only visible = engine.get_visible_state(game, "player1") # For spectators - sees neither player's hand visible = engine.get_spectator_state(game) ``` Hidden information: - Deck contents and order (both players) - Opponent's hand contents - Unrevealed prize cards - Energy deck order --- ## Key Classes ### GameState The complete game state. Contains: - `card_registry`: All card definitions used in this game - `players`: Dict of player_id -> `PlayerState` - `turn_order`, `current_player_id`, `turn_number` - `phase`: Current `TurnPhase` - `rules`: `RulesConfig` for this game - `winner`, `game_over`, `end_reason`: Game conclusion state ### PlayerState One player's state. Contains zones and per-turn counters: - Zones: `deck`, `hand`, `active`, `bench`, `prizes`, `discard`, `energy_deck`, `energy_zone` - Counters: `energy_attachments_this_turn`, `supporters_played_this_turn`, etc. - Score: `score` (points earned from knockouts) ### CardDefinition vs CardInstance - **CardDefinition**: Immutable template (the card as printed) - **CardInstance**: Mutable in-game state (damage, attached energy, status) ```python # Definition - what the card IS pikachu_def = CardDefinition( id="pikachu-001", name="Pikachu", hp=60, attacks=[...], ) # Instance - a specific copy in play pikachu_in_play = CardInstance( instance_id="game1-pikachu-0", definition_id="pikachu-001", damage=20, attached_energy=[lightning_energy_instance], status_conditions=[StatusCondition.PARALYZED], ) ``` ### Zone A collection of cards with operations: - `add(card)`, `remove(instance_id)`, `get(instance_id)` - `draw()`: Remove and return top card - `shuffle(rng)`: Randomize order - `__contains__`, `__len__`, `__iter__` --- ## Testing Patterns ### Use SeededRandom for Determinism ```python from app.core import create_rng def test_coin_flip(): rng = create_rng(seed=42) # Same seed = same sequence every time results = [rng.coin_flip() for _ in range(5)] assert results == [True, False, True, True, False] # Deterministic ``` ### Use Fixtures from conftest.py ```python # tests/core/conftest.py provides: # - sample_pikachu, sample_raichu, sample_charmander # - sample_lightning_energy, sample_fire_energy # - create_test_game_state() # - seeded_rng fixture def test_with_fixtures(sample_pikachu, seeded_rng): instance = CardInstance( instance_id="test-1", definition_id=sample_pikachu.id, ) ... ``` ### Test Docstrings Required Every test must explain "what" and "why": ```python def test_paralyzed_pokemon_cannot_attack(): """ Test that paralyzed Pokemon are blocked from attacking. Paralysis should prevent all attack actions until cleared at the end of the affected player's turn. """ ... ``` --- ## Security Rules ### Never Expose Hidden Information ```python # WRONG - leaks deck order response = {"deck": [card.model_dump() for card in player.deck.cards]} # CORRECT - only send count response = {"deck_count": len(player.deck)} ``` ### Server Authority All game logic runs server-side. The client sends intentions: ```python # Client sends: "I want to attack with attack index 0" action = AttackAction(attack_index=0) # Server validates and executes result = await engine.execute_action(game, player_id, action) # Server sends back result (never raw state) visible_state = engine.get_visible_state(game, player_id) ``` --- ## Module Independence (Offline Fork Support) The `app/core/` module is designed to be extractable as a standalone offline game: ### Rules for Core Module | DO | DON'T | |----|-------| | Accept `CardDefinition` objects as parameters | Import from `app.services` or `app.api` | | Use `RandomProvider` protocol for RNG | Import database session types | | Keep state self-contained in `GameState` | Make network calls or database queries | | Load configuration from `RulesConfig` | Require authentication or user sessions | ### Import Boundaries ```python # ALLOWED in app/core/ from app.core.models import CardDefinition, GameState from app.core.config import RulesConfig from app.core.rng import RandomProvider # FORBIDDEN in app/core/ from app.services.card_service import CardService # NO - DB dependency from app.api.deps import get_current_user # NO - Auth dependency from sqlalchemy.ext.asyncio import AsyncSession # NO - DB dependency ``` --- ## File Organization ``` app/core/ ├── __init__.py # Main public API exports (34 items) ├── AGENTS.md # This file ├── enums.py # All enums (foundational, zero deps) ├── config.py # RulesConfig and sub-configs ├── rng.py # RandomProvider implementations ├── engine.py # GameEngine orchestrator ├── rules_validator.py # Action validation ├── turn_manager.py # Turn/phase state machine ├── win_conditions.py # Win/loss detection ├── visibility.py # Hidden info filtering ├── models/ │ ├── __init__.py # Model exports (29 items) │ ├── card.py # CardDefinition, CardInstance │ ├── actions.py # Action union types │ └── game_state.py # GameState, PlayerState, Zone └── effects/ ├── __init__.py # Effect system exports (12 items) ├── base.py # EffectContext, EffectResult ├── registry.py # @effect_handler, resolve_effect └── handlers.py # Built-in effect handlers (14 registered) ``` --- ## Common Tasks ### Adding a New Effect Handler 1. Add handler in `effects/handlers.py`: ```python @effect_handler("my_effect") async def handle_my_effect(ctx: EffectContext) -> EffectResult: ... ``` 2. Effect is auto-registered on import 3. Reference in card definition: `effect_id="my_effect"` ### Adding a New Action Type 1. Add action class in `models/actions.py` 2. Add to `Action` union type 3. Add phase mapping to `VALID_PHASES_FOR_ACTION` 4. Add validation in `rules_validator.py` 5. Add execution in `engine.py._execute_action_internal()` ### Modifying Game Rules 1. Add config field to appropriate class in `config.py` 2. Update `RulesConfig` if adding new config class 3. Update validation logic in `rules_validator.py` 4. Update execution logic in `engine.py` or `turn_manager.py` --- ## Test Coverage | Module | Tests | Coverage | |--------|-------|----------| | config | 31 | 100% | | enums | 28 | 100% | | rng | 42 | 100% | | models/card | 77 | 98% | | models/actions | 48 | 100% | | models/game_state | 62 | 97% | | effects/base | 30 | 100% | | effects/registry | 21 | 100% | | effects/handlers | 79 | 95% | | rules_validator | 95 | 96% | | turn_manager | 60 | 94% | | win_conditions | 53 | 100% | | visibility | 44 | 100% | | engine | 65 | 93% | **Total: 826 tests, ~97% coverage** --- ## Quick Commands ```bash # Run all core tests cd backend && uv run pytest tests/core/ -v # Run specific module tests cd backend && uv run pytest tests/core/test_engine.py -v # Run with coverage cd backend && uv run pytest tests/core/ --cov=app/core --cov-report=term-missing # Type check cd backend && uv run mypy app/core/ # Interactive game demo cd backend && uv run python references/game_walkthrough.py ```