# Mantimon TCG

Guidelines for agentic coding agents working on this codebase.

## Project Overview

Mantimon TCG is a home-rule-modified Pokemon Trading Card Game web application inspired by the Gameboy Color game *Pokemon TCG*. The core experience is a **single-player RPG campaign**:

- **Campaign Mode**: Challenge NPCs at themed clubs, defeat Club Leaders to earn medals, collect all medals to face Grand Masters and become Champion
- **Collection Building**: Win matches to earn booster packs, build your card collection
- **Deck Building**: Construct decks from your collection to take on tougher opponents
- **Multiplayer (Optional)**: PvP matches for competitive play

## Tech Stack

### Frontend
| Technology | Purpose |
|------------|---------|
| Vue 3 | UI framework (Composition API + `<script setup>`) |
| Phaser 3 | Game canvas (matches, pack opening) |
| TypeScript | Type safety |
| Pinia | State management |
| Tailwind CSS | Styling |
| Socket.io-client | Real-time communication |
| Vite | Build tool |

### Backend
| Technology | Purpose |
|------------|---------|
| FastAPI | REST API framework |
| Python 3.11+ | Backend language |
| SQLAlchemy 2.0 | ORM (async) |
| PostgreSQL | Database |
| Redis | Caching, session storage |
| python-socketio | WebSocket server |
| Pydantic v2 | Validation |
| Alembic | Database migrations |

---

## Quick Commands

### Development Servers
```bash
cd frontend && npm run dev                    # Frontend dev server
cd backend && uv run uvicorn app.main:app --reload  # Backend dev server
```

### Testing
```bash
# Frontend (Vitest)
cd frontend && npm run test                   # Run all tests
cd frontend && npm run test -- path/to/file   # Run single test file
cd frontend && npm run test -- -t "test name" # Run by test name

# Backend (pytest with uv)
cd backend && uv run pytest                   # Run all tests
cd backend && uv run pytest tests/test_file.py           # Single file
cd backend && uv run pytest tests/test_file.py::test_fn  # Single test
cd backend && uv run pytest -k "test_name"    # By name pattern
cd backend && uv run pytest -x                # Stop on first failure
cd backend && uv run pytest --cov=app         # With coverage
```

### Linting & Formatting
```bash
# Frontend
cd frontend && npm run lint           # ESLint
cd frontend && npm run typecheck      # TypeScript check

# Backend (uses uv for all commands)
cd backend && uv run black app tests  # Format with Black
cd backend && uv run black --check .  # Check formatting (CI)
cd backend && uv run ruff check .     # Lint with Ruff
cd backend && uv run ruff check --fix . # Auto-fix lint issues
cd backend && uv run mypy app         # Type check
```

### Dependency Management (Backend)
```bash
cd backend && uv add <package>        # Add runtime dependency
cd backend && uv add --dev <package>  # Add dev dependency
cd backend && uv sync                 # Install all dependencies
cd backend && uv lock                 # Update lock file
```

---

## Code Style

### General Rules
- Line length: 100 characters max
- Indentation: 2 spaces (frontend), 4 spaces (backend)
- Trailing commas in multi-line structures
- Explicit over implicit

### TypeScript/Vue

**Import order** (separated by blank lines):
1. Standard library / Vue core
2. Third-party packages
3. Local imports (use `@/` alias)

```typescript
import { ref, computed } from 'vue'

import { useGameStore } from '@/stores/game'
import type { Card, GameState } from '@/types'

// Always use `import type` for type-only imports
import type { Player } from '@/types/player'

// Prefer const over let
const cards = ref<Card[]>([])
```

**Naming conventions:**
| Type | Convention | Example |
|------|------------|---------|
| Vue components | PascalCase | `CardHand.vue`, `GameBoard.vue` |
| Phaser scenes | PascalCase | `MatchScene.ts`, `PackOpeningScene.ts` |
| TypeScript files | camelCase | `useWebSocket.ts`, `cardUtils.ts` |
| Constants | UPPER_SNAKE_CASE | `MAX_HAND_SIZE`, `PRIZE_COUNT` |

### Python

**Import order** (separated by blank lines):
1. Standard library
2. Third-party packages
3. Local imports

```python
from typing import Optional

from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession

from app.models import Card, Player
from app.services.game_service import GameService
```

**Type hints required** for all function signatures:
```python
async def get_card(card_id: int, db: AsyncSession = Depends(get_db)) -> Card:
    ...
```

**Naming conventions:**
| Type | Convention | Example |
|------|------------|---------|
| Modules | snake_case | `game_engine.py`, `card_service.py` |
| Database tables | snake_case | `user_collections`, `match_history` |
| Constants | UPPER_SNAKE_CASE | `MAX_HAND_SIZE`, `PRIZE_COUNT` |

---

## Architecture Patterns

### Frontend: Vue + Phaser Integration
Phaser mounts as a Vue component. Communication via event bridge:
```typescript
// Vue -> Phaser
phaserGame.value?.events.emit('card:play', { cardId, targetId })

// Phaser -> Vue
phaserGame.value?.events.on('animation:complete', handleAnimationComplete)
```

### Frontend: State Management (Pinia)
```typescript
export const useGameStore = defineStore('game', () => {
  const gameState = ref<GameState | null>(null)
  const myHand = computed(() => gameState.value?.myHand ?? [])
  return { gameState, myHand }
})
```

