diff --git a/.claude/force-claude-reads.md b/.claude/force-claude-reads.md new file mode 100644 index 0000000..eb7def6 --- /dev/null +++ b/.claude/force-claude-reads.md @@ -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. \ No newline at end of file diff --git a/.claude/implementation/00-index.md b/.claude/implementation/00-index.md new file mode 100644 index 0000000..5030d20 --- /dev/null +++ b/.claude/implementation/00-index.md @@ -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 diff --git a/.claude/implementation/01-infrastructure.md b/.claude/implementation/01-infrastructure.md new file mode 100644 index 0000000..16e94a2 --- /dev/null +++ b/.claude/implementation/01-infrastructure.md @@ -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 \ No newline at end of file diff --git a/.claude/implementation/02-game-engine.md b/.claude/implementation/02-game-engine.md new file mode 100644 index 0000000..7d991c3 --- /dev/null +++ b/.claude/implementation/02-game-engine.md @@ -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) diff --git a/.claude/implementation/03-gameplay-features.md b/.claude/implementation/03-gameplay-features.md new file mode 100644 index 0000000..41fa8de --- /dev/null +++ b/.claude/implementation/03-gameplay-features.md @@ -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) \ No newline at end of file diff --git a/.claude/implementation/04-spectator-polish.md b/.claude/implementation/04-spectator-polish.md new file mode 100644 index 0000000..3203c37 --- /dev/null +++ b/.claude/implementation/04-spectator-polish.md @@ -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) \ No newline at end of file diff --git a/.claude/implementation/05-testing-launch.md b/.claude/implementation/05-testing-launch.md new file mode 100644 index 0000000..467ef3a --- /dev/null +++ b/.claude/implementation/05-testing-launch.md @@ -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 + +# 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 \ No newline at end of file diff --git a/.claude/implementation/api-reference.md b/.claude/implementation/api-reference.md new file mode 100644 index 0000000..701887b --- /dev/null +++ b/.claude/implementation/api-reference.md @@ -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 +``` + +## 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. \ No newline at end of file diff --git a/.claude/implementation/auth-system.md b/.claude/implementation/auth-system.md new file mode 100644 index 0000000..7ab284e --- /dev/null +++ b/.claude/implementation/auth-system.md @@ -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. \ No newline at end of file diff --git a/.claude/implementation/backend-architecture.md b/.claude/implementation/backend-architecture.md new file mode 100644 index 0000000..38e1f60 --- /dev/null +++ b/.claude/implementation/backend-architecture.md @@ -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. \ No newline at end of file diff --git a/.claude/implementation/database-design.md b/.claude/implementation/database-design.md new file mode 100644 index 0000000..5f25d35 --- /dev/null +++ b/.claude/implementation/database-design.md @@ -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. \ No newline at end of file diff --git a/.claude/implementation/frontend-architecture.md b/.claude/implementation/frontend-architecture.md new file mode 100644 index 0000000..738affd --- /dev/null +++ b/.claude/implementation/frontend-architecture.md @@ -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(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(null) + const loading = ref(false) + const error = ref(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) => { + 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 + + + +``` + +### 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 + + + +``` + +## 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. \ No newline at end of file diff --git a/.claude/implementation/testing-strategy.md b/.claude/implementation/testing-strategy.md new file mode 100644 index 0000000..5b25a23 --- /dev/null +++ b/.claude/implementation/testing-strategy.md @@ -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. \ No newline at end of file diff --git a/.claude/implementation/websocket-protocol.md b/.claude/implementation/websocket-protocol.md new file mode 100644 index 0000000..fd8eda0 --- /dev/null +++ b/.claude/implementation/websocket-protocol.md @@ -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. \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..db0ca2a --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..00c9816 --- /dev/null +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 68bc17f..a95a149 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..507eebb --- /dev/null +++ b/CLAUDE.md @@ -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__}.'` +- **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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e633df6 --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..6e64de2 --- /dev/null +++ b/backend/.dockerignore @@ -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 \ No newline at end of file diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md new file mode 100644 index 0000000..bb2578d --- /dev/null +++ b/backend/CLAUDE.md @@ -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 \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..b51d5ee --- /dev/null +++ b/backend/Dockerfile @@ -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"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..812d506 --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/frontend-pd/.dockerignore b/frontend-pd/.dockerignore new file mode 100644 index 0000000..8b0d10c --- /dev/null +++ b/frontend-pd/.dockerignore @@ -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 \ No newline at end of file diff --git a/frontend-pd/CLAUDE.md b/frontend-pd/CLAUDE.md new file mode 100644 index 0000000..d7173d2 --- /dev/null +++ b/frontend-pd/CLAUDE.md @@ -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 ` + + +``` + +### 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(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 + +``` + +### Advanced Player Card +```vue + +``` + +## 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 + +``` + +## Common Tasks + +### Adding a New Page +1. Create file in `pages/` directory +2. Use ` + + +``` + +### 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(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 + +``` + +## Common Tasks + +### Adding a New Page +1. Create file in `pages/` directory +2. Use `