Initial project setup: documentation and structure

- Add PROJECT_PLAN.md with 7-phase development roadmap
- Add CLAUDE.md with AI agent coding guidelines
- Add docs/ARCHITECTURE.md with technical deep-dive
- Add docs/GAME_RULES.md template for home rule definitions
- Create directory structure for frontend, backend, shared
This commit is contained in:
Cal Corum 2026-01-23 23:41:34 -06:00
commit f473f94bce
4 changed files with 1718 additions and 0 deletions

309
CLAUDE.md Normal file
View File

@ -0,0 +1,309 @@
# CLAUDE.md - Mantimon TCG
Guidelines for AI coding agents working on this project.
## Quick Reference
**Frontend dev server**: `cd frontend && npm run dev`
**Backend dev server**: `cd backend && uvicorn app.main:app --reload`
**Run frontend tests**: `cd frontend && npm run test`
**Run backend tests**: `cd backend && python -m pytest`
**Type check frontend**: `cd frontend && npm run typecheck`
**Lint frontend**: `cd frontend && npm run lint`
## Project Overview
Mantimon TCG is a home-rule-modified Pokemon Trading Card Game web application featuring:
- Single-player (AI opponents, puzzle mode)
- Multiplayer (real-time matches)
- Collection system (packs, crafting, deck building)
## 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 |
## Code Style
### General
- Line length: 100 characters max
- Indentation: 2 spaces (frontend), 4 spaces (backend)
- Trailing commas in multi-line structures
- Explicit over implicit
### TypeScript/Vue
```typescript
// Imports: stdlib, third-party, local (separated by blank lines)
import { ref, computed } from 'vue'
import { useGameStore } from '@/stores/game'
import type { Card, GameState } from '@/types'
// Always use type imports for types
import type { Player } from '@/types/player'
// Prefer const over let
const cards = ref<Card[]>([])
// Use descriptive names
const isPlayerTurn = computed(() => gameStore.currentPlayer === playerId)
// Component naming: PascalCase
// File naming: PascalCase for components, camelCase for utilities
```
### Python
```python
# Imports: stdlib, third-party, local (separated by blank lines)
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:
...
# Use Pydantic models for request/response
class PlayCardRequest(BaseModel):
card_id: int
target_id: Optional[int] = None
# Async by default for I/O operations
async def resolve_attack(attacker: Card, defender: Card) -> AttackResult:
...
```
### 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` |
| Python 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 is mounted as a Vue component. Communication happens via:
```typescript
// In Vue component
const phaserGame = ref<Phaser.Game | null>(null)
// Emit events to Phaser
phaserGame.value?.events.emit('card:play', { cardId, targetId })
// Listen to events from Phaser
phaserGame.value?.events.on('animation:complete', handleAnimationComplete)
```
### Frontend: State Management
```typescript
// Use Pinia stores for global state
// stores/game.ts
export const useGameStore = defineStore('game', () => {
const gameState = ref<GameState | null>(null)
const myHand = computed(() => gameState.value?.myHand ?? [])
function setGameState(state: GameState) {
gameState.value = state
}
return { gameState, myHand, setGameState }
})
```
### 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: 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
}
```
### Hidden Information
**Critical**: Server never sends hidden information to clients.
```python
def get_visible_state(game: Game, player_id: str) -> VisibleGameState:
return VisibleGameState(
my_hand=game.players[player_id].hand, # Full hand
my_prizes=game.players[player_id].prizes, # Can see own prizes
my_deck_count=len(game.players[player_id].deck), # Only count
opponent_hand_count=len(opponent.hand), # ONLY count
opponent_prizes_count=len(opponent.prizes), # ONLY count
opponent_deck_count=len(opponent.deck), # ONLY count
battlefield=game.battlefield, # Public
discard_piles=game.discard_piles, # Public
)
```
## Testing
### Frontend (Vitest)
```typescript
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import CardHand from '@/components/CardHand.vue'
describe('CardHand', () => {
it('renders cards in hand', () => {
const wrapper = mount(CardHand, {
props: {
cards: [{ id: '1', name: 'Pikachu' }]
}
})
expect(wrapper.text()).toContain('Pikachu')
})
})
```
### Backend (pytest)
```python
import pytest
from app.core.game_engine import GameEngine
@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.
"""
engine = GameEngine()
game = await engine.create_game(player_ids=["p1", "p2"])
initial_deck_size = len(game.players["p1"].deck)
initial_hand_size = len(game.players["p1"].hand)
await engine.draw_card(game, "p1")
assert len(game.players["p1"].deck) == initial_deck_size - 1
assert len(game.players["p1"].hand) == initial_hand_size + 1
```
## Directory-Specific Guidelines
See additional CLAUDE.md files in subdirectories:
- `frontend/CLAUDE.md` - Vue/Phaser specifics
- `backend/CLAUDE.md` - FastAPI/game engine specifics
- `backend/app/core/CLAUDE.md` - Game engine architecture
## Critical Rules
1. **Git**: Never commit directly to `main`. Create feature branches.
2. **Hidden Info**: Never send deck contents, opponent hand, or unrevealed prizes to client.
3. **Validation**: Always validate actions server-side. Never trust client.
4. **Tests**: Include docstrings explaining "what" and "why" for each test.
5. **Commits**: Do not commit without user approval.
6. **Phaser in Vue**: Keep Phaser scenes focused on rendering. Game logic lives in backend.

263
PROJECT_PLAN.md Normal file
View File

@ -0,0 +1,263 @@
# Mantimon TCG - Project Plan
A home-rule-modified Pokemon Trading Card Game implementation as a web application with single-player and multiplayer gameplay.
## Project Overview
| Attribute | Value |
|-----------|-------|
| **Project Name** | Mantimon TCG (placeholder) |
| **Frontend** | Vue 3 + Phaser 3 (hybrid architecture) |
| **Backend** | FastAPI + PostgreSQL + Socket.io |
| **Target Audience** | 10-100 concurrent users |
| **Timeline** | Hobby project, no deadline |
| **Card Acquisition** | Packs (gacha) + direct crafting |
| **AI Opponents** | Multiple difficulty tiers (Easy/Medium/Hard) |
| **Rule Basis** | Custom hybrid (modified energy, deck building, win conditions) |
## Goals
1. **Faithful TCG Experience**: Card-based gameplay with strategic depth
2. **Visual Polish**: Animated card interactions, shuffle effects, pack opening experience
3. **Flexible Rules Engine**: Support home-rule modifications without code changes
4. **Single & Multiplayer**: AI opponents, puzzles, and online PvP
5. **Collection System**: Pack opening, crafting, deck building
---
## High-Level Architecture
```
+----------------------------------------------------------------------+
| Vue 3 / Nuxt App |
+----------------------------------------------------------------------+
| Pages (Vue/DOM-based) | Game View (Phaser Canvas) |
| +----------------------------+ | +----------------------------+ |
| | - Login/Register | | | - Card rendering | |
| | - Collection browser | | | - Hand management | |
| | - Deck builder | | | - Board zones (bench/active)| |
| | - Pack opening (Phaser) | | | - Drag-and-drop play | |
| | - Lobby/matchmaking | | | - Attack animations | |
| | - Profile/stats | | | - Shuffle/draw animations | |
| | - Puzzle select | | | - Damage counters | |
| +----------------------------+ | +----------------------------+ |
+----------------------------------------------------------------------+
| Pinia State Management |
| - auth store, collection store, deck store, game store |
+----------------------------------------------------------------------+
| Socket.io Client + REST API |
+----------------------------------------------------------------------+
|
WebSocket + HTTP
|
+----------------------------------------------------------------------+
| FastAPI Backend |
+----------------------------------------------------------------------+
| REST Endpoints | WebSocket Handlers |
| - /auth/* | - game:action (play card, attack, etc) |
| - /cards/* (card data) | - game:state (sync) |
| - /collection/* | - matchmaking:* |
| - /decks/* | - lobby:* |
| - /packs/* (open packs) | |
| - /puzzles/* | |
+----------------------------------------------------------------------+
| Core Game Engine |
| +----------------------------------------------------------------+ |
| | - Turn state machine (Draw -> Main -> Attack -> End) | |
| | - Card effect resolver | |
| | - Rule validator (home rules configuration) | |
| | - Win condition checker | |
| | - AI decision engine (Easy/Medium/Hard) | |
| | - Puzzle scenario loader | |
| +----------------------------------------------------------------+ |
+----------------------------------------------------------------------+
| PostgreSQL | Redis |
| - Users/auth | - Active game state |
| - Card definitions | - Session cache |
| - Collections | - Matchmaking queue |
| - Decks | |
| - Match history | |
| - Puzzle definitions | |
+----------------------------------------------------------------------+
```
---
## Development Phases
### Phase 1: Foundation (Core Gameplay)
**Goal**: Get a single match working end-to-end.
| Task | Description |
|------|-------------|
| Card data model | Define schema for cards (attacks, HP, type, energy costs, effects) |
| Game state model | Zones: deck, hand, active, bench, prizes, discard |
| Turn state machine | Draw -> Attach energy -> Play cards -> Attack -> End |
| Basic rule engine | Energy attachment, retreat, attack resolution, knockouts |
| Phaser game scene | Render board, cards in hand, drag-to-play |
| Local 2-player | Hot-seat mode for testing |
**Milestone**: Two humans can play a basic match with a small card set (10-20 cards)
---
### Phase 2: Multiplayer Infrastructure
**Goal**: Connect two remote players.
| Task | Description |
|------|-------------|
| WebSocket game sync | Server-authoritative state, hidden info filtering |
| Matchmaking (basic) | Queue system, pair two players |
| Turn timer | Optional time limits per turn |
| Reconnection | Rejoin in-progress games |
| Lobby UI | See available matches, challenge friends |
**Milestone**: Two players can match online and complete a game
---
### Phase 3: Collection & Deck Building
**Goal**: Enable the constructed format.
| Task | Description |
|------|-------------|
| User collection model | Track which cards each user owns (with quantities) |
| Deck builder UI | Vue-based, filter/search, drag to deck |
| Deck validation | Enforce home rules (card limits, restrictions) |
| Starter collection | New accounts get basic cards |
| Card browser | View all cards in the game |
**Milestone**: Players can build decks from their collection and use them in matches
---
### Phase 4: Card Acquisition
**Goal**: Packs and crafting system.
| Task | Description |
|------|-------------|
| Pack definitions | Which cards in which packs, rarity weights |
| Pack opening (backend) | Secure RNG, award cards |
| Pack opening (UI) | Animated Phaser scene with card reveals |
| Currency system | Earned currency from wins/quests |
| Crafting/dust | Disenchant duplicates, craft specific cards |
**Milestone**: Players can earn/open packs and craft cards
---
### Phase 5: AI Opponents
**Goal**: Single-player viability.
| Task | Description |
|------|-------------|
| AI framework | Pluggable AI strategy interface |
| Easy AI | Random legal moves |
| Medium AI | Heuristic-based (prioritize knockouts, energy efficiency) |
| Hard AI | Minimax or MCTS with pruning |
| AI deck selection | Pre-built decks per difficulty |
**Milestone**: Players can practice against AI at selectable difficulty
---
### Phase 6: Puzzle Mode
**Goal**: Challenge scenarios.
| Task | Description |
|------|-------------|
| Puzzle data model | Fixed board state, win condition, move limit |
| Puzzle loader | Set up exact game state |
| Puzzle validation | Check if solution is correct |
| Puzzle editor (optional) | Create puzzles in-app |
| Puzzle progression | Unlock sequences, categories |
**Milestone**: Players can solve puzzles like "Win this turn"
---
### Phase 7: Polish & Extended Features
| Task | Description |
|------|-------------|
| Match history | Review past games |
| Player stats | Win rate, favorite decks, etc. |
| Seasonal rewards | Ranked ladders, reward tracks |
| Card effects system | Support complex abilities (abilities, trainers, etc.) |
| Sound effects | Audio feedback for actions |
| Mobile optimization | Touch controls, responsive layout |
---
## Key Technical Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Vue + Phaser integration | Mount Phaser as Vue component | Keep forms/menus in Vue, canvas gameplay in Phaser |
| Pack opening | Phaser scene | Allows for animated card reveals with pizazz |
| Deck builder | Vue-based | Form-heavy UI, filtering, better accessibility |
| Card effect system | Data-driven with scripted effects | JSON defines cards, effect IDs map to handler functions |
| AI architecture | Strategy pattern per difficulty | Easy to add new AI types, same interface |
| Hidden info | Server never sends opponent's hand/deck | Prevents cheating even if client is compromised |
| State sync | Full state on reconnect, deltas during play | Balance bandwidth vs. complexity |
---
## Complexity Estimates
| Component | Effort | Notes |
|-----------|--------|-------|
| Phaser game scene | High | Most time-intensive frontend work |
| Card effect system | High | Complex cards need flexible effect resolution |
| AI (Hard difficulty) | High | MCTS/minimax is non-trivial |
| Turn state machine | Medium | Similar patterns to other game projects |
| Collection/decks | Medium | CRUD with validation |
| Pack opening animation | Medium | Fun Phaser work, moderate complexity |
| Puzzles | Low-Medium | Subset of game engine, fixed states |
---
## Directory Structure
```
mantimon-tcg/
├── PROJECT_PLAN.md # This file
├── CLAUDE.md # AI agent guidelines
├── docs/
│ ├── ARCHITECTURE.md # Technical deep-dive
│ └── GAME_RULES.md # Home rule documentation
├── frontend/ # Vue 3 + Phaser 3 application
│ ├── src/
│ │ ├── components/ # Vue components
│ │ ├── pages/ # Route pages
│ │ ├── stores/ # Pinia stores
│ │ ├── game/ # Phaser scenes and game objects
│ │ ├── composables/ # Vue composables (useWebSocket, etc.)
│ │ └── assets/ # Images, sounds
│ └── package.json
├── backend/ # FastAPI application
│ ├── app/
│ │ ├── api/ # REST endpoints
│ │ ├── core/ # Game engine
│ │ ├── models/ # Pydantic + SQLAlchemy models
│ │ ├── services/ # Business logic
│ │ └── websocket/ # Socket.io handlers
│ └── pyproject.toml
└── shared/ # Shared types/schemas (if needed)
```
---
## Open Questions / Future Decisions
- [ ] Authentication method (Discord OAuth like baseball app? Email/password? Both?)
- [ ] Card art storage (local assets vs. CDN vs. external URLs?)
- [ ] Mobile app (PWA sufficient? Or native wrapper later?)
- [ ] Monetization (if any - cosmetics, premium packs, etc.)
- [ ] Tournament/event system for organized play

822
docs/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,822 @@
# Mantimon TCG - Technical Architecture
This document provides a detailed technical overview of the Mantimon TCG architecture.
## Table of Contents
1. [System Overview](#system-overview)
2. [Frontend Architecture](#frontend-architecture)
3. [Backend Architecture](#backend-architecture)
4. [Database Schema](#database-schema)
5. [Real-Time Communication](#real-time-communication)
6. [Game Engine](#game-engine)
7. [AI System](#ai-system)
8. [Security Considerations](#security-considerations)
---
## System Overview
```
+------------------+ +------------------+ +------------------+
| Browser | | Browser | | Browser |
| (Vue + Phaser) | | (Vue + Phaser) | | (Vue + Phaser) |
+--------+---------+ +--------+---------+ +--------+---------+
| | |
+------------------------+------------------------+
|
WebSocket + HTTP
|
+-------------+-------------+
| |
+--------v--------+ +--------v--------+
| FastAPI | | FastAPI |
| Instance 1 | | Instance 2 |
+--------+--------+ +--------+--------+
| |
+-------------+-------------+
|
+-------------------+-------------------+
| | |
+-------v-------+ +-------v-------+ +------v------+
| PostgreSQL | | Redis | | S3/CDN |
| (persistent) | | (realtime) | | (card art) |
+---------------+ +---------------+ +-------------+
```
---
## Frontend Architecture
### Technology Stack
- **Vue 3** with Composition API and `<script setup>` syntax
- **Phaser 3** for canvas-based game rendering
- **TypeScript** for type safety
- **Pinia** for state management
- **Tailwind CSS** for styling
- **Vite** for development and building
### Component Hierarchy
```
App.vue
├── layouts/
│ ├── DefaultLayout.vue # Nav, footer for non-game pages
│ └── GameLayout.vue # Minimal chrome for gameplay
├── pages/
│ ├── index.vue # Landing/home
│ ├── login.vue # Authentication
│ ├── collection.vue # Card collection browser
│ ├── decks/
│ │ ├── index.vue # Deck list
│ │ └── [id].vue # Deck builder/editor
│ ├── play/
│ │ ├── index.vue # Play menu (AI, PvP, Puzzle)
│ │ ├── match/[id].vue # Active game (mounts Phaser)
│ │ └── puzzle/[id].vue # Puzzle mode
│ ├── packs.vue # Pack opening (mounts Phaser)
│ └── profile.vue # User stats and settings
└── components/
├── cards/
│ ├── CardDisplay.vue # Single card render (DOM)
│ ├── CardGrid.vue # Grid of cards
│ └── CardDetail.vue # Full card modal
├── decks/
│ ├── DeckList.vue # List of user's decks
│ └── DeckBuilder.vue # Deck editing UI
├── game/
│ └── PhaserContainer.vue # Mounts Phaser canvas
└── common/
├── Modal.vue
├── Button.vue
└── Loading.vue
```
### Vue + Phaser Integration
Phaser is mounted within a Vue component and communicates via an event bridge:
```typescript
// components/game/PhaserContainer.vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import Phaser from 'phaser'
import { MatchScene } from '@/game/scenes/MatchScene'
import { useGameStore } from '@/stores/game'
const props = defineProps<{
gameId: string
}>()
const gameContainer = ref<HTMLDivElement>()
const phaserGame = ref<Phaser.Game>()
const gameStore = useGameStore()
onMounted(() => {
phaserGame.value = new Phaser.Game({
type: Phaser.AUTO,
parent: gameContainer.value,
width: 1280,
height: 720,
scene: [MatchScene],
backgroundColor: '#1a1a2e',
})
// Pass initial state to Phaser
phaserGame.value.registry.set('gameId', props.gameId)
// Bridge: Vue -> Phaser
gameStore.$onAction(({ name, args }) => {
if (name === 'setGameState') {
phaserGame.value?.events.emit('state:update', args[0])
}
})
// Bridge: Phaser -> Vue
phaserGame.value.events.on('player:action', (action) => {
gameStore.sendAction(action)
})
})
onUnmounted(() => {
phaserGame.value?.destroy(true)
})
</script>
<template>
<div ref="gameContainer" class="w-full h-full" />
</template>
```
### Phaser Scene Structure
```
game/
├── scenes/
│ ├── MatchScene.ts # Main gameplay
│ ├── PackOpeningScene.ts # Animated pack reveals
│ └── PuzzleScene.ts # Puzzle mode
├── objects/
│ ├── Card.ts # Card game object
│ ├── Hand.ts # Hand container
│ ├── Battlefield.ts # Active/bench zones
│ ├── Deck.ts # Deck pile with animations
│ └── DamageCounter.ts # Damage display
├── animations/
│ ├── cardAnimations.ts # Draw, play, flip
│ ├── attackAnimations.ts # Attack effects
│ └── shuffleAnimations.ts # Deck shuffle
└── utils/
└── tweenHelpers.ts # Common tween patterns
```
### State Management (Pinia)
```typescript
// stores/game.ts
export const useGameStore = defineStore('game', () => {
// State
const gameState = ref<GameState | null>(null)
const pendingAction = ref<Action | null>(null)
const socket = ref<Socket | null>(null)
// Getters
const myHand = computed(() => gameState.value?.myHand ?? [])
const isMyTurn = computed(() =>
gameState.value?.currentPlayer === gameState.value?.myPlayerId
)
const canPlayCard = computed(() =>
isMyTurn.value && gameState.value?.phase === 'main'
)
// Actions
async function connectToGame(gameId: string) {
socket.value = io('/game', { query: { gameId } })
socket.value.on('game:state', (state) => {
gameState.value = state
})
socket.value.on('game:error', (error) => {
// Handle error
})
}
function sendAction(action: Action) {
socket.value?.emit('game:action', action)
}
return {
gameState,
myHand,
isMyTurn,
canPlayCard,
connectToGame,
sendAction,
}
})
```
---
## Backend Architecture
### Technology Stack
- **FastAPI** for REST API and WebSocket handling
- **Python 3.11+** with async/await
- **SQLAlchemy 2.0** (async) for ORM
- **PostgreSQL** for persistent storage
- **Redis** for caching and real-time state
- **python-socketio** for WebSocket server
- **Pydantic v2** for validation
### Module Structure
```
backend/
├── app/
│ ├── main.py # FastAPI app, Socket.io setup
│ ├── config.py # Settings (pydantic-settings)
│ ├── dependencies.py # Dependency injection
│ │
│ ├── api/ # REST endpoints
│ │ ├── auth.py # Login, register, OAuth
│ │ ├── cards.py # Card definitions
│ │ ├── collection.py # User collection CRUD
│ │ ├── decks.py # Deck CRUD
│ │ ├── packs.py # Pack opening
│ │ ├── matchmaking.py # Queue management
│ │ └── puzzles.py # Puzzle definitions
│ │
│ ├── websocket/ # Real-time handlers
│ │ ├── handlers.py # Socket.io event handlers
│ │ ├── connection_manager.py
│ │ └── events.py # Event type definitions
│ │
│ ├── core/ # Game engine
│ │ ├── game_engine.py # Main orchestrator
│ │ ├── turn_manager.py # Turn/phase state machine
│ │ ├── card_effects.py # Effect resolution
│ │ ├── rules_validator.py # Action legality
│ │ ├── win_conditions.py # Win/loss detection
│ │ ├── state_manager.py # In-memory game state
│ │ └── ai/ # AI opponents
│ │ ├── base.py # AI interface
│ │ ├── easy.py # Random legal moves
│ │ ├── medium.py # Heuristic-based
│ │ └── hard.py # MCTS/minimax
│ │
│ ├── models/ # Data models
│ │ ├── database.py # SQLAlchemy models
│ │ └── schemas.py # Pydantic schemas
│ │
│ ├── services/ # Business logic
│ │ ├── auth_service.py
│ │ ├── card_service.py
│ │ ├── collection_service.py
│ │ ├── deck_service.py
│ │ ├── pack_service.py
│ │ ├── game_service.py
│ │ └── puzzle_service.py
│ │
│ └── database/ # DB utilities
│ ├── session.py # Async session factory
│ └── migrations/ # Alembic migrations
├── tests/
│ ├── test_game_engine.py
│ ├── test_card_effects.py
│ ├── test_ai.py
│ └── conftest.py
├── pyproject.toml
└── alembic.ini
```
### Request Flow
```
HTTP Request
v
FastAPI Router ─────────────────────────────┐
│ │
v v
Dependency Injection WebSocket Connection
(auth, db session) │
│ v
v Socket.io Handler
Service Layer │
│ v
v Game Engine
Repository/ORM (in-memory state)
│ │
v v
PostgreSQL Redis
(state cache)
```
---
## Database Schema
### Entity Relationship Diagram
```
+---------------+ +------------------+ +---------------+
| users | | collections | | cards |
+---------------+ +------------------+ +---------------+
| id (PK) |<──────| user_id (FK) | | id (PK) |
| username | | card_id (FK) |───────| name |
| email | | quantity | | set_id |
| password_hash | +------------------+ | hp |
| currency | | type |
| created_at | | attacks (JSON)|
+---------------+ | abilities |
│ | rarity |
│ +---------------+
│ +------------------+
│ | decks |
│ +------------------+
└────────>| id (PK) |
| user_id (FK) |
| name |
| cards (JSON) |
| is_valid |
| created_at |
+------------------+
+------------------+ +------------------+
| matches | | match_players |
+------------------+ +------------------+
| id (PK) |<──────| match_id (FK) |
| status | | user_id (FK) |
| winner_id (FK) | | deck_id (FK) |
| started_at | | result |
| ended_at | +------------------+
| game_log (JSON) |
+------------------+
+------------------+ +------------------+
| packs | | pack_contents |
+------------------+ +------------------+
| id (PK) |<──────| pack_id (FK) |
| name | | card_id (FK) |
| cost | | rarity |
| set_id | | weight |
+------------------+ +------------------+
+------------------+
| puzzles |
+------------------+
| id (PK) |
| name |
| description |
| difficulty |
| initial_state |
| solution |
| hints (JSON) |
+------------------+
```
### Key Models
```python
# models/database.py
from sqlalchemy import Column, Integer, String, JSON, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from app.database.session import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
email = Column(String(255), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
currency = Column(Integer, default=0)
created_at = Column(DateTime, server_default=func.now())
collections = relationship("Collection", back_populates="user")
decks = relationship("Deck", back_populates="user")
class Card(Base):
__tablename__ = "cards"
id = Column(String(50), primary_key=True) # e.g., "pikachu_base_001"
name = Column(String(100), nullable=False)
set_id = Column(String(50), nullable=False)
card_type = Column(String(20), nullable=False) # pokemon, trainer, energy
hp = Column(Integer, nullable=True) # Only for Pokemon
pokemon_type = Column(String(20), nullable=True) # fire, water, etc.
attacks = Column(JSON, default=[])
abilities = Column(JSON, default=[])
weakness = Column(JSON, nullable=True)
resistance = Column(JSON, nullable=True)
retreat_cost = Column(Integer, default=0)
rarity = Column(String(20), nullable=False)
image_url = Column(String(500), nullable=True)
class Deck(Base):
__tablename__ = "decks"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
name = Column(String(100), nullable=False)
cards = Column(JSON, nullable=False) # [{"card_id": "...", "quantity": 2}, ...]
is_valid = Column(Boolean, default=False)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, onupdate=func.now())
user = relationship("User", back_populates="decks")
```
---
## Real-Time Communication
### Socket.io Events
#### Client -> Server
| Event | Payload | Description |
|-------|---------|-------------|
| `game:action` | `{ type, ...params }` | Player performs action |
| `game:resign` | `{}` | Player resigns match |
| `matchmaking:join` | `{ deckId }` | Join matchmaking queue |
| `matchmaking:leave` | `{}` | Leave queue |
#### Server -> Client
| Event | Payload | Description |
|-------|---------|-------------|
| `game:state` | `VisibleGameState` | Full state update |
| `game:action_result` | `{ success, error? }` | Action confirmation |
| `game:ended` | `{ winner, reason }` | Match concluded |
| `matchmaking:found` | `{ gameId, opponent }` | Match found |
### Action Types
```python
class ActionType(str, Enum):
PLAY_POKEMON = "play_pokemon" # Play basic Pokemon to bench
EVOLVE = "evolve" # Evolve a Pokemon
ATTACH_ENERGY = "attach_energy" # Attach energy card
PLAY_TRAINER = "play_trainer" # Play trainer card
USE_ABILITY = "use_ability" # Activate Pokemon ability
ATTACK = "attack" # Declare attack
RETREAT = "retreat" # Retreat active Pokemon
PASS = "pass" # End turn / pass priority
```
### State Synchronization
```python
# websocket/handlers.py
@sio.on('game:action')
async def handle_action(sid, data: dict):
session = await get_session(sid)
game_id = session['game_id']
player_id = session['player_id']
game = await state_manager.get_game(game_id)
# Validate
result = await game_engine.validate_action(game, player_id, data)
if not result.valid:
await sio.emit('game:action_result', {
'success': False,
'error': result.reason
}, to=sid)
return
# Execute
new_state = await game_engine.execute_action(game, player_id, data)
await state_manager.save_game(game_id, new_state)
# Broadcast filtered state to each player
for pid, psid in game.player_sockets.items():
visible_state = game_engine.get_visible_state(new_state, pid)
await sio.emit('game:state', visible_state, to=psid)
# Check win conditions
winner = game_engine.check_win_condition(new_state)
if winner:
await handle_game_end(game_id, winner)
```
---
## Game Engine
### Turn Structure
```
GAME START
v
┌─────────────────────────────────┐
│ SETUP PHASE │
│ - Draw 7 cards each │
│ - Place active + bench │
│ - Set aside 6 prize cards │
│ - Flip coin for first turn │
└─────────────────────────────────┘
v
┌─────────────────────────────────┐
│ TURN START │◄──────────────────┐
│ - Draw 1 card │ │
└─────────────────────────────────┘ │
│ │
v │
┌─────────────────────────────────┐ │
│ MAIN PHASE (any order) │ │
│ - Attach 1 energy (once) │ │
│ - Play basic Pokemon to bench │ │
│ - Evolve Pokemon │ │
│ - Play trainer cards │ │
│ - Use abilities │ │
│ - Retreat active (once) │ │
└─────────────────────────────────┘ │
│ │
v │
┌─────────────────────────────────┐ │
│ ATTACK PHASE (optional) │ │
│ - Declare attack │ │
│ - Resolve damage + effects │ │
│ - Check knockouts │ │
│ - Take prize cards │ │
└─────────────────────────────────┘ │
│ │
v │
┌─────────────────────────────────┐ │
│ END PHASE │ │
│ - Apply end-of-turn effects │ │
│ - Switch to opponent │───────────────────┘
└─────────────────────────────────┘
v
┌─────────────────────────────────┐
│ WIN CONDITION CHECK │
│ - All prizes taken? │
│ - Opponent has no Pokemon? │
│ - Opponent can't draw? │
│ - Custom home rules? │
└─────────────────────────────────┘
```
### Card Effect System
Effects are data-driven with pluggable handlers:
```python
# core/card_effects.py
from typing import Protocol, Callable, Dict, Any
class EffectContext:
game: GameState
source_card: Card
source_player: str
target_card: Optional[Card]
target_player: Optional[str]
params: Dict[str, Any]
EffectHandler = Callable[[EffectContext], Awaitable[None]]
EFFECT_REGISTRY: Dict[str, EffectHandler] = {}
def effect_handler(name: str):
"""Decorator to register effect handlers."""
def decorator(func: EffectHandler):
EFFECT_REGISTRY[name] = func
return func
return decorator
# Example effects
@effect_handler("deal_damage")
async def deal_damage(ctx: EffectContext):
amount = ctx.params["amount"]
ctx.target_card.damage += amount
@effect_handler("heal")
async def heal(ctx: EffectContext):
amount = ctx.params["amount"]
ctx.target_card.damage = max(0, ctx.target_card.damage - amount)
@effect_handler("apply_status")
async def apply_status(ctx: EffectContext):
status = ctx.params["status"] # paralyzed, poisoned, asleep, etc.
ctx.target_card.status_conditions.append(status)
@effect_handler("draw_cards")
async def draw_cards(ctx: EffectContext):
count = ctx.params["count"]
player = ctx.game.players[ctx.source_player]
for _ in range(count):
if player.deck:
player.hand.append(player.deck.pop())
@effect_handler("coin_flip")
async def coin_flip(ctx: EffectContext):
"""Flip coin, execute sub-effect on heads."""
result = random.choice(["heads", "tails"])
if result == "heads" and "on_heads" in ctx.params:
sub_effect = ctx.params["on_heads"]
await resolve_effect(ctx.game, sub_effect, ctx)
```
---
## AI System
### Architecture
```python
# core/ai/base.py
from abc import ABC, abstractmethod
class AIPlayer(ABC):
"""Base class for AI opponents."""
@abstractmethod
async def choose_action(self, game_state: GameState) -> Action:
"""Given the current game state, return the best action."""
pass
def get_legal_actions(self, game_state: GameState) -> List[Action]:
"""Returns all legal actions for the AI player."""
actions = []
player = game_state.players[self.player_id]
# Can attach energy?
if not game_state.energy_attached_this_turn:
for card in player.hand:
if card.card_type == "energy":
for target in self.get_energy_targets(game_state):
actions.append(AttachEnergyAction(card.id, target.id))
# Can play Pokemon?
for card in player.hand:
if card.card_type == "pokemon" and card.stage == "basic":
if len(player.bench) < 5:
actions.append(PlayPokemonAction(card.id))
# ... more action types
actions.append(PassAction()) # Can always pass
return actions
```
### Difficulty Implementations
```python
# core/ai/easy.py
class EasyAI(AIPlayer):
"""Random legal moves with basic priorities."""
async def choose_action(self, game_state: GameState) -> Action:
actions = self.get_legal_actions(game_state)
# Slight preference: attack if possible
attacks = [a for a in actions if isinstance(a, AttackAction)]
if attacks and random.random() > 0.3:
return random.choice(attacks)
return random.choice(actions)
# core/ai/medium.py
class MediumAI(AIPlayer):
"""Heuristic-based decision making."""
async def choose_action(self, game_state: GameState) -> Action:
actions = self.get_legal_actions(game_state)
# Score each action
scored = [(self.score_action(game_state, a), a) for a in actions]
scored.sort(reverse=True, key=lambda x: x[0])
# Pick top action with some randomness
top_actions = [a for s, a in scored[:3]]
return random.choice(top_actions)
def score_action(self, state: GameState, action: Action) -> float:
score = 0.0
if isinstance(action, AttackAction):
# Prefer knockouts
damage = self.calculate_damage(state, action)
target = state.get_defending_pokemon()
if damage >= target.hp - target.damage:
score += 100 # Knockout!
else:
score += damage * 0.5
elif isinstance(action, PlayPokemonAction):
# Value bench presence
score += 10
# ... more heuristics
return score
# core/ai/hard.py
class HardAI(AIPlayer):
"""Monte Carlo Tree Search for strong play."""
def __init__(self, player_id: str, simulations: int = 1000):
super().__init__(player_id)
self.simulations = simulations
async def choose_action(self, game_state: GameState) -> Action:
root = MCTSNode(game_state, None, self.player_id)
for _ in range(self.simulations):
node = self.select(root)
node = self.expand(node)
result = self.simulate(node)
self.backpropagate(node, result)
# Return most visited child's action
best_child = max(root.children, key=lambda c: c.visits)
return best_child.action
```
---
## Security Considerations
### Hidden Information
**Never expose to clients:**
- Deck order (either player)
- Opponent's hand contents
- Unrevealed prize cards
- RNG seeds or future random results
```python
def get_visible_state(game: GameState, player_id: str) -> VisibleGameState:
"""Filter game state to only what player can see."""
opponent_id = get_opponent(game, player_id)
return VisibleGameState(
# My full information
my_hand=[card.to_dict() for card in game.players[player_id].hand],
my_deck_count=len(game.players[player_id].deck),
my_prizes=[
card.to_dict() if card.revealed else {"hidden": True}
for card in game.players[player_id].prizes
],
# Opponent's hidden information
opponent_hand_count=len(game.players[opponent_id].hand),
opponent_deck_count=len(game.players[opponent_id].deck),
opponent_prizes_remaining=len([
p for p in game.players[opponent_id].prizes if not p.taken
]),
# Public information
battlefield=game.battlefield.to_dict(),
discard_piles={
pid: [card.to_dict() for card in player.discard]
for pid, player in game.players.items()
},
current_turn=game.current_turn,
phase=game.phase,
)
```
### Server Authority
- All game logic runs server-side
- Client sends intentions, server validates and executes
- Never trust client-provided game state
- RNG uses `secrets` module for unpredictability
### Input Validation
```python
async def validate_action(game: GameState, player_id: str, action: dict) -> ValidationResult:
# Is it this player's turn?
if game.current_turn != player_id:
return ValidationResult(False, "Not your turn")
# Is this action type valid for current phase?
action_type = action.get("type")
if action_type not in VALID_ACTIONS_BY_PHASE[game.phase]:
return ValidationResult(False, f"Cannot {action_type} during {game.phase}")
# Validate specific action requirements
validator = ACTION_VALIDATORS.get(action_type)
if validator:
return await validator(game, player_id, action)
return ValidationResult(False, "Unknown action type")
```

324
docs/GAME_RULES.md Normal file
View File

@ -0,0 +1,324 @@
# Mantimon TCG - Game Rules
This document defines the home-rule modifications for Mantimon TCG, based on a custom hybrid of Pokemon TCG eras.
> **Note**: This is a template. Sections marked with `[TBD]` need to be filled in with your specific rule choices.
## Table of Contents
1. [Base Ruleset](#base-ruleset)
2. [Energy System Modifications](#energy-system-modifications)
3. [Deck Building Rules](#deck-building-rules)
4. [Win Conditions](#win-conditions)
5. [Turn Structure](#turn-structure)
6. [Card Types](#card-types)
7. [Status Conditions](#status-conditions)
8. [Glossary](#glossary)
---
## Base Ruleset
Mantimon TCG uses a **custom hybrid** ruleset, cherry-picking mechanics from multiple Pokemon TCG eras.
### Era Influences
| Mechanic | Era Source | Notes |
|----------|------------|-------|
| Basic turn structure | Classic (Base-Neo) | [TBD] |
| Pokemon stages | [TBD] | Basic, Stage 1, Stage 2, and/or EX/V variants? |
| Trainer card types | [TBD] | Item/Supporter/Stadium split, or classic Trainer? |
| Energy types | [TBD] | How many types? Special energy? |
### What's NOT Included
List of official mechanics that are explicitly excluded:
- [TBD] - e.g., "No VMAX or Gigantamax mechanics"
- [TBD]
- [TBD]
---
## Energy System Modifications
> **Home Rule Focus Area**: Energy attachment rules
### Standard Rule (for reference)
In standard Pokemon TCG, you may attach **one energy card** per turn from your hand to one of your Pokemon.
### Modified Rule
[TBD] - Describe your energy system changes here. Examples of possible modifications:
- [ ] **Multiple attachments**: Attach up to X energy per turn
- [ ] **Type-free energy**: All basic energy counts as colorless
- [ ] **Energy acceleration**: Specific cards/abilities that attach extra energy
- [ ] **Energy generation**: Start of turn gain energy tokens instead of cards
- [ ] **Shared energy pool**: Energy attached to a Pokemon can be used by bench
- [ ] **No energy cards**: Attacks cost action points instead
### Energy Types in Use
| Type | Symbol | Description |
|------|--------|-------------|
| [TBD] | | |
| [TBD] | | |
| Colorless | | Any energy satisfies colorless requirements |
### Special Energy
| Name | Effect |
|------|--------|
| [TBD] | |
| [TBD] | |
---
## Deck Building Rules
> **Home Rule Focus Area**: Card limits and restrictions
### Deck Size
| Rule | Value |
|------|-------|
| Minimum deck size | [TBD] cards |
| Maximum deck size | [TBD] cards |
| Exact size required? | [TBD] Yes/No |
### Card Limits
| Card Type | Max Copies | Notes |
|-----------|------------|-------|
| Pokemon (same name) | [TBD] | |
| Trainer cards (same name) | [TBD] | |
| Energy cards (basic, same type) | [TBD] | |
| Energy cards (special, same name) | [TBD] | |
### Required Cards
| Requirement | Value |
|-------------|-------|
| Minimum Basic Pokemon | [TBD] |
| Minimum Energy cards | [TBD] |
| Other requirements | [TBD] |
### Banned/Restricted Cards
| Card Name | Status | Reason |
|-----------|--------|--------|
| [TBD] | Banned | |
| [TBD] | Limited to 1 | |
### Format Rotations
[TBD] - Which card sets are legal? Do sets rotate out?
| Set Name | Set Code | Status |
|----------|----------|--------|
| [TBD] | | Legal |
| [TBD] | | Banned |
---
## Win Conditions
> **Home Rule Focus Area**: Victory conditions
### Standard Win Conditions (for reference)
1. Take all 6 prize cards
2. Knock out opponent's last Pokemon in play
3. Opponent cannot draw at start of turn
### Modified Win Conditions
[TBD] - Describe your win condition changes. Examples:
- [ ] **Prize count change**: Take [X] prizes instead of 6
- [ ] **Alternative victory**: Win by [condition]
- [ ] **Removed condition**: [Which standard condition is removed?]
- [ ] **Sudden death**: [Special rules for tiebreakers]
### Prize Card Rules
| Rule | Value |
|------|-------|
| Number of prizes | [TBD] |
| Prizes taken per knockout | [TBD] - (Standard: 1 for basic, 2 for EX/V) |
| Prize card selection | [TBD] - Random or chosen? |
---
## Turn Structure
### Turn Overview
```
1. DRAW PHASE
- Draw 1 card from deck
2. MAIN PHASE (any order, as many times as allowed)
- Attach energy (once per turn) [TBD if modified]
- Play Basic Pokemon to bench
- Evolve Pokemon
- Play Trainer cards
- Use Abilities
- Retreat Active Pokemon (once per turn)
3. ATTACK PHASE
- Declare attack (ends turn)
- OR pass without attacking
4. END PHASE
- Apply end-of-turn effects
- Check for knockouts
- Take prize cards if applicable
```
### Turn Modifications
[TBD] - Any changes to the turn structure?
| Modification | Description |
|--------------|-------------|
| [TBD] | |
### First Turn Rules
| Rule | Value |
|------|-------|
| First player draws on turn 1? | [TBD] Yes/No |
| First player can attack turn 1? | [TBD] Yes/No |
| First player can play Supporters turn 1? | [TBD] Yes/No |
---
## Card Types
### Pokemon Cards
| Term | Definition |
|------|------------|
| Basic Pokemon | Can be played directly from hand to bench |
| Stage 1 | Evolves from a Basic Pokemon |
| Stage 2 | Evolves from a Stage 1 Pokemon |
| [TBD] | [Any special Pokemon types like EX, V, etc.?] |
### Evolution Rules
| Rule | Value |
|------|-------|
| Can evolve same turn played? | No (unless card effect allows) |
| Can evolve same turn as previous evolution? | [TBD] |
| Evolution on first turn of game? | [TBD] |
### Trainer Cards
[TBD] - Which trainer card subtypes are in use?
| Type | Rules |
|------|-------|
| Item | [TBD] - Unlimited per turn? |
| Supporter | [TBD] - One per turn? |
| Stadium | [TBD] - Stays in play? |
| Tool | [TBD] - Attach to Pokemon? |
### Energy Cards
| Type | Description |
|------|-------------|
| Basic Energy | Provides 1 energy of its type |
| Special Energy | [TBD] - Custom effects? |
---
## Status Conditions
### Active Status Conditions
| Condition | Effect | Removal |
|-----------|--------|---------|
| Poisoned | [TBD] damage between turns | [TBD] |
| Burned | [TBD] damage + flip to continue | [TBD] |
| Asleep | Cannot attack or retreat, flip to wake | [TBD] |
| Paralyzed | Cannot attack or retreat for 1 turn | End of next turn |
| Confused | Flip to attack, damage self on tails | [TBD] |
### Condition Stacking
[TBD] - Can multiple conditions be active? Which override others?
---
## Glossary
| Term | Definition |
|------|------------|
| Active Pokemon | The Pokemon currently in the battle position |
| Bench | Area for up to 5 Pokemon not currently active |
| Discard Pile | Where knocked out Pokemon and used cards go |
| Prize Cards | Cards set aside at game start, taken on knockouts |
| Retreat | Switching active Pokemon with a benched Pokemon |
| [TBD] | [Add game-specific terms] |
---
## Changelog
Track rule changes over time:
| Date | Change | Reason |
|------|--------|--------|
| [Date] | Initial rules document created | Project start |
| | | |
---
## Notes for Implementation
This section is for developer reference when implementing rules in code.
### Rule Configuration Schema
Rules should be configurable via a JSON/YAML file for easy iteration:
```json
{
"deck": {
"min_size": 60,
"max_size": 60,
"max_copies_per_card": 4,
"min_basic_pokemon": 1
},
"prizes": {
"count": 6,
"per_knockout_basic": 1,
"per_knockout_ex": 2
},
"energy": {
"attachments_per_turn": 1
},
"first_turn": {
"can_draw": true,
"can_attack": false,
"can_play_supporter": false
},
"win_conditions": {
"all_prizes_taken": true,
"no_pokemon_in_play": true,
"cannot_draw": true
}
}
```
### Validation Hooks
When implementing rules engine, create validation hooks for:
- `validate_deck(deck) -> ValidationResult`
- `validate_action(game_state, action) -> ValidationResult`
- `check_win_conditions(game_state) -> Optional[WinResult]`
- `calculate_prizes_for_knockout(pokemon) -> int`