### Backend: Service Layer
**Never bypass services for business logic:**
```python
# CORRECT
card = await card_service.get_card(card_id)
result = await game_service.play_card(game_id, player_id, card_id)

# WRONG - direct DB access in endpoint
card = await db.execute(select(Card).where(Card.id == card_id))
```

### Backend: Async by Default
All I/O operations use async/await:
```python
async def resolve_attack(attacker: Card, defender: Card) -> AttackResult:
    ...
```

### Backend: WebSocket Events
```python
# All game actions go through WebSocket for real-time sync
@sio.on('game:action')
async def handle_game_action(sid, data):
    action_type = data['type']
    
    # Validate action is legal
    validation = await game_engine.validate_action(game_id, player_id, data)
    if not validation.valid:
        await sio.emit('game:error', {'message': validation.reason}, to=sid)
        return
    
    # Execute and broadcast
    new_state = await game_engine.execute_action(game_id, data)
    await broadcast_game_state(game_id, new_state)
```

---

## Game Engine Patterns

### Card Effect System
Cards are data-driven. Effects reference handler functions:

```python
# Card definition (JSON/DB)
{
    "id": "pikachu_base_001",
    "name": "Pikachu",
    "hp": 60,
    "type": "lightning",
    "attacks": [
        {
            "name": "Thunder Shock",
            "cost": ["lightning"],
            "damage": 20,
            "effect": "may_paralyze",  # References effect handler
            "effect_params": {"chance": 0.5}
        }
    ]
}

# Effect handler (Python)
@effect_handler("may_paralyze")
async def handle_may_paralyze(context: EffectContext, params: dict) -> None:
    if random.random() < params["chance"]:
        context.defender.add_status("paralyzed")
```

### Turn State Machine
```python
class TurnPhase(Enum):
    DRAW = "draw"
    MAIN = "main"
    ATTACK = "attack"
    END = "end"

# Transitions are explicit
VALID_TRANSITIONS = {
    TurnPhase.DRAW: [TurnPhase.MAIN],
    TurnPhase.MAIN: [TurnPhase.ATTACK, TurnPhase.END],  # Can skip attack
    TurnPhase.ATTACK: [TurnPhase.END],
    TurnPhase.END: [TurnPhase.DRAW],  # Next player's turn
}
```

---

## Testing Guidelines

### Test Docstrings Required
Every test must include a docstring explaining "what" and "why":
```python
@pytest.mark.asyncio
async def test_draw_card():
    """
    Test that drawing a card moves it from deck to hand.
    
    Verifies the fundamental draw mechanic works correctly
    and updates both zones appropriately.
    """
    # test implementation
```

### Frontend Tests (Vitest)
```typescript
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'

describe('CardHand', () => {
  it('renders cards in hand', () => {
    const wrapper = mount(CardHand, {
      props: { cards: [{ id: '1', name: 'Pikachu' }] }
    })
    expect(wrapper.text()).toContain('Pikachu')
  })
})
```

---

## Critical Security Rules

### Hidden Information
**Never expose to clients:**
- Deck order (either player)
- Opponent's hand contents
- Unrevealed prize cards
- RNG seeds or future random results

```python
# Correct: Only send counts for opponent's hidden zones
opponent_hand_count=len(opponent.hand)  # ONLY count, not contents
opponent_deck_count=len(opponent.deck)  # ONLY count
```

### Server Authority
- All game logic runs server-side
- Client sends intentions, server validates and executes
- Never trust client-provided game state

---

## Critical Rules Summary

1. **Git**: Never commit directly to `main`. Create feature branches.
2. **Commits**: Do not commit without user approval.
3. **Hidden Info**: Never send deck contents, opponent hand, or unrevealed prizes to client.
4. **Validation**: Always validate actions server-side. Never trust client.
5. **Tests**: Include docstrings explaining "what" and "why" for each test.
6. **Phaser in Vue**: Keep Phaser scenes focused on rendering. Game logic lives in backend.
7. **Services**: Never bypass the service layer for business logic.
8. **Core Engine Independence**: Keep `app/core/` decoupled from DB/network (see below).

---

## Core Engine Independence (Offline Fork Support)

> **Long-term goal**: The `backend/app/core/` module should remain forkable as a standalone offline game.

The game engine must stay **completely decoupled** from network and database concerns to enable a future offline/standalone version of the RPG campaign mode.

### Rules for `app/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 |
| Use sync logic (async wrappers at service layer) | Require authentication or user sessions |
| Load configuration from `RulesConfig` objects | Hard-code database connection strings |

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

### Why This Matters

See `docs/ARCHITECTURE.md#offline-standalone-fork` for full details. The core engine should be directly copyable to a standalone Python application with:
- Card definitions loaded from JSON files
- Save data stored locally
- No network or authentication requirements

---

## Project Structure

```
mantimon-tcg/
├── frontend/                # Vue 3 + Phaser 3
│   ├── src/
│   │   ├── components/      # Vue components
│   │   ├── pages/           # Route pages
│   │   ├── stores/          # Pinia stores
│   │   ├── game/            # Phaser scenes and game objects
│   │   └── composables/     # Vue composables
│   └── package.json
├── backend/                 # FastAPI + PostgreSQL
│   ├── app/
│   │   ├── api/             # REST endpoints
│   │   ├── core/            # Game engine
│   │   ├── models/          # Pydantic + SQLAlchemy models
│   │   ├── services/        # Business logic
│   │   └── websocket/       # Socket.io handlers
│   └── pyproject.toml
└── shared/                  # Shared types/schemas
```
