CLAUDE: Initial project setup - documentation and infrastructure
Add comprehensive project documentation and Docker infrastructure for Paper Dynasty Real-Time Game Engine - a web-based multiplayer baseball simulation platform replacing the legacy Google Sheets system. Documentation Added: - Complete PRD (Product Requirements Document) - Project README with dual development workflows - Implementation guide with 5-phase roadmap - Architecture docs (backend, frontend, database, WebSocket) - CLAUDE.md context files for each major directory Infrastructure Added: - Root docker-compose.yml for full stack orchestration - Dockerfiles for backend and both frontends (multi-stage builds) - .dockerignore files for optimal build context - .env.example with all required configuration - Updated .gitignore for Python, Node, Nuxt, and Docker Project Structure: - backend/ - FastAPI + Socket.io game engine (Python 3.11+) - frontend-sba/ - SBA League Nuxt 3 frontend - frontend-pd/ - PD League Nuxt 3 frontend - .claude/implementation/ - Detailed implementation guides Supports two development workflows: 1. Local dev (recommended): Services run natively with hot-reload 2. Full Docker: One-command stack orchestration for testing/demos Next: Phase 1 implementation (backend/frontend foundations)
This commit is contained in:
parent
6eb4882d3f
commit
5c75b935f0
57
.claude/force-claude-reads.md
Normal file
57
.claude/force-claude-reads.md
Normal file
@ -0,0 +1,57 @@
|
||||
⚠️ **CRITICAL SYSTEM WORKAROUND - ALWAYS FOLLOW** ⚠️
|
||||
|
||||
# Force CLAUDE.md Context Loading
|
||||
|
||||
## 🚨 MANDATORY BEHAVIOR - NO EXCEPTIONS 🚨
|
||||
|
||||
**ALWAYS** when the user mentions ANY file with `@` syntax (e.g., `@path/to/example.md`), you **MUST IMMEDIATELY** call the Read tool on that same file path. This is not optional - it's a critical system requirement.
|
||||
|
||||
### ❌ NEVER DO THIS:
|
||||
- Respond to @ mentions without calling Read tool first
|
||||
- Skip the manual Read call because you "already have the content"
|
||||
- Assume @ mentions provide complete context
|
||||
|
||||
### ✅ ALWAYS DO THIS:
|
||||
1. User mentions `@path/to/example.js`
|
||||
2. **IMMEDIATELY** call `Read` tool with that exact path
|
||||
3. **THEN** respond to the user
|
||||
|
||||
## Why This Is Critical
|
||||
|
||||
- `@` mentions provide file content but **ZERO CLAUDE.md context**
|
||||
- Manual Read tool calls load **ALL** relevant CLAUDE.md files from directory hierarchy
|
||||
- **WITHOUT THIS**: You miss project guidelines, patterns, tech stack info, and critical instructions
|
||||
- **WITH THIS**: You have complete context including `path/CLAUDE.md`, `path/to/CLAUDE.md`, etc.
|
||||
|
||||
## STOP AND CHECK Before Every Response
|
||||
|
||||
When you see `@filename` in user message:
|
||||
|
||||
□ Have I called Read tool on this file path?
|
||||
□ Do I now have CLAUDE.md context loaded?
|
||||
□ Am I following project-specific guidelines?
|
||||
|
||||
If ANY answer is NO - call Read tool immediately.
|
||||
|
||||
## Detailed Example
|
||||
|
||||
User says: "Look at @path/to/example/file.ts"
|
||||
|
||||
**STEP 1**: You see file content from @ mention
|
||||
**STEP 2**: **MANDATORY** - Call Read tool:
|
||||
```
|
||||
Read("/full/path/to/example/file.ts")
|
||||
```
|
||||
**STEP 3**: System loads `path/CLAUDE.md` and `path/to/CLAUDE.md` context
|
||||
**STEP 4**: Now respond with full project context
|
||||
|
||||
## Failure Consequences
|
||||
|
||||
**If you ignore this instruction:**
|
||||
- You'll miss any further guidelines intended to be applied
|
||||
- You'll likely provide incorrect or incomplete solutions
|
||||
- You'll likely violate project coding standards
|
||||
|
||||
## Remember: This Fixes A Bug
|
||||
|
||||
This workaround exists because @ mentions have a system limitation. Following this instruction is not optional - it's fixing broken behavior.
|
||||
165
.claude/implementation/00-index.md
Normal file
165
.claude/implementation/00-index.md
Normal file
@ -0,0 +1,165 @@
|
||||
# Paper Dynasty Real-Time Game Engine - Implementation Guide
|
||||
|
||||
## Table of Contents
|
||||
|
||||
### Architecture & Design
|
||||
- [Backend Architecture](./backend-architecture.md) - FastAPI structure, game engine, state management
|
||||
- [Frontend Architecture](./frontend-architecture.md) - Vue/Nuxt structure, shared components, league-specific apps
|
||||
- [Database Design](./database-design.md) - Schema, indexes, operations, migration strategy
|
||||
- [WebSocket Protocol](./websocket-protocol.md) - Event specifications, connection lifecycle, error handling
|
||||
|
||||
### Implementation Phases
|
||||
|
||||
#### Phase 1: Core Infrastructure (Weeks 1-3)
|
||||
- [01 - Infrastructure Setup](./01-infrastructure.md)
|
||||
- Backend foundation (FastAPI, PostgreSQL, Socket.io)
|
||||
- Frontend foundation (Nuxt 3, TypeScript, Tailwind)
|
||||
- Discord OAuth integration
|
||||
- WebSocket connection management
|
||||
- Basic session management
|
||||
|
||||
#### Phase 2: Game Engine Core (Weeks 4-6)
|
||||
- [02 - Game Engine](./02-game-engine.md)
|
||||
- In-memory game state management
|
||||
- Play resolution engine
|
||||
- League configuration system
|
||||
- Polymorphic player models
|
||||
- Database persistence layer
|
||||
- State recovery mechanism
|
||||
|
||||
#### Phase 3: Complete Game Features (Weeks 7-9)
|
||||
- [03 - Gameplay Features](./03-gameplay-features.md)
|
||||
- All strategic decision types
|
||||
- Substitution system
|
||||
- Pitching changes
|
||||
- Complete result charts (both leagues)
|
||||
- AI opponent integration
|
||||
- Async game mode support
|
||||
|
||||
#### Phase 4: Spectator & Polish (Weeks 10-11)
|
||||
- [04 - Spectator & Polish](./04-spectator-polish.md)
|
||||
- Spectator mode implementation
|
||||
- UI/UX refinements
|
||||
- Dice roll animations
|
||||
- Mobile touch optimization
|
||||
- Accessibility improvements
|
||||
- Performance optimization
|
||||
|
||||
#### Phase 5: Testing & Launch (Weeks 12-13)
|
||||
- [05 - Testing & Launch](./05-testing-launch.md)
|
||||
- Comprehensive testing strategy
|
||||
- Load testing procedures
|
||||
- Security audit checklist
|
||||
- Deployment procedures
|
||||
- Monitoring setup
|
||||
- Launch plan
|
||||
|
||||
### Cross-Cutting Concerns
|
||||
- [Authentication & Authorization](./auth-system.md) - Discord OAuth, session management, role-based access
|
||||
- [Testing Strategy](./testing-strategy.md) - Unit, integration, e2e, load testing approaches
|
||||
- [Deployment Guide](./deployment-guide.md) - Infrastructure setup, CI/CD, monitoring
|
||||
- [API Reference](./api-reference.md) - REST endpoints, request/response formats
|
||||
|
||||
## Implementation Status
|
||||
|
||||
| Component | Status | Phase | Notes |
|
||||
|-----------|--------|-------|-------|
|
||||
| Backend Foundation | Not Started | 1 | - |
|
||||
| Frontend Foundation | Not Started | 1 | - |
|
||||
| Discord OAuth | Not Started | 1 | - |
|
||||
| WebSocket Server | Not Started | 1 | - |
|
||||
| Game Engine Core | Not Started | 2 | - |
|
||||
| Database Schema | Not Started | 2 | - |
|
||||
| Player Models | Not Started | 2 | - |
|
||||
| Strategic Decisions | Not Started | 3 | - |
|
||||
| Substitutions | Not Started | 3 | - |
|
||||
| AI Opponent | Not Started | 3 | - |
|
||||
| Spectator Mode | Not Started | 4 | - |
|
||||
| UI Polish | Not Started | 4 | - |
|
||||
| Testing Suite | Not Started | 5 | - |
|
||||
| Deployment | Not Started | 5 | - |
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Review Architecture Documents** - Understand the system design before coding
|
||||
2. **Follow Phase Order** - Each phase builds on the previous
|
||||
3. **Update Status** - Keep the status table current as work progresses
|
||||
4. **Reference PRD** - Main PRD at `/prd-web-scorecard-1.1.md` for detailed requirements
|
||||
|
||||
## Key Decisions & Rationale
|
||||
|
||||
### Why Hybrid State Management?
|
||||
- **In-Memory**: Fast action processing, sub-500ms latency requirements
|
||||
- **PostgreSQL**: Persistence, recovery, play history, async operations
|
||||
- **Best of Both**: Performance without sacrificing reliability
|
||||
|
||||
### Why Separate Frontends?
|
||||
- **League Branding**: Each league maintains distinct identity
|
||||
- **Independent Deployment**: Can update one league without affecting other
|
||||
- **Shared Components**: 80%+ code reuse through component library
|
||||
- **Flexibility**: League-specific features without codebase pollution
|
||||
|
||||
### Why FastAPI + Socket.io?
|
||||
- **FastAPI**: Modern async Python, automatic OpenAPI docs, Pydantic validation
|
||||
- **Socket.io**: Mature WebSocket library, automatic reconnection, room support
|
||||
- **Python Ecosystem**: Rich data processing libraries for game logic
|
||||
|
||||
### Why Polymorphic Players?
|
||||
- **Type Safety**: Each league's player structure validated at runtime
|
||||
- **Maintainability**: League differences isolated in player classes
|
||||
- **Extensibility**: Easy to add new leagues or modify existing ones
|
||||
|
||||
## Critical Path Items
|
||||
|
||||
### Must Have for MVP
|
||||
- ✅ Game creation and lobby
|
||||
- ✅ Complete turn-based gameplay
|
||||
- ✅ Real-time WebSocket updates
|
||||
- ✅ Game persistence and recovery
|
||||
- ✅ Mobile-optimized UI
|
||||
- ✅ Discord authentication
|
||||
|
||||
### Nice to Have (Post-MVP)
|
||||
- 🔲 Roster management
|
||||
- 🔲 Marketplace
|
||||
- 🔲 Tournament system
|
||||
- 🔲 Advanced analytics
|
||||
- 🔲 Discord bot notifications
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Start Phase** - Read phase markdown file thoroughly
|
||||
2. **Create CLAUDE.md** - Add subdirectory-specific context
|
||||
3. **Implement** - Follow TDD where appropriate
|
||||
4. **Test** - Unit tests as you go, integration tests per milestone
|
||||
5. **Update Status** - Mark components complete in index
|
||||
6. **Review** - Check against PRD requirements before moving on
|
||||
|
||||
## Performance Budgets
|
||||
|
||||
| Metric | Target | Critical Threshold |
|
||||
|--------|--------|-------------------|
|
||||
| Action Response | < 500ms | < 1000ms |
|
||||
| WebSocket Delivery | < 200ms | < 500ms |
|
||||
| DB Write (async) | < 100ms | < 250ms |
|
||||
| State Recovery | < 2s | < 5s |
|
||||
| Page Load (3G) | < 3s | < 5s |
|
||||
|
||||
## Questions & Decisions Log
|
||||
|
||||
Track important decisions and open questions here as implementation progresses.
|
||||
|
||||
### Open Questions
|
||||
- [ ] Which VPS provider for deployment?
|
||||
- [ ] Specific Discord OAuth scope requirements?
|
||||
- [ ] AI opponent complexity level for MVP?
|
||||
- [ ] Spectator chat feature in MVP or post-MVP?
|
||||
|
||||
### Decisions Made
|
||||
- **2025-10-21**: Project initialized, implementation guide structure created
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-10-21
|
||||
**Phase**: Pre-Implementation
|
||||
**Next Milestone**: Phase 1 - Core Infrastructure Setup
|
||||
848
.claude/implementation/01-infrastructure.md
Normal file
848
.claude/implementation/01-infrastructure.md
Normal file
@ -0,0 +1,848 @@
|
||||
# Phase 1: Core Infrastructure Setup
|
||||
|
||||
**Duration**: Weeks 1-3
|
||||
**Goal**: Establish foundation for both backend and frontend with authentication and real-time communication
|
||||
|
||||
## Objectives
|
||||
|
||||
By end of Phase 1, you should have:
|
||||
- ✅ FastAPI backend running with PostgreSQL
|
||||
- ✅ Socket.io WebSocket server functional
|
||||
- ✅ Discord OAuth working for both leagues
|
||||
- ✅ Basic Nuxt 3 frontends operational
|
||||
- ✅ WebSocket connections established between frontend/backend
|
||||
- ✅ Users can authenticate and see connection status
|
||||
|
||||
## Development Workflow Options
|
||||
|
||||
This project supports **two development workflows**:
|
||||
|
||||
### Option 1: Local Development (Recommended for Daily Work)
|
||||
- Backend runs locally with Python hot-reload
|
||||
- Frontends run locally with Nuxt hot-reload
|
||||
- Only Redis runs in Docker
|
||||
- **Advantages**: Fast, easy debugging, instant code changes
|
||||
|
||||
### Option 2: Full Docker Orchestration
|
||||
- Everything runs in containers
|
||||
- One-command startup with `docker-compose up`
|
||||
- **Advantages**: Production-like environment, easy sharing
|
||||
|
||||
**See the project [README.md](../../../README.md) for detailed instructions on both workflows.**
|
||||
|
||||
For this guide, we'll focus on **local development** setup. Docker configuration files are already provided:
|
||||
- Root `docker-compose.yml` - Full stack orchestration
|
||||
- `backend/Dockerfile` - Backend container
|
||||
- `frontend-sba/Dockerfile` & `frontend-pd/Dockerfile` - Frontend containers
|
||||
|
||||
---
|
||||
|
||||
## Backend Setup
|
||||
|
||||
### 1. Initialize FastAPI Project
|
||||
|
||||
```bash
|
||||
# Create backend directory
|
||||
mkdir -p backend/app/{core,config,models,websocket,api,database,data,utils}
|
||||
mkdir -p backend/tests/{unit,integration,e2e}
|
||||
cd backend
|
||||
|
||||
# Create virtual environment
|
||||
python3.11 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Create requirements.txt
|
||||
cat > requirements.txt << EOF
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
python-socketio==5.10.0
|
||||
python-multipart==0.0.6
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
sqlalchemy==2.0.23
|
||||
alembic==1.12.1
|
||||
asyncpg==0.29.0
|
||||
psycopg2-binary==2.9.9
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-dotenv==1.0.0
|
||||
httpx==0.25.1
|
||||
redis==5.0.1
|
||||
aiofiles==23.2.1
|
||||
EOF
|
||||
|
||||
# Development dependencies
|
||||
cat > requirements-dev.txt << EOF
|
||||
-r requirements.txt
|
||||
pytest==7.4.3
|
||||
pytest-asyncio==0.21.1
|
||||
pytest-cov==4.1.0
|
||||
black==23.11.0
|
||||
flake8==6.1.0
|
||||
mypy==1.7.1
|
||||
httpx==0.25.1
|
||||
EOF
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
### 2. Setup FastAPI Application
|
||||
|
||||
**File**: `backend/app/main.py`
|
||||
```python
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import socketio
|
||||
|
||||
from app.config import get_settings
|
||||
from app.api.routes import games, auth, health
|
||||
from app.websocket.connection_manager import ConnectionManager
|
||||
from app.websocket.handlers import register_handlers
|
||||
from app.database.session import init_db
|
||||
from app.utils.logging import setup_logging
|
||||
|
||||
logger = logging.getLogger(f'{__name__}.main')
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Startup and shutdown events"""
|
||||
# Startup
|
||||
logger.info("Starting Paper Dynasty Game Backend")
|
||||
setup_logging()
|
||||
await init_db()
|
||||
logger.info("Database initialized")
|
||||
yield
|
||||
# Shutdown
|
||||
logger.info("Shutting down Paper Dynasty Game Backend")
|
||||
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title="Paper Dynasty Game Backend",
|
||||
description="Real-time baseball game engine for Paper Dynasty leagues",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS middleware
|
||||
settings = get_settings()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Initialize Socket.io
|
||||
sio = socketio.AsyncServer(
|
||||
async_mode='asgi',
|
||||
cors_allowed_origins=settings.cors_origins,
|
||||
logger=True,
|
||||
engineio_logger=False
|
||||
)
|
||||
|
||||
# Create Socket.io ASGI app
|
||||
socket_app = socketio.ASGIApp(sio, app)
|
||||
|
||||
# Initialize connection manager and register handlers
|
||||
connection_manager = ConnectionManager(sio)
|
||||
register_handlers(sio, connection_manager)
|
||||
|
||||
# Include API routes
|
||||
app.include_router(health.router, prefix="/api", tags=["health"])
|
||||
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
||||
app.include_router(games.router, prefix="/api/games", tags=["games"])
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Paper Dynasty Game Backend", "version": "1.0.0"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"app.main:socket_app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
log_level="info"
|
||||
)
|
||||
```
|
||||
|
||||
**File**: `backend/app/config.py`
|
||||
```python
|
||||
from functools import lru_cache
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings"""
|
||||
|
||||
# Application
|
||||
app_env: str = "development"
|
||||
debug: bool = True
|
||||
secret_key: str
|
||||
|
||||
# Database
|
||||
database_url: str
|
||||
db_pool_size: int = 20
|
||||
db_max_overflow: int = 10
|
||||
|
||||
# Discord OAuth
|
||||
discord_client_id: str
|
||||
discord_client_secret: str
|
||||
discord_redirect_uri: str
|
||||
|
||||
# League APIs
|
||||
sba_api_url: str
|
||||
sba_api_key: str
|
||||
pd_api_url: str
|
||||
pd_api_key: str
|
||||
|
||||
# WebSocket
|
||||
ws_heartbeat_interval: int = 30
|
||||
ws_connection_timeout: int = 60
|
||||
|
||||
# CORS
|
||||
cors_origins: list[str] = ["http://localhost:3000", "http://localhost:3001"]
|
||||
|
||||
# Game settings
|
||||
max_concurrent_games: int = 20
|
||||
game_idle_timeout: int = 86400 # 24 hours
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_settings() -> Settings:
|
||||
"""Get cached settings instance"""
|
||||
return Settings()
|
||||
```
|
||||
|
||||
**File**: `backend/.env.example`
|
||||
```bash
|
||||
# Application
|
||||
APP_ENV=development
|
||||
DEBUG=true
|
||||
SECRET_KEY=your-secret-key-change-in-production
|
||||
|
||||
# Database
|
||||
# Update with your actual database server hostname/IP and credentials
|
||||
DATABASE_URL=postgresql+asyncpg://paperdynasty:your-password@your-db-server:5432/paperdynasty_dev
|
||||
|
||||
# Discord OAuth
|
||||
DISCORD_CLIENT_ID=your-discord-client-id
|
||||
DISCORD_CLIENT_SECRET=your-discord-client-secret
|
||||
DISCORD_REDIRECT_URI=http://localhost:3000/auth/callback
|
||||
|
||||
# League APIs
|
||||
SBA_API_URL=https://sba-api.example.com
|
||||
SBA_API_KEY=your-sba-api-key
|
||||
PD_API_URL=https://pd-api.example.com
|
||||
PD_API_KEY=your-pd-api-key
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
|
||||
|
||||
# Redis (optional - for caching)
|
||||
# REDIS_URL=redis://localhost:6379
|
||||
```
|
||||
|
||||
### 3. Database Setup
|
||||
|
||||
**File**: `backend/app/database/session.py`
|
||||
```python
|
||||
import logging
|
||||
from typing import AsyncGenerator
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from sqlalchemy.orm import declarative_base
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
logger = logging.getLogger(f'{__name__}.session')
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# Create async engine
|
||||
engine = create_async_engine(
|
||||
settings.database_url,
|
||||
echo=settings.debug,
|
||||
pool_size=settings.db_pool_size,
|
||||
max_overflow=settings.db_max_overflow,
|
||||
)
|
||||
|
||||
# Create session factory
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
)
|
||||
|
||||
# Base class for models
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
async def init_db() -> None:
|
||||
"""Initialize database tables"""
|
||||
async with engine.begin() as conn:
|
||||
# Import all models here to ensure they're registered
|
||||
from app.models import db_models
|
||||
|
||||
# Create tables
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
logger.info("Database tables created")
|
||||
|
||||
|
||||
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""Dependency for getting database session"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
```
|
||||
|
||||
**File**: `backend/app/models/db_models.py`
|
||||
```python
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, JSON, Text, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
import uuid
|
||||
|
||||
from app.database.session import Base
|
||||
|
||||
|
||||
class Game(Base):
|
||||
"""Game model"""
|
||||
__tablename__ = "games"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
league_id = Column(String(50), nullable=False, index=True)
|
||||
home_team_id = Column(Integer, nullable=False)
|
||||
away_team_id = Column(Integer, nullable=False)
|
||||
status = Column(String(20), nullable=False, default="pending", index=True)
|
||||
game_mode = Column(String(20), nullable=False)
|
||||
visibility = Column(String(20), nullable=False)
|
||||
current_inning = Column(Integer)
|
||||
current_half = Column(String(10))
|
||||
home_score = Column(Integer, default=0)
|
||||
away_score = Column(Integer, default=0)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
||||
started_at = Column(DateTime)
|
||||
completed_at = Column(DateTime)
|
||||
winner_team_id = Column(Integer)
|
||||
metadata = Column(JSON, default=dict)
|
||||
|
||||
|
||||
class Play(Base):
|
||||
"""Play model"""
|
||||
__tablename__ = "plays"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id"), nullable=False, index=True)
|
||||
play_number = Column(Integer, nullable=False)
|
||||
inning = Column(Integer, nullable=False)
|
||||
half = Column(String(10), nullable=False)
|
||||
outs_before = Column(Integer, nullable=False)
|
||||
outs_recorded = Column(Integer, nullable=False)
|
||||
batter_id = Column(Integer, nullable=False)
|
||||
pitcher_id = Column(Integer, nullable=False)
|
||||
runners_before = Column(JSON)
|
||||
runners_after = Column(JSON)
|
||||
balls = Column(Integer)
|
||||
strikes = Column(Integer)
|
||||
defensive_positioning = Column(String(50))
|
||||
offensive_approach = Column(String(50))
|
||||
dice_roll = Column(Integer)
|
||||
hit_type = Column(String(50))
|
||||
result_description = Column(Text)
|
||||
runs_scored = Column(Integer, default=0)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
||||
metadata = Column(JSON, default=dict)
|
||||
|
||||
|
||||
class Lineup(Base):
|
||||
"""Lineup model"""
|
||||
__tablename__ = "lineups"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id"), nullable=False, index=True)
|
||||
team_id = Column(Integer, nullable=False, index=True)
|
||||
card_id = Column(Integer, nullable=False)
|
||||
position = Column(String(10), nullable=False)
|
||||
batting_order = Column(Integer)
|
||||
is_starter = Column(Boolean, default=True)
|
||||
is_active = Column(Boolean, default=True, index=True)
|
||||
entered_inning = Column(Integer, default=1)
|
||||
metadata = Column(JSON, default=dict)
|
||||
|
||||
|
||||
class GameSession(Base):
|
||||
"""Game session tracking"""
|
||||
__tablename__ = "game_sessions"
|
||||
|
||||
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id"), primary_key=True)
|
||||
connected_users = Column(JSON, default=dict)
|
||||
last_action_at = Column(DateTime, default=datetime.utcnow, index=True)
|
||||
state_snapshot = Column(JSON, default=dict)
|
||||
```
|
||||
|
||||
### 4. WebSocket Connection Manager
|
||||
|
||||
**File**: `backend/app/websocket/connection_manager.py`
|
||||
```python
|
||||
import logging
|
||||
from typing import Dict, Set
|
||||
import socketio
|
||||
|
||||
logger = logging.getLogger(f'{__name__}.ConnectionManager')
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
"""Manages WebSocket connections and rooms"""
|
||||
|
||||
def __init__(self, sio: socketio.AsyncServer):
|
||||
self.sio = sio
|
||||
self.user_sessions: Dict[str, str] = {} # sid -> user_id
|
||||
self.game_rooms: Dict[str, Set[str]] = {} # game_id -> set of sids
|
||||
|
||||
async def connect(self, sid: str, user_id: str) -> None:
|
||||
"""Register a new connection"""
|
||||
self.user_sessions[sid] = user_id
|
||||
logger.info(f"User {user_id} connected with session {sid}")
|
||||
|
||||
async def disconnect(self, sid: str) -> None:
|
||||
"""Handle disconnection"""
|
||||
user_id = self.user_sessions.pop(sid, None)
|
||||
if user_id:
|
||||
logger.info(f"User {user_id} disconnected (session {sid})")
|
||||
|
||||
# Remove from all game rooms
|
||||
for game_id, sids in self.game_rooms.items():
|
||||
if sid in sids:
|
||||
sids.remove(sid)
|
||||
await self.broadcast_to_game(
|
||||
game_id,
|
||||
"user_disconnected",
|
||||
{"user_id": user_id}
|
||||
)
|
||||
|
||||
async def join_game(self, sid: str, game_id: str, role: str) -> None:
|
||||
"""Add user to game room"""
|
||||
await self.sio.enter_room(sid, game_id)
|
||||
|
||||
if game_id not in self.game_rooms:
|
||||
self.game_rooms[game_id] = set()
|
||||
self.game_rooms[game_id].add(sid)
|
||||
|
||||
user_id = self.user_sessions.get(sid)
|
||||
logger.info(f"User {user_id} joined game {game_id} as {role}")
|
||||
|
||||
await self.broadcast_to_game(
|
||||
game_id,
|
||||
"user_connected",
|
||||
{"user_id": user_id, "role": role}
|
||||
)
|
||||
|
||||
async def leave_game(self, sid: str, game_id: str) -> None:
|
||||
"""Remove user from game room"""
|
||||
await self.sio.leave_room(sid, game_id)
|
||||
|
||||
if game_id in self.game_rooms:
|
||||
self.game_rooms[game_id].discard(sid)
|
||||
|
||||
user_id = self.user_sessions.get(sid)
|
||||
logger.info(f"User {user_id} left game {game_id}")
|
||||
|
||||
async def broadcast_to_game(
|
||||
self,
|
||||
game_id: str,
|
||||
event: str,
|
||||
data: dict
|
||||
) -> None:
|
||||
"""Broadcast event to all users in game room"""
|
||||
await self.sio.emit(event, data, room=game_id)
|
||||
logger.debug(f"Broadcast {event} to game {game_id}")
|
||||
|
||||
async def emit_to_user(self, sid: str, event: str, data: dict) -> None:
|
||||
"""Emit event to specific user"""
|
||||
await self.sio.emit(event, data, room=sid)
|
||||
|
||||
def get_game_participants(self, game_id: str) -> Set[str]:
|
||||
"""Get all session IDs in game room"""
|
||||
return self.game_rooms.get(game_id, set())
|
||||
```
|
||||
|
||||
**File**: `backend/app/websocket/handlers.py`
|
||||
```python
|
||||
import logging
|
||||
from socketio import AsyncServer
|
||||
|
||||
from app.websocket.connection_manager import ConnectionManager
|
||||
from app.utils.auth import verify_token
|
||||
|
||||
logger = logging.getLogger(f'{__name__}.handlers')
|
||||
|
||||
|
||||
def register_handlers(sio: AsyncServer, manager: ConnectionManager) -> None:
|
||||
"""Register all WebSocket event handlers"""
|
||||
|
||||
@sio.event
|
||||
async def connect(sid, environ, auth):
|
||||
"""Handle new connection"""
|
||||
try:
|
||||
# Verify JWT token
|
||||
token = auth.get("token")
|
||||
if not token:
|
||||
logger.warning(f"Connection {sid} rejected: no token")
|
||||
return False
|
||||
|
||||
user_data = verify_token(token)
|
||||
user_id = user_data.get("user_id")
|
||||
|
||||
if not user_id:
|
||||
logger.warning(f"Connection {sid} rejected: invalid token")
|
||||
return False
|
||||
|
||||
await manager.connect(sid, user_id)
|
||||
await sio.emit("connected", {"user_id": user_id}, room=sid)
|
||||
|
||||
logger.info(f"Connection {sid} accepted for user {user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Connection error: {e}")
|
||||
return False
|
||||
|
||||
@sio.event
|
||||
async def disconnect(sid):
|
||||
"""Handle disconnection"""
|
||||
await manager.disconnect(sid)
|
||||
|
||||
@sio.event
|
||||
async def join_game(sid, data):
|
||||
"""Handle join game request"""
|
||||
try:
|
||||
game_id = data.get("game_id")
|
||||
role = data.get("role", "player")
|
||||
|
||||
if not game_id:
|
||||
await manager.emit_to_user(
|
||||
sid,
|
||||
"error",
|
||||
{"message": "Missing game_id"}
|
||||
)
|
||||
return
|
||||
|
||||
# TODO: Verify user has access to game
|
||||
|
||||
await manager.join_game(sid, game_id, role)
|
||||
await manager.emit_to_user(
|
||||
sid,
|
||||
"game_joined",
|
||||
{"game_id": game_id, "role": role}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Join game error: {e}")
|
||||
await manager.emit_to_user(
|
||||
sid,
|
||||
"error",
|
||||
{"message": str(e)}
|
||||
)
|
||||
|
||||
@sio.event
|
||||
async def leave_game(sid, data):
|
||||
"""Handle leave game request"""
|
||||
try:
|
||||
game_id = data.get("game_id")
|
||||
if game_id:
|
||||
await manager.leave_game(sid, game_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Leave game error: {e}")
|
||||
|
||||
@sio.event
|
||||
async def heartbeat(sid):
|
||||
"""Handle heartbeat ping"""
|
||||
await sio.emit("heartbeat_ack", {}, room=sid)
|
||||
```
|
||||
|
||||
### 5. Logging Configuration
|
||||
|
||||
**File**: `backend/app/utils/logging.py`
|
||||
```python
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
"""Configure application logging"""
|
||||
|
||||
# Create logs directory
|
||||
log_dir = "logs"
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
# Log file name with date
|
||||
log_file = os.path.join(log_dir, f"app_{datetime.now().strftime('%Y%m%d')}.log")
|
||||
|
||||
# Create formatter
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
# Console handler
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# Rotating file handler
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
log_file,
|
||||
maxBytes=10 * 1024 * 1024, # 10MB
|
||||
backupCount=5
|
||||
)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
# Configure root logger
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
root_logger.addHandler(console_handler)
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
# Silence noisy loggers
|
||||
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
||||
logging.getLogger("socketio").setLevel(logging.INFO)
|
||||
logging.getLogger("engineio").setLevel(logging.WARNING)
|
||||
```
|
||||
|
||||
### 6. Database Setup
|
||||
|
||||
**Note**: This project uses existing PostgreSQL servers (dev and prod). You'll need to manually create the database before running the backend.
|
||||
|
||||
**Create Database on Your PostgreSQL Server**:
|
||||
```sql
|
||||
-- On your dev PostgreSQL server
|
||||
CREATE DATABASE paperdynasty_dev;
|
||||
CREATE USER paperdynasty WITH PASSWORD 'your-secure-password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE paperdynasty_dev TO paperdynasty;
|
||||
|
||||
-- Later, on your prod PostgreSQL server
|
||||
CREATE DATABASE paperdynasty_prod;
|
||||
-- Use a different, secure password for production
|
||||
```
|
||||
|
||||
**Test Connection**:
|
||||
```bash
|
||||
# Test that you can connect to the database
|
||||
psql postgresql://paperdynasty:your-password@your-db-server:5432/paperdynasty_dev
|
||||
```
|
||||
|
||||
### 7. Docker Compose for Redis (Development)
|
||||
|
||||
**File**: `backend/docker-compose.yml`
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
```
|
||||
|
||||
**Start Redis**:
|
||||
```bash
|
||||
cd backend
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Frontend Setup
|
||||
|
||||
### 1. Initialize Nuxt 3 Projects
|
||||
|
||||
```bash
|
||||
# Create SBA League frontend
|
||||
npx nuxi@latest init frontend-sba
|
||||
cd frontend-sba
|
||||
|
||||
# Install dependencies
|
||||
npm install @nuxtjs/tailwindcss @pinia/nuxt socket.io-client axios
|
||||
npm install -D @nuxtjs/eslint-config-typescript
|
||||
|
||||
# Initialize Tailwind
|
||||
npx tailwindcss init
|
||||
|
||||
cd ..
|
||||
|
||||
# Repeat for PD League
|
||||
npx nuxi@latest init frontend-pd
|
||||
cd frontend-pd
|
||||
npm install @nuxtjs/tailwindcss @pinia/nuxt socket.io-client axios
|
||||
npm install -D @nuxtjs/eslint-config-typescript
|
||||
npx tailwindcss init
|
||||
```
|
||||
|
||||
### 2. Configure Nuxt
|
||||
|
||||
**File**: `frontend-sba/nuxt.config.ts`
|
||||
```typescript
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
|
||||
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
leagueId: 'sba',
|
||||
leagueName: 'Super Baseball Alliance',
|
||||
apiUrl: process.env.NUXT_PUBLIC_API_URL || 'http://localhost:8000',
|
||||
wsUrl: process.env.NUXT_PUBLIC_WS_URL || 'http://localhost:8000',
|
||||
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID || '',
|
||||
discordRedirectUri: process.env.NUXT_PUBLIC_DISCORD_REDIRECT_URI || 'http://localhost:3000/auth/callback',
|
||||
}
|
||||
},
|
||||
|
||||
css: ['~/assets/css/tailwind.css'],
|
||||
|
||||
devtools: { enabled: true },
|
||||
|
||||
typescript: {
|
||||
strict: true,
|
||||
typeCheck: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Setup WebSocket Plugin
|
||||
|
||||
**File**: `frontend-sba/plugins/socket.client.ts`
|
||||
```typescript
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const config = useRuntimeConfig()
|
||||
let socket: Socket | null = null
|
||||
|
||||
const connect = (token: string) => {
|
||||
if (socket?.connected) return socket
|
||||
|
||||
socket = io(config.public.wsUrl, {
|
||||
auth: { token },
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionAttempts: 5
|
||||
})
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('WebSocket connected')
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('WebSocket disconnected')
|
||||
})
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('WebSocket connection error:', error)
|
||||
})
|
||||
|
||||
return socket
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
socket?.disconnect()
|
||||
socket = null
|
||||
}
|
||||
|
||||
return {
|
||||
provide: {
|
||||
socket: {
|
||||
connect,
|
||||
disconnect,
|
||||
get instance() {
|
||||
return socket
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Testing Phase 1
|
||||
|
||||
### Backend Tests
|
||||
|
||||
```bash
|
||||
# Run backend tests
|
||||
cd backend
|
||||
pytest tests/ -v
|
||||
|
||||
# Test WebSocket connection
|
||||
python -m pytest tests/integration/test_websocket.py -v
|
||||
|
||||
# Check code quality
|
||||
black app/ tests/
|
||||
flake8 app/ tests/
|
||||
mypy app/
|
||||
```
|
||||
|
||||
### Frontend Tests
|
||||
|
||||
```bash
|
||||
# Run frontend dev server
|
||||
cd frontend-sba
|
||||
npm run dev
|
||||
|
||||
# Test in browser
|
||||
# Navigate to http://localhost:3000
|
||||
# Check console for WebSocket connection
|
||||
```
|
||||
|
||||
## Phase 1 Completion Checklist
|
||||
|
||||
- [ ] PostgreSQL database created on existing server
|
||||
- [ ] Database connection tested successfully
|
||||
- [ ] FastAPI server running on port 8000
|
||||
- [ ] Socket.io WebSocket server operational
|
||||
- [ ] Database tables created successfully
|
||||
- [ ] Logging system working (check logs/ directory)
|
||||
- [ ] Redis running via Docker Compose
|
||||
- [ ] WebSocket connections established
|
||||
- [ ] Nuxt 3 apps running (SBA on 3000, PD on 3001)
|
||||
- [ ] CORS configured correctly
|
||||
- [ ] Health check endpoint responding
|
||||
- [ ] Basic error handling in place
|
||||
|
||||
## Next Steps
|
||||
|
||||
Once Phase 1 is complete, proceed to [Phase 2: Game Engine Core](./02-game-engine.md).
|
||||
|
||||
---
|
||||
|
||||
**Notes**:
|
||||
- Keep `.env` files secure and never commit to git
|
||||
- Test WebSocket connections thoroughly before proceeding
|
||||
- Document any deviations from this setup
|
||||
- Update CLAUDE.md in subdirectories as needed
|
||||
172
.claude/implementation/02-game-engine.md
Normal file
172
.claude/implementation/02-game-engine.md
Normal file
@ -0,0 +1,172 @@
|
||||
# Phase 2: Game Engine Core
|
||||
|
||||
**Duration**: Weeks 4-6
|
||||
**Status**: Not Started
|
||||
**Prerequisites**: Phase 1 Complete
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Build the core game simulation engine with in-memory state management, play resolution logic, and database persistence. Implement the polymorphic player model system and league configuration framework.
|
||||
|
||||
## Key Objectives
|
||||
|
||||
By end of Phase 2, you should have:
|
||||
- ✅ In-memory game state management working
|
||||
- ✅ Play resolution engine with dice rolls
|
||||
- ✅ League configuration system (SBA and PD configs)
|
||||
- ✅ Polymorphic player models (BasePlayer, SbaPlayer, PdPlayer)
|
||||
- ✅ Database persistence layer with async operations
|
||||
- ✅ State recovery mechanism from database
|
||||
- ✅ Basic game flow (start → plays → end)
|
||||
|
||||
## Major Components to Implement
|
||||
|
||||
### 1. Game Engine (`backend/app/core/game_engine.py`)
|
||||
- Game session initialization
|
||||
- Turn management (defensive → stolen base → offensive → resolution)
|
||||
- Action processing and validation
|
||||
- State update coordination
|
||||
- Event emission to WebSocket clients
|
||||
|
||||
### 2. State Manager (`backend/app/core/state_manager.py`)
|
||||
- In-memory game state dictionary
|
||||
- State CRUD operations
|
||||
- State lifecycle management
|
||||
- Cache eviction for idle games
|
||||
- State recovery from database
|
||||
|
||||
### 3. Play Resolver (`backend/app/core/play_resolver.py`)
|
||||
- Cryptographic dice rolling
|
||||
- Result chart lookup (league-specific)
|
||||
- Play outcome determination
|
||||
- Runner advancement logic
|
||||
- Score calculation
|
||||
|
||||
### 4. Dice System (`backend/app/core/dice.py`)
|
||||
- Secure random number generation
|
||||
- Roll logging and verification
|
||||
- Distribution testing
|
||||
|
||||
### 5. Polymorphic Player Models (`backend/app/models/player_models.py`)
|
||||
- BasePlayer abstract class
|
||||
- SbaPlayer implementation
|
||||
- PdPlayer implementation
|
||||
- Lineup factory method
|
||||
- Type guards for league-specific logic
|
||||
|
||||
### 6. League Configuration (`backend/app/config/`)
|
||||
- BaseGameConfig class
|
||||
- SbaConfig and PdConfig subclasses
|
||||
- Result charts (d20 tables) for each league
|
||||
- Config loader and versioning
|
||||
- Config validation
|
||||
|
||||
### 7. Database Operations (`backend/app/database/operations.py`)
|
||||
- Async play persistence
|
||||
- Game metadata updates
|
||||
- Lineup operations
|
||||
- State snapshot management
|
||||
- Bulk recovery queries
|
||||
|
||||
### 8. API Client (`backend/app/data/api_client.py`)
|
||||
- HTTP client for league REST APIs
|
||||
- Team data fetching
|
||||
- Roster data fetching
|
||||
- Player/card data fetching
|
||||
- Error handling and retries
|
||||
- Response caching (optional)
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Week 4**: State Manager + Database Operations
|
||||
- In-memory state structure
|
||||
- Basic CRUD operations
|
||||
- Database persistence layer
|
||||
- State recovery mechanism
|
||||
|
||||
2. **Week 5**: Game Engine + Play Resolver
|
||||
- Game initialization flow
|
||||
- Turn management
|
||||
- Dice rolling system
|
||||
- Basic play resolution (simplified charts)
|
||||
|
||||
3. **Week 6**: League Configs + Player Models
|
||||
- Polymorphic player architecture
|
||||
- League configuration system
|
||||
- Complete result charts
|
||||
- API client integration
|
||||
- End-to-end testing
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- State manager operations
|
||||
- Dice roll distribution
|
||||
- Play resolver outcomes
|
||||
- Player model instantiation
|
||||
- Config loading
|
||||
|
||||
### Integration Tests
|
||||
- Full game flow from start to end
|
||||
- State recovery from database
|
||||
- Multi-turn sequences
|
||||
- API client with mocked responses
|
||||
|
||||
### E2E Tests
|
||||
- Play a complete 9-inning game
|
||||
- Verify database persistence
|
||||
- Test state recovery mid-game
|
||||
|
||||
## Key Files to Create
|
||||
|
||||
```
|
||||
backend/app/
|
||||
├── core/
|
||||
│ ├── game_engine.py # Main game logic
|
||||
│ ├── state_manager.py # In-memory state
|
||||
│ ├── play_resolver.py # Play outcomes
|
||||
│ ├── dice.py # Random generation
|
||||
│ └── validators.py # Rule validation
|
||||
├── config/
|
||||
│ ├── base_config.py # Base configuration
|
||||
│ ├── league_configs.py # SBA/PD configs
|
||||
│ ├── result_charts.py # d20 tables
|
||||
│ └── loader.py # Config utilities
|
||||
├── models/
|
||||
│ ├── player_models.py # Polymorphic players
|
||||
│ └── game_models.py # Pydantic game models
|
||||
└── data/
|
||||
└── api_client.py # League API client
|
||||
```
|
||||
|
||||
## Reference Documents
|
||||
|
||||
- [Backend Architecture](./backend-architecture.md) - Complete backend structure
|
||||
- [Database Design](./database-design.md) - Schema and queries
|
||||
- [WebSocket Protocol](./websocket-protocol.md) - Event specifications
|
||||
- [PRD Lines 378-551](../prd-web-scorecard-1.1.md) - Polymorphic player architecture
|
||||
- [PRD Lines 780-846](../prd-web-scorecard-1.1.md) - League configuration system
|
||||
|
||||
## Deliverable
|
||||
|
||||
A working game backend that can:
|
||||
- Initialize a game with teams from league APIs
|
||||
- Process a complete at-bat (decisions → dice roll → outcome)
|
||||
- Update game state in memory and persist to database
|
||||
- Recover game state after backend restart
|
||||
- Handle basic substitutions
|
||||
|
||||
## Notes
|
||||
|
||||
- Focus on getting one at-bat working perfectly before expanding
|
||||
- Test dice roll distribution extensively
|
||||
- Validate all state transitions
|
||||
- Use simplified result charts initially, expand in Phase 3
|
||||
- Don't implement UI yet - test via WebSocket events or Python scripts
|
||||
|
||||
---
|
||||
|
||||
**Status**: Placeholder - to be expanded during implementation
|
||||
**Next Phase**: [03-gameplay-features.md](./03-gameplay-features.md)
|
||||
228
.claude/implementation/03-gameplay-features.md
Normal file
228
.claude/implementation/03-gameplay-features.md
Normal file
@ -0,0 +1,228 @@
|
||||
# Phase 3: Complete Game Features
|
||||
|
||||
**Duration**: Weeks 7-9
|
||||
**Status**: Not Started
|
||||
**Prerequisites**: Phase 2 Complete
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Implement all strategic decisions, complete result charts for both leagues, substitution system, pitching changes, AI opponent, and async game mode support. Complete the frontend game interface.
|
||||
|
||||
## Key Objectives
|
||||
|
||||
By end of Phase 3, you should have:
|
||||
- ✅ All strategic decision types implemented
|
||||
- ✅ Complete substitution system (pinch hitters, defensive replacements)
|
||||
- ✅ Pitching change logic with bullpen management
|
||||
- ✅ Full result charts for both SBA and PD leagues
|
||||
- ✅ AI opponent integration
|
||||
- ✅ Async game mode with notifications
|
||||
- ✅ Complete frontend game interface (mobile + desktop)
|
||||
- ✅ Play-by-play history display
|
||||
- ✅ Game creation and lobby UI
|
||||
|
||||
## Major Components to Implement
|
||||
|
||||
### Backend Features
|
||||
|
||||
#### 1. Strategic Decisions
|
||||
- Defensive positioning (standard, infield in, shifts)
|
||||
- Stolen base attempts (per runner)
|
||||
- Offensive approach (swing away, bunt, hit-and-run)
|
||||
- Intentional walk
|
||||
- Defensive substitutions mid-inning
|
||||
|
||||
#### 2. Substitution System
|
||||
- Pinch hitter logic
|
||||
- Pinch runner logic
|
||||
- Defensive replacement
|
||||
- Double switches (if applicable)
|
||||
- Validation (eligibility, roster constraints)
|
||||
- Lineup reordering after substitution
|
||||
|
||||
#### 3. Pitching Changes
|
||||
- Relief pitcher selection
|
||||
- Pitcher eligibility validation
|
||||
- Pitcher statistics tracking
|
||||
- Mound visit limits (optional)
|
||||
- Auto-pitcher fatigue (optional)
|
||||
|
||||
#### 4. Complete Result Charts
|
||||
- SBA league d20 tables (all situations)
|
||||
- PD league d20 tables (all situations)
|
||||
- Hit location logic
|
||||
- Runner advancement scenarios
|
||||
- Fielding errors (if applicable)
|
||||
- Extra-inning rules
|
||||
|
||||
#### 5. AI Opponent
|
||||
- Integration with existing AI logic
|
||||
- Decision-making algorithms
|
||||
- Timing simulation (avoid instant responses)
|
||||
- Difficulty levels (optional)
|
||||
- AI substitution logic
|
||||
|
||||
#### 6. Async Game Mode
|
||||
- Turn-based notification system
|
||||
- Email/Discord notifications
|
||||
- Turn timeout handling
|
||||
- "Your turn" indicator
|
||||
- Game pause/resume
|
||||
|
||||
### Frontend Features
|
||||
|
||||
#### 1. Game Creation Flow
|
||||
- Team selection
|
||||
- Opponent selection (human or AI)
|
||||
- Game mode selection (live, async, vs AI)
|
||||
- Visibility settings
|
||||
- Lineup confirmation
|
||||
- Game start
|
||||
|
||||
#### 2. Game Lobby
|
||||
- Pre-game waiting room
|
||||
- Lineup display for both teams
|
||||
- Ready/not ready status
|
||||
- Chat (optional)
|
||||
- Countdown to start
|
||||
|
||||
#### 3. Game Interface (Mobile)
|
||||
- Baseball diamond visualization
|
||||
- Score display (by inning)
|
||||
- Current situation (count, outs, runners)
|
||||
- Decision cards (bottom sheet)
|
||||
- Play-by-play feed (collapsible)
|
||||
- Action buttons (substitutions, pitching change)
|
||||
|
||||
#### 4. Game Interface (Desktop)
|
||||
- Two/three column layout
|
||||
- Expanded play-by-play
|
||||
- Statistics sidebar
|
||||
- Hover states for additional info
|
||||
- Keyboard shortcuts
|
||||
|
||||
#### 5. Decision Workflows
|
||||
- Defensive positioning selector
|
||||
- Stolen base attempt interface
|
||||
- Offensive approach selector
|
||||
- Substitution modal with roster
|
||||
- Pitching change modal with bullpen
|
||||
|
||||
#### 6. Play-by-Play Display
|
||||
- Scrollable history
|
||||
- Play descriptions
|
||||
- Score updates
|
||||
- Substitution notifications
|
||||
- Inning summaries
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Week 7**: Complete Backend Strategic Decisions
|
||||
- All decision types
|
||||
- Complete result charts
|
||||
- Validation logic
|
||||
- WebSocket handlers
|
||||
|
||||
2. **Week 8**: Substitutions + Frontend Game Interface
|
||||
- Substitution system (backend + frontend)
|
||||
- Pitching changes
|
||||
- Game interface UI (mobile-first)
|
||||
- Decision workflows
|
||||
|
||||
3. **Week 9**: AI + Async + Polish
|
||||
- AI opponent integration
|
||||
- Async game mode
|
||||
- Game creation + lobby UI
|
||||
- Play-by-play display
|
||||
- Integration testing
|
||||
|
||||
## Frontend Component Structure
|
||||
|
||||
```
|
||||
shared-components/src/components/
|
||||
├── Game/
|
||||
│ ├── GameBoard.vue
|
||||
│ ├── ScoreBoard.vue
|
||||
│ ├── PlayByPlay.vue
|
||||
│ ├── CurrentSituation.vue
|
||||
│ └── BaseRunners.vue
|
||||
├── Decisions/
|
||||
│ ├── DefensivePositioning.vue
|
||||
│ ├── StolenBaseAttempt.vue
|
||||
│ ├── OffensiveApproach.vue
|
||||
│ └── DecisionTimer.vue
|
||||
├── Actions/
|
||||
│ ├── SubstitutionModal.vue
|
||||
│ ├── PitchingChange.vue
|
||||
│ └── ActionButton.vue
|
||||
└── Display/
|
||||
├── PlayerCard.vue
|
||||
├── DiceRoll.vue
|
||||
├── PlayOutcome.vue
|
||||
└── ConnectionStatus.vue
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Backend Tests
|
||||
- Test all strategic decision combinations
|
||||
- Validate substitution rules
|
||||
- Test AI decision making
|
||||
- Verify async turn handling
|
||||
|
||||
### Frontend Tests
|
||||
- Component testing with Vitest
|
||||
- Mobile responsiveness testing
|
||||
- Decision workflow testing
|
||||
- WebSocket event handling
|
||||
|
||||
### E2E Tests
|
||||
- Complete game with all decision types
|
||||
- Game with substitutions
|
||||
- AI opponent game
|
||||
- Async game (simulated)
|
||||
|
||||
## Key Files to Create
|
||||
|
||||
**Backend**:
|
||||
- `backend/app/core/substitutions.py`
|
||||
- `backend/app/core/ai_opponent.py`
|
||||
- `backend/app/websocket/handlers.py` (expand)
|
||||
- `backend/app/config/result_charts.py` (complete)
|
||||
|
||||
**Frontend**:
|
||||
- All shared components listed above
|
||||
- `frontend-{league}/pages/games/create.vue`
|
||||
- `frontend-{league}/pages/games/[id].vue`
|
||||
- `frontend-{league}/store/game.ts`
|
||||
- `frontend-{league}/composables/useGameActions.ts`
|
||||
|
||||
## Reference Documents
|
||||
|
||||
- [Frontend Architecture](./frontend-architecture.md) - Component structure
|
||||
- [WebSocket Protocol](./websocket-protocol.md) - All event types
|
||||
- [PRD Lines 186-276](../prd-web-scorecard-1.1.md) - Strategic decisions and game modes
|
||||
|
||||
## Deliverable
|
||||
|
||||
A fully playable game system with:
|
||||
- All strategic options available
|
||||
- Complete substitution capabilities
|
||||
- AI opponent functional
|
||||
- Mobile and desktop UI complete
|
||||
- Async game mode operational
|
||||
|
||||
## Notes
|
||||
|
||||
- Prioritize mobile UI - 60%+ of users will play on mobile
|
||||
- Test result charts thoroughly for both leagues
|
||||
- Validate all substitution rules against baseball logic
|
||||
- AI should make "reasonable" decisions, not perfect ones
|
||||
- Async notifications critical for user engagement
|
||||
|
||||
---
|
||||
|
||||
**Status**: Placeholder - to be expanded during implementation
|
||||
**Next Phase**: [04-spectator-polish.md](./04-spectator-polish.md)
|
||||
254
.claude/implementation/04-spectator-polish.md
Normal file
254
.claude/implementation/04-spectator-polish.md
Normal file
@ -0,0 +1,254 @@
|
||||
# Phase 4: Spectator Mode & Polish
|
||||
|
||||
**Duration**: Weeks 10-11
|
||||
**Status**: Not Started
|
||||
**Prerequisites**: Phase 3 Complete
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Implement spectator mode for real-time game viewing, add UI/UX polish including animations, optimize mobile touch experience, and improve accessibility. Focus on production-ready user experience.
|
||||
|
||||
## Key Objectives
|
||||
|
||||
By end of Phase 4, you should have:
|
||||
- ✅ Spectator mode fully functional
|
||||
- ✅ Dice roll animations working
|
||||
- ✅ Smooth transitions and loading states
|
||||
- ✅ Mobile touch optimization
|
||||
- ✅ Accessibility improvements (WCAG 2.1 AA)
|
||||
- ✅ Error handling and recovery UX
|
||||
- ✅ Performance optimizations
|
||||
- ✅ Production-ready polish
|
||||
|
||||
## Major Components to Implement
|
||||
|
||||
### 1. Spectator Mode
|
||||
|
||||
**Backend**:
|
||||
- Spectator WebSocket events (read-only)
|
||||
- Spectator room management
|
||||
- Spectator count tracking
|
||||
- Permission enforcement
|
||||
|
||||
**Frontend**:
|
||||
- Spectator-specific game view
|
||||
- Read-only interface (no decision prompts)
|
||||
- Spectator count display
|
||||
- Join spectator link generation
|
||||
- Late join to active games
|
||||
|
||||
### 2. Animations & Transitions
|
||||
|
||||
**Dice Roll Animation**:
|
||||
- 3D dice roll effect or 2D animation
|
||||
- Suspenseful timing (1-2 seconds)
|
||||
- Synchronized across all clients
|
||||
- Sound effects (optional)
|
||||
|
||||
**Play Outcome Transitions**:
|
||||
- Runner movement animations
|
||||
- Score change animations
|
||||
- Smooth state transitions
|
||||
- Visual feedback for key moments
|
||||
|
||||
**UI Transitions**:
|
||||
- Page transitions
|
||||
- Modal animations
|
||||
- Accordion collapse/expand
|
||||
- Loading skeletons
|
||||
|
||||
### 3. Mobile Touch Optimization
|
||||
|
||||
**Touch Interactions**:
|
||||
- Swipe gestures (navigate play history)
|
||||
- Pull to refresh
|
||||
- Tap feedback (haptic if supported)
|
||||
- Touch target sizing (44x44px minimum)
|
||||
|
||||
**Layout Refinements**:
|
||||
- Safe area handling (iOS notch)
|
||||
- Keyboard avoidance
|
||||
- Scroll behavior optimization
|
||||
- Bottom sheet interactions
|
||||
|
||||
**Performance**:
|
||||
- Lazy loading images
|
||||
- Virtual scrolling for long lists
|
||||
- Debounced scroll handlers
|
||||
- Optimized re-renders
|
||||
|
||||
### 4. Accessibility Improvements
|
||||
|
||||
**Screen Reader Support**:
|
||||
- Semantic HTML elements
|
||||
- ARIA labels on interactive elements
|
||||
- Announcements for game events
|
||||
- Focus management in modals
|
||||
|
||||
**Keyboard Navigation**:
|
||||
- Tab order optimization
|
||||
- Keyboard shortcuts for actions
|
||||
- Focus indicators
|
||||
- Escape key handling
|
||||
|
||||
**Visual Accessibility**:
|
||||
- Color contrast validation (4.5:1 minimum)
|
||||
- Focus indicators
|
||||
- Text sizing options
|
||||
- Reduced motion support
|
||||
|
||||
**Accessible Notifications**:
|
||||
- Screen reader announcements for plays
|
||||
- Visual + auditory feedback options
|
||||
- Status updates in accessible format
|
||||
|
||||
### 5. Error Handling & Recovery
|
||||
|
||||
**Connection Issues**:
|
||||
- Graceful offline mode
|
||||
- Reconnection feedback
|
||||
- Action queue during disconnect
|
||||
- State sync on reconnect
|
||||
|
||||
**Error States**:
|
||||
- Friendly error messages
|
||||
- Recovery actions provided
|
||||
- Error boundaries (prevent crashes)
|
||||
- Fallback UI
|
||||
|
||||
**Loading States**:
|
||||
- Loading skeletons
|
||||
- Progress indicators
|
||||
- Timeout handling
|
||||
- Cancel operations
|
||||
|
||||
### 6. Performance Optimizations
|
||||
|
||||
**Frontend**:
|
||||
- Code splitting by route
|
||||
- Component lazy loading
|
||||
- Image optimization (WebP, lazy load)
|
||||
- Bundle size reduction
|
||||
- Memoization of expensive computations
|
||||
|
||||
**Backend**:
|
||||
- Response caching
|
||||
- Database query optimization
|
||||
- Connection pool tuning
|
||||
- Memory usage monitoring
|
||||
|
||||
**WebSocket**:
|
||||
- Message compression
|
||||
- Batch updates where possible
|
||||
- Efficient state diff broadcasts
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Week 10**: Spectator Mode + Animations
|
||||
- Spectator WebSocket events
|
||||
- Read-only game view
|
||||
- Dice roll animation
|
||||
- Play outcome transitions
|
||||
|
||||
2. **Week 11**: Polish + Accessibility + Performance
|
||||
- Mobile touch optimization
|
||||
- Accessibility audit and fixes
|
||||
- Error handling improvements
|
||||
- Performance profiling and optimization
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Spectator Mode Testing
|
||||
- Join as spectator during active game
|
||||
- Verify read-only constraints
|
||||
- Test late join (mid-game)
|
||||
- Multi-spectator scenarios
|
||||
|
||||
### Animation Testing
|
||||
- Verify timing synchronization
|
||||
- Test on various devices/browsers
|
||||
- Performance impact measurement
|
||||
- Reduced motion preference
|
||||
|
||||
### Accessibility Testing
|
||||
- Screen reader testing (NVDA, JAWS, VoiceOver)
|
||||
- Keyboard-only navigation
|
||||
- Color contrast validation tools
|
||||
- Automated accessibility audits (axe, Lighthouse)
|
||||
|
||||
### Performance Testing
|
||||
- Lighthouse audits
|
||||
- Mobile device testing (real devices)
|
||||
- Network throttling tests (3G simulation)
|
||||
- Memory leak detection
|
||||
|
||||
## Key Files to Create/Modify
|
||||
|
||||
**Backend**:
|
||||
- `backend/app/websocket/handlers.py` - Add spectator events
|
||||
- `backend/app/api/routes/spectate.py` - Spectator endpoints
|
||||
|
||||
**Frontend**:
|
||||
- `shared-components/src/components/Display/DiceRoll.vue` - Animation
|
||||
- `shared-components/src/components/Common/LoadingStates.vue`
|
||||
- `frontend-{league}/pages/spectate/[id].vue` - Spectator view
|
||||
- `frontend-{league}/composables/useAccessibility.ts`
|
||||
- `frontend-{league}/utils/animations.ts`
|
||||
|
||||
**Styles**:
|
||||
- `frontend-{league}/assets/css/animations.css`
|
||||
- `frontend-{league}/assets/css/accessibility.css`
|
||||
|
||||
## Reference Documents
|
||||
|
||||
- [Frontend Architecture](./frontend-architecture.md) - Component structure
|
||||
- [WebSocket Protocol](./websocket-protocol.md) - Spectator events
|
||||
- [PRD Lines 233-245](../prd-web-scorecard-1.1.md) - Spectator requirements
|
||||
- [PRD Lines 732-777](../prd-web-scorecard-1.1.md) - UX requirements
|
||||
|
||||
## Deliverable
|
||||
|
||||
A production-ready game interface with:
|
||||
- Spectator mode allowing real-time viewing
|
||||
- Polished animations and transitions
|
||||
- Optimized mobile touch experience
|
||||
- WCAG 2.1 AA accessibility compliance
|
||||
- Robust error handling and recovery
|
||||
- Performance meeting target metrics
|
||||
|
||||
## Performance Targets
|
||||
|
||||
- Page load: < 3s on 3G
|
||||
- Action response: < 500ms
|
||||
- Lighthouse score: > 90
|
||||
- Accessibility score: 100
|
||||
- First contentful paint: < 1.5s
|
||||
- Time to interactive: < 3.5s
|
||||
|
||||
## Accessibility Checklist
|
||||
|
||||
- [ ] All interactive elements keyboard accessible
|
||||
- [ ] Focus indicators visible and clear
|
||||
- [ ] Color contrast meets WCAG AA standards
|
||||
- [ ] Screen reader announcements for game events
|
||||
- [ ] Alternative text for all images
|
||||
- [ ] Form labels and error messages
|
||||
- [ ] Skip navigation links
|
||||
- [ ] Semantic HTML structure
|
||||
- [ ] Reduced motion preference respected
|
||||
- [ ] ARIA attributes used correctly
|
||||
|
||||
## Notes
|
||||
|
||||
- Test spectator mode with multiple simultaneous spectators
|
||||
- Animation should enhance, not distract from gameplay
|
||||
- Mobile touch targets must be large enough for thumbs
|
||||
- Accessibility is not optional - it's a requirement
|
||||
- Performance directly impacts user retention
|
||||
|
||||
---
|
||||
|
||||
**Status**: Placeholder - to be expanded during implementation
|
||||
**Next Phase**: [05-testing-launch.md](./05-testing-launch.md)
|
||||
373
.claude/implementation/05-testing-launch.md
Normal file
373
.claude/implementation/05-testing-launch.md
Normal file
@ -0,0 +1,373 @@
|
||||
# Phase 5: Testing & Launch
|
||||
|
||||
**Duration**: Weeks 12-13
|
||||
**Status**: Not Started
|
||||
**Prerequisites**: Phase 4 Complete
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Comprehensive testing across all layers, load testing for production readiness, security audit, deployment procedures, monitoring setup, and final launch with migration from Google Sheets system.
|
||||
|
||||
## Key Objectives
|
||||
|
||||
By end of Phase 5, you should have:
|
||||
- ✅ Comprehensive test coverage (>80%)
|
||||
- ✅ Load testing completed (10+ concurrent games)
|
||||
- ✅ Security audit passed
|
||||
- ✅ Production deployment complete
|
||||
- ✅ Monitoring and alerting configured
|
||||
- ✅ Documentation finalized
|
||||
- ✅ Beta testing completed
|
||||
- ✅ Full launch executed
|
||||
- ✅ Google Sheets system deprecated
|
||||
|
||||
## Major Components
|
||||
|
||||
### 1. Test Suite Development
|
||||
|
||||
#### Unit Tests
|
||||
- **Backend**: All core components (game engine, state manager, play resolver, validators)
|
||||
- **Frontend**: All composables and utility functions
|
||||
- **Coverage**: >80% code coverage
|
||||
- **Mocking**: Database, external APIs, WebSocket connections
|
||||
|
||||
#### Integration Tests
|
||||
- **Backend**: WebSocket event flow, database operations, API client
|
||||
- **Frontend**: Component integration, store interactions
|
||||
- **Cross-layer**: Frontend → WebSocket → Backend → Database
|
||||
|
||||
#### End-to-End Tests
|
||||
- **Complete Games**: Full 9-inning games in all modes
|
||||
- **Multi-User**: Two players simultaneously
|
||||
- **Spectator**: Spectator joining active games
|
||||
- **Reconnection**: Disconnect and reconnect scenarios
|
||||
- **State Recovery**: Backend restart during game
|
||||
- **Browser Testing**: Chrome, Safari, Firefox, Edge
|
||||
- **Device Testing**: iOS, Android devices (real hardware)
|
||||
|
||||
#### Load Tests
|
||||
- **Concurrent Games**: 10+ simultaneous games
|
||||
- **Concurrent Users**: 50+ connected WebSocket clients
|
||||
- **Database Load**: Operations under stress
|
||||
- **Memory Usage**: Monitor for leaks over extended period
|
||||
- **Response Times**: Validate latency targets
|
||||
|
||||
### 2. Security Audit
|
||||
|
||||
#### Authentication & Authorization
|
||||
- [ ] Discord OAuth implementation secure
|
||||
- [ ] JWT token handling correct
|
||||
- [ ] Token expiration and refresh working
|
||||
- [ ] Session management secure
|
||||
- [ ] Authorization checks on all actions
|
||||
|
||||
#### Input Validation
|
||||
- [ ] All WebSocket events validated
|
||||
- [ ] SQL injection prevention verified
|
||||
- [ ] XSS prevention in place
|
||||
- [ ] CSRF protection configured
|
||||
- [ ] Rate limiting functional
|
||||
|
||||
#### Data Security
|
||||
- [ ] HTTPS/WSS enforced
|
||||
- [ ] Sensitive data encrypted
|
||||
- [ ] Database credentials secured
|
||||
- [ ] Environment variables protected
|
||||
- [ ] Audit logging enabled
|
||||
|
||||
#### Game Integrity
|
||||
- [ ] Dice rolls cryptographically secure
|
||||
- [ ] All rules enforced server-side
|
||||
- [ ] No client-side game logic
|
||||
- [ ] State tampering prevented
|
||||
- [ ] Replay attacks prevented
|
||||
|
||||
### 3. Deployment Setup
|
||||
|
||||
#### Infrastructure
|
||||
- **Backend VPS**: Ubuntu 22.04 LTS, 4GB RAM minimum
|
||||
- **Database**: PostgreSQL 14+, separate or same VPS
|
||||
- **SSL**: Let's Encrypt certificates
|
||||
- **Reverse Proxy**: Nginx for HTTP/WebSocket
|
||||
- **Process Manager**: systemd or supervisor
|
||||
- **Firewall**: UFW configured
|
||||
|
||||
#### CI/CD Pipeline (Optional)
|
||||
- Automated testing on push
|
||||
- Deployment automation
|
||||
- Rollback procedures
|
||||
- Database migration automation
|
||||
|
||||
#### Environment Configuration
|
||||
- Production environment variables
|
||||
- Database connection strings
|
||||
- API keys secured
|
||||
- CORS origins configured
|
||||
- Logging configured
|
||||
|
||||
### 4. Monitoring & Alerting
|
||||
|
||||
#### Application Monitoring
|
||||
- **Uptime**: Track availability (target: 99.5%)
|
||||
- **Response Times**: Monitor latency
|
||||
- **Error Rates**: Track exceptions
|
||||
- **WebSocket Health**: Connection count, errors
|
||||
- **Memory Usage**: Detect leaks
|
||||
|
||||
#### Database Monitoring
|
||||
- **Query Performance**: Slow query log
|
||||
- **Connection Pool**: Utilization tracking
|
||||
- **Disk Usage**: Storage monitoring
|
||||
- **Backup Status**: Verify daily backups
|
||||
|
||||
#### Alerting Rules
|
||||
- HTTP 5xx errors > 5/minute
|
||||
- WebSocket failures > 10%
|
||||
- Database pool > 90% utilized
|
||||
- Memory usage > 85%
|
||||
- Disk usage > 80%
|
||||
- Backup failures
|
||||
|
||||
#### Logging Strategy
|
||||
- **Structured Logging**: JSON format
|
||||
- **Log Levels**: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
- **Log Rotation**: Daily rotation, 30-day retention
|
||||
- **Centralized Logs**: Optional log aggregation
|
||||
|
||||
### 5. Documentation
|
||||
|
||||
#### User Documentation
|
||||
- **Getting Started Guide**: How to play first game
|
||||
- **Game Rules**: Baseball rules explanation
|
||||
- **Strategic Guide**: Decision explanations
|
||||
- **FAQ**: Common questions
|
||||
- **Troubleshooting**: Common issues and solutions
|
||||
|
||||
#### Technical Documentation
|
||||
- **API Documentation**: OpenAPI/Swagger
|
||||
- **WebSocket Protocol**: Event specifications
|
||||
- **Database Schema**: Table descriptions
|
||||
- **Deployment Guide**: Step-by-step deployment
|
||||
- **Runbook**: Operations procedures
|
||||
|
||||
#### Code Documentation
|
||||
- Inline comments for complex logic
|
||||
- Function/class docstrings
|
||||
- README files in subdirectories
|
||||
- Architecture decision records (ADRs)
|
||||
|
||||
### 6. Beta Testing
|
||||
|
||||
#### Beta Tester Recruitment
|
||||
- 10-20 active players from each league
|
||||
- Mix of competitive and casual players
|
||||
- Mobile and desktop users
|
||||
- Technical and non-technical users
|
||||
|
||||
#### Beta Testing Period
|
||||
- **Duration**: 2 weeks
|
||||
- **Goals**: Find bugs, gather feedback, validate UX
|
||||
- **Feedback Collection**: Structured surveys, bug reports
|
||||
- **Communication**: Discord channel, daily check-ins
|
||||
|
||||
#### Beta Testing Checklist
|
||||
- [ ] Complete at least 50 games
|
||||
- [ ] Test all game modes (live, async, vs AI)
|
||||
- [ ] Test on multiple devices
|
||||
- [ ] Verify substitutions and pitching changes
|
||||
- [ ] Test spectator mode
|
||||
- [ ] Stress test with concurrent games
|
||||
- [ ] Gather user satisfaction feedback
|
||||
|
||||
### 7. Launch Plan
|
||||
|
||||
#### Pre-Launch (Week 12)
|
||||
- [ ] Complete all testing
|
||||
- [ ] Fix critical bugs
|
||||
- [ ] Security audit passed
|
||||
- [ ] Production deployment ready
|
||||
- [ ] Monitoring configured
|
||||
- [ ] Documentation complete
|
||||
- [ ] Beta testing complete
|
||||
|
||||
#### Soft Launch (Early Week 13)
|
||||
- [ ] Announce to beta testers
|
||||
- [ ] Limited access (invite-only)
|
||||
- [ ] Monitor closely for issues
|
||||
- [ ] Quick iteration on feedback
|
||||
- [ ] Prepare for full launch
|
||||
|
||||
#### Full Launch (Mid Week 13)
|
||||
- [ ] Announce to all league members
|
||||
- [ ] Migration guide published
|
||||
- [ ] Support channels active
|
||||
- [ ] Monitor system health
|
||||
- [ ] Be ready for rapid response
|
||||
|
||||
#### Post-Launch (Late Week 13+)
|
||||
- [ ] Google Sheets system deprecated
|
||||
- [ ] Collect user feedback
|
||||
- [ ] Monitor success metrics
|
||||
- [ ] Plan post-MVP features
|
||||
- [ ] Celebrate success! 🎉
|
||||
|
||||
## Testing Tools & Frameworks
|
||||
|
||||
### Backend Testing
|
||||
```bash
|
||||
# Unit and integration tests
|
||||
pytest backend/tests/ -v --cov=app --cov-report=html
|
||||
|
||||
# Load testing
|
||||
locust -f backend/tests/load/locustfile.py --host=https://api.paperdynasty.com
|
||||
```
|
||||
|
||||
### Frontend Testing
|
||||
```bash
|
||||
# Component tests
|
||||
npm run test:unit
|
||||
|
||||
# E2E tests
|
||||
npm run test:e2e
|
||||
|
||||
# Accessibility audit
|
||||
npm run lighthouse
|
||||
```
|
||||
|
||||
### Security Testing
|
||||
```bash
|
||||
# Dependency vulnerability scan
|
||||
safety check
|
||||
|
||||
# OWASP ZAP security scan
|
||||
zap-cli quick-scan https://paperdynasty.com
|
||||
```
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
### Pre-Deployment
|
||||
- [ ] All tests passing
|
||||
- [ ] Load tests completed
|
||||
- [ ] Security audit passed
|
||||
- [ ] Database migrations tested
|
||||
- [ ] Backup procedures verified
|
||||
- [ ] Monitoring configured
|
||||
- [ ] SSL certificates installed
|
||||
- [ ] Environment variables set
|
||||
- [ ] Documentation updated
|
||||
|
||||
### Deployment Steps
|
||||
1. [ ] Create database backup
|
||||
2. [ ] Deploy backend to VPS
|
||||
3. [ ] Run database migrations
|
||||
4. [ ] Deploy frontend builds
|
||||
5. [ ] Configure nginx
|
||||
6. [ ] Start backend services
|
||||
7. [ ] Verify WebSocket connections
|
||||
8. [ ] Test Discord OAuth
|
||||
9. [ ] Run smoke tests
|
||||
10. [ ] Monitor logs for errors
|
||||
|
||||
### Post-Deployment
|
||||
- [ ] All endpoints responding
|
||||
- [ ] WebSocket connections working
|
||||
- [ ] Test game created successfully
|
||||
- [ ] Monitor error rates
|
||||
- [ ] Check performance metrics
|
||||
- [ ] Verify backup running
|
||||
- [ ] Update DNS (if needed)
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If critical issues arise:
|
||||
|
||||
1. **Identify Issue**: Determine severity
|
||||
2. **Assess Impact**: How many users affected?
|
||||
3. **Decision**: Fix forward or rollback?
|
||||
|
||||
**Rollback Procedure**:
|
||||
```bash
|
||||
# 1. Stop current services
|
||||
sudo systemctl stop paperdynasty-backend
|
||||
|
||||
# 2. Restore previous version
|
||||
cd /opt/paperdynasty
|
||||
git checkout <previous-tag>
|
||||
|
||||
# 3. Rollback database migrations
|
||||
cd backend
|
||||
alembic downgrade -1
|
||||
|
||||
# 4. Restart services
|
||||
sudo systemctl start paperdynasty-backend
|
||||
|
||||
# 5. Verify functionality
|
||||
curl https://api.paperdynasty.com/health
|
||||
```
|
||||
|
||||
## Success Metrics (First Month)
|
||||
|
||||
### Migration Success
|
||||
- [ ] 90% of active players using web app
|
||||
- [ ] Zero critical bugs requiring rollback
|
||||
- [ ] < 5% of games abandoned due to technical issues
|
||||
|
||||
### Performance
|
||||
- [ ] 95th percentile action latency < 1s
|
||||
- [ ] WebSocket connection success rate > 99%
|
||||
- [ ] System uptime > 99.5%
|
||||
|
||||
### User Satisfaction
|
||||
- [ ] Net Promoter Score (NPS) > 50
|
||||
- [ ] Support tickets < 5/week
|
||||
- [ ] Positive feedback ratio > 80%
|
||||
|
||||
### Engagement
|
||||
- [ ] Average 5+ games per active player per week
|
||||
- [ ] 60%+ of games on mobile
|
||||
- [ ] Average session duration 30-60 minutes
|
||||
|
||||
## Reference Documents
|
||||
|
||||
- [Testing Strategy](./testing-strategy.md) (to be created)
|
||||
- [Deployment Guide](./deployment-guide.md) (to be created)
|
||||
- [PRD Lines 1063-1101](../prd-web-scorecard-1.1.md) - Testing strategy
|
||||
- [PRD Lines 1282-1328](../prd-web-scorecard-1.1.md) - Deployment checklist
|
||||
|
||||
## Key Files to Create
|
||||
|
||||
**Testing**:
|
||||
- `backend/tests/load/locustfile.py` - Load test scenarios
|
||||
- `backend/tests/e2e/test_full_game.py` - E2E test suite
|
||||
- `frontend-{league}/cypress/` - Cypress E2E tests
|
||||
- `scripts/test-websocket.py` - WebSocket testing script
|
||||
|
||||
**Deployment**:
|
||||
- `deploy/nginx.conf` - Nginx configuration
|
||||
- `deploy/systemd/paperdynasty.service` - Systemd service
|
||||
- `scripts/deploy.sh` - Deployment automation
|
||||
- `scripts/backup.sh` - Backup automation
|
||||
|
||||
**Documentation**:
|
||||
- `docs/user-guide.md` - User documentation
|
||||
- `docs/api-reference.md` - API documentation
|
||||
- `docs/deployment.md` - Deployment guide
|
||||
- `docs/troubleshooting.md` - Common issues
|
||||
|
||||
## Notes
|
||||
|
||||
- Testing is not optional - it's critical for success
|
||||
- Load testing must simulate realistic game scenarios
|
||||
- Security audit should be thorough - this is user data
|
||||
- Beta testing feedback is invaluable - listen carefully
|
||||
- Have rollback plan ready but hope not to use it
|
||||
- Monitor closely in first 48 hours post-launch
|
||||
- Be prepared for rapid response to issues
|
||||
- Communication with users is key during launch
|
||||
|
||||
---
|
||||
|
||||
**Status**: Placeholder - to be expanded during implementation
|
||||
**Deliverable**: Production-ready system replacing Google Sheets
|
||||
383
.claude/implementation/api-reference.md
Normal file
383
.claude/implementation/api-reference.md
Normal file
@ -0,0 +1,383 @@
|
||||
# API Reference
|
||||
|
||||
**Status**: Placeholder
|
||||
**Cross-Cutting Concern**: All Phases
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
REST API endpoints for game management, authentication, and data retrieval. WebSocket API covered in separate [WebSocket Protocol](./websocket-protocol.md) document.
|
||||
|
||||
## Base URLs
|
||||
|
||||
- **Production**: `https://api.paperdynasty.com`
|
||||
- **Development**: `http://localhost:8000`
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints except health checks require authentication via JWT token:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <jwt_token>
|
||||
```
|
||||
|
||||
## REST Endpoints
|
||||
|
||||
### Health & Status
|
||||
|
||||
#### GET /
|
||||
Get API information
|
||||
```json
|
||||
Response 200:
|
||||
{
|
||||
"message": "Paper Dynasty Game Backend",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/health
|
||||
Health check endpoint
|
||||
```json
|
||||
Response 200:
|
||||
{
|
||||
"status": "healthy",
|
||||
"database": "connected",
|
||||
"timestamp": "2025-10-21T19:45:23Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
#### POST /api/auth/discord/login
|
||||
Initiate Discord OAuth flow
|
||||
```json
|
||||
Response 200:
|
||||
{
|
||||
"redirect_url": "https://discord.com/api/oauth2/authorize?..."
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/auth/discord/callback
|
||||
OAuth callback endpoint (query params: code)
|
||||
```json
|
||||
Response 200:
|
||||
{
|
||||
"access_token": "jwt-token-here",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 86400,
|
||||
"user": {
|
||||
"id": "discord_snowflake_id",
|
||||
"username": "DiscordUser#1234",
|
||||
"avatar": "avatar_hash",
|
||||
"teams": [42, 99]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/auth/refresh
|
||||
Refresh JWT token
|
||||
```json
|
||||
Request:
|
||||
{
|
||||
"refresh_token": "refresh-token-here"
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"access_token": "new-jwt-token",
|
||||
"expires_in": 86400
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/auth/logout
|
||||
Logout user (invalidate token)
|
||||
```json
|
||||
Response 200:
|
||||
{
|
||||
"message": "Logged out successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Games
|
||||
|
||||
#### POST /api/games
|
||||
Create new game
|
||||
```json
|
||||
Request:
|
||||
{
|
||||
"league_id": "sba",
|
||||
"home_team_id": 42,
|
||||
"away_team_id": 99,
|
||||
"game_mode": "live", // "live", "async", "vs_ai"
|
||||
"visibility": "public" // "public", "private"
|
||||
}
|
||||
|
||||
Response 201:
|
||||
{
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "pending",
|
||||
"created_at": "2025-10-21T19:45:23Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/games/:id
|
||||
Get game details
|
||||
```json
|
||||
Response 200:
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"league_id": "sba",
|
||||
"home_team_id": 42,
|
||||
"away_team_id": 99,
|
||||
"status": "active",
|
||||
"game_mode": "live",
|
||||
"visibility": "public",
|
||||
"current_inning": 3,
|
||||
"current_half": "top",
|
||||
"home_score": 2,
|
||||
"away_score": 1,
|
||||
"created_at": "2025-10-21T19:30:00Z",
|
||||
"started_at": "2025-10-21T19:35:00Z",
|
||||
"completed_at": null,
|
||||
"winner_team_id": null
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/games
|
||||
List games (with filters)
|
||||
|
||||
Query Parameters:
|
||||
- `status` - Filter by status (pending, active, completed)
|
||||
- `league_id` - Filter by league
|
||||
- `team_id` - Filter by team participation
|
||||
- `limit` - Number of results (default: 50)
|
||||
- `offset` - Pagination offset
|
||||
|
||||
```json
|
||||
Response 200:
|
||||
{
|
||||
"games": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"league_id": "sba",
|
||||
"home_team_id": 42,
|
||||
"away_team_id": 99,
|
||||
"status": "active",
|
||||
"current_inning": 3,
|
||||
"home_score": 2,
|
||||
"away_score": 1,
|
||||
"created_at": "2025-10-21T19:30:00Z"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"limit": 50,
|
||||
"offset": 0
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/games/:id/history
|
||||
Get play-by-play history
|
||||
```json
|
||||
Response 200:
|
||||
{
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"plays": [
|
||||
{
|
||||
"play_number": 1,
|
||||
"inning": 1,
|
||||
"half": "top",
|
||||
"batter_id": 12345,
|
||||
"pitcher_id": 67890,
|
||||
"dice_roll": 5,
|
||||
"hit_type": "groundout",
|
||||
"result_description": "Groundout to shortstop",
|
||||
"runs_scored": 0,
|
||||
"outs_recorded": 1
|
||||
}
|
||||
],
|
||||
"total_plays": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### DELETE /api/games/:id
|
||||
Delete/abandon game (admin only)
|
||||
```json
|
||||
Response 200:
|
||||
{
|
||||
"message": "Game deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Lineups
|
||||
|
||||
#### GET /api/games/:id/lineups
|
||||
Get lineups for game
|
||||
```json
|
||||
Response 200:
|
||||
{
|
||||
"home": [
|
||||
{
|
||||
"card_id": 12345,
|
||||
"position": "CF",
|
||||
"batting_order": 1,
|
||||
"is_active": true,
|
||||
"player": {
|
||||
"id": 999,
|
||||
"name": "Mike Trout",
|
||||
"image": "https://..."
|
||||
}
|
||||
}
|
||||
],
|
||||
"away": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### Spectate
|
||||
|
||||
#### GET /api/spectate/:id/join
|
||||
Get spectator link for game
|
||||
```json
|
||||
Response 200:
|
||||
{
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"spectator_link": "https://sba.paperdynasty.com/spectate/550e8400-e29b-41d4-a716-446655440000",
|
||||
"can_spectate": true
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
### 400 Bad Request
|
||||
```json
|
||||
{
|
||||
"error": "Bad Request",
|
||||
"message": "Invalid game_mode: must be 'live', 'async', or 'vs_ai'",
|
||||
"details": {
|
||||
"field": "game_mode",
|
||||
"provided": "invalid_mode"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 401 Unauthorized
|
||||
```json
|
||||
{
|
||||
"error": "Unauthorized",
|
||||
"message": "Invalid or expired token"
|
||||
}
|
||||
```
|
||||
|
||||
### 403 Forbidden
|
||||
```json
|
||||
{
|
||||
"error": "Forbidden",
|
||||
"message": "You do not have permission to perform this action"
|
||||
}
|
||||
```
|
||||
|
||||
### 404 Not Found
|
||||
```json
|
||||
{
|
||||
"error": "Not Found",
|
||||
"message": "Game not found"
|
||||
}
|
||||
```
|
||||
|
||||
### 429 Too Many Requests
|
||||
```json
|
||||
{
|
||||
"error": "Rate Limit Exceeded",
|
||||
"message": "Too many requests. Please try again in 60 seconds.",
|
||||
"retry_after": 60
|
||||
}
|
||||
```
|
||||
|
||||
### 500 Internal Server Error
|
||||
```json
|
||||
{
|
||||
"error": "Internal Server Error",
|
||||
"message": "An unexpected error occurred",
|
||||
"request_id": "abc123xyz"
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limits
|
||||
|
||||
- **Authenticated requests**: 100 requests/minute per user
|
||||
- **Unauthenticated requests**: 20 requests/minute per IP
|
||||
- **Game creation**: 5 games/hour per user
|
||||
|
||||
Headers included in response:
|
||||
```http
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1735689600
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
List endpoints support pagination via `limit` and `offset`:
|
||||
|
||||
```http
|
||||
GET /api/games?limit=25&offset=50
|
||||
```
|
||||
|
||||
Response includes pagination metadata:
|
||||
```json
|
||||
{
|
||||
"data": [...],
|
||||
"pagination": {
|
||||
"total": 200,
|
||||
"limit": 25,
|
||||
"offset": 50,
|
||||
"has_more": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CORS
|
||||
|
||||
CORS headers are configured for allowed origins:
|
||||
- `https://sba.paperdynasty.com`
|
||||
- `https://pd.paperdynasty.com`
|
||||
- `http://localhost:3000` (development)
|
||||
- `http://localhost:3001` (development)
|
||||
|
||||
## OpenAPI Specification
|
||||
|
||||
Full OpenAPI/Swagger documentation available at:
|
||||
- **Development**: `http://localhost:8000/docs`
|
||||
- **Production**: `https://api.paperdynasty.com/docs`
|
||||
|
||||
## WebSocket API
|
||||
|
||||
WebSocket events documented separately in [WebSocket Protocol](./websocket-protocol.md).
|
||||
|
||||
## League-Specific APIs
|
||||
|
||||
The game backend integrates with league-specific REST APIs for team/player data:
|
||||
|
||||
### SBA League API
|
||||
- Base URL: Configured via `SBA_API_URL` env var
|
||||
- Authentication: API key via `SBA_API_KEY` env var
|
||||
|
||||
### PD League API
|
||||
- Base URL: Configured via `PD_API_URL` env var
|
||||
- Authentication: API key via `PD_API_KEY` env var
|
||||
|
||||
**Endpoints used**:
|
||||
- `GET /api/teams/:id` - Team details
|
||||
- `GET /api/teams/:id/roster` - Team roster
|
||||
- `GET /api/players/:id` - Player details
|
||||
- `GET /api/cards/:id` - Card details
|
||||
|
||||
## Reference Documents
|
||||
|
||||
- [WebSocket Protocol](./websocket-protocol.md) - Real-time event API
|
||||
- [Backend Architecture](./backend-architecture.md) - Implementation details
|
||||
- [PRD Lines 1106-1127](../prd-web-scorecard-1.1.md) - API endpoint reference
|
||||
|
||||
---
|
||||
|
||||
**Note**: This is a placeholder to be expanded with complete request/response examples during implementation. Full OpenAPI spec will be auto-generated by FastAPI.
|
||||
104
.claude/implementation/auth-system.md
Normal file
104
.claude/implementation/auth-system.md
Normal file
@ -0,0 +1,104 @@
|
||||
# Authentication & Authorization System
|
||||
|
||||
**Status**: Placeholder
|
||||
**Cross-Cutting Concern**: All Phases
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Discord OAuth-based authentication system with JWT session management and role-based access control for game actions.
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. Discord OAuth Flow
|
||||
|
||||
**Authorization Request**:
|
||||
```
|
||||
https://discord.com/api/oauth2/authorize
|
||||
?client_id={CLIENT_ID}
|
||||
&redirect_uri={REDIRECT_URI}
|
||||
&response_type=code
|
||||
&scope=identify
|
||||
```
|
||||
|
||||
**Token Exchange** (Backend):
|
||||
- Receive authorization code from callback
|
||||
- Exchange code for access token
|
||||
- Fetch user profile from Discord API
|
||||
- Lookup/create user record
|
||||
- Generate JWT token
|
||||
- Return JWT to frontend
|
||||
|
||||
**JWT Payload**:
|
||||
```json
|
||||
{
|
||||
"user_id": "discord_snowflake_id",
|
||||
"username": "DiscordUser#1234",
|
||||
"teams": [42, 99], // Team IDs user owns
|
||||
"exp": 1735689600, // Expiration timestamp
|
||||
"iat": 1735603200 // Issued at timestamp
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Session Management
|
||||
|
||||
**Frontend**:
|
||||
- Store JWT in localStorage or cookie
|
||||
- Include JWT in WebSocket auth header
|
||||
- Include JWT in HTTP API requests (Authorization: Bearer)
|
||||
- Auto-refresh before expiration (optional)
|
||||
- Clear on logout
|
||||
|
||||
**Backend**:
|
||||
- Verify JWT signature on every request
|
||||
- Check expiration
|
||||
- Extract user_id and teams from payload
|
||||
- Attach to request context
|
||||
|
||||
### 3. Authorization Rules
|
||||
|
||||
**Game Actions**:
|
||||
- User must own the team to make decisions
|
||||
- Spectators have read-only access
|
||||
- No actions allowed after game completion
|
||||
|
||||
**API Endpoints**:
|
||||
- `/api/games` - Authenticated users only
|
||||
- `/api/games/:id` - Public if game visibility is public
|
||||
- `/api/games/:id/join` - Must own a team in the game
|
||||
|
||||
**WebSocket Events**:
|
||||
- All action events require team ownership validation
|
||||
- Spectators can only receive events, not emit actions
|
||||
|
||||
### 4. Security Considerations
|
||||
|
||||
- **Token Storage**: HttpOnly cookies preferred over localStorage
|
||||
- **Token Expiration**: 24 hours, refresh after 12 hours
|
||||
- **Rate Limiting**: Per-user action limits
|
||||
- **Logout**: Invalidate token (blacklist or short expiry)
|
||||
- **HTTPS Only**: All communication encrypted
|
||||
|
||||
## Implementation Files
|
||||
|
||||
**Backend**:
|
||||
- `backend/app/api/routes/auth.py` - OAuth endpoints
|
||||
- `backend/app/utils/auth.py` - JWT utilities
|
||||
- `backend/app/middleware/auth.py` - Request authentication
|
||||
- `backend/app/websocket/auth.py` - WebSocket authentication
|
||||
|
||||
**Frontend**:
|
||||
- `frontend-{league}/pages/auth/login.vue` - Login page
|
||||
- `frontend-{league}/pages/auth/callback.vue` - OAuth callback
|
||||
- `frontend-{league}/store/auth.ts` - Auth state management
|
||||
- `frontend-{league}/middleware/auth.ts` - Route guards
|
||||
|
||||
## Reference Documents
|
||||
|
||||
- [PRD Lines 135-146](../prd-web-scorecard-1.1.md) - Authentication requirements
|
||||
- [PRD Lines 686-706](../prd-web-scorecard-1.1.md) - Security requirements
|
||||
|
||||
---
|
||||
|
||||
**Note**: This is a placeholder to be expanded with detailed implementation code during Phase 1.
|
||||
484
.claude/implementation/backend-architecture.md
Normal file
484
.claude/implementation/backend-architecture.md
Normal file
@ -0,0 +1,484 @@
|
||||
# Backend Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
FastAPI-based game backend serving as the central game engine for both SBA and PD leagues. Handles real-time WebSocket communication, game state management, and database persistence.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── main.py # FastAPI app initialization, CORS, middleware
|
||||
│ ├── config.py # Environment variables, settings
|
||||
│ │
|
||||
│ ├── core/ # Core game logic
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── game_engine.py # Main game simulation engine
|
||||
│ │ ├── state_manager.py # In-memory state management
|
||||
│ │ ├── play_resolver.py # Play outcome resolution logic
|
||||
│ │ ├── dice.py # Cryptographic random roll generation
|
||||
│ │ └── validators.py # Game rule validation
|
||||
│ │
|
||||
│ ├── config/ # League configurations
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── base_config.py # Shared BaseGameConfig
|
||||
│ │ ├── league_configs.py # SBA/PD specific configs
|
||||
│ │ ├── result_charts.py # d20 outcome tables
|
||||
│ │ └── loader.py # Config loading utilities
|
||||
│ │
|
||||
│ ├── models/ # Data models
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── game_models.py # Pydantic models for game state
|
||||
│ │ ├── player_models.py # Polymorphic player models
|
||||
│ │ ├── db_models.py # SQLAlchemy ORM models
|
||||
│ │ └── api_schemas.py # Request/response schemas
|
||||
│ │
|
||||
│ ├── websocket/ # WebSocket handling
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── connection_manager.py # Connection lifecycle
|
||||
│ │ ├── events.py # Event definitions
|
||||
│ │ ├── handlers.py # Action handlers
|
||||
│ │ └── rooms.py # Socket.io room management
|
||||
│ │
|
||||
│ ├── api/ # REST API endpoints
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── routes/
|
||||
│ │ │ ├── games.py # Game CRUD operations
|
||||
│ │ │ ├── auth.py # Discord OAuth endpoints
|
||||
│ │ │ └── health.py # Health check endpoints
|
||||
│ │ └── dependencies.py # FastAPI dependencies
|
||||
│ │
|
||||
│ ├── database/ # Database operations
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── session.py # DB connection management
|
||||
│ │ ├── operations.py # Async DB operations
|
||||
│ │ └── migrations/ # Alembic migrations
|
||||
│ │
|
||||
│ ├── data/ # External data integration
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── api_client.py # League REST API client
|
||||
│ │ ├── cache.py # Optional caching layer
|
||||
│ │ └── models.py # API response models
|
||||
│ │
|
||||
│ └── utils/ # Utilities
|
||||
│ ├── __init__.py
|
||||
│ ├── logging.py # Logging configuration
|
||||
│ ├── auth.py # JWT token handling
|
||||
│ └── errors.py # Custom exceptions
|
||||
│
|
||||
├── tests/
|
||||
│ ├── unit/ # Unit tests
|
||||
│ │ ├── test_game_engine.py
|
||||
│ │ ├── test_dice.py
|
||||
│ │ ├── test_play_resolver.py
|
||||
│ │ └── test_player_models.py
|
||||
│ ├── integration/ # Integration tests
|
||||
│ │ ├── test_websocket.py
|
||||
│ │ ├── test_database.py
|
||||
│ │ └── test_api_client.py
|
||||
│ └── e2e/ # End-to-end tests
|
||||
│ └── test_full_game.py
|
||||
│
|
||||
├── alembic.ini # Alembic configuration
|
||||
├── requirements.txt # Python dependencies
|
||||
├── requirements-dev.txt # Development dependencies
|
||||
├── docker-compose.yml # Local development setup
|
||||
├── Dockerfile # Production container
|
||||
└── pytest.ini # Pytest configuration
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Game Engine (`core/game_engine.py`)
|
||||
|
||||
**Responsibilities**:
|
||||
- Manage in-memory game state
|
||||
- Process player actions
|
||||
- Coordinate play resolution
|
||||
- Emit events to WebSocket clients
|
||||
|
||||
**Key Methods**:
|
||||
```python
|
||||
class GameEngine:
|
||||
def __init__(self, game_id: str, config: LeagueConfig)
|
||||
async def start_game(self) -> None
|
||||
async def process_defensive_positioning(self, user_id: str, positioning: str) -> PlayOutcome
|
||||
async def process_stolen_base_attempt(self, runner_positions: List[str]) -> PlayOutcome
|
||||
async def process_offensive_approach(self, approach: str) -> PlayOutcome
|
||||
async def make_substitution(self, card_id: int, position: str) -> None
|
||||
async def change_pitcher(self, card_id: int) -> None
|
||||
def get_current_state(self) -> GameState
|
||||
async def persist_play(self, play: Play) -> None
|
||||
```
|
||||
|
||||
### 2. State Manager (`core/state_manager.py`)
|
||||
|
||||
**Responsibilities**:
|
||||
- Hold active game states in memory
|
||||
- Provide fast read/write access
|
||||
- Handle state recovery from database
|
||||
- Manage state lifecycle (creation, updates, cleanup)
|
||||
|
||||
**Key Methods**:
|
||||
```python
|
||||
class StateManager:
|
||||
def create_game_state(self, game_id: str, config: LeagueConfig) -> GameState
|
||||
def get_game_state(self, game_id: str) -> Optional[GameState]
|
||||
def update_state(self, game_id: str, updates: dict) -> None
|
||||
async def recover_state(self, game_id: str) -> GameState
|
||||
def remove_state(self, game_id: str) -> None
|
||||
def get_active_games(self) -> List[str]
|
||||
```
|
||||
|
||||
**Data Structure**:
|
||||
```python
|
||||
@dataclass
|
||||
class GameState:
|
||||
game_id: str
|
||||
league_id: str
|
||||
inning: int
|
||||
half: str # 'top' or 'bottom'
|
||||
outs: int
|
||||
balls: int
|
||||
strikes: int
|
||||
home_score: int
|
||||
away_score: int
|
||||
runners: Dict[str, Optional[int]] # {'first': card_id, 'second': None, 'third': card_id}
|
||||
current_batter_idx: int
|
||||
lineups: Dict[str, List[Lineup]] # {'home': [...], 'away': [...]}
|
||||
current_decisions: Dict[str, Any] # Pending decisions
|
||||
play_history: List[int] # Play IDs
|
||||
metadata: dict
|
||||
```
|
||||
|
||||
### 3. Play Resolver (`core/play_resolver.py`)
|
||||
|
||||
**Responsibilities**:
|
||||
- Roll dice and determine outcomes
|
||||
- Apply league-specific result charts
|
||||
- Calculate runner advancement
|
||||
- Update game state based on outcome
|
||||
|
||||
**Key Methods**:
|
||||
```python
|
||||
class PlayResolver:
|
||||
def __init__(self, config: LeagueConfig)
|
||||
def roll_dice(self) -> int
|
||||
def resolve_at_bat(
|
||||
self,
|
||||
batter: PdPlayer | SbaPlayer,
|
||||
pitcher: PdPlayer | SbaPlayer,
|
||||
defensive_positioning: str,
|
||||
offensive_approach: str,
|
||||
count: Dict[str, int]
|
||||
) -> PlayOutcome
|
||||
def advance_runners(
|
||||
self,
|
||||
hit_type: str,
|
||||
runners_before: Dict[str, Optional[int]],
|
||||
hit_location: Optional[str] = None
|
||||
) -> Tuple[Dict[str, Optional[int]], int] # (runners_after, runs_scored)
|
||||
```
|
||||
|
||||
### 4. Polymorphic Player Models (`models/player_models.py`)
|
||||
|
||||
**Class Hierarchy**:
|
||||
```python
|
||||
class BasePlayer(BaseModel, ABC):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
@abstractmethod
|
||||
def get_image_url(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_display_data(self) -> dict:
|
||||
pass
|
||||
|
||||
class SbaPlayer(BasePlayer):
|
||||
image: str
|
||||
team: Optional[str] = None
|
||||
manager: Optional[str] = None
|
||||
|
||||
def get_image_url(self) -> str:
|
||||
return self.image
|
||||
|
||||
class PdPlayer(BasePlayer):
|
||||
image: str
|
||||
scouting_data: dict
|
||||
ratings: dict
|
||||
probabilities: dict
|
||||
|
||||
def get_image_url(self) -> str:
|
||||
return self.image
|
||||
|
||||
class Lineup(BaseModel):
|
||||
id: int
|
||||
game_id: str
|
||||
card_id: int
|
||||
position: str
|
||||
batting_order: Optional[int] = None
|
||||
player: Union[SbaPlayer, PdPlayer]
|
||||
|
||||
@classmethod
|
||||
def from_api_data(cls, game_config: LeagueConfig, api_data: dict) -> 'Lineup':
|
||||
player_data = api_data.pop('player')
|
||||
if game_config.league_id == "sba":
|
||||
player = SbaPlayer(**player_data)
|
||||
elif game_config.league_id == "pd":
|
||||
player = PdPlayer(**player_data)
|
||||
else:
|
||||
raise ValueError(f"Unknown league: {game_config.league_id}")
|
||||
return cls(player=player, **api_data)
|
||||
```
|
||||
|
||||
### 5. WebSocket Connection Manager (`websocket/connection_manager.py`)
|
||||
|
||||
**Responsibilities**:
|
||||
- Manage Socket.io connections
|
||||
- Handle room assignments (game rooms)
|
||||
- Broadcast events to participants
|
||||
- Handle disconnections and reconnections
|
||||
|
||||
**Key Methods**:
|
||||
```python
|
||||
class ConnectionManager:
|
||||
def __init__(self, sio: socketio.AsyncServer)
|
||||
async def connect(self, sid: str, user_id: str) -> None
|
||||
async def disconnect(self, sid: str) -> None
|
||||
async def join_game(self, sid: str, game_id: str, role: str) -> None
|
||||
async def leave_game(self, sid: str, game_id: str) -> None
|
||||
async def broadcast_to_game(self, game_id: str, event: str, data: dict) -> None
|
||||
async def emit_to_user(self, sid: str, event: str, data: dict) -> None
|
||||
def get_game_participants(self, game_id: str) -> List[str]
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Game Action Processing
|
||||
|
||||
```
|
||||
1. Client sends WebSocket event
|
||||
↓
|
||||
2. handlers.py receives and validates
|
||||
↓
|
||||
3. Verify user authorization
|
||||
↓
|
||||
4. Load game state from StateManager
|
||||
↓
|
||||
5. GameEngine processes action
|
||||
↓
|
||||
6. PlayResolver determines outcome (if dice roll needed)
|
||||
↓
|
||||
7. StateManager updates in-memory state
|
||||
↓
|
||||
8. Database operations (async, non-blocking)
|
||||
↓
|
||||
9. ConnectionManager broadcasts update to all clients
|
||||
↓
|
||||
10. Clients update UI
|
||||
```
|
||||
|
||||
### State Recovery on Reconnect
|
||||
|
||||
```
|
||||
1. Client reconnects to WebSocket
|
||||
↓
|
||||
2. Client sends join_game event
|
||||
↓
|
||||
3. Check if state exists in StateManager
|
||||
↓
|
||||
4. If not in memory:
|
||||
a. Load game metadata from database
|
||||
b. Load all plays from database
|
||||
c. Replay plays through GameEngine
|
||||
d. Store reconstructed state in StateManager
|
||||
↓
|
||||
5. Send current state to client
|
||||
↓
|
||||
6. Client resumes gameplay
|
||||
```
|
||||
|
||||
## League Configuration System
|
||||
|
||||
### Config Loading
|
||||
|
||||
```python
|
||||
# config/base_config.py
|
||||
@dataclass
|
||||
class BaseGameConfig:
|
||||
innings: int = 9
|
||||
outs_per_inning: int = 3
|
||||
strikes_for_out: int = 3
|
||||
balls_for_walk: int = 4
|
||||
roster_size: int = 26
|
||||
positions: List[str] = field(default_factory=lambda: ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'])
|
||||
|
||||
# config/league_configs.py
|
||||
@dataclass
|
||||
class SbaConfig(BaseGameConfig):
|
||||
league_id: str = "sba"
|
||||
league_name: str = "Super Baseball Alliance"
|
||||
api_url: str = field(default_factory=lambda: os.getenv("SBA_API_URL"))
|
||||
result_charts: dict = field(default_factory=lambda: load_sba_charts())
|
||||
special_rules: dict = field(default_factory=dict)
|
||||
|
||||
@dataclass
|
||||
class PdConfig(BaseGameConfig):
|
||||
league_id: str = "pd"
|
||||
league_name: str = "Paper Dynasty"
|
||||
api_url: str = field(default_factory=lambda: os.getenv("PD_API_URL"))
|
||||
result_charts: dict = field(default_factory=lambda: load_pd_charts())
|
||||
special_rules: dict = field(default_factory=dict)
|
||||
|
||||
# config/loader.py
|
||||
def get_league_config(league_id: str) -> BaseGameConfig:
|
||||
configs = {
|
||||
"sba": SbaConfig,
|
||||
"pd": PdConfig
|
||||
}
|
||||
if league_id not in configs:
|
||||
raise ValueError(f"Unknown league: {league_id}")
|
||||
return configs[league_id]()
|
||||
```
|
||||
|
||||
## Database Integration
|
||||
|
||||
### Async Operations
|
||||
|
||||
All database operations use async patterns to avoid blocking game logic:
|
||||
|
||||
```python
|
||||
# database/operations.py
|
||||
async def save_play(play: Play) -> int:
|
||||
"""Save play to database asynchronously"""
|
||||
async with get_session() as session:
|
||||
db_play = DbPlay(**play.dict())
|
||||
session.add(db_play)
|
||||
await session.commit()
|
||||
await session.refresh(db_play)
|
||||
return db_play.id
|
||||
|
||||
async def load_game_plays(game_id: str) -> List[Play]:
|
||||
"""Load all plays for a game"""
|
||||
async with get_session() as session:
|
||||
result = await session.execute(
|
||||
select(DbPlay)
|
||||
.where(DbPlay.game_id == game_id)
|
||||
.order_by(DbPlay.play_number)
|
||||
)
|
||||
db_plays = result.scalars().all()
|
||||
return [Play.from_orm(p) for p in db_plays]
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Authentication
|
||||
- Discord OAuth tokens validated on REST endpoints
|
||||
- WebSocket connections require valid JWT token
|
||||
- Token expiration and refresh handled
|
||||
|
||||
### Authorization
|
||||
- Verify user owns team before allowing actions
|
||||
- Spectator permissions enforced (read-only)
|
||||
- Rate limiting on API endpoints
|
||||
|
||||
### Game Integrity
|
||||
- All rule validation server-side
|
||||
- Cryptographically secure dice rolls
|
||||
- Audit trail of all plays
|
||||
- State validation before each action
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### In-Memory State
|
||||
- Active games kept in memory for fast access
|
||||
- Idle games evicted after timeout (configurable)
|
||||
- State recovery only when needed
|
||||
|
||||
### Async Database Writes
|
||||
- Play persistence doesn't block game logic
|
||||
- Connection pooling for efficiency
|
||||
- Batch writes where appropriate
|
||||
|
||||
### Caching Layer (Optional)
|
||||
- Cache team/roster data from league APIs
|
||||
- Redis for distributed caching (if multi-server)
|
||||
- TTL-based cache invalidation
|
||||
|
||||
## Logging Strategy
|
||||
|
||||
### Log Levels
|
||||
- **DEBUG**: State transitions, action processing details
|
||||
- **INFO**: Game started/completed, player joins/leaves
|
||||
- **WARNING**: Invalid actions, timeouts, retry attempts
|
||||
- **ERROR**: Database errors, API failures, state corruption
|
||||
- **CRITICAL**: System failures requiring immediate attention
|
||||
|
||||
### Log Format
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(f'{__name__}.GameEngine')
|
||||
|
||||
# Usage
|
||||
logger.info(f"Game {game_id} started", extra={
|
||||
"game_id": game_id,
|
||||
"league_id": league_id,
|
||||
"home_team": home_team_id,
|
||||
"away_team": away_team_id
|
||||
})
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Custom Exceptions
|
||||
```python
|
||||
class GameEngineError(Exception):
|
||||
"""Base exception for game engine errors"""
|
||||
pass
|
||||
|
||||
class InvalidActionError(GameEngineError):
|
||||
"""Raised when action violates game rules"""
|
||||
pass
|
||||
|
||||
class StateRecoveryError(GameEngineError):
|
||||
"""Raised when state cannot be recovered"""
|
||||
pass
|
||||
|
||||
class UnauthorizedActionError(GameEngineError):
|
||||
"""Raised when user not authorized for action"""
|
||||
pass
|
||||
```
|
||||
|
||||
### Error Recovery
|
||||
- Automatic reconnection for database issues
|
||||
- State recovery from last known good state
|
||||
- Graceful degradation when league API unavailable
|
||||
- Transaction rollback on database errors
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Test each core component in isolation
|
||||
- Mock external dependencies (DB, APIs)
|
||||
- Verify dice roll randomness and distribution
|
||||
- Test all play resolution scenarios
|
||||
|
||||
### Integration Tests
|
||||
- Test WebSocket event flow
|
||||
- Test database persistence and recovery
|
||||
- Test league config loading
|
||||
- Test API client interactions
|
||||
|
||||
### Performance Tests
|
||||
- Measure action processing time
|
||||
- Test concurrent game handling
|
||||
- Monitor memory usage under load
|
||||
- Test state recovery speed
|
||||
|
||||
---
|
||||
|
||||
**Next Steps**: See [02-game-engine.md](./02-game-engine.md) for implementation details.
|
||||
687
.claude/implementation/database-design.md
Normal file
687
.claude/implementation/database-design.md
Normal file
@ -0,0 +1,687 @@
|
||||
# Database Design
|
||||
|
||||
## Overview
|
||||
|
||||
PostgreSQL database for persistent storage of game data, play history, and session management. Designed for fast writes (async) and efficient recovery.
|
||||
|
||||
## Schema
|
||||
|
||||
### Games Table
|
||||
|
||||
Stores game metadata and current state.
|
||||
|
||||
```sql
|
||||
CREATE TABLE games (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
league_id VARCHAR(50) NOT NULL,
|
||||
home_team_id INTEGER NOT NULL,
|
||||
away_team_id INTEGER NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
game_mode VARCHAR(20) NOT NULL,
|
||||
visibility VARCHAR(20) NOT NULL,
|
||||
current_inning INTEGER,
|
||||
current_half VARCHAR(10),
|
||||
home_score INTEGER DEFAULT 0,
|
||||
away_score INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
winner_team_id INTEGER,
|
||||
metadata JSONB DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_games_league ON games(league_id);
|
||||
CREATE INDEX idx_games_status ON games(status);
|
||||
CREATE INDEX idx_games_teams ON games(home_team_id, away_team_id);
|
||||
CREATE INDEX idx_games_created ON games(created_at DESC);
|
||||
|
||||
-- Comments
|
||||
COMMENT ON COLUMN games.status IS 'pending, active, completed, abandoned';
|
||||
COMMENT ON COLUMN games.game_mode IS 'live, async, vs_ai';
|
||||
COMMENT ON COLUMN games.visibility IS 'public, private';
|
||||
COMMENT ON COLUMN games.metadata IS 'League-specific data, config version, etc.';
|
||||
```
|
||||
|
||||
### Plays Table
|
||||
|
||||
Records every play that occurs in a game.
|
||||
|
||||
```sql
|
||||
CREATE TABLE plays (
|
||||
id SERIAL PRIMARY KEY,
|
||||
game_id UUID NOT NULL REFERENCES games(id) ON DELETE CASCADE,
|
||||
play_number INTEGER NOT NULL,
|
||||
inning INTEGER NOT NULL,
|
||||
half VARCHAR(10) NOT NULL,
|
||||
outs_before INTEGER NOT NULL,
|
||||
outs_recorded INTEGER NOT NULL,
|
||||
batter_id INTEGER NOT NULL,
|
||||
pitcher_id INTEGER NOT NULL,
|
||||
runners_before JSONB,
|
||||
runners_after JSONB,
|
||||
balls INTEGER,
|
||||
strikes INTEGER,
|
||||
defensive_positioning VARCHAR(50),
|
||||
offensive_approach VARCHAR(50),
|
||||
dice_roll INTEGER,
|
||||
hit_type VARCHAR(50),
|
||||
result_description TEXT,
|
||||
runs_scored INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
|
||||
UNIQUE(game_id, play_number)
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_plays_game ON plays(game_id);
|
||||
CREATE INDEX idx_plays_game_number ON plays(game_id, play_number);
|
||||
CREATE INDEX idx_plays_created ON plays(created_at);
|
||||
|
||||
-- Comments
|
||||
COMMENT ON COLUMN plays.half IS 'top or bottom';
|
||||
COMMENT ON COLUMN plays.runners_before IS '{"first": card_id, "second": null, "third": card_id}';
|
||||
COMMENT ON COLUMN plays.hit_type IS 'single, double, triple, homerun, out, walk, strikeout, etc.';
|
||||
COMMENT ON COLUMN plays.metadata IS 'Additional play data, stolen base results, etc.';
|
||||
```
|
||||
|
||||
### Lineups Table
|
||||
|
||||
Stores lineup information for each game.
|
||||
|
||||
```sql
|
||||
CREATE TABLE lineups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
game_id UUID NOT NULL REFERENCES games(id) ON DELETE CASCADE,
|
||||
team_id INTEGER NOT NULL,
|
||||
card_id INTEGER NOT NULL,
|
||||
position VARCHAR(10) NOT NULL,
|
||||
batting_order INTEGER,
|
||||
is_starter BOOLEAN DEFAULT TRUE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
entered_inning INTEGER DEFAULT 1,
|
||||
metadata JSONB DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_lineups_game ON lineups(game_id);
|
||||
CREATE INDEX idx_lineups_team ON lineups(team_id);
|
||||
CREATE INDEX idx_lineups_active ON lineups(game_id, is_active);
|
||||
CREATE INDEX idx_lineups_game_team ON lineups(game_id, team_id);
|
||||
|
||||
-- Comments
|
||||
COMMENT ON COLUMN lineups.position IS 'P, C, 1B, 2B, 3B, SS, LF, CF, RF, DH';
|
||||
COMMENT ON COLUMN lineups.is_active IS 'FALSE when substituted out';
|
||||
COMMENT ON COLUMN lineups.entered_inning IS 'Inning when player entered game';
|
||||
```
|
||||
|
||||
### Game Sessions Table
|
||||
|
||||
Tracks active WebSocket sessions and state snapshots.
|
||||
|
||||
```sql
|
||||
CREATE TABLE game_sessions (
|
||||
game_id UUID PRIMARY KEY REFERENCES games(id) ON DELETE CASCADE,
|
||||
connected_users JSONB DEFAULT '{}'::jsonb,
|
||||
last_action_at TIMESTAMP DEFAULT NOW(),
|
||||
state_snapshot JSONB DEFAULT '{}'::jsonb,
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_sessions_last_action ON game_sessions(last_action_at);
|
||||
|
||||
-- Comments
|
||||
COMMENT ON COLUMN game_sessions.connected_users IS '{"user_id": {"role": "home_gm", "last_seen": "timestamp"}}';
|
||||
COMMENT ON COLUMN game_sessions.state_snapshot IS 'Full game state for quick recovery';
|
||||
```
|
||||
|
||||
### Users Table (Optional - if not using external auth exclusively)
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
discord_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
discord_username VARCHAR(100),
|
||||
discord_avatar VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
last_login TIMESTAMP,
|
||||
metadata JSONB DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_users_discord ON users(discord_id);
|
||||
|
||||
-- Comments
|
||||
COMMENT ON TABLE users IS 'Optional user cache if needed for offline queries';
|
||||
```
|
||||
|
||||
## Data Types & Constraints
|
||||
|
||||
### JSONB Fields
|
||||
|
||||
#### games.metadata
|
||||
```json
|
||||
{
|
||||
"league_config_version": "1.0",
|
||||
"home_gm_id": "discord_snowflake_123",
|
||||
"away_gm_id": "discord_snowflake_456",
|
||||
"home_gm_ready": true,
|
||||
"away_gm_ready": false,
|
||||
"starting_pitcher_home": 12345,
|
||||
"starting_pitcher_away": 67890
|
||||
}
|
||||
```
|
||||
|
||||
#### plays.runners_before / runners_after
|
||||
```json
|
||||
{
|
||||
"first": 12345, // card_id or null
|
||||
"second": null,
|
||||
"third": 67890
|
||||
}
|
||||
```
|
||||
|
||||
#### plays.metadata
|
||||
```json
|
||||
{
|
||||
"stolen_base_attempts": {
|
||||
"second": true,
|
||||
"third": false
|
||||
},
|
||||
"stolen_base_results": {
|
||||
"second": "safe"
|
||||
},
|
||||
"hit_location": "left_field",
|
||||
"fielder": 54321
|
||||
}
|
||||
```
|
||||
|
||||
#### lineups.metadata
|
||||
```json
|
||||
{
|
||||
"substitution_reason": "pinch_hitter",
|
||||
"replaced_card_id": 11111,
|
||||
"pitcher_stats": {
|
||||
"innings_pitched": 6.0,
|
||||
"hits_allowed": 4,
|
||||
"runs_allowed": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### game_sessions.state_snapshot
|
||||
```json
|
||||
{
|
||||
"inning": 3,
|
||||
"half": "top",
|
||||
"outs": 2,
|
||||
"balls": 2,
|
||||
"strikes": 1,
|
||||
"home_score": 2,
|
||||
"away_score": 1,
|
||||
"runners": {"first": null, "second": 12345, "third": null},
|
||||
"current_batter_idx": 3,
|
||||
"current_decisions": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Queries
|
||||
|
||||
### Common Read Queries
|
||||
|
||||
#### Get Game with Latest Play
|
||||
```sql
|
||||
SELECT
|
||||
g.*,
|
||||
p.play_number as last_play,
|
||||
p.result_description as last_play_description
|
||||
FROM games g
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT * FROM plays
|
||||
WHERE game_id = g.id
|
||||
ORDER BY play_number DESC
|
||||
LIMIT 1
|
||||
) p ON TRUE
|
||||
WHERE g.id = $1;
|
||||
```
|
||||
|
||||
#### Get Active Lineup for Team
|
||||
```sql
|
||||
SELECT
|
||||
l.*,
|
||||
l.batting_order,
|
||||
l.position
|
||||
FROM lineups l
|
||||
WHERE l.game_id = $1
|
||||
AND l.team_id = $2
|
||||
AND l.is_active = TRUE
|
||||
ORDER BY l.batting_order NULLS LAST;
|
||||
```
|
||||
|
||||
#### Get Play History for Game
|
||||
```sql
|
||||
SELECT
|
||||
play_number,
|
||||
inning,
|
||||
half,
|
||||
outs_before,
|
||||
batter_id,
|
||||
pitcher_id,
|
||||
dice_roll,
|
||||
hit_type,
|
||||
result_description,
|
||||
runs_scored,
|
||||
created_at
|
||||
FROM plays
|
||||
WHERE game_id = $1
|
||||
ORDER BY play_number;
|
||||
```
|
||||
|
||||
#### Get Active Games for User
|
||||
```sql
|
||||
SELECT
|
||||
g.*,
|
||||
CASE
|
||||
WHEN g.home_team_id = ANY($2) THEN 'home'
|
||||
WHEN g.away_team_id = ANY($2) THEN 'away'
|
||||
END as role,
|
||||
(SELECT COUNT(*) FROM plays WHERE game_id = g.id) as play_count
|
||||
FROM games g
|
||||
WHERE g.status IN ('pending', 'active')
|
||||
AND (g.home_team_id = ANY($2) OR g.away_team_id = ANY($2))
|
||||
ORDER BY g.created_at DESC;
|
||||
|
||||
-- $1 = user_id, $2 = array of team_ids user owns
|
||||
```
|
||||
|
||||
#### Get Recent Completed Games
|
||||
```sql
|
||||
SELECT
|
||||
g.id,
|
||||
g.league_id,
|
||||
g.home_team_id,
|
||||
g.away_team_id,
|
||||
g.home_score,
|
||||
g.away_score,
|
||||
g.winner_team_id,
|
||||
g.completed_at,
|
||||
EXTRACT(EPOCH FROM (g.completed_at - g.started_at))/60 as duration_minutes
|
||||
FROM games g
|
||||
WHERE g.status = 'completed'
|
||||
AND g.league_id = $1
|
||||
ORDER BY g.completed_at DESC
|
||||
LIMIT 50;
|
||||
```
|
||||
|
||||
### Common Write Queries
|
||||
|
||||
#### Create New Game
|
||||
```sql
|
||||
INSERT INTO games (
|
||||
league_id,
|
||||
home_team_id,
|
||||
away_team_id,
|
||||
status,
|
||||
game_mode,
|
||||
visibility,
|
||||
metadata
|
||||
) VALUES (
|
||||
$1, $2, $3, 'pending', $4, $5, $6
|
||||
)
|
||||
RETURNING id;
|
||||
```
|
||||
|
||||
#### Insert Play
|
||||
```sql
|
||||
INSERT INTO plays (
|
||||
game_id,
|
||||
play_number,
|
||||
inning,
|
||||
half,
|
||||
outs_before,
|
||||
outs_recorded,
|
||||
batter_id,
|
||||
pitcher_id,
|
||||
runners_before,
|
||||
runners_after,
|
||||
balls,
|
||||
strikes,
|
||||
defensive_positioning,
|
||||
offensive_approach,
|
||||
dice_roll,
|
||||
hit_type,
|
||||
result_description,
|
||||
runs_scored,
|
||||
metadata
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||
$11, $12, $13, $14, $15, $16, $17, $18, $19
|
||||
)
|
||||
RETURNING id;
|
||||
```
|
||||
|
||||
#### Update Game Score
|
||||
```sql
|
||||
UPDATE games
|
||||
SET
|
||||
current_inning = $2,
|
||||
current_half = $3,
|
||||
home_score = $4,
|
||||
away_score = $5
|
||||
WHERE id = $1;
|
||||
```
|
||||
|
||||
#### Make Substitution
|
||||
```sql
|
||||
-- Deactivate old player
|
||||
UPDATE lineups
|
||||
SET is_active = FALSE
|
||||
WHERE game_id = $1
|
||||
AND card_id = $2;
|
||||
|
||||
-- Insert new player
|
||||
INSERT INTO lineups (
|
||||
game_id,
|
||||
team_id,
|
||||
card_id,
|
||||
position,
|
||||
batting_order,
|
||||
is_starter,
|
||||
is_active,
|
||||
entered_inning
|
||||
) VALUES (
|
||||
$1, $3, $4, $5, $6, FALSE, TRUE, $7
|
||||
);
|
||||
```
|
||||
|
||||
#### Complete Game
|
||||
```sql
|
||||
UPDATE games
|
||||
SET
|
||||
status = 'completed',
|
||||
completed_at = NOW(),
|
||||
winner_team_id = $2
|
||||
WHERE id = $1;
|
||||
```
|
||||
|
||||
### State Recovery Query
|
||||
|
||||
```sql
|
||||
-- Get all data needed to rebuild game state
|
||||
WITH game_info AS (
|
||||
SELECT * FROM games WHERE id = $1
|
||||
),
|
||||
lineup_data AS (
|
||||
SELECT * FROM lineups WHERE game_id = $1 AND is_active = TRUE
|
||||
),
|
||||
play_data AS (
|
||||
SELECT * FROM plays WHERE game_id = $1 ORDER BY play_number
|
||||
)
|
||||
SELECT
|
||||
json_build_object(
|
||||
'game', (SELECT row_to_json(g) FROM game_info g),
|
||||
'lineups', (SELECT json_agg(l) FROM lineup_data l),
|
||||
'plays', (SELECT json_agg(p) FROM play_data p)
|
||||
) as game_state;
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Index Strategy
|
||||
|
||||
**Games Table**:
|
||||
- `idx_games_league`: Filter by league
|
||||
- `idx_games_status`: Find active/completed games
|
||||
- `idx_games_teams`: Find games by team
|
||||
- `idx_games_created`: Recent games list
|
||||
|
||||
**Plays Table**:
|
||||
- `idx_plays_game`: All plays for a game (most common)
|
||||
- `idx_plays_game_number`: Specific play lookup
|
||||
- `idx_plays_created`: Recent plays across all games
|
||||
|
||||
**Lineups Table**:
|
||||
- `idx_lineups_game`: All lineups for a game
|
||||
- `idx_lineups_team`: Team's lineup across games
|
||||
- `idx_lineups_active`: Active players only
|
||||
- `idx_lineups_game_team`: Combined filter (most efficient)
|
||||
|
||||
### Query Optimization Tips
|
||||
|
||||
1. **Use EXPLAIN ANALYZE** for slow queries
|
||||
2. **Avoid SELECT \***: Specify needed columns
|
||||
3. **Use LATERAL joins** for correlated subqueries
|
||||
4. **Leverage JSONB indexes** if filtering on metadata fields frequently
|
||||
5. **Partition plays table** if growing very large (by created_at)
|
||||
|
||||
### Connection Pooling
|
||||
|
||||
```python
|
||||
# SQLAlchemy async engine with connection pooling
|
||||
engine = create_async_engine(
|
||||
database_url,
|
||||
pool_size=20, # Number of persistent connections
|
||||
max_overflow=10, # Additional connections when needed
|
||||
pool_timeout=30, # Seconds to wait for connection
|
||||
pool_recycle=3600, # Recycle connections after 1 hour
|
||||
pool_pre_ping=True # Verify connection health before use
|
||||
)
|
||||
```
|
||||
|
||||
## Data Integrity
|
||||
|
||||
### Foreign Key Constraints
|
||||
|
||||
All foreign keys include `ON DELETE CASCADE` to ensure referential integrity:
|
||||
- When a game is deleted, all plays and lineups are automatically deleted
|
||||
- Prevents orphaned records
|
||||
|
||||
### Check Constraints
|
||||
|
||||
```sql
|
||||
-- Add validation constraints
|
||||
ALTER TABLE games
|
||||
ADD CONSTRAINT check_status
|
||||
CHECK (status IN ('pending', 'active', 'completed', 'abandoned'));
|
||||
|
||||
ALTER TABLE games
|
||||
ADD CONSTRAINT check_game_mode
|
||||
CHECK (game_mode IN ('live', 'async', 'vs_ai'));
|
||||
|
||||
ALTER TABLE games
|
||||
ADD CONSTRAINT check_scores
|
||||
CHECK (home_score >= 0 AND away_score >= 0);
|
||||
|
||||
ALTER TABLE plays
|
||||
ADD CONSTRAINT check_half
|
||||
CHECK (half IN ('top', 'bottom'));
|
||||
|
||||
ALTER TABLE plays
|
||||
ADD CONSTRAINT check_outs
|
||||
CHECK (outs_before >= 0 AND outs_before <= 2 AND outs_recorded >= 0);
|
||||
|
||||
ALTER TABLE lineups
|
||||
ADD CONSTRAINT check_position
|
||||
CHECK (position IN ('P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH'));
|
||||
```
|
||||
|
||||
## Backup & Recovery
|
||||
|
||||
### Daily Backups
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# daily-backup.sh
|
||||
|
||||
DATE=$(date +%Y%m%d)
|
||||
BACKUP_DIR="/backups/paperdynasty"
|
||||
DB_NAME="paperdynasty"
|
||||
|
||||
# Create backup directory
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Full database backup
|
||||
pg_dump -U paperdynasty -F c -b -v -f "$BACKUP_DIR/full_backup_$DATE.dump" $DB_NAME
|
||||
|
||||
# Compress
|
||||
gzip "$BACKUP_DIR/full_backup_$DATE.dump"
|
||||
|
||||
# Keep only last 30 days
|
||||
find $BACKUP_DIR -name "full_backup_*.dump.gz" -mtime +30 -delete
|
||||
|
||||
echo "Backup completed: $DATE"
|
||||
```
|
||||
|
||||
### Point-in-Time Recovery Setup
|
||||
|
||||
```sql
|
||||
-- Enable WAL archiving (postgresql.conf)
|
||||
wal_level = replica
|
||||
archive_mode = on
|
||||
archive_command = 'cp %p /var/lib/postgresql/wal_archive/%f'
|
||||
```
|
||||
|
||||
### Recovery Procedure
|
||||
|
||||
```bash
|
||||
# Restore from backup
|
||||
pg_restore -U paperdynasty -d paperdynasty -c backup_file.dump
|
||||
|
||||
# Verify data
|
||||
psql -U paperdynasty -d paperdynasty -c "SELECT COUNT(*) FROM games;"
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Using Alembic
|
||||
|
||||
```bash
|
||||
# Initialize Alembic
|
||||
cd backend
|
||||
alembic init alembic
|
||||
|
||||
# Create migration
|
||||
alembic revision -m "create initial tables"
|
||||
|
||||
# Apply migration
|
||||
alembic upgrade head
|
||||
|
||||
# Rollback
|
||||
alembic downgrade -1
|
||||
```
|
||||
|
||||
### Example Migration
|
||||
|
||||
```python
|
||||
# alembic/versions/001_create_initial_tables.py
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'games',
|
||||
sa.Column('id', UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column('league_id', sa.String(50), nullable=False),
|
||||
# ... other columns
|
||||
)
|
||||
|
||||
# Create indexes
|
||||
op.create_index('idx_games_league', 'games', ['league_id'])
|
||||
op.create_index('idx_games_status', 'games', ['status'])
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('games')
|
||||
```
|
||||
|
||||
## Monitoring Queries
|
||||
|
||||
### Active Connections
|
||||
```sql
|
||||
SELECT
|
||||
count(*) as total_connections,
|
||||
count(*) FILTER (WHERE state = 'active') as active_queries,
|
||||
count(*) FILTER (WHERE state = 'idle') as idle_connections
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = 'paperdynasty';
|
||||
```
|
||||
|
||||
### Slow Queries
|
||||
```sql
|
||||
SELECT
|
||||
query,
|
||||
calls,
|
||||
mean_exec_time,
|
||||
max_exec_time,
|
||||
total_exec_time
|
||||
FROM pg_stat_statements
|
||||
WHERE query LIKE '%games%'
|
||||
ORDER BY mean_exec_time DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
### Table Sizes
|
||||
```sql
|
||||
SELECT
|
||||
tablename,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
|
||||
```
|
||||
|
||||
### Index Usage
|
||||
```sql
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
indexname,
|
||||
idx_scan as index_scans,
|
||||
idx_tup_read as tuples_read,
|
||||
idx_tup_fetch as tuples_fetched
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY idx_scan DESC;
|
||||
```
|
||||
|
||||
## Testing Data
|
||||
|
||||
### Seed Development Data
|
||||
|
||||
```sql
|
||||
-- Insert test game
|
||||
INSERT INTO games (id, league_id, home_team_id, away_team_id, status, game_mode, visibility)
|
||||
VALUES (
|
||||
'550e8400-e29b-41d4-a716-446655440000',
|
||||
'sba',
|
||||
1,
|
||||
2,
|
||||
'active',
|
||||
'live',
|
||||
'public'
|
||||
);
|
||||
|
||||
-- Insert test lineups
|
||||
INSERT INTO lineups (game_id, team_id, card_id, position, batting_order, is_starter, is_active)
|
||||
VALUES
|
||||
('550e8400-e29b-41d4-a716-446655440000', 1, 101, 'P', NULL, TRUE, TRUE),
|
||||
('550e8400-e29b-41d4-a716-446655440000', 1, 102, 'C', 9, TRUE, TRUE),
|
||||
('550e8400-e29b-41d4-a716-446655440000', 1, 103, 'CF', 1, TRUE, TRUE);
|
||||
-- ... more players
|
||||
|
||||
-- Insert test play
|
||||
INSERT INTO plays (
|
||||
game_id, play_number, inning, half, outs_before, outs_recorded,
|
||||
batter_id, pitcher_id, dice_roll, hit_type, result_description, runs_scored
|
||||
) VALUES (
|
||||
'550e8400-e29b-41d4-a716-446655440000',
|
||||
1, 1, 'top', 0, 1, 103, 201, 5, 'groundout',
|
||||
'Groundout to shortstop', 0
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Next Steps**: See [01-infrastructure.md](./01-infrastructure.md) for database setup instructions.
|
||||
779
.claude/implementation/frontend-architecture.md
Normal file
779
.claude/implementation/frontend-architecture.md
Normal file
@ -0,0 +1,779 @@
|
||||
# Frontend Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Two separate Nuxt 3 applications (one per league) with maximum code reuse through a shared component library. Mobile-first design with real-time WebSocket updates.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
### Per-League Frontend (`frontend-sba/` and `frontend-pd/`)
|
||||
|
||||
```
|
||||
frontend-{league}/
|
||||
├── assets/ # Static assets
|
||||
│ ├── css/
|
||||
│ │ └── tailwind.css # Tailwind imports
|
||||
│ └── images/
|
||||
│ ├── logo.png
|
||||
│ └── field-bg.svg
|
||||
│
|
||||
├── components/ # League-specific components
|
||||
│ ├── Branding/
|
||||
│ │ ├── Header.vue
|
||||
│ │ ├── Footer.vue
|
||||
│ │ └── Logo.vue
|
||||
│ └── League/
|
||||
│ └── SpecialFeatures.vue # League-specific UI elements
|
||||
│
|
||||
├── composables/ # Vue composables
|
||||
│ ├── useAuth.ts # Authentication state
|
||||
│ ├── useWebSocket.ts # WebSocket connection
|
||||
│ ├── useGameState.ts # Game state management
|
||||
│ └── useLeagueConfig.ts # League-specific config
|
||||
│
|
||||
├── layouts/
|
||||
│ ├── default.vue # Standard layout
|
||||
│ ├── game.vue # Game view layout
|
||||
│ └── auth.vue # Auth pages layout
|
||||
│
|
||||
├── pages/
|
||||
│ ├── index.vue # Home/dashboard
|
||||
│ ├── games/
|
||||
│ │ ├── [id].vue # Game view
|
||||
│ │ ├── create.vue # Create new game
|
||||
│ │ └── history.vue # Completed games
|
||||
│ ├── auth/
|
||||
│ │ ├── login.vue
|
||||
│ │ └── callback.vue # Discord OAuth callback
|
||||
│ └── spectate/
|
||||
│ └── [id].vue # Spectator view
|
||||
│
|
||||
├── plugins/
|
||||
│ ├── socket.client.ts # Socket.io plugin
|
||||
│ └── auth.ts # Auth plugin
|
||||
│
|
||||
├── store/ # Pinia stores
|
||||
│ ├── auth.ts # Authentication state
|
||||
│ ├── game.ts # Current game state
|
||||
│ ├── games.ts # Games list
|
||||
│ └── ui.ts # UI state (modals, toasts)
|
||||
│
|
||||
├── types/
|
||||
│ ├── game.ts # Game-related types
|
||||
│ ├── player.ts # Player types
|
||||
│ ├── api.ts # API response types
|
||||
│ └── websocket.ts # WebSocket event types
|
||||
│
|
||||
├── utils/
|
||||
│ ├── api.ts # API client
|
||||
│ ├── formatters.ts # Data formatting utilities
|
||||
│ └── validators.ts # Input validation
|
||||
│
|
||||
├── middleware/
|
||||
│ ├── auth.ts # Auth guard
|
||||
│ └── game-access.ts # Game access validation
|
||||
│
|
||||
├── app.vue # Root component
|
||||
├── nuxt.config.ts # Nuxt configuration
|
||||
├── tailwind.config.js # Tailwind configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
└── package.json
|
||||
|
||||
```
|
||||
|
||||
### Shared Component Library (`shared-components/`)
|
||||
|
||||
```
|
||||
shared-components/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── Game/
|
||||
│ │ │ ├── GameBoard.vue # Baseball diamond visualization
|
||||
│ │ │ ├── ScoreBoard.vue # Score display
|
||||
│ │ │ ├── PlayByPlay.vue # Play history feed
|
||||
│ │ │ ├── CurrentSituation.vue # Current game context
|
||||
│ │ │ └── BaseRunners.vue # Runner indicators
|
||||
│ │ │
|
||||
│ │ ├── Decisions/
|
||||
│ │ │ ├── DefensivePositioning.vue
|
||||
│ │ │ ├── StolenBaseAttempt.vue
|
||||
│ │ │ ├── OffensiveApproach.vue
|
||||
│ │ │ └── DecisionTimer.vue
|
||||
│ │ │
|
||||
│ │ ├── Actions/
|
||||
│ │ │ ├── SubstitutionModal.vue
|
||||
│ │ │ ├── PitchingChange.vue
|
||||
│ │ │ └── ActionButton.vue
|
||||
│ │ │
|
||||
│ │ ├── Display/
|
||||
│ │ │ ├── PlayerCard.vue # Player card display
|
||||
│ │ │ ├── DiceRoll.vue # Dice animation
|
||||
│ │ │ ├── PlayOutcome.vue # Play result display
|
||||
│ │ │ └── ConnectionStatus.vue # WebSocket status
|
||||
│ │ │
|
||||
│ │ └── Common/
|
||||
│ │ ├── Button.vue
|
||||
│ │ ├── Modal.vue
|
||||
│ │ ├── Toast.vue
|
||||
│ │ └── Loading.vue
|
||||
│ │
|
||||
│ ├── composables/
|
||||
│ │ ├── useGameActions.ts # Shared game actions
|
||||
│ │ └── useGameDisplay.ts # Shared display logic
|
||||
│ │
|
||||
│ └── types/
|
||||
│ └── index.ts # Shared TypeScript types
|
||||
│
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. WebSocket Composable (`composables/useWebSocket.ts`)
|
||||
|
||||
**Responsibilities**:
|
||||
- Establish and maintain WebSocket connection
|
||||
- Handle reconnection logic
|
||||
- Emit events to server
|
||||
- Subscribe to server events
|
||||
- Connection status monitoring
|
||||
|
||||
**Implementation**:
|
||||
```typescript
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
|
||||
export const useWebSocket = () => {
|
||||
const socket = ref<Socket | null>(null)
|
||||
const connected = ref(false)
|
||||
const reconnecting = ref(false)
|
||||
|
||||
const connect = (token: string) => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
socket.value = io(config.public.wsUrl, {
|
||||
auth: { token },
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionAttempts: 5
|
||||
})
|
||||
|
||||
socket.value.on('connect', () => {
|
||||
connected.value = true
|
||||
reconnecting.value = false
|
||||
})
|
||||
|
||||
socket.value.on('disconnect', () => {
|
||||
connected.value = false
|
||||
})
|
||||
|
||||
socket.value.on('reconnecting', () => {
|
||||
reconnecting.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
socket.value?.disconnect()
|
||||
socket.value = null
|
||||
connected.value = false
|
||||
}
|
||||
|
||||
const emit = (event: string, data: any) => {
|
||||
if (!socket.value?.connected) {
|
||||
throw new Error('WebSocket not connected')
|
||||
}
|
||||
socket.value.emit(event, data)
|
||||
}
|
||||
|
||||
const on = (event: string, handler: (...args: any[]) => void) => {
|
||||
socket.value?.on(event, handler)
|
||||
}
|
||||
|
||||
const off = (event: string, handler?: (...args: any[]) => void) => {
|
||||
socket.value?.off(event, handler)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnect()
|
||||
})
|
||||
|
||||
return {
|
||||
socket,
|
||||
connected,
|
||||
reconnecting,
|
||||
connect,
|
||||
disconnect,
|
||||
emit,
|
||||
on,
|
||||
off
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Game State Store (`store/game.ts`)
|
||||
|
||||
**Responsibilities**:
|
||||
- Hold current game state
|
||||
- Update state from WebSocket events
|
||||
- Provide computed properties for UI
|
||||
- Handle optimistic updates
|
||||
|
||||
**Implementation**:
|
||||
```typescript
|
||||
import { defineStore } from 'pinia'
|
||||
import type { GameState, PlayOutcome } from '~/types/game'
|
||||
|
||||
export const useGameStore = defineStore('game', () => {
|
||||
// State
|
||||
const gameState = ref<GameState | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const pendingAction = ref(false)
|
||||
|
||||
// Computed
|
||||
const inning = computed(() => gameState.value?.inning ?? 1)
|
||||
const half = computed(() => gameState.value?.half ?? 'top')
|
||||
const outs = computed(() => gameState.value?.outs ?? 0)
|
||||
const score = computed(() => ({
|
||||
home: gameState.value?.home_score ?? 0,
|
||||
away: gameState.value?.away_score ?? 0
|
||||
}))
|
||||
const runners = computed(() => gameState.value?.runners ?? {})
|
||||
const currentBatter = computed(() => gameState.value?.current_batter)
|
||||
const currentPitcher = computed(() => gameState.value?.current_pitcher)
|
||||
const isMyTurn = computed(() => {
|
||||
// Logic to determine if it's user's turn
|
||||
return gameState.value?.decision_required?.user_id === useAuthStore().userId
|
||||
})
|
||||
|
||||
// Actions
|
||||
const setGameState = (state: GameState) => {
|
||||
gameState.value = state
|
||||
loading.value = false
|
||||
error.value = null
|
||||
}
|
||||
|
||||
const updateState = (updates: Partial<GameState>) => {
|
||||
if (gameState.value) {
|
||||
gameState.value = { ...gameState.value, ...updates }
|
||||
}
|
||||
}
|
||||
|
||||
const handlePlayCompleted = (outcome: PlayOutcome) => {
|
||||
// Update state based on play outcome
|
||||
if (gameState.value) {
|
||||
updateState({
|
||||
outs: outcome.outs_after,
|
||||
runners: outcome.runners_after,
|
||||
home_score: outcome.home_score,
|
||||
away_score: outcome.away_score
|
||||
})
|
||||
}
|
||||
pendingAction.value = false
|
||||
}
|
||||
|
||||
const setError = (message: string) => {
|
||||
error.value = message
|
||||
loading.value = false
|
||||
pendingAction.value = false
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
gameState.value = null
|
||||
loading.value = false
|
||||
error.value = null
|
||||
pendingAction.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
gameState,
|
||||
loading,
|
||||
error,
|
||||
pendingAction,
|
||||
// Computed
|
||||
inning,
|
||||
half,
|
||||
outs,
|
||||
score,
|
||||
runners,
|
||||
currentBatter,
|
||||
currentPitcher,
|
||||
isMyTurn,
|
||||
// Actions
|
||||
setGameState,
|
||||
updateState,
|
||||
handlePlayCompleted,
|
||||
setError,
|
||||
reset
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Game Board Component (`shared-components/Game/GameBoard.vue`)
|
||||
|
||||
**Responsibilities**:
|
||||
- Visual baseball diamond
|
||||
- Show runners on base
|
||||
- Highlight active bases
|
||||
- Responsive scaling
|
||||
|
||||
**Implementation**:
|
||||
```vue
|
||||
<template>
|
||||
<div class="game-board relative w-full aspect-square max-w-md mx-auto">
|
||||
<!-- Diamond SVG -->
|
||||
<svg viewBox="0 0 100 100" class="w-full h-full">
|
||||
<!-- Diamond shape -->
|
||||
<path
|
||||
d="M 50 10 L 90 50 L 50 90 L 10 50 Z"
|
||||
class="fill-green-600 stroke-white stroke-2"
|
||||
/>
|
||||
|
||||
<!-- Bases -->
|
||||
<rect
|
||||
v-for="base in bases"
|
||||
:key="base.name"
|
||||
:x="base.x"
|
||||
:y="base.y"
|
||||
width="8"
|
||||
height="8"
|
||||
:class="[
|
||||
'stroke-white stroke-2',
|
||||
hasRunner(base.name) ? 'fill-yellow-400' : 'fill-white'
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- Home plate -->
|
||||
<polygon
|
||||
points="50,88 48,90 52,90"
|
||||
class="fill-white stroke-white"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Runner indicators -->
|
||||
<div
|
||||
v-for="(runner, base) in runners"
|
||||
:key="base"
|
||||
:class="getRunnerPositionClass(base)"
|
||||
class="absolute"
|
||||
>
|
||||
<PlayerAvatar
|
||||
v-if="runner"
|
||||
:player-id="runner"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Batter indicator -->
|
||||
<div class="absolute bottom-2 left-1/2 -translate-x-1/2">
|
||||
<span class="text-xs text-white font-bold">
|
||||
{{ currentBatter?.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Runners } from '~/types/game'
|
||||
|
||||
interface Props {
|
||||
runners: Runners
|
||||
currentBatter?: { name: string }
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const bases = [
|
||||
{ name: 'first', x: 84, y: 46 },
|
||||
{ name: 'second', x: 46, y: 6 },
|
||||
{ name: 'third', x: 6, y: 46 }
|
||||
]
|
||||
|
||||
const hasRunner = (base: string) => {
|
||||
return props.runners[base] !== null
|
||||
}
|
||||
|
||||
const getRunnerPositionClass = (base: string) => {
|
||||
const positions = {
|
||||
first: 'right-8 top-1/2 -translate-y-1/2',
|
||||
second: 'top-8 left-1/2 -translate-x-1/2',
|
||||
third: 'left-8 top-1/2 -translate-y-1/2'
|
||||
}
|
||||
return positions[base as keyof typeof positions]
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 4. Decision Flow Component (`shared-components/Decisions/DefensivePositioning.vue`)
|
||||
|
||||
**Responsibilities**:
|
||||
- Present defensive positioning options
|
||||
- Validate selection
|
||||
- Submit decision via WebSocket
|
||||
- Show loading state
|
||||
|
||||
**Implementation**:
|
||||
```vue
|
||||
<template>
|
||||
<div class="decision-card bg-white rounded-lg shadow-lg p-6">
|
||||
<h3 class="text-lg font-bold mb-4">Set Defensive Positioning</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
v-for="option in positioningOptions"
|
||||
:key="option.value"
|
||||
@click="selectPositioning(option.value)"
|
||||
:disabled="submitting"
|
||||
class="w-full py-3 px-4 rounded-lg border-2 transition-colors"
|
||||
:class="[
|
||||
selected === option.value
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-300 hover:border-blue-300',
|
||||
submitting && 'opacity-50 cursor-not-allowed'
|
||||
]"
|
||||
>
|
||||
<div class="text-left">
|
||||
<div class="font-semibold">{{ option.label }}</div>
|
||||
<div class="text-sm text-gray-600">{{ option.description }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-between items-center">
|
||||
<DecisionTimer
|
||||
v-if="timeoutSeconds"
|
||||
:seconds="timeoutSeconds"
|
||||
@timeout="handleTimeout"
|
||||
/>
|
||||
<button
|
||||
@click="submitDecision"
|
||||
:disabled="!selected || submitting"
|
||||
class="btn-primary"
|
||||
>
|
||||
{{ submitting ? 'Submitting...' : 'Confirm' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
timeoutSeconds?: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
submit: [positioning: string]
|
||||
timeout: []
|
||||
}>()
|
||||
|
||||
const selected = ref<string | null>(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
const positioningOptions = [
|
||||
{
|
||||
value: 'standard',
|
||||
label: 'Standard',
|
||||
description: 'Normal defensive alignment'
|
||||
},
|
||||
{
|
||||
value: 'infield_in',
|
||||
label: 'Infield In',
|
||||
description: 'Drawn in to prevent runs'
|
||||
},
|
||||
{
|
||||
value: 'shift_left',
|
||||
label: 'Shift Left',
|
||||
description: 'Shifted for pull hitter'
|
||||
},
|
||||
{
|
||||
value: 'shift_right',
|
||||
label: 'Shift Right',
|
||||
description: 'Shifted for opposite field'
|
||||
}
|
||||
]
|
||||
|
||||
const selectPositioning = (value: string) => {
|
||||
if (!submitting.value) {
|
||||
selected.value = value
|
||||
}
|
||||
}
|
||||
|
||||
const submitDecision = async () => {
|
||||
if (!selected.value || submitting.value) return
|
||||
|
||||
submitting.value = true
|
||||
emit('submit', selected.value)
|
||||
}
|
||||
|
||||
const handleTimeout = () => {
|
||||
emit('timeout')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## State Management Architecture
|
||||
|
||||
### Pinia Stores
|
||||
|
||||
**Auth Store**:
|
||||
- User authentication state
|
||||
- Discord profile data
|
||||
- Team ownership information
|
||||
- Token management
|
||||
|
||||
**Game Store**:
|
||||
- Current game state
|
||||
- Real-time updates from WebSocket
|
||||
- Pending actions
|
||||
- Error handling
|
||||
|
||||
**Games List Store**:
|
||||
- Active games list
|
||||
- Completed games history
|
||||
- Game filtering
|
||||
|
||||
**UI Store**:
|
||||
- Modal states
|
||||
- Toast notifications
|
||||
- Loading indicators
|
||||
- Theme preferences
|
||||
|
||||
## Mobile-First Responsive Design
|
||||
|
||||
### Breakpoints
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
theme: {
|
||||
screens: {
|
||||
'xs': '375px', // Small phones
|
||||
'sm': '640px', // Large phones
|
||||
'md': '768px', // Tablets
|
||||
'lg': '1024px', // Desktop
|
||||
'xl': '1280px', // Large desktop
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Layout Strategy
|
||||
|
||||
**Mobile (< 768px)**:
|
||||
- Single column layout
|
||||
- Bottom sheet for decisions
|
||||
- Sticky scoreboard at top
|
||||
- Collapsible play-by-play
|
||||
- Full-screen game board
|
||||
|
||||
**Tablet (768px - 1024px)**:
|
||||
- Two column layout (game + sidebar)
|
||||
- Larger game board
|
||||
- Side panel for decisions
|
||||
- Expanded play history
|
||||
|
||||
**Desktop (> 1024px)**:
|
||||
- Three column layout (optional)
|
||||
- Full game board center
|
||||
- Decision panel right
|
||||
- Stats panel left
|
||||
|
||||
## WebSocket Event Handling
|
||||
|
||||
### Event Listeners Setup
|
||||
|
||||
```typescript
|
||||
// composables/useGameActions.ts
|
||||
export const useGameActions = (gameId: string) => {
|
||||
const { socket, emit, on, off } = useWebSocket()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
onMounted(() => {
|
||||
// Join game room
|
||||
emit('join_game', { game_id: gameId, role: 'player' })
|
||||
|
||||
// Listen for state updates
|
||||
on('game_state_update', (data: GameState) => {
|
||||
gameStore.setGameState(data)
|
||||
})
|
||||
|
||||
on('play_completed', (data: PlayOutcome) => {
|
||||
gameStore.handlePlayCompleted(data)
|
||||
})
|
||||
|
||||
on('dice_rolled', (data: DiceRoll) => {
|
||||
// Trigger dice animation
|
||||
showDiceRoll(data.roll, data.animation_duration)
|
||||
})
|
||||
|
||||
on('decision_required', (data: DecisionPrompt) => {
|
||||
// Show decision UI
|
||||
gameStore.setDecisionRequired(data)
|
||||
})
|
||||
|
||||
on('invalid_action', (data: ErrorData) => {
|
||||
gameStore.setError(data.message)
|
||||
})
|
||||
|
||||
on('game_error', (data: ErrorData) => {
|
||||
gameStore.setError(data.message)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Clean up listeners
|
||||
off('game_state_update')
|
||||
off('play_completed')
|
||||
off('dice_rolled')
|
||||
off('decision_required')
|
||||
off('invalid_action')
|
||||
off('game_error')
|
||||
|
||||
// Leave game room
|
||||
emit('leave_game', { game_id: gameId })
|
||||
})
|
||||
|
||||
// Action methods
|
||||
const setDefense = (positioning: string) => {
|
||||
emit('set_defense', { game_id: gameId, positioning })
|
||||
}
|
||||
|
||||
const setStolenBase = (runners: string[]) => {
|
||||
emit('set_stolen_base', { game_id: gameId, runners })
|
||||
}
|
||||
|
||||
const setOffensiveApproach = (approach: string) => {
|
||||
emit('set_offensive_approach', { game_id: gameId, approach })
|
||||
}
|
||||
|
||||
return {
|
||||
setDefense,
|
||||
setStolenBase,
|
||||
setOffensiveApproach
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## League-Specific Customization
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
// frontend-sba/nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
leagueId: 'sba',
|
||||
leagueName: 'Super Baseball Alliance',
|
||||
apiUrl: process.env.NUXT_PUBLIC_API_URL,
|
||||
wsUrl: process.env.NUXT_PUBLIC_WS_URL,
|
||||
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID,
|
||||
primaryColor: '#1e40af', // Blue
|
||||
secondaryColor: '#dc2626' // Red
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// frontend-pd/nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
leagueId: 'pd',
|
||||
leagueName: 'Paper Dynasty',
|
||||
apiUrl: process.env.NUXT_PUBLIC_API_URL,
|
||||
wsUrl: process.env.NUXT_PUBLIC_WS_URL,
|
||||
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID,
|
||||
primaryColor: '#16a34a', // Green
|
||||
secondaryColor: '#ea580c' // Orange
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Theming
|
||||
|
||||
```typescript
|
||||
// composables/useLeagueConfig.ts
|
||||
export const useLeagueConfig = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
return {
|
||||
leagueId: config.public.leagueId,
|
||||
leagueName: config.public.leagueName,
|
||||
colors: {
|
||||
primary: config.public.primaryColor,
|
||||
secondary: config.public.secondaryColor
|
||||
},
|
||||
features: {
|
||||
showScoutingData: config.public.leagueId === 'pd',
|
||||
useSimplePlayerCards: config.public.leagueId === 'sba'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### Code Splitting
|
||||
- Lazy load game components
|
||||
- Route-based code splitting
|
||||
- Dynamic imports for heavy libraries
|
||||
|
||||
### Asset Optimization
|
||||
- Image lazy loading
|
||||
- SVG sprites for icons
|
||||
- Optimized font loading
|
||||
|
||||
### State Updates
|
||||
- Debounce non-critical updates
|
||||
- Optimistic UI updates
|
||||
- Efficient re-rendering with `v-memo`
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Network Errors
|
||||
```typescript
|
||||
const handleNetworkError = (error: Error) => {
|
||||
const uiStore = useUiStore()
|
||||
|
||||
if (error.message.includes('WebSocket')) {
|
||||
uiStore.showToast({
|
||||
type: 'error',
|
||||
message: 'Connection lost. Reconnecting...'
|
||||
})
|
||||
} else {
|
||||
uiStore.showToast({
|
||||
type: 'error',
|
||||
message: 'Network error. Please try again.'
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Game Errors
|
||||
```typescript
|
||||
const handleGameError = (error: GameError) => {
|
||||
const uiStore = useUiStore()
|
||||
|
||||
uiStore.showModal({
|
||||
title: 'Game Error',
|
||||
message: error.message,
|
||||
actions: [
|
||||
{
|
||||
label: 'Retry',
|
||||
handler: () => retryLastAction()
|
||||
},
|
||||
{
|
||||
label: 'Reload Game',
|
||||
handler: () => reloadGameState()
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Next Steps**: See [03-gameplay-features.md](./03-gameplay-features.md) for gameplay implementation details.
|
||||
322
.claude/implementation/testing-strategy.md
Normal file
322
.claude/implementation/testing-strategy.md
Normal file
@ -0,0 +1,322 @@
|
||||
# Testing Strategy
|
||||
|
||||
**Status**: Placeholder
|
||||
**Cross-Cutting Concern**: All Phases, Critical in Phase 5
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Comprehensive testing approach covering unit, integration, E2E, load, and security testing to ensure production-ready quality.
|
||||
|
||||
## Testing Pyramid
|
||||
|
||||
```
|
||||
/\
|
||||
/ \ E2E Tests (10%)
|
||||
/ \ - Full game flows
|
||||
/------\ - Multi-user scenarios
|
||||
/ \
|
||||
/ Integ. \ Integration Tests (30%)
|
||||
/ Tests \ - WebSocket flows
|
||||
/ \ - Database operations
|
||||
/----------------\
|
||||
/ \ Unit Tests (60%)
|
||||
/ Unit Tests \- Core logic
|
||||
/______________________\- Pure functions
|
||||
```
|
||||
|
||||
## Test Coverage Goals
|
||||
|
||||
- **Overall Coverage**: >80%
|
||||
- **Core Game Logic**: >90%
|
||||
- **WebSocket Handlers**: >85%
|
||||
- **Database Operations**: >75%
|
||||
- **Frontend Components**: >70%
|
||||
|
||||
## Testing Tools
|
||||
|
||||
### Backend
|
||||
- **pytest**: Unit and integration tests
|
||||
- **pytest-asyncio**: Async test support
|
||||
- **pytest-cov**: Coverage reporting
|
||||
- **httpx**: API client testing
|
||||
- **python-socketio[client]**: WebSocket testing
|
||||
- **factory_boy**: Test data generation
|
||||
- **Faker**: Fake data generation
|
||||
|
||||
### Frontend
|
||||
- **Vitest**: Unit testing (fast, Vite-native)
|
||||
- **Testing Library**: Component testing
|
||||
- **Cypress**: E2E testing
|
||||
- **Playwright**: Alternative E2E testing
|
||||
- **Lighthouse CI**: Performance testing
|
||||
- **axe-core**: Accessibility testing
|
||||
|
||||
### Load Testing
|
||||
- **Locust**: Distributed load testing
|
||||
- **Artillery**: Alternative load testing
|
||||
|
||||
### Security Testing
|
||||
- **Safety**: Dependency vulnerability scanning
|
||||
- **Bandit**: Python security linting
|
||||
- **OWASP ZAP**: Security scanning
|
||||
- **npm audit**: Frontend dependency scanning
|
||||
|
||||
## Test Organization
|
||||
|
||||
### Backend Structure
|
||||
```
|
||||
backend/tests/
|
||||
├── unit/
|
||||
│ ├── test_game_engine.py
|
||||
│ ├── test_state_manager.py
|
||||
│ ├── test_play_resolver.py
|
||||
│ ├── test_dice.py
|
||||
│ └── test_validators.py
|
||||
├── integration/
|
||||
│ ├── test_websocket.py
|
||||
│ ├── test_database.py
|
||||
│ ├── test_api_client.py
|
||||
│ └── test_full_turn.py
|
||||
├── e2e/
|
||||
│ ├── test_full_game.py
|
||||
│ ├── test_reconnection.py
|
||||
│ └── test_state_recovery.py
|
||||
├── load/
|
||||
│ └── locustfile.py
|
||||
├── conftest.py # Shared fixtures
|
||||
└── factories.py # Test data factories
|
||||
```
|
||||
|
||||
### Frontend Structure
|
||||
```
|
||||
frontend-{league}/
|
||||
├── tests/
|
||||
│ ├── unit/
|
||||
│ │ ├── composables/
|
||||
│ │ └── utils/
|
||||
│ └── components/
|
||||
│ ├── Game/
|
||||
│ ├── Decisions/
|
||||
│ └── Display/
|
||||
└── cypress/
|
||||
├── e2e/
|
||||
│ ├── game-flow.cy.ts
|
||||
│ ├── auth.cy.ts
|
||||
│ └── spectator.cy.ts
|
||||
└── support/
|
||||
```
|
||||
|
||||
## Key Test Scenarios
|
||||
|
||||
### Unit Tests
|
||||
- Dice roll distribution (statistical validation)
|
||||
- Play outcome resolution (all d20 results)
|
||||
- State transitions
|
||||
- Input validation
|
||||
- Player model instantiation
|
||||
|
||||
### Integration Tests
|
||||
- WebSocket event flow (connect → join → action → update)
|
||||
- Database persistence and recovery
|
||||
- API client responses
|
||||
- Multi-turn game sequences
|
||||
|
||||
### E2E Tests
|
||||
- Complete 9-inning game
|
||||
- Game with substitutions
|
||||
- AI opponent game
|
||||
- Spectator joining active game
|
||||
- Reconnection after disconnect
|
||||
- Multiple concurrent games
|
||||
|
||||
### Load Tests
|
||||
- 10 concurrent games
|
||||
- 50 concurrent WebSocket connections
|
||||
- Sustained load over 30 minutes
|
||||
- Spike testing (sudden load increase)
|
||||
|
||||
### Security Tests
|
||||
- SQL injection attempts
|
||||
- XSS attempts
|
||||
- CSRF protection
|
||||
- Authorization bypass attempts
|
||||
- Rate limit enforcement
|
||||
|
||||
## Example Test Cases
|
||||
|
||||
### Unit Test (Backend)
|
||||
```python
|
||||
# tests/unit/test_dice.py
|
||||
import pytest
|
||||
from app.core.dice import DiceRoller
|
||||
|
||||
def test_dice_roll_range():
|
||||
"""Test that dice rolls are within valid range"""
|
||||
roller = DiceRoller()
|
||||
for _ in range(1000):
|
||||
roll = roller.roll_d20()
|
||||
assert 1 <= roll <= 20
|
||||
|
||||
def test_dice_distribution():
|
||||
"""Test that dice rolls are reasonably distributed"""
|
||||
roller = DiceRoller()
|
||||
rolls = [roller.roll_d20() for _ in range(10000)]
|
||||
|
||||
# Each number should appear roughly 500 times (±10%)
|
||||
for num in range(1, 21):
|
||||
count = rolls.count(num)
|
||||
assert 450 <= count <= 550
|
||||
```
|
||||
|
||||
### Integration Test (Backend)
|
||||
```python
|
||||
# tests/integration/test_websocket.py
|
||||
import pytest
|
||||
import socketio
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_game_action_flow(sio_client, test_game):
|
||||
"""Test complete action flow through WebSocket"""
|
||||
|
||||
# Connect
|
||||
await sio_client.connect('http://localhost:8000', auth={'token': 'valid-jwt'})
|
||||
|
||||
# Join game
|
||||
await sio_client.emit('join_game', {'game_id': test_game.id, 'role': 'player'})
|
||||
|
||||
# Wait for game state
|
||||
response = await sio_client.receive()
|
||||
assert response[0] == 'game_state_update'
|
||||
|
||||
# Send defensive decision
|
||||
await sio_client.emit('set_defense', {
|
||||
'game_id': test_game.id,
|
||||
'positioning': 'standard'
|
||||
})
|
||||
|
||||
# Verify decision recorded
|
||||
response = await sio_client.receive()
|
||||
assert response[0] == 'decision_recorded'
|
||||
```
|
||||
|
||||
### Component Test (Frontend)
|
||||
```typescript
|
||||
// tests/components/Game/GameBoard.spec.ts
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import GameBoard from '@/components/Game/GameBoard.vue'
|
||||
|
||||
describe('GameBoard', () => {
|
||||
it('displays runners on base correctly', () => {
|
||||
const runners = {
|
||||
first: null,
|
||||
second: 12345,
|
||||
third: null
|
||||
}
|
||||
|
||||
const wrapper = mount(GameBoard, {
|
||||
props: { runners }
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-base="second"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-base="first"]').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### E2E Test (Frontend)
|
||||
```typescript
|
||||
// cypress/e2e/game-flow.cy.ts
|
||||
describe('Complete Game Flow', () => {
|
||||
it('can play through an at-bat', () => {
|
||||
cy.login() // Custom command
|
||||
cy.visit('/games/create')
|
||||
|
||||
cy.get('[data-test="create-game"]').click()
|
||||
cy.get('[data-test="game-mode-live"]').click()
|
||||
cy.get('[data-test="start-game"]').click()
|
||||
|
||||
// Wait for game to start
|
||||
cy.get('[data-test="game-board"]').should('be.visible')
|
||||
|
||||
// Make defensive decision
|
||||
cy.get('[data-test="defense-standard"]').click()
|
||||
cy.get('[data-test="confirm-decision"]').click()
|
||||
|
||||
// Verify decision recorded
|
||||
cy.get('[data-test="waiting-indicator"]').should('be.visible')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
### GitHub Actions Example
|
||||
```yaml
|
||||
name: Test Suite
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
backend-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: pip install -r backend/requirements-dev.txt
|
||||
- run: pytest backend/tests/ --cov --cov-report=xml
|
||||
- uses: codecov/codecov-action@v3
|
||||
|
||||
frontend-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
- run: npm ci
|
||||
- run: npm run test:unit
|
||||
- run: npm run test:e2e
|
||||
```
|
||||
|
||||
## Test Data Management
|
||||
|
||||
### Fixtures
|
||||
- **Shared fixtures** in `conftest.py`
|
||||
- **Factory pattern** for creating test objects
|
||||
- **Database seeding** for integration tests
|
||||
- **Mock data** for external APIs
|
||||
|
||||
### Test Isolation
|
||||
- Each test should be independent
|
||||
- Database rollback after each test
|
||||
- Clean in-memory state between tests
|
||||
- No shared mutable state
|
||||
|
||||
## Performance Testing
|
||||
|
||||
### Metrics to Track
|
||||
- Response time (p50, p95, p99)
|
||||
- Throughput (requests/second)
|
||||
- Error rate
|
||||
- CPU and memory usage
|
||||
- Database query time
|
||||
|
||||
### Load Test Scenarios
|
||||
1. **Normal Load**: 5 concurrent games
|
||||
2. **Peak Load**: 10 concurrent games
|
||||
3. **Stress Test**: 20 concurrent games (breaking point)
|
||||
4. **Spike Test**: 2 → 10 games instantly
|
||||
|
||||
## Reference Documents
|
||||
|
||||
- [PRD Lines 1063-1101](../prd-web-scorecard-1.1.md) - Testing strategy
|
||||
- [05-testing-launch.md](./05-testing-launch.md) - Phase 5 testing details
|
||||
|
||||
---
|
||||
|
||||
**Note**: This is a placeholder to be expanded with specific test implementations during development.
|
||||
668
.claude/implementation/websocket-protocol.md
Normal file
668
.claude/implementation/websocket-protocol.md
Normal file
@ -0,0 +1,668 @@
|
||||
# WebSocket Protocol Specification
|
||||
|
||||
## Overview
|
||||
|
||||
Real-time bidirectional communication protocol between game clients and backend server using Socket.io. All game state updates, player actions, and system events transmitted via WebSocket.
|
||||
|
||||
## Connection Lifecycle
|
||||
|
||||
### 1. Initial Connection
|
||||
|
||||
**Client → Server**
|
||||
```typescript
|
||||
import { io } from 'socket.io-client'
|
||||
|
||||
const socket = io('wss://api.paperdynasty.com', {
|
||||
auth: {
|
||||
token: 'jwt-token-here'
|
||||
},
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionAttempts: 5
|
||||
})
|
||||
```
|
||||
|
||||
**Server → Client**
|
||||
```json
|
||||
{
|
||||
"event": "connected",
|
||||
"data": {
|
||||
"user_id": "123456789",
|
||||
"connection_id": "abc123xyz"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Joining a Game
|
||||
|
||||
**Client → Server**
|
||||
```json
|
||||
{
|
||||
"event": "join_game",
|
||||
"data": {
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"role": "player" // "player" or "spectator"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Server → Client** (Success)
|
||||
```json
|
||||
{
|
||||
"event": "game_joined",
|
||||
"data": {
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"role": "player",
|
||||
"team_id": 42
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Server → All Participants** (User Joined Notification)
|
||||
```json
|
||||
{
|
||||
"event": "user_connected",
|
||||
"data": {
|
||||
"user_id": "123456789",
|
||||
"role": "player",
|
||||
"team_id": 42,
|
||||
"timestamp": "2025-10-21T19:45:23Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Receiving Game State
|
||||
|
||||
**Server → Client** (Full State on Join)
|
||||
```json
|
||||
{
|
||||
"event": "game_state_update",
|
||||
"data": {
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "active",
|
||||
"inning": 3,
|
||||
"half": "top",
|
||||
"outs": 2,
|
||||
"balls": 2,
|
||||
"strikes": 1,
|
||||
"home_score": 2,
|
||||
"away_score": 1,
|
||||
"runners": {
|
||||
"first": null,
|
||||
"second": 12345,
|
||||
"third": null
|
||||
},
|
||||
"current_batter": {
|
||||
"card_id": 67890,
|
||||
"player_id": 999,
|
||||
"name": "Mike Trout",
|
||||
"position": "CF",
|
||||
"batting_avg": 0.305,
|
||||
"image": "https://..."
|
||||
},
|
||||
"current_pitcher": {
|
||||
"card_id": 11111,
|
||||
"player_id": 888,
|
||||
"name": "Sandy Alcantara",
|
||||
"position": "P",
|
||||
"era": 2.45,
|
||||
"image": "https://..."
|
||||
},
|
||||
"decision_required": {
|
||||
"type": "set_defense",
|
||||
"team_id": 42,
|
||||
"user_id": "123456789",
|
||||
"timeout_seconds": 30
|
||||
},
|
||||
"play_history": [
|
||||
{
|
||||
"play_number": 44,
|
||||
"inning": 3,
|
||||
"description": "Groundout to second base",
|
||||
"runs_scored": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Heartbeat
|
||||
|
||||
**Client → Server** (Every 30 seconds)
|
||||
```json
|
||||
{
|
||||
"event": "heartbeat"
|
||||
}
|
||||
```
|
||||
|
||||
**Server → Client**
|
||||
```json
|
||||
{
|
||||
"event": "heartbeat_ack",
|
||||
"data": {
|
||||
"timestamp": "2025-10-21T19:45:23Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Disconnection
|
||||
|
||||
**Client → Server**
|
||||
```json
|
||||
{
|
||||
"event": "leave_game",
|
||||
"data": {
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Server → All Participants**
|
||||
```json
|
||||
{
|
||||
"event": "user_disconnected",
|
||||
"data": {
|
||||
"user_id": "123456789",
|
||||
"timestamp": "2025-10-21T19:46:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Game Action Events
|
||||
|
||||
### 1. Set Defensive Positioning
|
||||
|
||||
**Client → Server**
|
||||
```json
|
||||
{
|
||||
"event": "set_defense",
|
||||
"data": {
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"positioning": "standard" // "standard", "infield_in", "shift_left", "shift_right"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Server → Client** (Acknowledgment)
|
||||
```json
|
||||
{
|
||||
"event": "decision_recorded",
|
||||
"data": {
|
||||
"type": "set_defense",
|
||||
"positioning": "standard",
|
||||
"timestamp": "2025-10-21T19:45:25Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Server → All Participants** (Next Decision Required)
|
||||
```json
|
||||
{
|
||||
"event": "decision_required",
|
||||
"data": {
|
||||
"type": "set_stolen_base",
|
||||
"team_id": 42,
|
||||
"user_id": "123456789",
|
||||
"runners": ["second"],
|
||||
"timeout_seconds": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Stolen Base Attempt
|
||||
|
||||
**Client → Server**
|
||||
```json
|
||||
{
|
||||
"event": "set_stolen_base",
|
||||
"data": {
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"attempts": {
|
||||
"second": true, // Runner on second attempts
|
||||
"third": false // No runner on third
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Offensive Approach
|
||||
|
||||
**Client → Server**
|
||||
```json
|
||||
{
|
||||
"event": "set_offensive_approach",
|
||||
"data": {
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"approach": "swing_away" // "swing_away", "bunt", "hit_and_run"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Dice Roll & Play Resolution
|
||||
|
||||
**Server → All Participants** (Dice Roll Animation)
|
||||
```json
|
||||
{
|
||||
"event": "dice_rolled",
|
||||
"data": {
|
||||
"roll": 14,
|
||||
"animation_duration": 2000,
|
||||
"timestamp": "2025-10-21T19:45:30Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Server → All Participants** (Play Outcome)
|
||||
```json
|
||||
{
|
||||
"event": "play_completed",
|
||||
"data": {
|
||||
"play_number": 45,
|
||||
"inning": 3,
|
||||
"half": "top",
|
||||
"dice_roll": 14,
|
||||
"result_type": "single",
|
||||
"hit_location": "left_field",
|
||||
"description": "Mike Trout singles to left field. Runner advances to third.",
|
||||
"batter": {
|
||||
"card_id": 67890,
|
||||
"name": "Mike Trout"
|
||||
},
|
||||
"pitcher": {
|
||||
"card_id": 11111,
|
||||
"name": "Sandy Alcantara"
|
||||
},
|
||||
"outs_before": 2,
|
||||
"outs_recorded": 0,
|
||||
"outs_after": 2,
|
||||
"runners_before": {
|
||||
"first": null,
|
||||
"second": 12345,
|
||||
"third": null
|
||||
},
|
||||
"runners_after": {
|
||||
"first": 67890,
|
||||
"second": null,
|
||||
"third": 12345
|
||||
},
|
||||
"runs_scored": 0,
|
||||
"home_score": 2,
|
||||
"away_score": 1,
|
||||
"timestamp": "2025-10-21T19:45:32Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Play Result Selection
|
||||
|
||||
When multiple outcomes possible (e.g., hit location choices):
|
||||
|
||||
**Server → Offensive Player**
|
||||
```json
|
||||
{
|
||||
"event": "select_play_result",
|
||||
"data": {
|
||||
"play_number": 45,
|
||||
"options": [
|
||||
{
|
||||
"value": "single_left",
|
||||
"label": "Single to Left",
|
||||
"description": "Runner advances to third"
|
||||
},
|
||||
{
|
||||
"value": "single_center",
|
||||
"label": "Single to Center",
|
||||
"description": "Runner scores"
|
||||
}
|
||||
],
|
||||
"timeout_seconds": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Client → Server**
|
||||
```json
|
||||
{
|
||||
"event": "select_play_result",
|
||||
"data": {
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"play_number": 45,
|
||||
"selection": "single_center"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Substitution
|
||||
|
||||
**Client → Server**
|
||||
```json
|
||||
{
|
||||
"event": "substitute_player",
|
||||
"data": {
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"card_out": 67890,
|
||||
"card_in": 55555,
|
||||
"position": "CF",
|
||||
"batting_order": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Server → All Participants**
|
||||
```json
|
||||
{
|
||||
"event": "substitution_made",
|
||||
"data": {
|
||||
"team_id": 42,
|
||||
"player_out": {
|
||||
"card_id": 67890,
|
||||
"name": "Mike Trout"
|
||||
},
|
||||
"player_in": {
|
||||
"card_id": 55555,
|
||||
"name": "Byron Buxton",
|
||||
"position": "CF",
|
||||
"batting_order": 3
|
||||
},
|
||||
"inning": 3,
|
||||
"timestamp": "2025-10-21T19:46:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Pitching Change
|
||||
|
||||
**Client → Server**
|
||||
```json
|
||||
{
|
||||
"event": "change_pitcher",
|
||||
"data": {
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"pitcher_out": 11111,
|
||||
"pitcher_in": 22222
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Server → All Participants**
|
||||
```json
|
||||
{
|
||||
"event": "pitcher_changed",
|
||||
"data": {
|
||||
"team_id": 42,
|
||||
"pitcher_out": {
|
||||
"card_id": 11111,
|
||||
"name": "Sandy Alcantara",
|
||||
"final_line": "6 IP, 4 H, 1 R, 1 ER, 2 BB, 8 K"
|
||||
},
|
||||
"pitcher_in": {
|
||||
"card_id": 22222,
|
||||
"name": "Edwin Diaz",
|
||||
"position": "P"
|
||||
},
|
||||
"inning": 7,
|
||||
"timestamp": "2025-10-21T19:47:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Events
|
||||
|
||||
### 1. Inning Change
|
||||
|
||||
**Server → All Participants**
|
||||
```json
|
||||
{
|
||||
"event": "inning_change",
|
||||
"data": {
|
||||
"inning": 4,
|
||||
"half": "top",
|
||||
"home_score": 2,
|
||||
"away_score": 1,
|
||||
"timestamp": "2025-10-21T19:48:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Game Ended
|
||||
|
||||
**Server → All Participants**
|
||||
```json
|
||||
{
|
||||
"event": "game_ended",
|
||||
"data": {
|
||||
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"winner_team_id": 42,
|
||||
"final_score": {
|
||||
"home": 5,
|
||||
"away": 3
|
||||
},
|
||||
"innings": 9,
|
||||
"duration_minutes": 87,
|
||||
"mvp": {
|
||||
"card_id": 67890,
|
||||
"name": "Mike Trout",
|
||||
"stats": "3-4, 2 R, 2 RBI, HR"
|
||||
},
|
||||
"completed_at": "2025-10-21T20:15:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Decision Timeout Warning
|
||||
|
||||
**Server → User**
|
||||
```json
|
||||
{
|
||||
"event": "decision_timeout_warning",
|
||||
"data": {
|
||||
"decision_type": "set_defense",
|
||||
"seconds_remaining": 10,
|
||||
"default_action": "standard"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Auto-Decision Made
|
||||
|
||||
**Server → All Participants**
|
||||
```json
|
||||
{
|
||||
"event": "auto_decision",
|
||||
"data": {
|
||||
"decision_type": "set_defense",
|
||||
"team_id": 42,
|
||||
"action_taken": "standard",
|
||||
"reason": "timeout",
|
||||
"timestamp": "2025-10-21T19:45:55Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Events
|
||||
|
||||
### 1. Invalid Action
|
||||
|
||||
**Server → Client**
|
||||
```json
|
||||
{
|
||||
"event": "invalid_action",
|
||||
"data": {
|
||||
"action": "set_defense",
|
||||
"reason": "Not your turn",
|
||||
"current_decision": {
|
||||
"type": "set_offense",
|
||||
"team_id": 99
|
||||
},
|
||||
"timestamp": "2025-10-21T19:46:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Connection Error
|
||||
|
||||
**Server → Client**
|
||||
```json
|
||||
{
|
||||
"event": "connection_error",
|
||||
"data": {
|
||||
"code": "AUTH_FAILED",
|
||||
"message": "Invalid or expired token",
|
||||
"reconnect": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Game Error
|
||||
|
||||
**Server → All Participants**
|
||||
```json
|
||||
{
|
||||
"event": "game_error",
|
||||
"data": {
|
||||
"code": "STATE_RECOVERY_FAILED",
|
||||
"message": "Unable to recover game state",
|
||||
"severity": "critical",
|
||||
"recovery_options": [
|
||||
"reload_game",
|
||||
"contact_support"
|
||||
],
|
||||
"timestamp": "2025-10-21T19:46:30Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Per-User Limits
|
||||
- **Actions**: 10 per second
|
||||
- **Heartbeats**: 1 per 10 seconds minimum
|
||||
- **Invalid actions**: 5 per minute (after that, temporary ban)
|
||||
|
||||
### Response on Rate Limit
|
||||
```json
|
||||
{
|
||||
"event": "rate_limit_exceeded",
|
||||
"data": {
|
||||
"action": "set_defense",
|
||||
"limit": 10,
|
||||
"window": "1 second",
|
||||
"retry_after": 1000,
|
||||
"timestamp": "2025-10-21T19:46:35Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reconnection Protocol
|
||||
|
||||
### Automatic Reconnection
|
||||
1. Client detects disconnect
|
||||
2. Client attempts reconnect with backoff (1s, 2s, 4s, 8s, 16s)
|
||||
3. On successful reconnect, client sends `join_game` event
|
||||
4. Server checks if game state exists in memory
|
||||
5. If not, server recovers state from database
|
||||
6. Server sends full `game_state_update` to client
|
||||
7. Client resumes from current state
|
||||
|
||||
### Client Implementation
|
||||
```typescript
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Disconnected, will attempt reconnect')
|
||||
// Socket.io handles reconnection automatically
|
||||
})
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Reconnected')
|
||||
// Rejoin game room
|
||||
socket.emit('join_game', { game_id: currentGameId, role: 'player' })
|
||||
})
|
||||
```
|
||||
|
||||
## Testing WebSocket Events
|
||||
|
||||
### Using Python Client
|
||||
```python
|
||||
import socketio
|
||||
|
||||
sio = socketio.Client()
|
||||
|
||||
@sio.event
|
||||
def connect():
|
||||
print('Connected')
|
||||
sio.emit('join_game', {'game_id': 'test-game', 'role': 'player'})
|
||||
|
||||
@sio.event
|
||||
def game_state_update(data):
|
||||
print(f'Game state: {data}')
|
||||
|
||||
sio.connect('http://localhost:8000', auth={'token': 'jwt-token'})
|
||||
sio.wait()
|
||||
```
|
||||
|
||||
### Using Browser Console
|
||||
```javascript
|
||||
const socket = io('http://localhost:8000', {
|
||||
auth: { token: 'jwt-token' }
|
||||
})
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected')
|
||||
socket.emit('join_game', { game_id: 'test-game', role: 'player' })
|
||||
})
|
||||
|
||||
socket.on('game_state_update', (data) => {
|
||||
console.log('Game state:', data)
|
||||
})
|
||||
```
|
||||
|
||||
## Event Flow Diagrams
|
||||
|
||||
### Typical At-Bat Flow
|
||||
```
|
||||
1. [Server → All] decision_required (set_defense)
|
||||
2. [Client → Server] set_defense
|
||||
3. [Server → All] decision_recorded
|
||||
|
||||
4. [Server → All] decision_required (set_stolen_base) [if runners on base]
|
||||
5. [Client → Server] set_stolen_base
|
||||
6. [Server → All] decision_recorded
|
||||
|
||||
7. [Server → All] decision_required (set_offensive_approach)
|
||||
8. [Client → Server] set_offensive_approach
|
||||
9. [Server → All] decision_recorded
|
||||
|
||||
10. [Server → All] dice_rolled
|
||||
11. [Server → All] play_completed
|
||||
12. [Server → All] game_state_update
|
||||
|
||||
13. Loop to step 1 for next at-bat
|
||||
```
|
||||
|
||||
### Substitution Flow
|
||||
```
|
||||
1. [Client → Server] substitute_player
|
||||
2. [Server validates]
|
||||
3. [Server → All] substitution_made
|
||||
4. [Server → All] game_state_update (with new lineup)
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Authentication
|
||||
- JWT token required for initial connection
|
||||
- Token verified on every connection attempt
|
||||
- Token refresh handled by HTTP API, not WebSocket
|
||||
|
||||
### Authorization
|
||||
- User can only perform actions for their team
|
||||
- Spectators receive read-only events
|
||||
- Server validates all actions against game rules
|
||||
|
||||
### Data Validation
|
||||
- All incoming events validated against Pydantic schemas
|
||||
- Invalid events logged and rejected
|
||||
- Repeated invalid events result in disconnect
|
||||
|
||||
---
|
||||
|
||||
**Implementation**: See [backend-architecture.md](./backend-architecture.md) for connection manager implementation.
|
||||
77
.dockerignore
Normal file
77
.dockerignore
Normal file
@ -0,0 +1,77 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
.venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
tests/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Data files
|
||||
data/
|
||||
*.json
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
|
||||
# Documentation
|
||||
.claude/
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# CI/CD
|
||||
.github/
|
||||
.gitlab-ci.yml
|
||||
.gitlab/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
47
.env.example
Normal file
47
.env.example
Normal file
@ -0,0 +1,47 @@
|
||||
# Paper Dynasty Game Engine - Environment Variables
|
||||
# Copy this file to .env and update with your actual values
|
||||
|
||||
# ============================================================================
|
||||
# Application
|
||||
# ============================================================================
|
||||
APP_ENV=development
|
||||
DEBUG=true
|
||||
SECRET_KEY=your-secret-key-at-least-32-characters-long
|
||||
|
||||
# ============================================================================
|
||||
# Database
|
||||
# ============================================================================
|
||||
# Update with your actual PostgreSQL server credentials
|
||||
# Format: postgresql+asyncpg://username:password@hostname:port/database
|
||||
DATABASE_URL=postgresql+asyncpg://paperdynasty:your-password@your-db-server:5432/paperdynasty_dev
|
||||
|
||||
# ============================================================================
|
||||
# Discord OAuth
|
||||
# ============================================================================
|
||||
# Get these from Discord Developer Portal: https://discord.com/developers/applications
|
||||
DISCORD_CLIENT_ID=your-discord-client-id
|
||||
DISCORD_CLIENT_SECRET=your-discord-client-secret
|
||||
DISCORD_REDIRECT_URI=http://localhost:3000/auth/callback
|
||||
|
||||
# ============================================================================
|
||||
# League REST APIs
|
||||
# ============================================================================
|
||||
# SBA League API
|
||||
SBA_API_URL=https://sba-api.example.com
|
||||
SBA_API_KEY=your-sba-api-key
|
||||
|
||||
# PD League API
|
||||
PD_API_URL=https://pd-api.example.com
|
||||
PD_API_KEY=your-pd-api-key
|
||||
|
||||
# ============================================================================
|
||||
# CORS Origins (comma-separated)
|
||||
# ============================================================================
|
||||
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
|
||||
|
||||
# ============================================================================
|
||||
# Redis (optional - for caching)
|
||||
# ============================================================================
|
||||
# When using Docker Compose, Redis is automatically available at redis://redis:6379
|
||||
# When running locally, use redis://localhost:6379
|
||||
# REDIS_URL=redis://localhost:6379
|
||||
43
.gitignore
vendored
43
.gitignore
vendored
@ -158,3 +158,46 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Nuxt
|
||||
.nuxt/
|
||||
.output/
|
||||
.nitro/
|
||||
dist/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
backend/logs/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Docker volumes
|
||||
postgres_data/
|
||||
redis_data/
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Project-specific
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.production
|
||||
|
||||
155
CLAUDE.md
Normal file
155
CLAUDE.md
Normal file
@ -0,0 +1,155 @@
|
||||
# 🚨 CRITICAL: @ MENTION HANDLING 🚨
|
||||
When ANY file is mentioned with @ syntax, you MUST IMMEDIATELY call Read tool on that file BEFORE responding.
|
||||
You will see automatic loads of any @ mentioned filed, this is NOT ENOUGH, it only loads the file contents.
|
||||
You MUST perform Read tool calls on the files directly, even if they were @ included.
|
||||
This is NOT optional - it loads required CLAUDE.md context. along the file path.
|
||||
See @./.claude/force-claude-reads.md for details.
|
||||
|
||||
---
|
||||
|
||||
# Paper Dynasty Real-Time Game Engine - Development Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
Web-based real-time multiplayer baseball simulation platform replacing legacy Google Sheets system. Consists of:
|
||||
- **Shared Backend**: FastAPI (Python 3.11+) with WebSocket support, PostgreSQL persistence
|
||||
- **Dual Frontends**: Separate Vue 3/Nuxt 3 apps per league (SBA and PD) with shared component library
|
||||
|
||||
**Critical Business Driver**: Legacy Google Sheets being deprecated - this is mission-critical replacement.
|
||||
|
||||
## Architecture Principles
|
||||
|
||||
### Backend Philosophy
|
||||
- **Hybrid State Model**: In-memory game state for performance + PostgreSQL for persistence/recovery
|
||||
- **League-Agnostic Core**: Polymorphic player models, config-driven league variations
|
||||
- **Async-First**: All I/O operations use async/await patterns
|
||||
- **Type Safety**: Pydantic models for validation, SQLAlchemy for ORM
|
||||
|
||||
### Frontend Philosophy
|
||||
- **Mobile-First**: Primary design target is mobile portrait mode
|
||||
- **Real-Time Updates**: WebSocket (Socket.io) for all game state changes
|
||||
- **Shared Components**: Maximize reuse between league frontends
|
||||
- **Type Safety**: TypeScript with strict mode
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Backend
|
||||
- FastAPI + Socket.io (WebSocket)
|
||||
- PostgreSQL 14+ with SQLAlchemy 2.0
|
||||
- Pydantic for data validation
|
||||
- pytest for testing
|
||||
|
||||
### Frontend (Per League)
|
||||
- Vue 3 Composition API + Nuxt 3
|
||||
- TypeScript (strict mode)
|
||||
- Tailwind CSS
|
||||
- Pinia for state management
|
||||
- Socket.io-client
|
||||
- @nuxtjs/auth-next (Discord OAuth)
|
||||
|
||||
## Key Technical Patterns
|
||||
|
||||
### Polymorphic Player Architecture
|
||||
Use factory pattern for league-specific player types:
|
||||
- `BasePlayer` (abstract base)
|
||||
- `SbaPlayer` (simple model)
|
||||
- `PdPlayer` (detailed scouting data)
|
||||
- `Lineup.from_api_data(config, data)` factory method
|
||||
|
||||
### WebSocket Event Flow
|
||||
1. Player action → WebSocket → Backend
|
||||
2. Validate against in-memory state
|
||||
3. Process + resolve outcome
|
||||
4. Update in-memory state
|
||||
5. Async write to PostgreSQL
|
||||
6. Broadcast state update to all clients
|
||||
|
||||
### Game State Recovery
|
||||
On reconnect: Load plays from DB → Replay to rebuild state → Send current state
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
strat-gameplay-webapp/
|
||||
├── backend/ # FastAPI game engine
|
||||
│ ├── app/
|
||||
│ │ ├── core/ # Game engine, dice, state management
|
||||
│ │ ├── config/ # League configs and result charts
|
||||
│ │ ├── websocket/ # Socket.io handlers
|
||||
│ │ ├── models/ # Pydantic + SQLAlchemy models
|
||||
│ │ └── api/ # REST endpoints
|
||||
│ └── tests/
|
||||
├── frontend-sba/ # SBA League Nuxt app
|
||||
├── frontend-pd/ # PD League Nuxt app
|
||||
└── shared-components/ # Shared Vue components (optional)
|
||||
```
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Quality
|
||||
- **Python**: Dataclasses preferred, rotating loggers with `f'{__name__}.<className>'`
|
||||
- **Error Handling**: "Raise or Return" pattern - no Optional unless required
|
||||
- **Testing**: Run tests freely without asking permission
|
||||
- **Imports**: Always verify imports during code review to prevent NameErrors
|
||||
- **Git Commits**: Prefix with "CLAUDE: "
|
||||
|
||||
### Performance Targets
|
||||
- Action response: < 500ms
|
||||
- WebSocket delivery: < 200ms
|
||||
- DB writes: < 100ms (async)
|
||||
- State recovery: < 2 seconds
|
||||
|
||||
### Security Requirements
|
||||
- Discord OAuth for authentication
|
||||
- Server-side game logic only (zero client trust)
|
||||
- Cryptographically secure dice rolls
|
||||
- All rules enforced server-side
|
||||
|
||||
## Phase 1 MVP Scope (Weeks 1-13)
|
||||
|
||||
**Core Deliverables**:
|
||||
1. Authentication (Discord OAuth)
|
||||
2. Game creation & lobby
|
||||
3. Complete turn-based gameplay with all strategic decisions
|
||||
4. Real-time WebSocket updates
|
||||
5. Game persistence & recovery
|
||||
6. Spectator mode
|
||||
7. Mobile-optimized UI
|
||||
8. AI opponent support
|
||||
|
||||
**Explicitly Out of Scope for MVP**:
|
||||
- Roster management
|
||||
- Marketplace
|
||||
- Tournaments
|
||||
- Advanced analytics
|
||||
|
||||
## Critical References
|
||||
|
||||
- **Full PRD**: `/mnt/NV2/Development/strat-gameplay-webapp/prd-web-scorecard-1.1.md`
|
||||
- **Player Model Architecture**: PRD lines 378-551
|
||||
- **Database Schema**: PRD lines 553-628
|
||||
- **WebSocket Events**: PRD lines 630-669
|
||||
- **League Config System**: PRD lines 780-846
|
||||
|
||||
## League Differences
|
||||
|
||||
### SBA League
|
||||
- Minimal player data (id, name, image)
|
||||
- Simpler rules configuration
|
||||
|
||||
### PD League
|
||||
- Detailed scouting data on players
|
||||
- Complex probability calculations
|
||||
- Additional analytics requirements
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- 90% player migration within 1 month
|
||||
- 99.5% uptime
|
||||
- < 500ms average action latency
|
||||
- 60%+ mobile usage
|
||||
- Zero data corruption
|
||||
|
||||
---
|
||||
|
||||
**Note**: Subdirectories will have their own CLAUDE.md files with implementation-specific details to minimize context usage.
|
||||
287
README.md
Normal file
287
README.md
Normal file
@ -0,0 +1,287 @@
|
||||
# Paper Dynasty Real-Time Game Engine
|
||||
|
||||
Web-based real-time multiplayer baseball simulation platform replacing the legacy Google Sheets system.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
strat-gameplay-webapp/
|
||||
├── backend/ # FastAPI game engine
|
||||
├── frontend-sba/ # SBA League Nuxt frontend
|
||||
├── frontend-pd/ # PD League Nuxt frontend
|
||||
├── .claude/ # Claude AI implementation guides
|
||||
├── docker-compose.yml # Full stack orchestration
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Two Development Workflows
|
||||
|
||||
### Option 1: Local Development (Recommended for Daily Work)
|
||||
|
||||
**Best for:** Fast hot-reload, quick iteration, debugging
|
||||
|
||||
**Services:**
|
||||
- ✅ Backend runs locally (Python hot-reload)
|
||||
- ✅ Frontends run locally (Nuxt hot-reload)
|
||||
- ✅ Redis in Docker (lightweight)
|
||||
- ✅ PostgreSQL on your existing server
|
||||
|
||||
**Setup:**
|
||||
|
||||
1. **Environment Setup**
|
||||
```bash
|
||||
# Copy environment template
|
||||
cp .env.example .env
|
||||
# Edit .env with your database credentials and API keys
|
||||
```
|
||||
|
||||
2. **Start Redis** (in one terminal)
|
||||
```bash
|
||||
cd backend
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
3. **Start Backend** (in another terminal)
|
||||
```bash
|
||||
cd backend
|
||||
source venv/bin/activate # or 'venv\Scripts\activate' on Windows
|
||||
python -m app.main
|
||||
```
|
||||
Backend will be available at http://localhost:8000
|
||||
|
||||
4. **Start SBA Frontend** (in another terminal)
|
||||
```bash
|
||||
cd frontend-sba
|
||||
npm run dev
|
||||
```
|
||||
SBA frontend will be available at http://localhost:3000
|
||||
|
||||
5. **Start PD Frontend** (in another terminal)
|
||||
```bash
|
||||
cd frontend-pd
|
||||
npm run dev
|
||||
```
|
||||
PD frontend will be available at http://localhost:3001
|
||||
|
||||
**Advantages:**
|
||||
- ⚡ Instant hot-reload on code changes
|
||||
- 🐛 Easy debugging (native debuggers work)
|
||||
- 💨 Fast startup times
|
||||
- 🔧 Simple to restart individual services
|
||||
|
||||
---
|
||||
|
||||
### Option 2: Full Docker Orchestration
|
||||
|
||||
**Best for:** Integration testing, demos, production-like environment
|
||||
|
||||
**Services:**
|
||||
- ✅ Everything runs in containers
|
||||
- ✅ Consistent environment
|
||||
- ✅ One command to start everything
|
||||
|
||||
**Setup:**
|
||||
|
||||
1. **Environment Setup**
|
||||
```bash
|
||||
# Copy environment template
|
||||
cp .env.example .env
|
||||
# Edit .env with your database credentials and API keys
|
||||
```
|
||||
|
||||
2. **Start Everything**
|
||||
```bash
|
||||
# From project root
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Or run in background:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
3. **View Logs**
|
||||
```bash
|
||||
# All services
|
||||
docker-compose logs -f
|
||||
|
||||
# Specific service
|
||||
docker-compose logs -f backend
|
||||
docker-compose logs -f frontend-sba
|
||||
```
|
||||
|
||||
4. **Stop Everything**
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
**Advantages:**
|
||||
- 🎯 Production-like environment
|
||||
- 🚀 One-command startup
|
||||
- 🔄 Easy to share with team
|
||||
- ✅ CI/CD ready
|
||||
|
||||
---
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
# Run server
|
||||
python -m app.main
|
||||
|
||||
# Run tests
|
||||
pytest tests/ -v
|
||||
|
||||
# Code formatting
|
||||
black app/ tests/
|
||||
|
||||
# Linting
|
||||
flake8 app/ tests/
|
||||
|
||||
# Type checking
|
||||
mypy app/
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd frontend-sba # or frontend-pd
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run dev server
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
|
||||
# Lint
|
||||
npm run lint
|
||||
|
||||
# Type check
|
||||
npm run type-check
|
||||
```
|
||||
|
||||
## Database Setup
|
||||
|
||||
This project uses an existing PostgreSQL server. You need to manually create the database:
|
||||
|
||||
```sql
|
||||
-- On your PostgreSQL server
|
||||
CREATE DATABASE paperdynasty_dev;
|
||||
CREATE USER paperdynasty WITH PASSWORD 'your-secure-password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE paperdynasty_dev TO paperdynasty;
|
||||
```
|
||||
|
||||
Then update `DATABASE_URL` in `.env`:
|
||||
```
|
||||
DATABASE_URL=postgresql+asyncpg://paperdynasty:your-password@your-db-server:5432/paperdynasty_dev
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Copy `.env.example` to `.env` and configure:
|
||||
|
||||
### Required
|
||||
- `DATABASE_URL` - PostgreSQL connection string
|
||||
- `SECRET_KEY` - Application secret key (at least 32 characters)
|
||||
- `DISCORD_CLIENT_ID` - Discord OAuth client ID
|
||||
- `DISCORD_CLIENT_SECRET` - Discord OAuth secret
|
||||
- `SBA_API_URL` / `SBA_API_KEY` - SBA League API credentials
|
||||
- `PD_API_URL` / `PD_API_KEY` - PD League API credentials
|
||||
|
||||
### Optional
|
||||
- `REDIS_URL` - Redis connection (auto-configured in Docker)
|
||||
- `CORS_ORIGINS` - Allowed origins (defaults to localhost:3000,3001)
|
||||
|
||||
## Available Services
|
||||
|
||||
When running, the following services are available:
|
||||
|
||||
| Service | URL | Description |
|
||||
|---------|-----|-------------|
|
||||
| Backend API | http://localhost:8000 | FastAPI REST API |
|
||||
| Backend Docs | http://localhost:8000/docs | Interactive API documentation |
|
||||
| SBA Frontend | http://localhost:3000 | SBA League web app |
|
||||
| PD Frontend | http://localhost:3001 | PD League web app |
|
||||
| Redis | localhost:6379 | Cache (not exposed via HTTP) |
|
||||
|
||||
## Health Checks
|
||||
|
||||
```bash
|
||||
# Backend health
|
||||
curl http://localhost:8000/api/health
|
||||
|
||||
# Or visit in browser
|
||||
open http://localhost:8000/api/health
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Backend won't start
|
||||
- Check `DATABASE_URL` is correct in `.env`
|
||||
- Verify PostgreSQL database exists
|
||||
- Ensure Redis is running (`docker-compose up` in backend/)
|
||||
- Check logs for specific errors
|
||||
|
||||
### Frontend won't connect to backend
|
||||
- Verify backend is running at http://localhost:8000
|
||||
- Check CORS settings in backend `.env`
|
||||
- Clear browser cache and cookies
|
||||
- Check browser console for errors
|
||||
|
||||
### Docker containers won't start
|
||||
- Ensure `.env` file exists with all required variables
|
||||
- Run `docker-compose down` then `docker-compose up` again
|
||||
- Check `docker-compose logs` for specific errors
|
||||
- Verify no port conflicts (8000, 3000, 3001, 6379)
|
||||
|
||||
### Database connection fails
|
||||
- Verify PostgreSQL server is accessible
|
||||
- Check firewall rules allow connection
|
||||
- Confirm database and user exist
|
||||
- Test connection with `psql` directly
|
||||
|
||||
## Documentation
|
||||
|
||||
- **Full PRD**: See `/prd-web-scorecard-1.1.md`
|
||||
- **Implementation Guide**: See `.claude/implementation/00-index.md`
|
||||
- **Architecture Docs**: See `.claude/implementation/` directory
|
||||
|
||||
## Tech Stack
|
||||
|
||||
### Backend
|
||||
- **Framework**: FastAPI (Python 3.11+)
|
||||
- **WebSocket**: Socket.io
|
||||
- **Database**: PostgreSQL 14+ with SQLAlchemy
|
||||
- **Cache**: Redis 7
|
||||
- **Auth**: Discord OAuth with JWT
|
||||
|
||||
### Frontend
|
||||
- **Framework**: Vue 3 + Nuxt 3
|
||||
- **Language**: TypeScript
|
||||
- **Styling**: Tailwind CSS
|
||||
- **State**: Pinia
|
||||
- **WebSocket**: Socket.io-client
|
||||
|
||||
## Contributing
|
||||
|
||||
See `.claude/implementation/` for detailed implementation guides and architecture documentation.
|
||||
|
||||
## License
|
||||
|
||||
Proprietary - Paper Dynasty League System
|
||||
51
backend/.dockerignore
Normal file
51
backend/.dockerignore
Normal file
@ -0,0 +1,51 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
335
backend/CLAUDE.md
Normal file
335
backend/CLAUDE.md
Normal file
@ -0,0 +1,335 @@
|
||||
# Backend - Paper Dynasty Game Engine
|
||||
|
||||
## Overview
|
||||
|
||||
FastAPI-based real-time game backend handling WebSocket communication, game state management, and database persistence for both SBA and PD leagues.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Framework**: FastAPI (Python 3.11+)
|
||||
- **WebSocket**: Socket.io (python-socketio)
|
||||
- **Database**: PostgreSQL 14+ with SQLAlchemy 2.0 (async)
|
||||
- **ORM**: SQLAlchemy with asyncpg driver
|
||||
- **Validation**: Pydantic v2
|
||||
- **Testing**: pytest with pytest-asyncio
|
||||
- **Code Quality**: black, flake8, mypy
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── main.py # FastAPI app + Socket.io initialization
|
||||
│ ├── config.py # Settings with pydantic-settings
|
||||
│ │
|
||||
│ ├── core/ # Game logic (Phase 2+)
|
||||
│ │ ├── game_engine.py # Main game simulation
|
||||
│ │ ├── state_manager.py # In-memory state
|
||||
│ │ ├── play_resolver.py # Play outcome resolution
|
||||
│ │ ├── dice.py # Secure random rolls
|
||||
│ │ └── validators.py # Rule validation
|
||||
│ │
|
||||
│ ├── config/ # League configurations (Phase 2+)
|
||||
│ │ ├── base_config.py # Shared configuration
|
||||
│ │ ├── league_configs.py # SBA/PD specific
|
||||
│ │ └── result_charts.py # d20 outcome tables
|
||||
│ │
|
||||
│ ├── models/ # Data models
|
||||
│ │ ├── db_models.py # SQLAlchemy ORM models
|
||||
│ │ ├── game_models.py # Pydantic game state models (Phase 2+)
|
||||
│ │ └── player_models.py # Polymorphic player models (Phase 2+)
|
||||
│ │
|
||||
│ ├── websocket/ # WebSocket handling
|
||||
│ │ ├── connection_manager.py # Connection lifecycle
|
||||
│ │ ├── handlers.py # Event handlers
|
||||
│ │ └── events.py # Event definitions (Phase 2+)
|
||||
│ │
|
||||
│ ├── api/ # REST API
|
||||
│ │ ├── routes/
|
||||
│ │ │ ├── health.py # Health check endpoints
|
||||
│ │ │ ├── auth.py # Discord OAuth (Phase 1)
|
||||
│ │ │ └── games.py # Game CRUD (Phase 2+)
|
||||
│ │ └── dependencies.py # FastAPI dependencies
|
||||
│ │
|
||||
│ ├── database/ # Database layer
|
||||
│ │ ├── session.py # Async session management
|
||||
│ │ └── operations.py # DB operations (Phase 2+)
|
||||
│ │
|
||||
│ ├── data/ # External data (Phase 2+)
|
||||
│ │ ├── api_client.py # League REST API client
|
||||
│ │ └── cache.py # Caching layer
|
||||
│ │
|
||||
│ └── utils/ # Utilities
|
||||
│ ├── logging.py # Logging setup
|
||||
│ └── auth.py # JWT utilities (Phase 1)
|
||||
│
|
||||
├── tests/
|
||||
│ ├── unit/ # Unit tests
|
||||
│ ├── integration/ # Integration tests
|
||||
│ └── e2e/ # End-to-end tests
|
||||
│
|
||||
├── logs/ # Application logs (gitignored)
|
||||
├── venv/ # Virtual environment (gitignored)
|
||||
├── .env # Environment variables (gitignored)
|
||||
├── .env.example # Environment template
|
||||
├── requirements.txt # Production dependencies
|
||||
├── requirements-dev.txt # Dev dependencies
|
||||
├── Dockerfile # Container definition
|
||||
├── docker-compose.yml # Redis for local dev
|
||||
└── pytest.ini # Pytest configuration
|
||||
```
|
||||
|
||||
## Key Architectural Patterns
|
||||
|
||||
### 1. Hybrid State Management
|
||||
- **In-Memory**: Active game states for fast access (<500ms response)
|
||||
- **PostgreSQL**: Persistent storage for recovery and history
|
||||
- **Pattern**: Write-through cache (update memory + async DB write)
|
||||
|
||||
### 2. Polymorphic Player Models
|
||||
```python
|
||||
# Base class with abstract methods
|
||||
class BasePlayer(BaseModel, ABC):
|
||||
@abstractmethod
|
||||
def get_image_url(self) -> str: ...
|
||||
|
||||
# League-specific implementations
|
||||
class SbaPlayer(BasePlayer): ...
|
||||
class PdPlayer(BasePlayer): ...
|
||||
|
||||
# Factory pattern for instantiation
|
||||
Lineup.from_api_data(config, data)
|
||||
```
|
||||
|
||||
### 3. League-Agnostic Core
|
||||
- Game engine works for any league
|
||||
- League-specific logic in config classes
|
||||
- Result charts loaded per league
|
||||
|
||||
### 4. Async-First
|
||||
- All database operations use `async/await`
|
||||
- Database writes don't block game logic
|
||||
- Connection pooling for efficiency
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Daily Development
|
||||
```bash
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Start Redis (in separate terminal)
|
||||
docker-compose up
|
||||
|
||||
# Run backend with hot-reload
|
||||
python -m app.main
|
||||
|
||||
# Backend available at http://localhost:8000
|
||||
# API docs at http://localhost:8000/docs
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run all tests
|
||||
pytest tests/ -v
|
||||
|
||||
# Run with coverage
|
||||
pytest tests/ --cov=app --cov-report=html
|
||||
|
||||
# Run specific test file
|
||||
pytest tests/unit/test_game_engine.py -v
|
||||
|
||||
# Type checking
|
||||
mypy app/
|
||||
|
||||
# Code formatting
|
||||
black app/ tests/
|
||||
|
||||
# Linting
|
||||
flake8 app/ tests/
|
||||
```
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### Python Style
|
||||
- **Formatting**: Black with default settings
|
||||
- **Line Length**: 88 characters (black default)
|
||||
- **Imports**: Group stdlib, third-party, local (isort compatible)
|
||||
- **Type Hints**: Required for all public functions
|
||||
- **Docstrings**: Google style for classes and public methods
|
||||
|
||||
### Logging Pattern
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(f'{__name__}.ClassName')
|
||||
|
||||
# Usage
|
||||
logger.info(f"User {user_id} connected")
|
||||
logger.error(f"Failed to process action: {error}", exc_info=True)
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
- **Raise or Return**: Never return `Optional` unless specifically required
|
||||
- **Custom Exceptions**: Use for domain-specific errors
|
||||
- **Logging**: Always log exceptions with context
|
||||
|
||||
### Dataclasses
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class GameState:
|
||||
game_id: str
|
||||
inning: int
|
||||
outs: int
|
||||
# ... fields
|
||||
```
|
||||
|
||||
## Database Patterns
|
||||
|
||||
### Async Session Usage
|
||||
```python
|
||||
from app.database.session import get_session
|
||||
|
||||
async def some_function():
|
||||
async with get_session() as session:
|
||||
result = await session.execute(query)
|
||||
# session.commit() happens automatically
|
||||
```
|
||||
|
||||
### Model Definitions
|
||||
```python
|
||||
from app.database.session import Base
|
||||
from sqlalchemy import Column, String, Integer
|
||||
|
||||
class Game(Base):
|
||||
__tablename__ = "games"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True)
|
||||
# ... columns
|
||||
```
|
||||
|
||||
## WebSocket Patterns
|
||||
|
||||
### Event Handler Registration
|
||||
```python
|
||||
@sio.event
|
||||
async def some_event(sid, data):
|
||||
"""Handle some_event from client"""
|
||||
try:
|
||||
# Validate data
|
||||
# Process action
|
||||
# Emit response
|
||||
await sio.emit('response_event', result, room=sid)
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling event: {e}")
|
||||
await sio.emit('error', {'message': str(e)}, room=sid)
|
||||
```
|
||||
|
||||
### Broadcasting
|
||||
```python
|
||||
# To specific game room
|
||||
await connection_manager.broadcast_to_game(
|
||||
game_id,
|
||||
'game_state_update',
|
||||
state_data
|
||||
)
|
||||
|
||||
# To specific user
|
||||
await connection_manager.emit_to_user(
|
||||
sid,
|
||||
'decision_required',
|
||||
decision_data
|
||||
)
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Required in `.env`:
|
||||
```bash
|
||||
# Database
|
||||
DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/dbname
|
||||
|
||||
# Application
|
||||
SECRET_KEY=your-secret-key-at-least-32-chars
|
||||
|
||||
# Discord OAuth
|
||||
DISCORD_CLIENT_ID=your-client-id
|
||||
DISCORD_CLIENT_SECRET=your-client-secret
|
||||
|
||||
# League APIs
|
||||
SBA_API_URL=https://sba-api.example.com
|
||||
SBA_API_KEY=your-api-key
|
||||
PD_API_URL=https://pd-api.example.com
|
||||
PD_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
## Performance Targets
|
||||
|
||||
- **Action Response**: < 500ms from user action to state update
|
||||
- **WebSocket Delivery**: < 200ms
|
||||
- **Database Write**: < 100ms (async, non-blocking)
|
||||
- **State Recovery**: < 2 seconds
|
||||
- **Concurrent Games**: Support 10+ simultaneous games
|
||||
- **Memory**: < 1GB with 10 active games
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Authentication**: All WebSocket connections require valid JWT
|
||||
- **Authorization**: Verify team ownership before allowing actions
|
||||
- **Input Validation**: Pydantic models validate all inputs
|
||||
- **SQL Injection**: Prevented by SQLAlchemy ORM
|
||||
- **Dice Rolls**: Cryptographically secure random generation
|
||||
- **Server-Side Logic**: All game rules enforced server-side
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New API Endpoint
|
||||
1. Create route in `app/api/routes/`
|
||||
2. Define Pydantic request/response models
|
||||
3. Add dependency injection if needed
|
||||
4. Register router in `app/main.py`
|
||||
|
||||
### Adding a New WebSocket Event
|
||||
1. Define event handler in `app/websocket/handlers.py`
|
||||
2. Register with `@sio.event` decorator
|
||||
3. Validate data with Pydantic
|
||||
4. Add corresponding client handling in frontend
|
||||
|
||||
### Adding a New Database Model
|
||||
1. Define SQLAlchemy model in `app/models/db_models.py`
|
||||
2. Create Alembic migration: `alembic revision --autogenerate -m "description"`
|
||||
3. Apply migration: `alembic upgrade head`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Import Errors
|
||||
- Ensure virtual environment is activated
|
||||
- Check `PYTHONPATH` if using custom structure
|
||||
- Verify all `__init__.py` files exist
|
||||
|
||||
### Database Connection Issues
|
||||
- Verify `DATABASE_URL` in `.env` is correct
|
||||
- Test connection: `psql $DATABASE_URL`
|
||||
- Check firewall/network access
|
||||
- Verify database exists
|
||||
|
||||
### WebSocket Not Connecting
|
||||
- Check CORS settings in `config.py`
|
||||
- Verify token is being sent from client
|
||||
- Check logs for connection errors
|
||||
- Ensure Socket.io versions match (client/server)
|
||||
|
||||
## References
|
||||
|
||||
- **Implementation Guide**: `../.claude/implementation/01-infrastructure.md`
|
||||
- **Backend Architecture**: `../.claude/implementation/backend-architecture.md`
|
||||
- **WebSocket Protocol**: `../.claude/implementation/websocket-protocol.md`
|
||||
- **Database Design**: `../.claude/implementation/database-design.md`
|
||||
- **Full PRD**: `../prd-web-scorecard-1.1.md`
|
||||
|
||||
---
|
||||
|
||||
**Current Phase**: Phase 1 - Core Infrastructure
|
||||
**Next Phase**: Phase 2 - Game Engine Core
|
||||
66
backend/Dockerfile
Normal file
66
backend/Dockerfile
Normal file
@ -0,0 +1,66 @@
|
||||
# Backend Dockerfile for Paper Dynasty Game Engine
|
||||
# Multi-stage build for optimized production image
|
||||
|
||||
FROM python:3.11-slim as base
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PIP_NO_CACHE_DIR=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
postgresql-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
# Development stage
|
||||
FROM base as development
|
||||
|
||||
# Copy requirements
|
||||
COPY requirements.txt requirements-dev.txt ./
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install -r requirements-dev.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run with uvicorn reload for development
|
||||
CMD ["python", "-m", "uvicorn", "app.main:socket_app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
|
||||
# Production stage
|
||||
FROM base as production
|
||||
|
||||
# Copy requirements (production only)
|
||||
COPY requirements.txt ./
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 appuser && \
|
||||
chown -R appuser:appuser /app
|
||||
|
||||
# Copy application code
|
||||
COPY --chown=appuser:appuser . .
|
||||
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/api/health || exit 1
|
||||
|
||||
# Run with production server
|
||||
CMD ["python", "-m", "uvicorn", "app.main:socket_app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
||||
121
docker-compose.yml
Normal file
121
docker-compose.yml
Normal file
@ -0,0 +1,121 @@
|
||||
# Paper Dynasty Game Engine - Full Stack Orchestration
|
||||
# Use this for integration testing, demos, or when you want everything containerized
|
||||
# For daily dev, see README.md for local development workflow
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Redis cache (shared dependency)
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
# FastAPI Game Backend
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
# Application
|
||||
- APP_ENV=development
|
||||
- DEBUG=true
|
||||
- SECRET_KEY=${SECRET_KEY:-dev-secret-key-change-in-production}
|
||||
|
||||
# Database (using host machine's PostgreSQL)
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
|
||||
# Redis
|
||||
- REDIS_URL=redis://redis:6379
|
||||
|
||||
# Discord OAuth
|
||||
- DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
|
||||
- DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET}
|
||||
- DISCORD_REDIRECT_URI=${DISCORD_REDIRECT_URI:-http://localhost:3000/auth/callback}
|
||||
|
||||
# League APIs
|
||||
- SBA_API_URL=${SBA_API_URL}
|
||||
- SBA_API_KEY=${SBA_API_KEY}
|
||||
- PD_API_URL=${PD_API_URL}
|
||||
- PD_API_KEY=${PD_API_KEY}
|
||||
|
||||
# CORS
|
||||
- CORS_ORIGINS=http://localhost:3000,http://localhost:3001
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
# Mount source code for hot-reload during development
|
||||
- ./backend/app:/app/app:ro
|
||||
- ./backend/logs:/app/logs
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# SBA League Frontend
|
||||
frontend-sba:
|
||||
build:
|
||||
context: ./frontend-sba
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NUXT_PUBLIC_LEAGUE_ID=sba
|
||||
- NUXT_PUBLIC_LEAGUE_NAME=Super Baseball Alliance
|
||||
- NUXT_PUBLIC_API_URL=http://localhost:8000
|
||||
- NUXT_PUBLIC_WS_URL=http://localhost:8000
|
||||
- NUXT_PUBLIC_DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
|
||||
- NUXT_PUBLIC_DISCORD_REDIRECT_URI=http://localhost:3000/auth/callback
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
# Mount source for hot-reload
|
||||
- ./frontend-sba:/app
|
||||
- /app/node_modules
|
||||
- /app/.nuxt
|
||||
restart: unless-stopped
|
||||
|
||||
# PD League Frontend
|
||||
frontend-pd:
|
||||
build:
|
||||
context: ./frontend-pd
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
- NUXT_PUBLIC_LEAGUE_ID=pd
|
||||
- NUXT_PUBLIC_LEAGUE_NAME=Paper Dynasty
|
||||
- NUXT_PUBLIC_API_URL=http://localhost:8000
|
||||
- NUXT_PUBLIC_WS_URL=http://localhost:8000
|
||||
- NUXT_PUBLIC_DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
|
||||
- NUXT_PUBLIC_DISCORD_REDIRECT_URI=http://localhost:3001/auth/callback
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
# Mount source for hot-reload
|
||||
- ./frontend-pd:/app
|
||||
- /app/node_modules
|
||||
- /app/.nuxt
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: paperdynasty-network
|
||||
52
frontend-pd/.dockerignore
Normal file
52
frontend-pd/.dockerignore
Normal file
@ -0,0 +1,52 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Build outputs
|
||||
.output/
|
||||
.nuxt/
|
||||
dist/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Nuxt specific
|
||||
.nuxt/
|
||||
.output/
|
||||
nuxt.d.ts
|
||||
581
frontend-pd/CLAUDE.md
Normal file
581
frontend-pd/CLAUDE.md
Normal file
@ -0,0 +1,581 @@
|
||||
# Frontend PD - Paper Dynasty Web App
|
||||
|
||||
## Overview
|
||||
|
||||
Vue 3 + Nuxt 3 frontend for the PD (Paper Dynasty) league. Provides real-time game interface with WebSocket communication to the game backend.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Framework**: Nuxt 3 (Vue 3 Composition API)
|
||||
- **Language**: TypeScript (strict mode)
|
||||
- **Styling**: Tailwind CSS
|
||||
- **State Management**: Pinia
|
||||
- **WebSocket**: Socket.io-client
|
||||
- **HTTP Client**: Axios (or Nuxt's built-in fetch)
|
||||
- **Auth**: Discord OAuth with JWT
|
||||
|
||||
## League-Specific Characteristics
|
||||
|
||||
### PD League
|
||||
- **Player Data**: Detailed model with scouting data, ratings, probabilities
|
||||
- **Focus**: Advanced analytics and detailed player evaluation
|
||||
- **Branding**: Green primary color (#16a34a)
|
||||
- **API**: PD-specific REST API for team/player data with analytics
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
frontend-pd/
|
||||
├── assets/
|
||||
│ ├── css/
|
||||
│ │ └── tailwind.css # Tailwind imports
|
||||
│ └── images/ # PD branding assets
|
||||
│
|
||||
├── components/
|
||||
│ ├── Branding/ # PD-specific branding
|
||||
│ │ ├── Header.vue
|
||||
│ │ ├── Footer.vue
|
||||
│ │ └── Logo.vue
|
||||
│ └── League/ # PD-specific features
|
||||
│ ├── PlayerCardDetailed.vue # Player cards with scouting
|
||||
│ └── ScoutingPanel.vue # Scouting data display
|
||||
│
|
||||
├── composables/
|
||||
│ ├── useAuth.ts # Authentication state
|
||||
│ ├── useWebSocket.ts # WebSocket connection
|
||||
│ ├── useGameState.ts # Game state management
|
||||
│ └── useLeagueConfig.ts # PD-specific config
|
||||
│
|
||||
├── layouts/
|
||||
│ ├── default.vue # Standard layout
|
||||
│ ├── game.vue # Game view layout
|
||||
│ └── auth.vue # Auth pages layout
|
||||
│
|
||||
├── pages/
|
||||
│ ├── index.vue # Home/dashboard
|
||||
│ ├── games/
|
||||
│ │ ├── [id].vue # Game view
|
||||
│ │ ├── create.vue # Create new game
|
||||
│ │ └── history.vue # Completed games
|
||||
│ ├── auth/
|
||||
│ │ ├── login.vue
|
||||
│ │ └── callback.vue # Discord OAuth callback
|
||||
│ └── spectate/
|
||||
│ └── [id].vue # Spectator view
|
||||
│
|
||||
├── plugins/
|
||||
│ ├── socket.client.ts # Socket.io plugin
|
||||
│ └── auth.ts # Auth plugin
|
||||
│
|
||||
├── store/ # Pinia stores
|
||||
│ ├── auth.ts # Authentication state
|
||||
│ ├── game.ts # Current game state
|
||||
│ ├── games.ts # Games list
|
||||
│ └── ui.ts # UI state (modals, toasts)
|
||||
│
|
||||
├── types/
|
||||
│ ├── game.ts # Game-related types
|
||||
│ ├── player.ts # PD player types (with scouting)
|
||||
│ ├── api.ts # API response types
|
||||
│ └── websocket.ts # WebSocket event types
|
||||
│
|
||||
├── utils/
|
||||
│ ├── api.ts # API client
|
||||
│ ├── formatters.ts # Data formatting utilities
|
||||
│ └── validators.ts # Input validation
|
||||
│
|
||||
├── middleware/
|
||||
│ ├── auth.ts # Auth guard
|
||||
│ └── game-access.ts # Game access validation
|
||||
│
|
||||
├── public/ # Static assets
|
||||
├── app.vue # Root component
|
||||
├── nuxt.config.ts # Nuxt configuration
|
||||
├── tailwind.config.js # Tailwind configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Shared Components
|
||||
|
||||
Many components are shared between SBA and PD frontends. These will be located in a shared component library:
|
||||
|
||||
**Shared**:
|
||||
- Game board visualization
|
||||
- Play-by-play feed
|
||||
- Dice roll animations
|
||||
- Decision input forms
|
||||
- WebSocket connection status
|
||||
|
||||
**PD-Specific**:
|
||||
- PD branding (header, footer, colors)
|
||||
- Detailed player card display (with scouting data)
|
||||
- Scouting data panels
|
||||
- Advanced analytics views
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Daily Development
|
||||
```bash
|
||||
# Install dependencies (first time)
|
||||
npm install
|
||||
|
||||
# Run dev server with hot-reload
|
||||
npm run dev
|
||||
|
||||
# Frontend available at http://localhost:3001
|
||||
```
|
||||
|
||||
### Building
|
||||
```bash
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
|
||||
# Generate static site (if needed)
|
||||
npm run generate
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Type checking
|
||||
npm run type-check
|
||||
|
||||
# Linting
|
||||
npm run lint
|
||||
|
||||
# Fix linting issues
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### Vue/TypeScript Style
|
||||
- **Composition API**: Use `<script setup>` syntax
|
||||
- **TypeScript**: Strict mode, explicit types for props/emits
|
||||
- **Component Names**: PascalCase for components
|
||||
- **File Names**: PascalCase for components, kebab-case for utilities
|
||||
|
||||
### Component Structure
|
||||
```vue
|
||||
<template>
|
||||
<div class="component-wrapper">
|
||||
<!-- Template content -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Imports
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// Props/Emits with TypeScript
|
||||
interface Props {
|
||||
gameId: string
|
||||
showScouting: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [value: string]
|
||||
close: []
|
||||
}>()
|
||||
|
||||
// Reactive state
|
||||
const localState = ref('')
|
||||
|
||||
// Computed properties
|
||||
const displayMode = computed(() => {
|
||||
return props.showScouting ? 'detailed' : 'simple'
|
||||
})
|
||||
|
||||
// Methods
|
||||
const handleClick = () => {
|
||||
emit('update', localState.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Component-specific styles */
|
||||
.component-wrapper {
|
||||
@apply p-4 bg-white rounded-lg;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Composable Pattern
|
||||
```typescript
|
||||
// composables/useGameActions.ts
|
||||
export const useGameActions = (gameId: string) => {
|
||||
const { socket } = useWebSocket()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
const setDefense = (positioning: string) => {
|
||||
if (!socket.value?.connected) {
|
||||
throw new Error('Not connected')
|
||||
}
|
||||
socket.value.emit('set_defense', { game_id: gameId, positioning })
|
||||
}
|
||||
|
||||
return {
|
||||
setDefense,
|
||||
// ... other actions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Store Pattern (Pinia)
|
||||
```typescript
|
||||
// store/game.ts
|
||||
export const useGameStore = defineStore('game', () => {
|
||||
// State
|
||||
const gameState = ref<GameState | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// Computed
|
||||
const currentInning = computed(() => gameState.value?.inning ?? 1)
|
||||
|
||||
// Actions
|
||||
const setGameState = (state: GameState) => {
|
||||
gameState.value = state
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
gameState,
|
||||
loading,
|
||||
// Computed
|
||||
currentInning,
|
||||
// Actions
|
||||
setGameState,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
Create `.env` file with:
|
||||
```bash
|
||||
NUXT_PUBLIC_LEAGUE_ID=pd
|
||||
NUXT_PUBLIC_LEAGUE_NAME=Paper Dynasty
|
||||
NUXT_PUBLIC_API_URL=http://localhost:8000
|
||||
NUXT_PUBLIC_WS_URL=http://localhost:8000
|
||||
NUXT_PUBLIC_DISCORD_CLIENT_ID=your-client-id
|
||||
NUXT_PUBLIC_DISCORD_REDIRECT_URI=http://localhost:3001/auth/callback
|
||||
```
|
||||
|
||||
### Nuxt Config
|
||||
```typescript
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
|
||||
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
leagueId: 'pd',
|
||||
leagueName: 'Paper Dynasty',
|
||||
apiUrl: process.env.NUXT_PUBLIC_API_URL || 'http://localhost:8000',
|
||||
// ... other config
|
||||
}
|
||||
},
|
||||
|
||||
css: ['~/assets/css/tailwind.css'],
|
||||
|
||||
typescript: {
|
||||
strict: true,
|
||||
typeCheck: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Tailwind Config (PD Theme)
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#16a34a', // PD Green
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
// ... other shades
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '#ea580c', // PD Orange
|
||||
// ... other shades
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket Integration
|
||||
|
||||
### Connection Management
|
||||
```typescript
|
||||
// composables/useWebSocket.ts
|
||||
const { $socket } = useNuxtApp()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
onMounted(() => {
|
||||
if (authStore.token) {
|
||||
$socket.connect(authStore.token)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
$socket.disconnect()
|
||||
})
|
||||
```
|
||||
|
||||
### Event Handling
|
||||
```typescript
|
||||
// composables/useGameEvents.ts
|
||||
export const useGameEvents = () => {
|
||||
const { socket } = useWebSocket()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
onMounted(() => {
|
||||
socket.value?.on('game_state_update', (data: GameState) => {
|
||||
gameStore.setGameState(data)
|
||||
})
|
||||
|
||||
socket.value?.on('play_completed', (data: PlayOutcome) => {
|
||||
gameStore.handlePlayCompleted(data)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
socket.value?.off('game_state_update')
|
||||
socket.value?.off('play_completed')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Type Definitions
|
||||
|
||||
### PD Player Type (with Scouting Data)
|
||||
```typescript
|
||||
// types/player.ts
|
||||
export interface PdPlayer {
|
||||
id: number
|
||||
name: string
|
||||
image: string
|
||||
scouting_data: {
|
||||
power: number
|
||||
contact: number
|
||||
speed: number
|
||||
fielding: number
|
||||
// ... other scouting metrics
|
||||
}
|
||||
ratings: {
|
||||
overall: number
|
||||
batting: number
|
||||
pitching?: number
|
||||
// ... other ratings
|
||||
}
|
||||
probabilities: {
|
||||
single: number
|
||||
double: number
|
||||
triple: number
|
||||
homerun: number
|
||||
walk: number
|
||||
strikeout: number
|
||||
// ... other probabilities
|
||||
}
|
||||
}
|
||||
|
||||
export interface Lineup {
|
||||
id: number
|
||||
game_id: string
|
||||
card_id: number
|
||||
position: string
|
||||
batting_order?: number
|
||||
is_starter: boolean
|
||||
is_active: boolean
|
||||
player: PdPlayer
|
||||
}
|
||||
```
|
||||
|
||||
### Game State Type
|
||||
```typescript
|
||||
// types/game.ts
|
||||
export interface GameState {
|
||||
game_id: string
|
||||
status: 'pending' | 'active' | 'completed'
|
||||
inning: number
|
||||
half: 'top' | 'bottom'
|
||||
outs: number
|
||||
balls: number
|
||||
strikes: number
|
||||
home_score: number
|
||||
away_score: number
|
||||
runners: {
|
||||
first: number | null
|
||||
second: number | null
|
||||
third: number | null
|
||||
}
|
||||
current_batter: PdPlayer | null
|
||||
current_pitcher: PdPlayer | null
|
||||
}
|
||||
```
|
||||
|
||||
## PD-Specific Features
|
||||
|
||||
### Scouting Data Display
|
||||
```vue
|
||||
<template>
|
||||
<div class="scouting-panel">
|
||||
<h3 class="text-lg font-bold mb-2">Scouting Report</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div v-for="(value, key) in player.scouting_data" :key="key">
|
||||
<span class="text-sm text-gray-600">{{ formatLabel(key) }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="bg-primary h-2 rounded-full"
|
||||
:style="{ width: `${value}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm font-semibold">{{ value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Advanced Player Card
|
||||
```vue
|
||||
<template>
|
||||
<div class="player-card">
|
||||
<!-- Basic Info -->
|
||||
<img :src="player.image" :alt="player.name" />
|
||||
<h3>{{ player.name }}</h3>
|
||||
|
||||
<!-- Ratings -->
|
||||
<div class="ratings">
|
||||
<span>Overall: {{ player.ratings.overall }}</span>
|
||||
<span>Batting: {{ player.ratings.batting }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Probabilities (collapsible on mobile) -->
|
||||
<details class="md:open">
|
||||
<summary>Probabilities</summary>
|
||||
<div class="grid grid-cols-2 gap-1 text-sm">
|
||||
<div v-for="(prob, outcome) in player.probabilities" :key="outcome">
|
||||
{{ outcome }}: {{ (prob * 100).toFixed(1) }}%
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Mobile-First Design
|
||||
|
||||
### Responsive Breakpoints
|
||||
- **xs**: 375px (Small phones)
|
||||
- **sm**: 640px (Large phones)
|
||||
- **md**: 768px (Tablets)
|
||||
- **lg**: 1024px (Desktop)
|
||||
|
||||
### Mobile Layout Principles
|
||||
- Single column layout on mobile
|
||||
- Bottom sheet for decision inputs
|
||||
- Sticky scoreboard at top
|
||||
- Touch-friendly buttons (44x44px minimum)
|
||||
- Swipe gestures for navigation
|
||||
- Collapsible scouting data on mobile
|
||||
|
||||
### Example Responsive Component
|
||||
```vue
|
||||
<template>
|
||||
<div class="game-view">
|
||||
<!-- Sticky scoreboard -->
|
||||
<div class="sticky top-0 z-10 bg-white shadow">
|
||||
<ScoreBoard :score="score" />
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="container mx-auto p-4">
|
||||
<!-- Mobile: stacked, Desktop: grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<div class="lg:col-span-2">
|
||||
<GameBoard :state="gameState" />
|
||||
</div>
|
||||
<div>
|
||||
<ScoutingPanel :player="currentBatter" />
|
||||
<PlayByPlay :plays="plays" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Page
|
||||
1. Create file in `pages/` directory
|
||||
2. Use `<script setup>` with TypeScript
|
||||
3. Add necessary composables (auth, websocket, etc.)
|
||||
4. Define route meta if needed
|
||||
|
||||
### Adding a New Component
|
||||
1. Create in appropriate `components/` subdirectory
|
||||
2. Define Props/Emits interfaces
|
||||
3. Use Tailwind for styling
|
||||
4. Export for use in other components
|
||||
|
||||
### Adding a New Store
|
||||
1. Create in `store/` directory
|
||||
2. Use Composition API syntax
|
||||
3. Define state, computed, and actions
|
||||
4. Export with `defineStore`
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Code Splitting**: Auto by Nuxt routes
|
||||
- **Lazy Loading**: Use `defineAsyncComponent` for heavy components (especially scouting panels)
|
||||
- **Image Optimization**: Use Nuxt Image module
|
||||
- **State Management**: Keep only necessary data in stores
|
||||
- **WebSocket**: Throttle/debounce frequent updates
|
||||
- **Scouting Data**: Lazy load detailed analytics
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### WebSocket Won't Connect
|
||||
- Check backend is running at `NUXT_PUBLIC_WS_URL`
|
||||
- Verify token is valid
|
||||
- Check browser console for errors
|
||||
- Ensure CORS is configured correctly on backend
|
||||
|
||||
### Type Errors
|
||||
- Run `npm run type-check` to see all errors
|
||||
- Ensure types are imported correctly
|
||||
- Check for mismatched types in props/emits
|
||||
- Verify PdPlayer type matches backend structure
|
||||
|
||||
### Hot Reload Not Working
|
||||
- Restart dev server
|
||||
- Clear `.nuxt` directory: `rm -rf .nuxt`
|
||||
- Check for syntax errors in components
|
||||
|
||||
## References
|
||||
|
||||
- **Implementation Guide**: `../.claude/implementation/01-infrastructure.md`
|
||||
- **Frontend Architecture**: `../.claude/implementation/frontend-architecture.md`
|
||||
- **WebSocket Protocol**: `../.claude/implementation/websocket-protocol.md`
|
||||
- **Full PRD**: `../prd-web-scorecard-1.1.md`
|
||||
|
||||
---
|
||||
|
||||
**League**: PD (Paper Dynasty)
|
||||
**Port**: 3001
|
||||
**Current Phase**: Phase 1 - Core Infrastructure
|
||||
76
frontend-pd/Dockerfile
Normal file
76
frontend-pd/Dockerfile
Normal file
@ -0,0 +1,76 @@
|
||||
# Frontend Dockerfile for PD League
|
||||
# Multi-stage build for optimized production image
|
||||
|
||||
FROM node:18-alpine as base
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Development stage
|
||||
FROM base as development
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install all dependencies (including dev)
|
||||
RUN npm ci
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Set development environment
|
||||
ENV NODE_ENV=development
|
||||
|
||||
# Run development server with hot-reload
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
|
||||
# Build stage
|
||||
FROM base as builder
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Build application
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM base as production
|
||||
|
||||
# Set production environment
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install production dependencies only
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Copy built application from builder
|
||||
COPY --from=builder /app/.output /app/.output
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nuxt -u 1001 && \
|
||||
chown -R nuxt:nodejs /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER nuxt
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:3000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
||||
|
||||
# Run production server
|
||||
CMD ["node", ".output/server/index.mjs"]
|
||||
52
frontend-sba/.dockerignore
Normal file
52
frontend-sba/.dockerignore
Normal file
@ -0,0 +1,52 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Build outputs
|
||||
.output/
|
||||
.nuxt/
|
||||
dist/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Nuxt specific
|
||||
.nuxt/
|
||||
.output/
|
||||
nuxt.d.ts
|
||||
499
frontend-sba/CLAUDE.md
Normal file
499
frontend-sba/CLAUDE.md
Normal file
@ -0,0 +1,499 @@
|
||||
# Frontend SBA - Strat-O-Matic Baseball Association Web App
|
||||
|
||||
## Overview
|
||||
|
||||
Vue 3 + Nuxt 3 frontend for the SBa (Strat-O-Matic Baseball Association) league. Provides real-time game interface with WebSocket communication to the game backend.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Framework**: Nuxt 3 (Vue 3 Composition API)
|
||||
- **Language**: TypeScript (strict mode)
|
||||
- **Styling**: Tailwind CSS
|
||||
- **State Management**: Pinia
|
||||
- **WebSocket**: Socket.io-client
|
||||
- **HTTP Client**: Axios (or Nuxt's built-in fetch)
|
||||
- **Auth**: Discord OAuth with JWT
|
||||
|
||||
## League-Specific Characteristics
|
||||
|
||||
### SBA League
|
||||
- **Player Data**: Simple model (id, name, image)
|
||||
- **Focus**: Straightforward card-based gameplay
|
||||
- **Branding**: Blue primary color (#1e40af)
|
||||
- **API**: SBA-specific REST API for team/player data
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
frontend-sba/
|
||||
├── assets/
|
||||
│ ├── css/
|
||||
│ │ └── tailwind.css # Tailwind imports
|
||||
│ └── images/ # SBA branding assets
|
||||
│
|
||||
├── components/
|
||||
│ ├── Branding/ # SBA-specific branding
|
||||
│ │ ├── Header.vue
|
||||
│ │ ├── Footer.vue
|
||||
│ │ └── Logo.vue
|
||||
│ └── League/ # SBA-specific features
|
||||
│ └── PlayerCardSimple.vue # Simple player cards
|
||||
│
|
||||
├── composables/
|
||||
│ ├── useAuth.ts # Authentication state
|
||||
│ ├── useWebSocket.ts # WebSocket connection
|
||||
│ ├── useGameState.ts # Game state management
|
||||
│ └── useLeagueConfig.ts # SBA-specific config
|
||||
│
|
||||
├── layouts/
|
||||
│ ├── default.vue # Standard layout
|
||||
│ ├── game.vue # Game view layout
|
||||
│ └── auth.vue # Auth pages layout
|
||||
│
|
||||
├── pages/
|
||||
│ ├── index.vue # Home/dashboard
|
||||
│ ├── games/
|
||||
│ │ ├── [id].vue # Game view
|
||||
│ │ ├── create.vue # Create new game
|
||||
│ │ └── history.vue # Completed games
|
||||
│ ├── auth/
|
||||
│ │ ├── login.vue
|
||||
│ │ └── callback.vue # Discord OAuth callback
|
||||
│ └── spectate/
|
||||
│ └── [id].vue # Spectator view
|
||||
│
|
||||
├── plugins/
|
||||
│ ├── socket.client.ts # Socket.io plugin
|
||||
│ └── auth.ts # Auth plugin
|
||||
│
|
||||
├── store/ # Pinia stores
|
||||
│ ├── auth.ts # Authentication state
|
||||
│ ├── game.ts # Current game state
|
||||
│ ├── games.ts # Games list
|
||||
│ └── ui.ts # UI state (modals, toasts)
|
||||
│
|
||||
├── types/
|
||||
│ ├── game.ts # Game-related types
|
||||
│ ├── player.ts # SBA player types
|
||||
│ ├── api.ts # API response types
|
||||
│ └── websocket.ts # WebSocket event types
|
||||
│
|
||||
├── utils/
|
||||
│ ├── api.ts # API client
|
||||
│ ├── formatters.ts # Data formatting utilities
|
||||
│ └── validators.ts # Input validation
|
||||
│
|
||||
├── middleware/
|
||||
│ ├── auth.ts # Auth guard
|
||||
│ └── game-access.ts # Game access validation
|
||||
│
|
||||
├── public/ # Static assets
|
||||
├── app.vue # Root component
|
||||
├── nuxt.config.ts # Nuxt configuration
|
||||
├── tailwind.config.js # Tailwind configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Shared Components
|
||||
|
||||
Many components are shared between SBA and PD frontends. These will be located in a shared component library:
|
||||
|
||||
**Shared**:
|
||||
- Game board visualization
|
||||
- Play-by-play feed
|
||||
- Dice roll animations
|
||||
- Decision input forms
|
||||
- WebSocket connection status
|
||||
|
||||
**SBA-Specific**:
|
||||
- SBA branding (header, footer, colors)
|
||||
- Simple player card display (no scouting data)
|
||||
- League-specific theming
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Daily Development
|
||||
```bash
|
||||
# Install dependencies (first time)
|
||||
npm install
|
||||
|
||||
# Run dev server with hot-reload
|
||||
npm run dev
|
||||
|
||||
# Frontend available at http://localhost:3000
|
||||
```
|
||||
|
||||
### Building
|
||||
```bash
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
|
||||
# Generate static site (if needed)
|
||||
npm run generate
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Type checking
|
||||
npm run type-check
|
||||
|
||||
# Linting
|
||||
npm run lint
|
||||
|
||||
# Fix linting issues
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### Vue/TypeScript Style
|
||||
- **Composition API**: Use `<script setup>` syntax
|
||||
- **TypeScript**: Strict mode, explicit types for props/emits
|
||||
- **Component Names**: PascalCase for components
|
||||
- **File Names**: PascalCase for components, kebab-case for utilities
|
||||
|
||||
### Component Structure
|
||||
```vue
|
||||
<template>
|
||||
<div class="component-wrapper">
|
||||
<!-- Template content -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Imports
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// Props/Emits with TypeScript
|
||||
interface Props {
|
||||
gameId: string
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [value: string]
|
||||
close: []
|
||||
}>()
|
||||
|
||||
// Reactive state
|
||||
const localState = ref('')
|
||||
|
||||
// Computed properties
|
||||
const displayValue = computed(() => {
|
||||
return props.isActive ? 'Active' : 'Inactive'
|
||||
})
|
||||
|
||||
// Methods
|
||||
const handleClick = () => {
|
||||
emit('update', localState.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Component-specific styles */
|
||||
.component-wrapper {
|
||||
@apply p-4 bg-white rounded-lg;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Composable Pattern
|
||||
```typescript
|
||||
// composables/useGameActions.ts
|
||||
export const useGameActions = (gameId: string) => {
|
||||
const { socket } = useWebSocket()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
const setDefense = (positioning: string) => {
|
||||
if (!socket.value?.connected) {
|
||||
throw new Error('Not connected')
|
||||
}
|
||||
socket.value.emit('set_defense', { game_id: gameId, positioning })
|
||||
}
|
||||
|
||||
return {
|
||||
setDefense,
|
||||
// ... other actions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Store Pattern (Pinia)
|
||||
```typescript
|
||||
// store/game.ts
|
||||
export const useGameStore = defineStore('game', () => {
|
||||
// State
|
||||
const gameState = ref<GameState | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// Computed
|
||||
const currentInning = computed(() => gameState.value?.inning ?? 1)
|
||||
|
||||
// Actions
|
||||
const setGameState = (state: GameState) => {
|
||||
gameState.value = state
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
gameState,
|
||||
loading,
|
||||
// Computed
|
||||
currentInning,
|
||||
// Actions
|
||||
setGameState,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
Create `.env` file with:
|
||||
```bash
|
||||
NUXT_PUBLIC_LEAGUE_ID=sba
|
||||
NUXT_PUBLIC_LEAGUE_NAME=Super Baseball Alliance
|
||||
NUXT_PUBLIC_API_URL=http://localhost:8000
|
||||
NUXT_PUBLIC_WS_URL=http://localhost:8000
|
||||
NUXT_PUBLIC_DISCORD_CLIENT_ID=your-client-id
|
||||
NUXT_PUBLIC_DISCORD_REDIRECT_URI=http://localhost:3000/auth/callback
|
||||
```
|
||||
|
||||
### Nuxt Config
|
||||
```typescript
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
|
||||
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
leagueId: 'sba',
|
||||
leagueName: 'Super Baseball Alliance',
|
||||
apiUrl: process.env.NUXT_PUBLIC_API_URL || 'http://localhost:8000',
|
||||
// ... other config
|
||||
}
|
||||
},
|
||||
|
||||
css: ['~/assets/css/tailwind.css'],
|
||||
|
||||
typescript: {
|
||||
strict: true,
|
||||
typeCheck: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Tailwind Config (SBA Theme)
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#1e40af', // SBA Blue
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
// ... other shades
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '#dc2626', // SBA Red
|
||||
// ... other shades
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket Integration
|
||||
|
||||
### Connection Management
|
||||
```typescript
|
||||
// composables/useWebSocket.ts
|
||||
const { $socket } = useNuxtApp()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
onMounted(() => {
|
||||
if (authStore.token) {
|
||||
$socket.connect(authStore.token)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
$socket.disconnect()
|
||||
})
|
||||
```
|
||||
|
||||
### Event Handling
|
||||
```typescript
|
||||
// composables/useGameEvents.ts
|
||||
export const useGameEvents = () => {
|
||||
const { socket } = useWebSocket()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
onMounted(() => {
|
||||
socket.value?.on('game_state_update', (data: GameState) => {
|
||||
gameStore.setGameState(data)
|
||||
})
|
||||
|
||||
socket.value?.on('play_completed', (data: PlayOutcome) => {
|
||||
gameStore.handlePlayCompleted(data)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
socket.value?.off('game_state_update')
|
||||
socket.value?.off('play_completed')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Type Definitions
|
||||
|
||||
### SBA Player Type
|
||||
```typescript
|
||||
// types/player.ts
|
||||
export interface SbaPlayer {
|
||||
id: number
|
||||
name: string
|
||||
image: string
|
||||
team?: string
|
||||
manager?: string
|
||||
}
|
||||
|
||||
export interface Lineup {
|
||||
id: number
|
||||
game_id: string
|
||||
card_id: number
|
||||
position: string
|
||||
batting_order?: number
|
||||
is_starter: boolean
|
||||
is_active: boolean
|
||||
player: SbaPlayer
|
||||
}
|
||||
```
|
||||
|
||||
### Game State Type
|
||||
```typescript
|
||||
// types/game.ts
|
||||
export interface GameState {
|
||||
game_id: string
|
||||
status: 'pending' | 'active' | 'completed'
|
||||
inning: number
|
||||
half: 'top' | 'bottom'
|
||||
outs: number
|
||||
balls: number
|
||||
strikes: number
|
||||
home_score: number
|
||||
away_score: number
|
||||
runners: {
|
||||
first: number | null
|
||||
second: number | null
|
||||
third: number | null
|
||||
}
|
||||
current_batter: SbaPlayer | null
|
||||
current_pitcher: SbaPlayer | null
|
||||
}
|
||||
```
|
||||
|
||||
## Mobile-First Design
|
||||
|
||||
### Responsive Breakpoints
|
||||
- **xs**: 375px (Small phones)
|
||||
- **sm**: 640px (Large phones)
|
||||
- **md**: 768px (Tablets)
|
||||
- **lg**: 1024px (Desktop)
|
||||
|
||||
### Mobile Layout Principles
|
||||
- Single column layout on mobile
|
||||
- Bottom sheet for decision inputs
|
||||
- Sticky scoreboard at top
|
||||
- Touch-friendly buttons (44x44px minimum)
|
||||
- Swipe gestures for navigation
|
||||
|
||||
### Example Responsive Component
|
||||
```vue
|
||||
<template>
|
||||
<div class="game-view">
|
||||
<!-- Sticky scoreboard -->
|
||||
<div class="sticky top-0 z-10 bg-white shadow">
|
||||
<ScoreBoard :score="score" />
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="container mx-auto p-4">
|
||||
<!-- Mobile: stacked, Desktop: grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<GameBoard :state="gameState" />
|
||||
<PlayByPlay :plays="plays" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Page
|
||||
1. Create file in `pages/` directory
|
||||
2. Use `<script setup>` with TypeScript
|
||||
3. Add necessary composables (auth, websocket, etc.)
|
||||
4. Define route meta if needed
|
||||
|
||||
### Adding a New Component
|
||||
1. Create in appropriate `components/` subdirectory
|
||||
2. Define Props/Emits interfaces
|
||||
3. Use Tailwind for styling
|
||||
4. Export for use in other components
|
||||
|
||||
### Adding a New Store
|
||||
1. Create in `store/` directory
|
||||
2. Use Composition API syntax
|
||||
3. Define state, computed, and actions
|
||||
4. Export with `defineStore`
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Code Splitting**: Auto by Nuxt routes
|
||||
- **Lazy Loading**: Use `defineAsyncComponent` for heavy components
|
||||
- **Image Optimization**: Use Nuxt Image module
|
||||
- **State Management**: Keep only necessary data in stores
|
||||
- **WebSocket**: Throttle/debounce frequent updates
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### WebSocket Won't Connect
|
||||
- Check backend is running at `NUXT_PUBLIC_WS_URL`
|
||||
- Verify token is valid
|
||||
- Check browser console for errors
|
||||
- Ensure CORS is configured correctly on backend
|
||||
|
||||
### Type Errors
|
||||
- Run `npm run type-check` to see all errors
|
||||
- Ensure types are imported correctly
|
||||
- Check for mismatched types in props/emits
|
||||
|
||||
### Hot Reload Not Working
|
||||
- Restart dev server
|
||||
- Clear `.nuxt` directory: `rm -rf .nuxt`
|
||||
- Check for syntax errors in components
|
||||
|
||||
## References
|
||||
|
||||
- **Implementation Guide**: `../.claude/implementation/01-infrastructure.md`
|
||||
- **Frontend Architecture**: `../.claude/implementation/frontend-architecture.md`
|
||||
- **WebSocket Protocol**: `../.claude/implementation/websocket-protocol.md`
|
||||
- **Full PRD**: `../prd-web-scorecard-1.1.md`
|
||||
|
||||
---
|
||||
|
||||
**League**: SBA (Super Baseball Alliance)
|
||||
**Port**: 3000
|
||||
**Current Phase**: Phase 1 - Core Infrastructure
|
||||
76
frontend-sba/Dockerfile
Normal file
76
frontend-sba/Dockerfile
Normal file
@ -0,0 +1,76 @@
|
||||
# Frontend Dockerfile for SBA League
|
||||
# Multi-stage build for optimized production image
|
||||
|
||||
FROM node:18-alpine as base
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Development stage
|
||||
FROM base as development
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install all dependencies (including dev)
|
||||
RUN npm ci
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Set development environment
|
||||
ENV NODE_ENV=development
|
||||
|
||||
# Run development server with hot-reload
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
|
||||
# Build stage
|
||||
FROM base as builder
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Build application
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM base as production
|
||||
|
||||
# Set production environment
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install production dependencies only
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Copy built application from builder
|
||||
COPY --from=builder /app/.output /app/.output
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nuxt -u 1001 && \
|
||||
chown -R nuxt:nodejs /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER nuxt
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:3000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
||||
|
||||
# Run production server
|
||||
CMD ["node", ".output/server/index.mjs"]
|
||||
1436
prd-web-scorecard-1.1.md
Normal file
1436
prd-web-scorecard-1.1.md
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user