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