# Mantimon TCG Backend - AI Agent Guidelines This document defines architecture requirements, patterns, and constraints for AI agents working on this codebase. ## Critical Architecture Requirement: Offline Fork Support > **The `app/core/` module must remain extractable as a standalone offline game.** This is a primary design goal. The core game engine should work without network, database, or authentication dependencies. ### Module Independence Rules | Module | Can Import From | Cannot Import From | |--------|-----------------|-------------------| | `app/core/` | Python stdlib, pydantic | `app/services/`, `app/api/`, `app/db/`, sqlalchemy | | `app/services/` | `app/core/`, `app/repositories/` | `app/api/` | | `app/repositories/` | `app/core/`, `app/db/` | `app/services/`, `app/api/` | | `app/api/` | All modules | - | ### Import Boundary Examples ```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 import CardService # NO - service dependency from app.api.deps import get_current_user # NO - auth dependency from sqlalchemy.ext.asyncio import AsyncSession # NO - DB dependency ``` --- ## Stateless Backend - Config from Request **The backend is stateless. Rules and configuration come from the request, provided by the frontend.** This is a key architectural principle: the frontend knows what context the user is in (campaign mode, freeplay mode, custom rules) and tells the API what rules to apply. ### Why Frontend Provides Config 1. **Simplicity**: No complex server-side config management 2. **Flexibility**: Same backend supports multiple game modes 3. **Testability**: Pure functions with explicit inputs 4. **Offline Fork**: No server-side config to replicate ### Pattern: Pure Functions with Config Parameter ```python # CORRECT - Config comes from caller def validate_deck( cards: dict[str, int], energy_cards: dict[str, int], deck_config: DeckConfig, # Provided by frontend card_lookup: Callable[[str], CardDefinition | None], owned_cards: dict[str, int] | None = None, ) -> ValidationResult: """Pure function - all inputs from caller.""" ... ``` ### Forbidden Patterns ```python # WRONG - Service with baked-in config class DeckValidator: def __init__(self, config: DeckConfig): # Config at construction self._config = config # WRONG - Default config hides dependency def validate_deck(cards, config: DeckConfig | None = None): config = config or DeckConfig() # Hidden creation! ``` --- ## Dependency Injection for Services **Services use constructor-based dependency injection for repositories and other services.** Services still use DI for data access, but config comes from method parameters (request). ### Required Pattern ```python class DeckService: """Dependencies injected via constructor. Config from method params.""" def __init__( self, deck_repository: DeckRepository, card_service: CardService, collection_repository: CollectionRepository | None = None, ) -> None: self._deck_repo = deck_repository self._card_service = card_service self._collection_repo = collection_repository async def create_deck( self, user_id: UUID, cards: dict[str, int], energy_cards: dict[str, int], deck_config: DeckConfig, # Config from request! max_decks: int, ) -> DeckEntry: ... ``` ### Why This Matters 1. **Testability**: Dependencies can be mocked without patching globals 2. **Offline Fork**: Services can be swapped for local implementations 3. **Explicit Dependencies**: Constructor shows all requirements 4. **Stateless Operations**: Config comes from request, not server state --- ## Repository Protocol Pattern Services access data through repository protocols, not directly through ORM models. ### Pattern ```python # Protocol defines interface (app/repositories/protocols.py) class CollectionRepository(Protocol): async def get_all(self, user_id: UUID) -> list[CollectionEntry]: ... async def upsert(self, ...) -> CollectionEntry: ... # PostgreSQL implementation (app/repositories/postgres/) class PostgresCollectionRepository: def __init__(self, db: AsyncSession) -> None: self._db = db # Service uses protocol, not implementation class CollectionService: def __init__(self, repository: CollectionRepository, card_service: CardService): self._repo = repository self._card_service = card_service ``` ### Benefits - **Offline Fork**: Can implement `LocalCollectionRepository` using JSON files - **Testing**: Can inject mock repositories - **Decoupling**: Services don't know about SQLAlchemy --- ## UNSET Sentinel Pattern For nullable fields that can be explicitly cleared (set to `None`), use the `UNSET` sentinel to distinguish between "not provided" (keep existing) and "set to null" (clear the value). ### Pattern ```python from app.repositories.protocols import UNSET # In repository/service method signatures async def update( self, deck_id: UUID, name: str | None = None, # None means "don't change" description: str | None = UNSET, # type: ignore[assignment] # UNSET = keep existing, None = clear, str = set new value ) -> DeckEntry | None: ... if description is not UNSET: record.description = description # Could be None (clear) or string (set) ``` ### API Layer Usage ```python # Check if field was explicitly provided in request description = deck_in.description if "description" in deck_in.model_fields_set else UNSET ``` ### When to Use - Fields that can be meaningfully set to `None` (descriptions, notes, optional refs) - Not needed for fields where `None` means "don't update" (name, cards, etc.) --- ## Configuration Classes Game rules are defined in `app/core/config.py` as Pydantic models. These are passed from the frontend as request parameters. ### Available Config Classes ```python from app.core.config import DeckConfig, RulesConfig # DeckConfig - deck building rules class DeckConfig(BaseModel): min_size: int = 40 max_size: int = 40 max_copies_per_card: int = 4 min_basic_pokemon: int = 1 energy_deck_size: int = 20 # Frontend can customize for different modes freeplay_config = DeckConfig(max_copies_per_card=10) # Relaxed rules campaign_config = DeckConfig() # Standard rules ``` ### API Endpoints Accept Config ```python @router.post("/decks") async def create_deck( deck_in: DeckCreate, # Contains deck_config from frontend current_user: User = Depends(get_current_user), deck_service: DeckService = Depends(get_deck_service), ): return await deck_service.create_deck( user_id=current_user.id, cards=deck_in.cards, deck_config=deck_in.deck_config, # From request ... ) ``` --- ## Testing Requirements ### Test Docstrings Required Every test must have a docstring explaining "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. """ ... ``` ### Unit Tests vs Integration Tests | Directory | Purpose | Database | |-----------|---------|----------| | `tests/unit/` | Pure unit tests, mocked dependencies | No | | `tests/services/` | Integration tests with real DB | Yes | | `tests/core/` | Core engine tests | No | | `tests/api/` | API endpoint tests | Yes | ### Use Seeded RNG for Determinism ```python from app.core import create_rng def test_coin_flip(): rng = create_rng(seed=42) results = [rng.coin_flip() for _ in range(5)] assert results == [True, False, True, True, False] # Deterministic ``` --- ## Code Organization ``` app/ ├── core/ # Game engine (MUST be standalone-capable) │ ├── models/ # Pydantic models for game state │ ├── effects/ # Effect handler system │ ├── config.py # RulesConfig and sub-configs │ └── engine.py # GameEngine orchestrator ├── services/ # Business logic (uses repositories) ├── repositories/ # Data access layer │ ├── protocols.py # Repository protocols (interfaces) │ └── postgres/ # PostgreSQL implementations ├── schemas/ # Pydantic schemas for API ├── api/ # FastAPI routes ├── db/ # Database models and migrations └── data/ # Static data (starter decks, etc.) ``` --- ## Quick Reference ### Creating a New Service 1. Define constructor with repository and service dependencies 2. Use repository protocols for data access 3. Accept config (DeckConfig, RulesConfig) as **method parameters**, not constructor params 4. Keep business logic separate from data access ### Creating a Repository 1. Define protocol in `app/repositories/protocols.py` 2. Create DTO dataclasses for protocol returns 3. Implement in `app/repositories/postgres/` 4. Use `_to_dto()` method to convert ORM -> DTO ### Adding Static Data 1. Place in `app/data/` module 2. Use Protocol for any config dependencies 3. Provide validation functions that accept config as parameter --- ## Common Mistakes to Avoid | Mistake | Correct Approach | |---------|------------------| | `get_card_service()` in method body | Inject `CardService` via constructor | | `config or DeckConfig()` default | Make config required method parameter | | Baking config into service constructor | Accept config as method parameter (from request) | | Importing from `app.services` in `app.core` | Core must remain standalone | | Hardcoded magic numbers | Use values from config parameter | | Tests without docstrings | Always explain what and why | | Unit tests in `tests/services/` | Use `tests/unit/` for no-DB tests | --- ## See Also - `app/core/AGENTS.md` - Detailed core engine guidelines - `app/core/README.md` - Core module documentation - `app/core/effects/README.md` - Effect system documentation