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:
commit
f473f94bce
309
CLAUDE.md
Normal file
309
CLAUDE.md
Normal 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
263
PROJECT_PLAN.md
Normal 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
822
docs/ARCHITECTURE.md
Normal 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
324
docs/GAME_RULES.md
Normal 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`
|
||||
Loading…
Reference in New Issue
Block a user