mantimon-tcg/AGENTS.md
Cal Corum c3623d9541 Consolidate CLAUDE.md into AGENTS.md
Merged unique content from CLAUDE.md into AGENTS.md:
- Project Overview section (campaign mode description)
- Full Tech Stack tables (frontend and backend)
- WebSocket Events pattern
- Game Engine Patterns (Card Effect System, Turn State Machine)

Removed CLAUDE.md to standardize on AGENTS.md for agent guidelines.
2026-01-26 14:21:29 -06:00

12 KiB

AGENTS.md - 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

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

Testing

# 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

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

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)
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
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:

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:

// Vue -> Phaser
phaserGame.value?.events.emit('card:play', { cardId, targetId })

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

Frontend: State Management (Pinia)

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:

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

async def resolve_attack(attacker: Card, defender: Card) -> AttackResult:
    ...

Backend: WebSocket Events

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

# 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

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":

@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)

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

# 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