# 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, services, and external resources.**

Services use DI for data access and external dependencies, but config comes from method parameters (request).

### Required Pattern: Services and Repositories

```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:
        ...
```

### Required Pattern: Factory Injection for External Resources

For external resources (Redis, databases, engines), inject factory functions rather than instances:

```python
from collections.abc import AsyncIterator, Callable

# Type aliases for factories
RedisFactory = Callable[[], AsyncIterator["Redis"]]
EngineFactory = Callable[[GameState], GameEngine]
TokenVerifier = Callable[[str], UUID | None]

class ConnectionManager:
    """External resources injected as factories."""

    def __init__(
        self,
        conn_ttl_seconds: int = 3600,
        redis_factory: RedisFactory | None = None,  # Factory, not instance
    ) -> None:
        self.conn_ttl_seconds = conn_ttl_seconds
        # Fall back to global only if not provided
        self._get_redis = redis_factory if redis_factory is not None else get_redis

    async def register_connection(self, sid: str, user_id: UUID) -> None:
        async with self._get_redis() as redis:  # Use injected factory
            await redis.hset(...)
```

```python
class GameService:
    """Engine created via injected factory for testability."""

    def __init__(
        self,
        state_manager: GameStateManager | None = None,
        engine_factory: EngineFactory | None = None,  # Factory for engine creation
    ) -> None:
        self._state_manager = state_manager or game_state_manager
        self._engine_factory = engine_factory or self._default_engine_factory

    async def execute_action(self, game_id: str, player_id: str, action: Action):
        state = await self.get_game_state(game_id)
        engine = self._engine_factory(state)  # Create via factory
        result = await engine.execute_action(state, player_id, action)
```

### Global Singletons with DI Support

Create global instances for production, but always support injection for testing:

```python
# Global singleton for production use
connection_manager = ConnectionManager()

# Tests inject mocks via constructor - no patching needed
def test_register_connection(mock_redis):
    @asynccontextmanager
    async def mock_redis_factory():
        yield mock_redis

    manager = ConnectionManager(redis_factory=mock_redis_factory)
    # Test with injected mock...
```

### 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
5. **No Monkey Patching**: Tests use constructor injection, not `patch()`

---

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

### No Monkey Patching - Use Dependency Injection

**Never use `patch()` or `monkeypatch` for unit tests.** Instead, inject mocks via constructor.

```python
# WRONG - Monkey patching
async def test_execute_action():
    with patch.object(game_service, "_create_engine") as mock:
        mock.return_value = mock_engine
        result = await game_service.execute_action(...)

# CORRECT - Dependency injection
@pytest.fixture
def mock_engine():
    engine = MagicMock()
    engine.execute_action = AsyncMock(return_value=ActionResult(success=True))
    return engine

@pytest.fixture
def game_service(mock_state_manager, mock_engine):
    return GameService(
        state_manager=mock_state_manager,
        engine_factory=lambda state: mock_engine,  # Inject via constructor
    )

async def test_execute_action(game_service, mock_engine):
    result = await game_service.execute_action(...)
    mock_engine.execute_action.assert_called_once()
```

### Factory Fixtures for External Resources

```python
@pytest.fixture
def mock_redis():
    redis = AsyncMock()
    redis.hset = AsyncMock()
    redis.hget = AsyncMock(return_value=None)
    return redis

@pytest.fixture
def connection_manager(mock_redis):
    @asynccontextmanager
    async def mock_redis_factory():
        yield mock_redis

    return ConnectionManager(redis_factory=mock_redis_factory)
```

### 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)
│   ├── game_service.py       # Game orchestration (uses engine_factory DI)
│   ├── connection_manager.py # WebSocket tracking (uses redis_factory DI)
│   └── ...
├── socketio/       # WebSocket real-time communication
│   ├── server.py   # Socket.IO server setup and handlers
│   └── auth.py     # AuthHandler class (uses token_verifier DI)
├── 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 |
| Using `patch()` in unit tests | Inject mocks via constructor DI |
| `async with get_redis()` in method body | Inject `redis_factory` via constructor |
| Importing dependencies inside functions | Import at module level, inject via constructor |

---

## 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
- `app/services/README.md` - Services layer patterns
- `app/socketio/README.md` - WebSocket module documentation
