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.
14 KiB
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)
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):
# 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
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
# 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:
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:
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:
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:
# 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 gameplayers: Dict of player_id ->PlayerStateturn_order,current_player_id,turn_numberphase: CurrentTurnPhaserules:RulesConfigfor this gamewinner,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)
# 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 cardshuffle(rng): Randomize order__contains__,__len__,__iter__
Testing Patterns
Use SeededRandom for Determinism
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
# 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":
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
# 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:
# 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
# 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
- Add handler in
effects/handlers.py:@effect_handler("my_effect") async def handle_my_effect(ctx: EffectContext) -> EffectResult: ... - Effect is auto-registered on import
- Reference in card definition:
effect_id="my_effect"
Adding a New Action Type
- Add action class in
models/actions.py - Add to
Actionunion type - Add phase mapping to
VALID_PHASES_FOR_ACTION - Add validation in
rules_validator.py - Add execution in
engine.py._execute_action_internal()
Modifying Game Rules
- Add config field to appropriate class in
config.py - Update
RulesConfigif adding new config class - Update validation logic in
rules_validator.py - Update execution logic in
engine.pyorturn_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
# 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