Creates comprehensive documentation for AI agents working with app/core/: - Architecture overview with component diagram - Import patterns (correct enum imports from app.core.enums) - Core patterns: game creation, action execution, effects, config - Key classes: GameState, PlayerState, CardDefinition, CardInstance, Zone - Testing patterns with SeededRandom for determinism - Security rules for hidden information - Module independence guidelines for offline fork support - File organization and quick commands Updates PROJECT_PLAN.json: 31/32 tasks complete.
455 lines
14 KiB
Markdown
455 lines
14 KiB
Markdown
# 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
|
|
```
|