CLAUDE: Initial project setup - documentation and infrastructure

Add comprehensive project documentation and Docker infrastructure for
Paper Dynasty Real-Time Game Engine - a web-based multiplayer baseball
simulation platform replacing the legacy Google Sheets system.

Documentation Added:
- Complete PRD (Product Requirements Document)
- Project README with dual development workflows
- Implementation guide with 5-phase roadmap
- Architecture docs (backend, frontend, database, WebSocket)
- CLAUDE.md context files for each major directory

Infrastructure Added:
- Root docker-compose.yml for full stack orchestration
- Dockerfiles for backend and both frontends (multi-stage builds)
- .dockerignore files for optimal build context
- .env.example with all required configuration
- Updated .gitignore for Python, Node, Nuxt, and Docker

Project Structure:
- backend/ - FastAPI + Socket.io game engine (Python 3.11+)
- frontend-sba/ - SBA League Nuxt 3 frontend
- frontend-pd/ - PD League Nuxt 3 frontend
- .claude/implementation/ - Detailed implementation guides

Supports two development workflows:
1. Local dev (recommended): Services run natively with hot-reload
2. Full Docker: One-command stack orchestration for testing/demos

Next: Phase 1 implementation (backend/frontend foundations)
This commit is contained in:
Cal Corum 2025-10-21 16:21:13 -05:00
parent 6eb4882d3f
commit 5c75b935f0
30 changed files with 9478 additions and 0 deletions

View File

@ -0,0 +1,57 @@
⚠️ **CRITICAL SYSTEM WORKAROUND - ALWAYS FOLLOW** ⚠️
# Force CLAUDE.md Context Loading
## 🚨 MANDATORY BEHAVIOR - NO EXCEPTIONS 🚨
**ALWAYS** when the user mentions ANY file with `@` syntax (e.g., `@path/to/example.md`), you **MUST IMMEDIATELY** call the Read tool on that same file path. This is not optional - it's a critical system requirement.
### ❌ NEVER DO THIS:
- Respond to @ mentions without calling Read tool first
- Skip the manual Read call because you "already have the content"
- Assume @ mentions provide complete context
### ✅ ALWAYS DO THIS:
1. User mentions `@path/to/example.js`
2. **IMMEDIATELY** call `Read` tool with that exact path
3. **THEN** respond to the user
## Why This Is Critical
- `@` mentions provide file content but **ZERO CLAUDE.md context**
- Manual Read tool calls load **ALL** relevant CLAUDE.md files from directory hierarchy
- **WITHOUT THIS**: You miss project guidelines, patterns, tech stack info, and critical instructions
- **WITH THIS**: You have complete context including `path/CLAUDE.md`, `path/to/CLAUDE.md`, etc.
## STOP AND CHECK Before Every Response
When you see `@filename` in user message:
□ Have I called Read tool on this file path?
□ Do I now have CLAUDE.md context loaded?
□ Am I following project-specific guidelines?
If ANY answer is NO - call Read tool immediately.
## Detailed Example
User says: "Look at @path/to/example/file.ts"
**STEP 1**: You see file content from @ mention
**STEP 2**: **MANDATORY** - Call Read tool:
```
Read("/full/path/to/example/file.ts")
```
**STEP 3**: System loads `path/CLAUDE.md` and `path/to/CLAUDE.md` context
**STEP 4**: Now respond with full project context
## Failure Consequences
**If you ignore this instruction:**
- You'll miss any further guidelines intended to be applied
- You'll likely provide incorrect or incomplete solutions
- You'll likely violate project coding standards
## Remember: This Fixes A Bug
This workaround exists because @ mentions have a system limitation. Following this instruction is not optional - it's fixing broken behavior.

View File

@ -0,0 +1,165 @@
# Paper Dynasty Real-Time Game Engine - Implementation Guide
## Table of Contents
### Architecture & Design
- [Backend Architecture](./backend-architecture.md) - FastAPI structure, game engine, state management
- [Frontend Architecture](./frontend-architecture.md) - Vue/Nuxt structure, shared components, league-specific apps
- [Database Design](./database-design.md) - Schema, indexes, operations, migration strategy
- [WebSocket Protocol](./websocket-protocol.md) - Event specifications, connection lifecycle, error handling
### Implementation Phases
#### Phase 1: Core Infrastructure (Weeks 1-3)
- [01 - Infrastructure Setup](./01-infrastructure.md)
- Backend foundation (FastAPI, PostgreSQL, Socket.io)
- Frontend foundation (Nuxt 3, TypeScript, Tailwind)
- Discord OAuth integration
- WebSocket connection management
- Basic session management
#### Phase 2: Game Engine Core (Weeks 4-6)
- [02 - Game Engine](./02-game-engine.md)
- In-memory game state management
- Play resolution engine
- League configuration system
- Polymorphic player models
- Database persistence layer
- State recovery mechanism
#### Phase 3: Complete Game Features (Weeks 7-9)
- [03 - Gameplay Features](./03-gameplay-features.md)
- All strategic decision types
- Substitution system
- Pitching changes
- Complete result charts (both leagues)
- AI opponent integration
- Async game mode support
#### Phase 4: Spectator & Polish (Weeks 10-11)
- [04 - Spectator & Polish](./04-spectator-polish.md)
- Spectator mode implementation
- UI/UX refinements
- Dice roll animations
- Mobile touch optimization
- Accessibility improvements
- Performance optimization
#### Phase 5: Testing & Launch (Weeks 12-13)
- [05 - Testing & Launch](./05-testing-launch.md)
- Comprehensive testing strategy
- Load testing procedures
- Security audit checklist
- Deployment procedures
- Monitoring setup
- Launch plan
### Cross-Cutting Concerns
- [Authentication & Authorization](./auth-system.md) - Discord OAuth, session management, role-based access
- [Testing Strategy](./testing-strategy.md) - Unit, integration, e2e, load testing approaches
- [Deployment Guide](./deployment-guide.md) - Infrastructure setup, CI/CD, monitoring
- [API Reference](./api-reference.md) - REST endpoints, request/response formats
## Implementation Status
| Component | Status | Phase | Notes |
|-----------|--------|-------|-------|
| Backend Foundation | Not Started | 1 | - |
| Frontend Foundation | Not Started | 1 | - |
| Discord OAuth | Not Started | 1 | - |
| WebSocket Server | Not Started | 1 | - |
| Game Engine Core | Not Started | 2 | - |
| Database Schema | Not Started | 2 | - |
| Player Models | Not Started | 2 | - |
| Strategic Decisions | Not Started | 3 | - |
| Substitutions | Not Started | 3 | - |
| AI Opponent | Not Started | 3 | - |
| Spectator Mode | Not Started | 4 | - |
| UI Polish | Not Started | 4 | - |
| Testing Suite | Not Started | 5 | - |
| Deployment | Not Started | 5 | - |
## Quick Start
1. **Review Architecture Documents** - Understand the system design before coding
2. **Follow Phase Order** - Each phase builds on the previous
3. **Update Status** - Keep the status table current as work progresses
4. **Reference PRD** - Main PRD at `/prd-web-scorecard-1.1.md` for detailed requirements
## Key Decisions & Rationale
### Why Hybrid State Management?
- **In-Memory**: Fast action processing, sub-500ms latency requirements
- **PostgreSQL**: Persistence, recovery, play history, async operations
- **Best of Both**: Performance without sacrificing reliability
### Why Separate Frontends?
- **League Branding**: Each league maintains distinct identity
- **Independent Deployment**: Can update one league without affecting other
- **Shared Components**: 80%+ code reuse through component library
- **Flexibility**: League-specific features without codebase pollution
### Why FastAPI + Socket.io?
- **FastAPI**: Modern async Python, automatic OpenAPI docs, Pydantic validation
- **Socket.io**: Mature WebSocket library, automatic reconnection, room support
- **Python Ecosystem**: Rich data processing libraries for game logic
### Why Polymorphic Players?
- **Type Safety**: Each league's player structure validated at runtime
- **Maintainability**: League differences isolated in player classes
- **Extensibility**: Easy to add new leagues or modify existing ones
## Critical Path Items
### Must Have for MVP
- ✅ Game creation and lobby
- ✅ Complete turn-based gameplay
- ✅ Real-time WebSocket updates
- ✅ Game persistence and recovery
- ✅ Mobile-optimized UI
- ✅ Discord authentication
### Nice to Have (Post-MVP)
- 🔲 Roster management
- 🔲 Marketplace
- 🔲 Tournament system
- 🔲 Advanced analytics
- 🔲 Discord bot notifications
## Development Workflow
1. **Start Phase** - Read phase markdown file thoroughly
2. **Create CLAUDE.md** - Add subdirectory-specific context
3. **Implement** - Follow TDD where appropriate
4. **Test** - Unit tests as you go, integration tests per milestone
5. **Update Status** - Mark components complete in index
6. **Review** - Check against PRD requirements before moving on
## Performance Budgets
| Metric | Target | Critical Threshold |
|--------|--------|-------------------|
| Action Response | < 500ms | < 1000ms |
| WebSocket Delivery | < 200ms | < 500ms |
| DB Write (async) | < 100ms | < 250ms |
| State Recovery | < 2s | < 5s |
| Page Load (3G) | < 3s | < 5s |
## Questions & Decisions Log
Track important decisions and open questions here as implementation progresses.
### Open Questions
- [ ] Which VPS provider for deployment?
- [ ] Specific Discord OAuth scope requirements?
- [ ] AI opponent complexity level for MVP?
- [ ] Spectator chat feature in MVP or post-MVP?
### Decisions Made
- **2025-10-21**: Project initialized, implementation guide structure created
---
**Last Updated**: 2025-10-21
**Phase**: Pre-Implementation
**Next Milestone**: Phase 1 - Core Infrastructure Setup

View File

@ -0,0 +1,848 @@
# Phase 1: Core Infrastructure Setup
**Duration**: Weeks 1-3
**Goal**: Establish foundation for both backend and frontend with authentication and real-time communication
## Objectives
By end of Phase 1, you should have:
- ✅ FastAPI backend running with PostgreSQL
- ✅ Socket.io WebSocket server functional
- ✅ Discord OAuth working for both leagues
- ✅ Basic Nuxt 3 frontends operational
- ✅ WebSocket connections established between frontend/backend
- ✅ Users can authenticate and see connection status
## Development Workflow Options
This project supports **two development workflows**:
### Option 1: Local Development (Recommended for Daily Work)
- Backend runs locally with Python hot-reload
- Frontends run locally with Nuxt hot-reload
- Only Redis runs in Docker
- **Advantages**: Fast, easy debugging, instant code changes
### Option 2: Full Docker Orchestration
- Everything runs in containers
- One-command startup with `docker-compose up`
- **Advantages**: Production-like environment, easy sharing
**See the project [README.md](../../../README.md) for detailed instructions on both workflows.**
For this guide, we'll focus on **local development** setup. Docker configuration files are already provided:
- Root `docker-compose.yml` - Full stack orchestration
- `backend/Dockerfile` - Backend container
- `frontend-sba/Dockerfile` & `frontend-pd/Dockerfile` - Frontend containers
---
## Backend Setup
### 1. Initialize FastAPI Project
```bash
# Create backend directory
mkdir -p backend/app/{core,config,models,websocket,api,database,data,utils}
mkdir -p backend/tests/{unit,integration,e2e}
cd backend
# Create virtual environment
python3.11 -m venv venv
source venv/bin/activate
# Create requirements.txt
cat > requirements.txt << EOF
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-socketio==5.10.0
python-multipart==0.0.6
pydantic==2.5.0
pydantic-settings==2.1.0
sqlalchemy==2.0.23
alembic==1.12.1
asyncpg==0.29.0
psycopg2-binary==2.9.9
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-dotenv==1.0.0
httpx==0.25.1
redis==5.0.1
aiofiles==23.2.1
EOF
# Development dependencies
cat > requirements-dev.txt << EOF
-r requirements.txt
pytest==7.4.3
pytest-asyncio==0.21.1
pytest-cov==4.1.0
black==23.11.0
flake8==6.1.0
mypy==1.7.1
httpx==0.25.1
EOF
# Install dependencies
pip install -r requirements-dev.txt
```
### 2. Setup FastAPI Application
**File**: `backend/app/main.py`
```python
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import socketio
from app.config import get_settings
from app.api.routes import games, auth, health
from app.websocket.connection_manager import ConnectionManager
from app.websocket.handlers import register_handlers
from app.database.session import init_db
from app.utils.logging import setup_logging
logger = logging.getLogger(f'{__name__}.main')
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Startup and shutdown events"""
# Startup
logger.info("Starting Paper Dynasty Game Backend")
setup_logging()
await init_db()
logger.info("Database initialized")
yield
# Shutdown
logger.info("Shutting down Paper Dynasty Game Backend")
# Initialize FastAPI app
app = FastAPI(
title="Paper Dynasty Game Backend",
description="Real-time baseball game engine for Paper Dynasty leagues",
version="1.0.0",
lifespan=lifespan
)
# CORS middleware
settings = get_settings()
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Initialize Socket.io
sio = socketio.AsyncServer(
async_mode='asgi',
cors_allowed_origins=settings.cors_origins,
logger=True,
engineio_logger=False
)
# Create Socket.io ASGI app
socket_app = socketio.ASGIApp(sio, app)
# Initialize connection manager and register handlers
connection_manager = ConnectionManager(sio)
register_handlers(sio, connection_manager)
# Include API routes
app.include_router(health.router, prefix="/api", tags=["health"])
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(games.router, prefix="/api/games", tags=["games"])
@app.get("/")
async def root():
return {"message": "Paper Dynasty Game Backend", "version": "1.0.0"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:socket_app",
host="0.0.0.0",
port=8000,
reload=True,
log_level="info"
)
```
**File**: `backend/app/config.py`
```python
from functools import lru_cache
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Application settings"""
# Application
app_env: str = "development"
debug: bool = True
secret_key: str
# Database
database_url: str
db_pool_size: int = 20
db_max_overflow: int = 10
# Discord OAuth
discord_client_id: str
discord_client_secret: str
discord_redirect_uri: str
# League APIs
sba_api_url: str
sba_api_key: str
pd_api_url: str
pd_api_key: str
# WebSocket
ws_heartbeat_interval: int = 30
ws_connection_timeout: int = 60
# CORS
cors_origins: list[str] = ["http://localhost:3000", "http://localhost:3001"]
# Game settings
max_concurrent_games: int = 20
game_idle_timeout: int = 86400 # 24 hours
class Config:
env_file = ".env"
case_sensitive = False
@lru_cache()
def get_settings() -> Settings:
"""Get cached settings instance"""
return Settings()
```
**File**: `backend/.env.example`
```bash
# Application
APP_ENV=development
DEBUG=true
SECRET_KEY=your-secret-key-change-in-production
# Database
# Update with your actual database server hostname/IP and credentials
DATABASE_URL=postgresql+asyncpg://paperdynasty:your-password@your-db-server:5432/paperdynasty_dev
# Discord OAuth
DISCORD_CLIENT_ID=your-discord-client-id
DISCORD_CLIENT_SECRET=your-discord-client-secret
DISCORD_REDIRECT_URI=http://localhost:3000/auth/callback
# League APIs
SBA_API_URL=https://sba-api.example.com
SBA_API_KEY=your-sba-api-key
PD_API_URL=https://pd-api.example.com
PD_API_KEY=your-pd-api-key
# CORS
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
# Redis (optional - for caching)
# REDIS_URL=redis://localhost:6379
```
### 3. Database Setup
**File**: `backend/app/database/session.py`
```python
import logging
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import declarative_base
from app.config import get_settings
logger = logging.getLogger(f'{__name__}.session')
settings = get_settings()
# Create async engine
engine = create_async_engine(
settings.database_url,
echo=settings.debug,
pool_size=settings.db_pool_size,
max_overflow=settings.db_max_overflow,
)
# Create session factory
AsyncSessionLocal = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False,
)
# Base class for models
Base = declarative_base()
async def init_db() -> None:
"""Initialize database tables"""
async with engine.begin() as conn:
# Import all models here to ensure they're registered
from app.models import db_models
# Create tables
await conn.run_sync(Base.metadata.create_all)
logger.info("Database tables created")
async def get_session() -> AsyncGenerator[AsyncSession, None]:
"""Dependency for getting database session"""
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
```
**File**: `backend/app/models/db_models.py`
```python
from datetime import datetime
from sqlalchemy import Column, Integer, String, Boolean, DateTime, JSON, Text, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
import uuid
from app.database.session import Base
class Game(Base):
"""Game model"""
__tablename__ = "games"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
league_id = Column(String(50), nullable=False, index=True)
home_team_id = Column(Integer, nullable=False)
away_team_id = Column(Integer, nullable=False)
status = Column(String(20), nullable=False, default="pending", index=True)
game_mode = Column(String(20), nullable=False)
visibility = Column(String(20), nullable=False)
current_inning = Column(Integer)
current_half = Column(String(10))
home_score = Column(Integer, default=0)
away_score = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow, index=True)
started_at = Column(DateTime)
completed_at = Column(DateTime)
winner_team_id = Column(Integer)
metadata = Column(JSON, default=dict)
class Play(Base):
"""Play model"""
__tablename__ = "plays"
id = Column(Integer, primary_key=True, autoincrement=True)
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id"), nullable=False, index=True)
play_number = Column(Integer, nullable=False)
inning = Column(Integer, nullable=False)
half = Column(String(10), nullable=False)
outs_before = Column(Integer, nullable=False)
outs_recorded = Column(Integer, nullable=False)
batter_id = Column(Integer, nullable=False)
pitcher_id = Column(Integer, nullable=False)
runners_before = Column(JSON)
runners_after = Column(JSON)
balls = Column(Integer)
strikes = Column(Integer)
defensive_positioning = Column(String(50))
offensive_approach = Column(String(50))
dice_roll = Column(Integer)
hit_type = Column(String(50))
result_description = Column(Text)
runs_scored = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow, index=True)
metadata = Column(JSON, default=dict)
class Lineup(Base):
"""Lineup model"""
__tablename__ = "lineups"
id = Column(Integer, primary_key=True, autoincrement=True)
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id"), nullable=False, index=True)
team_id = Column(Integer, nullable=False, index=True)
card_id = Column(Integer, nullable=False)
position = Column(String(10), nullable=False)
batting_order = Column(Integer)
is_starter = Column(Boolean, default=True)
is_active = Column(Boolean, default=True, index=True)
entered_inning = Column(Integer, default=1)
metadata = Column(JSON, default=dict)
class GameSession(Base):
"""Game session tracking"""
__tablename__ = "game_sessions"
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id"), primary_key=True)
connected_users = Column(JSON, default=dict)
last_action_at = Column(DateTime, default=datetime.utcnow, index=True)
state_snapshot = Column(JSON, default=dict)
```
### 4. WebSocket Connection Manager
**File**: `backend/app/websocket/connection_manager.py`
```python
import logging
from typing import Dict, Set
import socketio
logger = logging.getLogger(f'{__name__}.ConnectionManager')
class ConnectionManager:
"""Manages WebSocket connections and rooms"""
def __init__(self, sio: socketio.AsyncServer):
self.sio = sio
self.user_sessions: Dict[str, str] = {} # sid -> user_id
self.game_rooms: Dict[str, Set[str]] = {} # game_id -> set of sids
async def connect(self, sid: str, user_id: str) -> None:
"""Register a new connection"""
self.user_sessions[sid] = user_id
logger.info(f"User {user_id} connected with session {sid}")
async def disconnect(self, sid: str) -> None:
"""Handle disconnection"""
user_id = self.user_sessions.pop(sid, None)
if user_id:
logger.info(f"User {user_id} disconnected (session {sid})")
# Remove from all game rooms
for game_id, sids in self.game_rooms.items():
if sid in sids:
sids.remove(sid)
await self.broadcast_to_game(
game_id,
"user_disconnected",
{"user_id": user_id}
)
async def join_game(self, sid: str, game_id: str, role: str) -> None:
"""Add user to game room"""
await self.sio.enter_room(sid, game_id)
if game_id not in self.game_rooms:
self.game_rooms[game_id] = set()
self.game_rooms[game_id].add(sid)
user_id = self.user_sessions.get(sid)
logger.info(f"User {user_id} joined game {game_id} as {role}")
await self.broadcast_to_game(
game_id,
"user_connected",
{"user_id": user_id, "role": role}
)
async def leave_game(self, sid: str, game_id: str) -> None:
"""Remove user from game room"""
await self.sio.leave_room(sid, game_id)
if game_id in self.game_rooms:
self.game_rooms[game_id].discard(sid)
user_id = self.user_sessions.get(sid)
logger.info(f"User {user_id} left game {game_id}")
async def broadcast_to_game(
self,
game_id: str,
event: str,
data: dict
) -> None:
"""Broadcast event to all users in game room"""
await self.sio.emit(event, data, room=game_id)
logger.debug(f"Broadcast {event} to game {game_id}")
async def emit_to_user(self, sid: str, event: str, data: dict) -> None:
"""Emit event to specific user"""
await self.sio.emit(event, data, room=sid)
def get_game_participants(self, game_id: str) -> Set[str]:
"""Get all session IDs in game room"""
return self.game_rooms.get(game_id, set())
```
**File**: `backend/app/websocket/handlers.py`
```python
import logging
from socketio import AsyncServer
from app.websocket.connection_manager import ConnectionManager
from app.utils.auth import verify_token
logger = logging.getLogger(f'{__name__}.handlers')
def register_handlers(sio: AsyncServer, manager: ConnectionManager) -> None:
"""Register all WebSocket event handlers"""
@sio.event
async def connect(sid, environ, auth):
"""Handle new connection"""
try:
# Verify JWT token
token = auth.get("token")
if not token:
logger.warning(f"Connection {sid} rejected: no token")
return False
user_data = verify_token(token)
user_id = user_data.get("user_id")
if not user_id:
logger.warning(f"Connection {sid} rejected: invalid token")
return False
await manager.connect(sid, user_id)
await sio.emit("connected", {"user_id": user_id}, room=sid)
logger.info(f"Connection {sid} accepted for user {user_id}")
return True
except Exception as e:
logger.error(f"Connection error: {e}")
return False
@sio.event
async def disconnect(sid):
"""Handle disconnection"""
await manager.disconnect(sid)
@sio.event
async def join_game(sid, data):
"""Handle join game request"""
try:
game_id = data.get("game_id")
role = data.get("role", "player")
if not game_id:
await manager.emit_to_user(
sid,
"error",
{"message": "Missing game_id"}
)
return
# TODO: Verify user has access to game
await manager.join_game(sid, game_id, role)
await manager.emit_to_user(
sid,
"game_joined",
{"game_id": game_id, "role": role}
)
except Exception as e:
logger.error(f"Join game error: {e}")
await manager.emit_to_user(
sid,
"error",
{"message": str(e)}
)
@sio.event
async def leave_game(sid, data):
"""Handle leave game request"""
try:
game_id = data.get("game_id")
if game_id:
await manager.leave_game(sid, game_id)
except Exception as e:
logger.error(f"Leave game error: {e}")
@sio.event
async def heartbeat(sid):
"""Handle heartbeat ping"""
await sio.emit("heartbeat_ack", {}, room=sid)
```
### 5. Logging Configuration
**File**: `backend/app/utils/logging.py`
```python
import logging
import logging.handlers
import os
from datetime import datetime
def setup_logging() -> None:
"""Configure application logging"""
# Create logs directory
log_dir = "logs"
os.makedirs(log_dir, exist_ok=True)
# Log file name with date
log_file = os.path.join(log_dir, f"app_{datetime.now().strftime('%Y%m%d')}.log")
# Create formatter
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
# Rotating file handler
file_handler = logging.handlers.RotatingFileHandler(
log_file,
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
root_logger.addHandler(console_handler)
root_logger.addHandler(file_handler)
# Silence noisy loggers
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
logging.getLogger("socketio").setLevel(logging.INFO)
logging.getLogger("engineio").setLevel(logging.WARNING)
```
### 6. Database Setup
**Note**: This project uses existing PostgreSQL servers (dev and prod). You'll need to manually create the database before running the backend.
**Create Database on Your PostgreSQL Server**:
```sql
-- On your dev PostgreSQL server
CREATE DATABASE paperdynasty_dev;
CREATE USER paperdynasty WITH PASSWORD 'your-secure-password';
GRANT ALL PRIVILEGES ON DATABASE paperdynasty_dev TO paperdynasty;
-- Later, on your prod PostgreSQL server
CREATE DATABASE paperdynasty_prod;
-- Use a different, secure password for production
```
**Test Connection**:
```bash
# Test that you can connect to the database
psql postgresql://paperdynasty:your-password@your-db-server:5432/paperdynasty_dev
```
### 7. Docker Compose for Redis (Development)
**File**: `backend/docker-compose.yml`
```yaml
version: '3.8'
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
redis_data:
```
**Start Redis**:
```bash
cd backend
docker-compose up -d
```
## Frontend Setup
### 1. Initialize Nuxt 3 Projects
```bash
# Create SBA League frontend
npx nuxi@latest init frontend-sba
cd frontend-sba
# Install dependencies
npm install @nuxtjs/tailwindcss @pinia/nuxt socket.io-client axios
npm install -D @nuxtjs/eslint-config-typescript
# Initialize Tailwind
npx tailwindcss init
cd ..
# Repeat for PD League
npx nuxi@latest init frontend-pd
cd frontend-pd
npm install @nuxtjs/tailwindcss @pinia/nuxt socket.io-client axios
npm install -D @nuxtjs/eslint-config-typescript
npx tailwindcss init
```
### 2. Configure Nuxt
**File**: `frontend-sba/nuxt.config.ts`
```typescript
export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
runtimeConfig: {
public: {
leagueId: 'sba',
leagueName: 'Super Baseball Alliance',
apiUrl: process.env.NUXT_PUBLIC_API_URL || 'http://localhost:8000',
wsUrl: process.env.NUXT_PUBLIC_WS_URL || 'http://localhost:8000',
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID || '',
discordRedirectUri: process.env.NUXT_PUBLIC_DISCORD_REDIRECT_URI || 'http://localhost:3000/auth/callback',
}
},
css: ['~/assets/css/tailwind.css'],
devtools: { enabled: true },
typescript: {
strict: true,
typeCheck: true
}
})
```
### 3. Setup WebSocket Plugin
**File**: `frontend-sba/plugins/socket.client.ts`
```typescript
import { io, Socket } from 'socket.io-client'
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig()
let socket: Socket | null = null
const connect = (token: string) => {
if (socket?.connected) return socket
socket = io(config.public.wsUrl, {
auth: { token },
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5
})
socket.on('connect', () => {
console.log('WebSocket connected')
})
socket.on('disconnect', () => {
console.log('WebSocket disconnected')
})
socket.on('connect_error', (error) => {
console.error('WebSocket connection error:', error)
})
return socket
}
const disconnect = () => {
socket?.disconnect()
socket = null
}
return {
provide: {
socket: {
connect,
disconnect,
get instance() {
return socket
}
}
}
}
})
```
## Testing Phase 1
### Backend Tests
```bash
# Run backend tests
cd backend
pytest tests/ -v
# Test WebSocket connection
python -m pytest tests/integration/test_websocket.py -v
# Check code quality
black app/ tests/
flake8 app/ tests/
mypy app/
```
### Frontend Tests
```bash
# Run frontend dev server
cd frontend-sba
npm run dev
# Test in browser
# Navigate to http://localhost:3000
# Check console for WebSocket connection
```
## Phase 1 Completion Checklist
- [ ] PostgreSQL database created on existing server
- [ ] Database connection tested successfully
- [ ] FastAPI server running on port 8000
- [ ] Socket.io WebSocket server operational
- [ ] Database tables created successfully
- [ ] Logging system working (check logs/ directory)
- [ ] Redis running via Docker Compose
- [ ] WebSocket connections established
- [ ] Nuxt 3 apps running (SBA on 3000, PD on 3001)
- [ ] CORS configured correctly
- [ ] Health check endpoint responding
- [ ] Basic error handling in place
## Next Steps
Once Phase 1 is complete, proceed to [Phase 2: Game Engine Core](./02-game-engine.md).
---
**Notes**:
- Keep `.env` files secure and never commit to git
- Test WebSocket connections thoroughly before proceeding
- Document any deviations from this setup
- Update CLAUDE.md in subdirectories as needed

View File

@ -0,0 +1,172 @@
# Phase 2: Game Engine Core
**Duration**: Weeks 4-6
**Status**: Not Started
**Prerequisites**: Phase 1 Complete
---
## Overview
Build the core game simulation engine with in-memory state management, play resolution logic, and database persistence. Implement the polymorphic player model system and league configuration framework.
## Key Objectives
By end of Phase 2, you should have:
- ✅ In-memory game state management working
- ✅ Play resolution engine with dice rolls
- ✅ League configuration system (SBA and PD configs)
- ✅ Polymorphic player models (BasePlayer, SbaPlayer, PdPlayer)
- ✅ Database persistence layer with async operations
- ✅ State recovery mechanism from database
- ✅ Basic game flow (start → plays → end)
## Major Components to Implement
### 1. Game Engine (`backend/app/core/game_engine.py`)
- Game session initialization
- Turn management (defensive → stolen base → offensive → resolution)
- Action processing and validation
- State update coordination
- Event emission to WebSocket clients
### 2. State Manager (`backend/app/core/state_manager.py`)
- In-memory game state dictionary
- State CRUD operations
- State lifecycle management
- Cache eviction for idle games
- State recovery from database
### 3. Play Resolver (`backend/app/core/play_resolver.py`)
- Cryptographic dice rolling
- Result chart lookup (league-specific)
- Play outcome determination
- Runner advancement logic
- Score calculation
### 4. Dice System (`backend/app/core/dice.py`)
- Secure random number generation
- Roll logging and verification
- Distribution testing
### 5. Polymorphic Player Models (`backend/app/models/player_models.py`)
- BasePlayer abstract class
- SbaPlayer implementation
- PdPlayer implementation
- Lineup factory method
- Type guards for league-specific logic
### 6. League Configuration (`backend/app/config/`)
- BaseGameConfig class
- SbaConfig and PdConfig subclasses
- Result charts (d20 tables) for each league
- Config loader and versioning
- Config validation
### 7. Database Operations (`backend/app/database/operations.py`)
- Async play persistence
- Game metadata updates
- Lineup operations
- State snapshot management
- Bulk recovery queries
### 8. API Client (`backend/app/data/api_client.py`)
- HTTP client for league REST APIs
- Team data fetching
- Roster data fetching
- Player/card data fetching
- Error handling and retries
- Response caching (optional)
## Implementation Order
1. **Week 4**: State Manager + Database Operations
- In-memory state structure
- Basic CRUD operations
- Database persistence layer
- State recovery mechanism
2. **Week 5**: Game Engine + Play Resolver
- Game initialization flow
- Turn management
- Dice rolling system
- Basic play resolution (simplified charts)
3. **Week 6**: League Configs + Player Models
- Polymorphic player architecture
- League configuration system
- Complete result charts
- API client integration
- End-to-end testing
## Testing Strategy
### Unit Tests
- State manager operations
- Dice roll distribution
- Play resolver outcomes
- Player model instantiation
- Config loading
### Integration Tests
- Full game flow from start to end
- State recovery from database
- Multi-turn sequences
- API client with mocked responses
### E2E Tests
- Play a complete 9-inning game
- Verify database persistence
- Test state recovery mid-game
## Key Files to Create
```
backend/app/
├── core/
│ ├── game_engine.py # Main game logic
│ ├── state_manager.py # In-memory state
│ ├── play_resolver.py # Play outcomes
│ ├── dice.py # Random generation
│ └── validators.py # Rule validation
├── config/
│ ├── base_config.py # Base configuration
│ ├── league_configs.py # SBA/PD configs
│ ├── result_charts.py # d20 tables
│ └── loader.py # Config utilities
├── models/
│ ├── player_models.py # Polymorphic players
│ └── game_models.py # Pydantic game models
└── data/
└── api_client.py # League API client
```
## Reference Documents
- [Backend Architecture](./backend-architecture.md) - Complete backend structure
- [Database Design](./database-design.md) - Schema and queries
- [WebSocket Protocol](./websocket-protocol.md) - Event specifications
- [PRD Lines 378-551](../prd-web-scorecard-1.1.md) - Polymorphic player architecture
- [PRD Lines 780-846](../prd-web-scorecard-1.1.md) - League configuration system
## Deliverable
A working game backend that can:
- Initialize a game with teams from league APIs
- Process a complete at-bat (decisions → dice roll → outcome)
- Update game state in memory and persist to database
- Recover game state after backend restart
- Handle basic substitutions
## Notes
- Focus on getting one at-bat working perfectly before expanding
- Test dice roll distribution extensively
- Validate all state transitions
- Use simplified result charts initially, expand in Phase 3
- Don't implement UI yet - test via WebSocket events or Python scripts
---
**Status**: Placeholder - to be expanded during implementation
**Next Phase**: [03-gameplay-features.md](./03-gameplay-features.md)

View File

@ -0,0 +1,228 @@
# Phase 3: Complete Game Features
**Duration**: Weeks 7-9
**Status**: Not Started
**Prerequisites**: Phase 2 Complete
---
## Overview
Implement all strategic decisions, complete result charts for both leagues, substitution system, pitching changes, AI opponent, and async game mode support. Complete the frontend game interface.
## Key Objectives
By end of Phase 3, you should have:
- ✅ All strategic decision types implemented
- ✅ Complete substitution system (pinch hitters, defensive replacements)
- ✅ Pitching change logic with bullpen management
- ✅ Full result charts for both SBA and PD leagues
- ✅ AI opponent integration
- ✅ Async game mode with notifications
- ✅ Complete frontend game interface (mobile + desktop)
- ✅ Play-by-play history display
- ✅ Game creation and lobby UI
## Major Components to Implement
### Backend Features
#### 1. Strategic Decisions
- Defensive positioning (standard, infield in, shifts)
- Stolen base attempts (per runner)
- Offensive approach (swing away, bunt, hit-and-run)
- Intentional walk
- Defensive substitutions mid-inning
#### 2. Substitution System
- Pinch hitter logic
- Pinch runner logic
- Defensive replacement
- Double switches (if applicable)
- Validation (eligibility, roster constraints)
- Lineup reordering after substitution
#### 3. Pitching Changes
- Relief pitcher selection
- Pitcher eligibility validation
- Pitcher statistics tracking
- Mound visit limits (optional)
- Auto-pitcher fatigue (optional)
#### 4. Complete Result Charts
- SBA league d20 tables (all situations)
- PD league d20 tables (all situations)
- Hit location logic
- Runner advancement scenarios
- Fielding errors (if applicable)
- Extra-inning rules
#### 5. AI Opponent
- Integration with existing AI logic
- Decision-making algorithms
- Timing simulation (avoid instant responses)
- Difficulty levels (optional)
- AI substitution logic
#### 6. Async Game Mode
- Turn-based notification system
- Email/Discord notifications
- Turn timeout handling
- "Your turn" indicator
- Game pause/resume
### Frontend Features
#### 1. Game Creation Flow
- Team selection
- Opponent selection (human or AI)
- Game mode selection (live, async, vs AI)
- Visibility settings
- Lineup confirmation
- Game start
#### 2. Game Lobby
- Pre-game waiting room
- Lineup display for both teams
- Ready/not ready status
- Chat (optional)
- Countdown to start
#### 3. Game Interface (Mobile)
- Baseball diamond visualization
- Score display (by inning)
- Current situation (count, outs, runners)
- Decision cards (bottom sheet)
- Play-by-play feed (collapsible)
- Action buttons (substitutions, pitching change)
#### 4. Game Interface (Desktop)
- Two/three column layout
- Expanded play-by-play
- Statistics sidebar
- Hover states for additional info
- Keyboard shortcuts
#### 5. Decision Workflows
- Defensive positioning selector
- Stolen base attempt interface
- Offensive approach selector
- Substitution modal with roster
- Pitching change modal with bullpen
#### 6. Play-by-Play Display
- Scrollable history
- Play descriptions
- Score updates
- Substitution notifications
- Inning summaries
## Implementation Order
1. **Week 7**: Complete Backend Strategic Decisions
- All decision types
- Complete result charts
- Validation logic
- WebSocket handlers
2. **Week 8**: Substitutions + Frontend Game Interface
- Substitution system (backend + frontend)
- Pitching changes
- Game interface UI (mobile-first)
- Decision workflows
3. **Week 9**: AI + Async + Polish
- AI opponent integration
- Async game mode
- Game creation + lobby UI
- Play-by-play display
- Integration testing
## Frontend Component Structure
```
shared-components/src/components/
├── Game/
│ ├── GameBoard.vue
│ ├── ScoreBoard.vue
│ ├── PlayByPlay.vue
│ ├── CurrentSituation.vue
│ └── BaseRunners.vue
├── Decisions/
│ ├── DefensivePositioning.vue
│ ├── StolenBaseAttempt.vue
│ ├── OffensiveApproach.vue
│ └── DecisionTimer.vue
├── Actions/
│ ├── SubstitutionModal.vue
│ ├── PitchingChange.vue
│ └── ActionButton.vue
└── Display/
├── PlayerCard.vue
├── DiceRoll.vue
├── PlayOutcome.vue
└── ConnectionStatus.vue
```
## Testing Strategy
### Backend Tests
- Test all strategic decision combinations
- Validate substitution rules
- Test AI decision making
- Verify async turn handling
### Frontend Tests
- Component testing with Vitest
- Mobile responsiveness testing
- Decision workflow testing
- WebSocket event handling
### E2E Tests
- Complete game with all decision types
- Game with substitutions
- AI opponent game
- Async game (simulated)
## Key Files to Create
**Backend**:
- `backend/app/core/substitutions.py`
- `backend/app/core/ai_opponent.py`
- `backend/app/websocket/handlers.py` (expand)
- `backend/app/config/result_charts.py` (complete)
**Frontend**:
- All shared components listed above
- `frontend-{league}/pages/games/create.vue`
- `frontend-{league}/pages/games/[id].vue`
- `frontend-{league}/store/game.ts`
- `frontend-{league}/composables/useGameActions.ts`
## Reference Documents
- [Frontend Architecture](./frontend-architecture.md) - Component structure
- [WebSocket Protocol](./websocket-protocol.md) - All event types
- [PRD Lines 186-276](../prd-web-scorecard-1.1.md) - Strategic decisions and game modes
## Deliverable
A fully playable game system with:
- All strategic options available
- Complete substitution capabilities
- AI opponent functional
- Mobile and desktop UI complete
- Async game mode operational
## Notes
- Prioritize mobile UI - 60%+ of users will play on mobile
- Test result charts thoroughly for both leagues
- Validate all substitution rules against baseball logic
- AI should make "reasonable" decisions, not perfect ones
- Async notifications critical for user engagement
---
**Status**: Placeholder - to be expanded during implementation
**Next Phase**: [04-spectator-polish.md](./04-spectator-polish.md)

View File

@ -0,0 +1,254 @@
# Phase 4: Spectator Mode & Polish
**Duration**: Weeks 10-11
**Status**: Not Started
**Prerequisites**: Phase 3 Complete
---
## Overview
Implement spectator mode for real-time game viewing, add UI/UX polish including animations, optimize mobile touch experience, and improve accessibility. Focus on production-ready user experience.
## Key Objectives
By end of Phase 4, you should have:
- ✅ Spectator mode fully functional
- ✅ Dice roll animations working
- ✅ Smooth transitions and loading states
- ✅ Mobile touch optimization
- ✅ Accessibility improvements (WCAG 2.1 AA)
- ✅ Error handling and recovery UX
- ✅ Performance optimizations
- ✅ Production-ready polish
## Major Components to Implement
### 1. Spectator Mode
**Backend**:
- Spectator WebSocket events (read-only)
- Spectator room management
- Spectator count tracking
- Permission enforcement
**Frontend**:
- Spectator-specific game view
- Read-only interface (no decision prompts)
- Spectator count display
- Join spectator link generation
- Late join to active games
### 2. Animations & Transitions
**Dice Roll Animation**:
- 3D dice roll effect or 2D animation
- Suspenseful timing (1-2 seconds)
- Synchronized across all clients
- Sound effects (optional)
**Play Outcome Transitions**:
- Runner movement animations
- Score change animations
- Smooth state transitions
- Visual feedback for key moments
**UI Transitions**:
- Page transitions
- Modal animations
- Accordion collapse/expand
- Loading skeletons
### 3. Mobile Touch Optimization
**Touch Interactions**:
- Swipe gestures (navigate play history)
- Pull to refresh
- Tap feedback (haptic if supported)
- Touch target sizing (44x44px minimum)
**Layout Refinements**:
- Safe area handling (iOS notch)
- Keyboard avoidance
- Scroll behavior optimization
- Bottom sheet interactions
**Performance**:
- Lazy loading images
- Virtual scrolling for long lists
- Debounced scroll handlers
- Optimized re-renders
### 4. Accessibility Improvements
**Screen Reader Support**:
- Semantic HTML elements
- ARIA labels on interactive elements
- Announcements for game events
- Focus management in modals
**Keyboard Navigation**:
- Tab order optimization
- Keyboard shortcuts for actions
- Focus indicators
- Escape key handling
**Visual Accessibility**:
- Color contrast validation (4.5:1 minimum)
- Focus indicators
- Text sizing options
- Reduced motion support
**Accessible Notifications**:
- Screen reader announcements for plays
- Visual + auditory feedback options
- Status updates in accessible format
### 5. Error Handling & Recovery
**Connection Issues**:
- Graceful offline mode
- Reconnection feedback
- Action queue during disconnect
- State sync on reconnect
**Error States**:
- Friendly error messages
- Recovery actions provided
- Error boundaries (prevent crashes)
- Fallback UI
**Loading States**:
- Loading skeletons
- Progress indicators
- Timeout handling
- Cancel operations
### 6. Performance Optimizations
**Frontend**:
- Code splitting by route
- Component lazy loading
- Image optimization (WebP, lazy load)
- Bundle size reduction
- Memoization of expensive computations
**Backend**:
- Response caching
- Database query optimization
- Connection pool tuning
- Memory usage monitoring
**WebSocket**:
- Message compression
- Batch updates where possible
- Efficient state diff broadcasts
## Implementation Order
1. **Week 10**: Spectator Mode + Animations
- Spectator WebSocket events
- Read-only game view
- Dice roll animation
- Play outcome transitions
2. **Week 11**: Polish + Accessibility + Performance
- Mobile touch optimization
- Accessibility audit and fixes
- Error handling improvements
- Performance profiling and optimization
## Testing Strategy
### Spectator Mode Testing
- Join as spectator during active game
- Verify read-only constraints
- Test late join (mid-game)
- Multi-spectator scenarios
### Animation Testing
- Verify timing synchronization
- Test on various devices/browsers
- Performance impact measurement
- Reduced motion preference
### Accessibility Testing
- Screen reader testing (NVDA, JAWS, VoiceOver)
- Keyboard-only navigation
- Color contrast validation tools
- Automated accessibility audits (axe, Lighthouse)
### Performance Testing
- Lighthouse audits
- Mobile device testing (real devices)
- Network throttling tests (3G simulation)
- Memory leak detection
## Key Files to Create/Modify
**Backend**:
- `backend/app/websocket/handlers.py` - Add spectator events
- `backend/app/api/routes/spectate.py` - Spectator endpoints
**Frontend**:
- `shared-components/src/components/Display/DiceRoll.vue` - Animation
- `shared-components/src/components/Common/LoadingStates.vue`
- `frontend-{league}/pages/spectate/[id].vue` - Spectator view
- `frontend-{league}/composables/useAccessibility.ts`
- `frontend-{league}/utils/animations.ts`
**Styles**:
- `frontend-{league}/assets/css/animations.css`
- `frontend-{league}/assets/css/accessibility.css`
## Reference Documents
- [Frontend Architecture](./frontend-architecture.md) - Component structure
- [WebSocket Protocol](./websocket-protocol.md) - Spectator events
- [PRD Lines 233-245](../prd-web-scorecard-1.1.md) - Spectator requirements
- [PRD Lines 732-777](../prd-web-scorecard-1.1.md) - UX requirements
## Deliverable
A production-ready game interface with:
- Spectator mode allowing real-time viewing
- Polished animations and transitions
- Optimized mobile touch experience
- WCAG 2.1 AA accessibility compliance
- Robust error handling and recovery
- Performance meeting target metrics
## Performance Targets
- Page load: < 3s on 3G
- Action response: < 500ms
- Lighthouse score: > 90
- Accessibility score: 100
- First contentful paint: < 1.5s
- Time to interactive: < 3.5s
## Accessibility Checklist
- [ ] All interactive elements keyboard accessible
- [ ] Focus indicators visible and clear
- [ ] Color contrast meets WCAG AA standards
- [ ] Screen reader announcements for game events
- [ ] Alternative text for all images
- [ ] Form labels and error messages
- [ ] Skip navigation links
- [ ] Semantic HTML structure
- [ ] Reduced motion preference respected
- [ ] ARIA attributes used correctly
## Notes
- Test spectator mode with multiple simultaneous spectators
- Animation should enhance, not distract from gameplay
- Mobile touch targets must be large enough for thumbs
- Accessibility is not optional - it's a requirement
- Performance directly impacts user retention
---
**Status**: Placeholder - to be expanded during implementation
**Next Phase**: [05-testing-launch.md](./05-testing-launch.md)

View File

@ -0,0 +1,373 @@
# Phase 5: Testing & Launch
**Duration**: Weeks 12-13
**Status**: Not Started
**Prerequisites**: Phase 4 Complete
---
## Overview
Comprehensive testing across all layers, load testing for production readiness, security audit, deployment procedures, monitoring setup, and final launch with migration from Google Sheets system.
## Key Objectives
By end of Phase 5, you should have:
- ✅ Comprehensive test coverage (>80%)
- ✅ Load testing completed (10+ concurrent games)
- ✅ Security audit passed
- ✅ Production deployment complete
- ✅ Monitoring and alerting configured
- ✅ Documentation finalized
- ✅ Beta testing completed
- ✅ Full launch executed
- ✅ Google Sheets system deprecated
## Major Components
### 1. Test Suite Development
#### Unit Tests
- **Backend**: All core components (game engine, state manager, play resolver, validators)
- **Frontend**: All composables and utility functions
- **Coverage**: >80% code coverage
- **Mocking**: Database, external APIs, WebSocket connections
#### Integration Tests
- **Backend**: WebSocket event flow, database operations, API client
- **Frontend**: Component integration, store interactions
- **Cross-layer**: Frontend → WebSocket → Backend → Database
#### End-to-End Tests
- **Complete Games**: Full 9-inning games in all modes
- **Multi-User**: Two players simultaneously
- **Spectator**: Spectator joining active games
- **Reconnection**: Disconnect and reconnect scenarios
- **State Recovery**: Backend restart during game
- **Browser Testing**: Chrome, Safari, Firefox, Edge
- **Device Testing**: iOS, Android devices (real hardware)
#### Load Tests
- **Concurrent Games**: 10+ simultaneous games
- **Concurrent Users**: 50+ connected WebSocket clients
- **Database Load**: Operations under stress
- **Memory Usage**: Monitor for leaks over extended period
- **Response Times**: Validate latency targets
### 2. Security Audit
#### Authentication & Authorization
- [ ] Discord OAuth implementation secure
- [ ] JWT token handling correct
- [ ] Token expiration and refresh working
- [ ] Session management secure
- [ ] Authorization checks on all actions
#### Input Validation
- [ ] All WebSocket events validated
- [ ] SQL injection prevention verified
- [ ] XSS prevention in place
- [ ] CSRF protection configured
- [ ] Rate limiting functional
#### Data Security
- [ ] HTTPS/WSS enforced
- [ ] Sensitive data encrypted
- [ ] Database credentials secured
- [ ] Environment variables protected
- [ ] Audit logging enabled
#### Game Integrity
- [ ] Dice rolls cryptographically secure
- [ ] All rules enforced server-side
- [ ] No client-side game logic
- [ ] State tampering prevented
- [ ] Replay attacks prevented
### 3. Deployment Setup
#### Infrastructure
- **Backend VPS**: Ubuntu 22.04 LTS, 4GB RAM minimum
- **Database**: PostgreSQL 14+, separate or same VPS
- **SSL**: Let's Encrypt certificates
- **Reverse Proxy**: Nginx for HTTP/WebSocket
- **Process Manager**: systemd or supervisor
- **Firewall**: UFW configured
#### CI/CD Pipeline (Optional)
- Automated testing on push
- Deployment automation
- Rollback procedures
- Database migration automation
#### Environment Configuration
- Production environment variables
- Database connection strings
- API keys secured
- CORS origins configured
- Logging configured
### 4. Monitoring & Alerting
#### Application Monitoring
- **Uptime**: Track availability (target: 99.5%)
- **Response Times**: Monitor latency
- **Error Rates**: Track exceptions
- **WebSocket Health**: Connection count, errors
- **Memory Usage**: Detect leaks
#### Database Monitoring
- **Query Performance**: Slow query log
- **Connection Pool**: Utilization tracking
- **Disk Usage**: Storage monitoring
- **Backup Status**: Verify daily backups
#### Alerting Rules
- HTTP 5xx errors > 5/minute
- WebSocket failures > 10%
- Database pool > 90% utilized
- Memory usage > 85%
- Disk usage > 80%
- Backup failures
#### Logging Strategy
- **Structured Logging**: JSON format
- **Log Levels**: DEBUG, INFO, WARNING, ERROR, CRITICAL
- **Log Rotation**: Daily rotation, 30-day retention
- **Centralized Logs**: Optional log aggregation
### 5. Documentation
#### User Documentation
- **Getting Started Guide**: How to play first game
- **Game Rules**: Baseball rules explanation
- **Strategic Guide**: Decision explanations
- **FAQ**: Common questions
- **Troubleshooting**: Common issues and solutions
#### Technical Documentation
- **API Documentation**: OpenAPI/Swagger
- **WebSocket Protocol**: Event specifications
- **Database Schema**: Table descriptions
- **Deployment Guide**: Step-by-step deployment
- **Runbook**: Operations procedures
#### Code Documentation
- Inline comments for complex logic
- Function/class docstrings
- README files in subdirectories
- Architecture decision records (ADRs)
### 6. Beta Testing
#### Beta Tester Recruitment
- 10-20 active players from each league
- Mix of competitive and casual players
- Mobile and desktop users
- Technical and non-technical users
#### Beta Testing Period
- **Duration**: 2 weeks
- **Goals**: Find bugs, gather feedback, validate UX
- **Feedback Collection**: Structured surveys, bug reports
- **Communication**: Discord channel, daily check-ins
#### Beta Testing Checklist
- [ ] Complete at least 50 games
- [ ] Test all game modes (live, async, vs AI)
- [ ] Test on multiple devices
- [ ] Verify substitutions and pitching changes
- [ ] Test spectator mode
- [ ] Stress test with concurrent games
- [ ] Gather user satisfaction feedback
### 7. Launch Plan
#### Pre-Launch (Week 12)
- [ ] Complete all testing
- [ ] Fix critical bugs
- [ ] Security audit passed
- [ ] Production deployment ready
- [ ] Monitoring configured
- [ ] Documentation complete
- [ ] Beta testing complete
#### Soft Launch (Early Week 13)
- [ ] Announce to beta testers
- [ ] Limited access (invite-only)
- [ ] Monitor closely for issues
- [ ] Quick iteration on feedback
- [ ] Prepare for full launch
#### Full Launch (Mid Week 13)
- [ ] Announce to all league members
- [ ] Migration guide published
- [ ] Support channels active
- [ ] Monitor system health
- [ ] Be ready for rapid response
#### Post-Launch (Late Week 13+)
- [ ] Google Sheets system deprecated
- [ ] Collect user feedback
- [ ] Monitor success metrics
- [ ] Plan post-MVP features
- [ ] Celebrate success! 🎉
## Testing Tools & Frameworks
### Backend Testing
```bash
# Unit and integration tests
pytest backend/tests/ -v --cov=app --cov-report=html
# Load testing
locust -f backend/tests/load/locustfile.py --host=https://api.paperdynasty.com
```
### Frontend Testing
```bash
# Component tests
npm run test:unit
# E2E tests
npm run test:e2e
# Accessibility audit
npm run lighthouse
```
### Security Testing
```bash
# Dependency vulnerability scan
safety check
# OWASP ZAP security scan
zap-cli quick-scan https://paperdynasty.com
```
## Deployment Checklist
### Pre-Deployment
- [ ] All tests passing
- [ ] Load tests completed
- [ ] Security audit passed
- [ ] Database migrations tested
- [ ] Backup procedures verified
- [ ] Monitoring configured
- [ ] SSL certificates installed
- [ ] Environment variables set
- [ ] Documentation updated
### Deployment Steps
1. [ ] Create database backup
2. [ ] Deploy backend to VPS
3. [ ] Run database migrations
4. [ ] Deploy frontend builds
5. [ ] Configure nginx
6. [ ] Start backend services
7. [ ] Verify WebSocket connections
8. [ ] Test Discord OAuth
9. [ ] Run smoke tests
10. [ ] Monitor logs for errors
### Post-Deployment
- [ ] All endpoints responding
- [ ] WebSocket connections working
- [ ] Test game created successfully
- [ ] Monitor error rates
- [ ] Check performance metrics
- [ ] Verify backup running
- [ ] Update DNS (if needed)
## Rollback Plan
If critical issues arise:
1. **Identify Issue**: Determine severity
2. **Assess Impact**: How many users affected?
3. **Decision**: Fix forward or rollback?
**Rollback Procedure**:
```bash
# 1. Stop current services
sudo systemctl stop paperdynasty-backend
# 2. Restore previous version
cd /opt/paperdynasty
git checkout <previous-tag>
# 3. Rollback database migrations
cd backend
alembic downgrade -1
# 4. Restart services
sudo systemctl start paperdynasty-backend
# 5. Verify functionality
curl https://api.paperdynasty.com/health
```
## Success Metrics (First Month)
### Migration Success
- [ ] 90% of active players using web app
- [ ] Zero critical bugs requiring rollback
- [ ] < 5% of games abandoned due to technical issues
### Performance
- [ ] 95th percentile action latency < 1s
- [ ] WebSocket connection success rate > 99%
- [ ] System uptime > 99.5%
### User Satisfaction
- [ ] Net Promoter Score (NPS) > 50
- [ ] Support tickets < 5/week
- [ ] Positive feedback ratio > 80%
### Engagement
- [ ] Average 5+ games per active player per week
- [ ] 60%+ of games on mobile
- [ ] Average session duration 30-60 minutes
## Reference Documents
- [Testing Strategy](./testing-strategy.md) (to be created)
- [Deployment Guide](./deployment-guide.md) (to be created)
- [PRD Lines 1063-1101](../prd-web-scorecard-1.1.md) - Testing strategy
- [PRD Lines 1282-1328](../prd-web-scorecard-1.1.md) - Deployment checklist
## Key Files to Create
**Testing**:
- `backend/tests/load/locustfile.py` - Load test scenarios
- `backend/tests/e2e/test_full_game.py` - E2E test suite
- `frontend-{league}/cypress/` - Cypress E2E tests
- `scripts/test-websocket.py` - WebSocket testing script
**Deployment**:
- `deploy/nginx.conf` - Nginx configuration
- `deploy/systemd/paperdynasty.service` - Systemd service
- `scripts/deploy.sh` - Deployment automation
- `scripts/backup.sh` - Backup automation
**Documentation**:
- `docs/user-guide.md` - User documentation
- `docs/api-reference.md` - API documentation
- `docs/deployment.md` - Deployment guide
- `docs/troubleshooting.md` - Common issues
## Notes
- Testing is not optional - it's critical for success
- Load testing must simulate realistic game scenarios
- Security audit should be thorough - this is user data
- Beta testing feedback is invaluable - listen carefully
- Have rollback plan ready but hope not to use it
- Monitor closely in first 48 hours post-launch
- Be prepared for rapid response to issues
- Communication with users is key during launch
---
**Status**: Placeholder - to be expanded during implementation
**Deliverable**: Production-ready system replacing Google Sheets

View File

@ -0,0 +1,383 @@
# API Reference
**Status**: Placeholder
**Cross-Cutting Concern**: All Phases
---
## Overview
REST API endpoints for game management, authentication, and data retrieval. WebSocket API covered in separate [WebSocket Protocol](./websocket-protocol.md) document.
## Base URLs
- **Production**: `https://api.paperdynasty.com`
- **Development**: `http://localhost:8000`
## Authentication
All endpoints except health checks require authentication via JWT token:
```http
Authorization: Bearer <jwt_token>
```
## REST Endpoints
### Health & Status
#### GET /
Get API information
```json
Response 200:
{
"message": "Paper Dynasty Game Backend",
"version": "1.0.0"
}
```
#### GET /api/health
Health check endpoint
```json
Response 200:
{
"status": "healthy",
"database": "connected",
"timestamp": "2025-10-21T19:45:23Z"
}
```
### Authentication
#### POST /api/auth/discord/login
Initiate Discord OAuth flow
```json
Response 200:
{
"redirect_url": "https://discord.com/api/oauth2/authorize?..."
}
```
#### GET /api/auth/discord/callback
OAuth callback endpoint (query params: code)
```json
Response 200:
{
"access_token": "jwt-token-here",
"token_type": "Bearer",
"expires_in": 86400,
"user": {
"id": "discord_snowflake_id",
"username": "DiscordUser#1234",
"avatar": "avatar_hash",
"teams": [42, 99]
}
}
```
#### POST /api/auth/refresh
Refresh JWT token
```json
Request:
{
"refresh_token": "refresh-token-here"
}
Response 200:
{
"access_token": "new-jwt-token",
"expires_in": 86400
}
```
#### POST /api/auth/logout
Logout user (invalidate token)
```json
Response 200:
{
"message": "Logged out successfully"
}
```
### Games
#### POST /api/games
Create new game
```json
Request:
{
"league_id": "sba",
"home_team_id": 42,
"away_team_id": 99,
"game_mode": "live", // "live", "async", "vs_ai"
"visibility": "public" // "public", "private"
}
Response 201:
{
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"created_at": "2025-10-21T19:45:23Z"
}
```
#### GET /api/games/:id
Get game details
```json
Response 200:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"league_id": "sba",
"home_team_id": 42,
"away_team_id": 99,
"status": "active",
"game_mode": "live",
"visibility": "public",
"current_inning": 3,
"current_half": "top",
"home_score": 2,
"away_score": 1,
"created_at": "2025-10-21T19:30:00Z",
"started_at": "2025-10-21T19:35:00Z",
"completed_at": null,
"winner_team_id": null
}
```
#### GET /api/games
List games (with filters)
Query Parameters:
- `status` - Filter by status (pending, active, completed)
- `league_id` - Filter by league
- `team_id` - Filter by team participation
- `limit` - Number of results (default: 50)
- `offset` - Pagination offset
```json
Response 200:
{
"games": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"league_id": "sba",
"home_team_id": 42,
"away_team_id": 99,
"status": "active",
"current_inning": 3,
"home_score": 2,
"away_score": 1,
"created_at": "2025-10-21T19:30:00Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}
```
#### GET /api/games/:id/history
Get play-by-play history
```json
Response 200:
{
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"plays": [
{
"play_number": 1,
"inning": 1,
"half": "top",
"batter_id": 12345,
"pitcher_id": 67890,
"dice_roll": 5,
"hit_type": "groundout",
"result_description": "Groundout to shortstop",
"runs_scored": 0,
"outs_recorded": 1
}
],
"total_plays": 1
}
```
#### DELETE /api/games/:id
Delete/abandon game (admin only)
```json
Response 200:
{
"message": "Game deleted successfully"
}
```
### Lineups
#### GET /api/games/:id/lineups
Get lineups for game
```json
Response 200:
{
"home": [
{
"card_id": 12345,
"position": "CF",
"batting_order": 1,
"is_active": true,
"player": {
"id": 999,
"name": "Mike Trout",
"image": "https://..."
}
}
],
"away": [...]
}
```
### Spectate
#### GET /api/spectate/:id/join
Get spectator link for game
```json
Response 200:
{
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"spectator_link": "https://sba.paperdynasty.com/spectate/550e8400-e29b-41d4-a716-446655440000",
"can_spectate": true
}
```
## Error Responses
### 400 Bad Request
```json
{
"error": "Bad Request",
"message": "Invalid game_mode: must be 'live', 'async', or 'vs_ai'",
"details": {
"field": "game_mode",
"provided": "invalid_mode"
}
}
```
### 401 Unauthorized
```json
{
"error": "Unauthorized",
"message": "Invalid or expired token"
}
```
### 403 Forbidden
```json
{
"error": "Forbidden",
"message": "You do not have permission to perform this action"
}
```
### 404 Not Found
```json
{
"error": "Not Found",
"message": "Game not found"
}
```
### 429 Too Many Requests
```json
{
"error": "Rate Limit Exceeded",
"message": "Too many requests. Please try again in 60 seconds.",
"retry_after": 60
}
```
### 500 Internal Server Error
```json
{
"error": "Internal Server Error",
"message": "An unexpected error occurred",
"request_id": "abc123xyz"
}
```
## Rate Limits
- **Authenticated requests**: 100 requests/minute per user
- **Unauthenticated requests**: 20 requests/minute per IP
- **Game creation**: 5 games/hour per user
Headers included in response:
```http
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1735689600
```
## Pagination
List endpoints support pagination via `limit` and `offset`:
```http
GET /api/games?limit=25&offset=50
```
Response includes pagination metadata:
```json
{
"data": [...],
"pagination": {
"total": 200,
"limit": 25,
"offset": 50,
"has_more": true
}
}
```
## CORS
CORS headers are configured for allowed origins:
- `https://sba.paperdynasty.com`
- `https://pd.paperdynasty.com`
- `http://localhost:3000` (development)
- `http://localhost:3001` (development)
## OpenAPI Specification
Full OpenAPI/Swagger documentation available at:
- **Development**: `http://localhost:8000/docs`
- **Production**: `https://api.paperdynasty.com/docs`
## WebSocket API
WebSocket events documented separately in [WebSocket Protocol](./websocket-protocol.md).
## League-Specific APIs
The game backend integrates with league-specific REST APIs for team/player data:
### SBA League API
- Base URL: Configured via `SBA_API_URL` env var
- Authentication: API key via `SBA_API_KEY` env var
### PD League API
- Base URL: Configured via `PD_API_URL` env var
- Authentication: API key via `PD_API_KEY` env var
**Endpoints used**:
- `GET /api/teams/:id` - Team details
- `GET /api/teams/:id/roster` - Team roster
- `GET /api/players/:id` - Player details
- `GET /api/cards/:id` - Card details
## Reference Documents
- [WebSocket Protocol](./websocket-protocol.md) - Real-time event API
- [Backend Architecture](./backend-architecture.md) - Implementation details
- [PRD Lines 1106-1127](../prd-web-scorecard-1.1.md) - API endpoint reference
---
**Note**: This is a placeholder to be expanded with complete request/response examples during implementation. Full OpenAPI spec will be auto-generated by FastAPI.

View File

@ -0,0 +1,104 @@
# Authentication & Authorization System
**Status**: Placeholder
**Cross-Cutting Concern**: All Phases
---
## Overview
Discord OAuth-based authentication system with JWT session management and role-based access control for game actions.
## Key Components
### 1. Discord OAuth Flow
**Authorization Request**:
```
https://discord.com/api/oauth2/authorize
?client_id={CLIENT_ID}
&redirect_uri={REDIRECT_URI}
&response_type=code
&scope=identify
```
**Token Exchange** (Backend):
- Receive authorization code from callback
- Exchange code for access token
- Fetch user profile from Discord API
- Lookup/create user record
- Generate JWT token
- Return JWT to frontend
**JWT Payload**:
```json
{
"user_id": "discord_snowflake_id",
"username": "DiscordUser#1234",
"teams": [42, 99], // Team IDs user owns
"exp": 1735689600, // Expiration timestamp
"iat": 1735603200 // Issued at timestamp
}
```
### 2. Session Management
**Frontend**:
- Store JWT in localStorage or cookie
- Include JWT in WebSocket auth header
- Include JWT in HTTP API requests (Authorization: Bearer)
- Auto-refresh before expiration (optional)
- Clear on logout
**Backend**:
- Verify JWT signature on every request
- Check expiration
- Extract user_id and teams from payload
- Attach to request context
### 3. Authorization Rules
**Game Actions**:
- User must own the team to make decisions
- Spectators have read-only access
- No actions allowed after game completion
**API Endpoints**:
- `/api/games` - Authenticated users only
- `/api/games/:id` - Public if game visibility is public
- `/api/games/:id/join` - Must own a team in the game
**WebSocket Events**:
- All action events require team ownership validation
- Spectators can only receive events, not emit actions
### 4. Security Considerations
- **Token Storage**: HttpOnly cookies preferred over localStorage
- **Token Expiration**: 24 hours, refresh after 12 hours
- **Rate Limiting**: Per-user action limits
- **Logout**: Invalidate token (blacklist or short expiry)
- **HTTPS Only**: All communication encrypted
## Implementation Files
**Backend**:
- `backend/app/api/routes/auth.py` - OAuth endpoints
- `backend/app/utils/auth.py` - JWT utilities
- `backend/app/middleware/auth.py` - Request authentication
- `backend/app/websocket/auth.py` - WebSocket authentication
**Frontend**:
- `frontend-{league}/pages/auth/login.vue` - Login page
- `frontend-{league}/pages/auth/callback.vue` - OAuth callback
- `frontend-{league}/store/auth.ts` - Auth state management
- `frontend-{league}/middleware/auth.ts` - Route guards
## Reference Documents
- [PRD Lines 135-146](../prd-web-scorecard-1.1.md) - Authentication requirements
- [PRD Lines 686-706](../prd-web-scorecard-1.1.md) - Security requirements
---
**Note**: This is a placeholder to be expanded with detailed implementation code during Phase 1.

View File

@ -0,0 +1,484 @@
# Backend Architecture
## Overview
FastAPI-based game backend serving as the central game engine for both SBA and PD leagues. Handles real-time WebSocket communication, game state management, and database persistence.
## Directory Structure
```
backend/
├── app/
│ ├── main.py # FastAPI app initialization, CORS, middleware
│ ├── config.py # Environment variables, settings
│ │
│ ├── core/ # Core game logic
│ │ ├── __init__.py
│ │ ├── game_engine.py # Main game simulation engine
│ │ ├── state_manager.py # In-memory state management
│ │ ├── play_resolver.py # Play outcome resolution logic
│ │ ├── dice.py # Cryptographic random roll generation
│ │ └── validators.py # Game rule validation
│ │
│ ├── config/ # League configurations
│ │ ├── __init__.py
│ │ ├── base_config.py # Shared BaseGameConfig
│ │ ├── league_configs.py # SBA/PD specific configs
│ │ ├── result_charts.py # d20 outcome tables
│ │ └── loader.py # Config loading utilities
│ │
│ ├── models/ # Data models
│ │ ├── __init__.py
│ │ ├── game_models.py # Pydantic models for game state
│ │ ├── player_models.py # Polymorphic player models
│ │ ├── db_models.py # SQLAlchemy ORM models
│ │ └── api_schemas.py # Request/response schemas
│ │
│ ├── websocket/ # WebSocket handling
│ │ ├── __init__.py
│ │ ├── connection_manager.py # Connection lifecycle
│ │ ├── events.py # Event definitions
│ │ ├── handlers.py # Action handlers
│ │ └── rooms.py # Socket.io room management
│ │
│ ├── api/ # REST API endpoints
│ │ ├── __init__.py
│ │ ├── routes/
│ │ │ ├── games.py # Game CRUD operations
│ │ │ ├── auth.py # Discord OAuth endpoints
│ │ │ └── health.py # Health check endpoints
│ │ └── dependencies.py # FastAPI dependencies
│ │
│ ├── database/ # Database operations
│ │ ├── __init__.py
│ │ ├── session.py # DB connection management
│ │ ├── operations.py # Async DB operations
│ │ └── migrations/ # Alembic migrations
│ │
│ ├── data/ # External data integration
│ │ ├── __init__.py
│ │ ├── api_client.py # League REST API client
│ │ ├── cache.py # Optional caching layer
│ │ └── models.py # API response models
│ │
│ └── utils/ # Utilities
│ ├── __init__.py
│ ├── logging.py # Logging configuration
│ ├── auth.py # JWT token handling
│ └── errors.py # Custom exceptions
├── tests/
│ ├── unit/ # Unit tests
│ │ ├── test_game_engine.py
│ │ ├── test_dice.py
│ │ ├── test_play_resolver.py
│ │ └── test_player_models.py
│ ├── integration/ # Integration tests
│ │ ├── test_websocket.py
│ │ ├── test_database.py
│ │ └── test_api_client.py
│ └── e2e/ # End-to-end tests
│ └── test_full_game.py
├── alembic.ini # Alembic configuration
├── requirements.txt # Python dependencies
├── requirements-dev.txt # Development dependencies
├── docker-compose.yml # Local development setup
├── Dockerfile # Production container
└── pytest.ini # Pytest configuration
```
## Core Components
### 1. Game Engine (`core/game_engine.py`)
**Responsibilities**:
- Manage in-memory game state
- Process player actions
- Coordinate play resolution
- Emit events to WebSocket clients
**Key Methods**:
```python
class GameEngine:
def __init__(self, game_id: str, config: LeagueConfig)
async def start_game(self) -> None
async def process_defensive_positioning(self, user_id: str, positioning: str) -> PlayOutcome
async def process_stolen_base_attempt(self, runner_positions: List[str]) -> PlayOutcome
async def process_offensive_approach(self, approach: str) -> PlayOutcome
async def make_substitution(self, card_id: int, position: str) -> None
async def change_pitcher(self, card_id: int) -> None
def get_current_state(self) -> GameState
async def persist_play(self, play: Play) -> None
```
### 2. State Manager (`core/state_manager.py`)
**Responsibilities**:
- Hold active game states in memory
- Provide fast read/write access
- Handle state recovery from database
- Manage state lifecycle (creation, updates, cleanup)
**Key Methods**:
```python
class StateManager:
def create_game_state(self, game_id: str, config: LeagueConfig) -> GameState
def get_game_state(self, game_id: str) -> Optional[GameState]
def update_state(self, game_id: str, updates: dict) -> None
async def recover_state(self, game_id: str) -> GameState
def remove_state(self, game_id: str) -> None
def get_active_games(self) -> List[str]
```
**Data Structure**:
```python
@dataclass
class GameState:
game_id: str
league_id: str
inning: int
half: str # 'top' or 'bottom'
outs: int
balls: int
strikes: int
home_score: int
away_score: int
runners: Dict[str, Optional[int]] # {'first': card_id, 'second': None, 'third': card_id}
current_batter_idx: int
lineups: Dict[str, List[Lineup]] # {'home': [...], 'away': [...]}
current_decisions: Dict[str, Any] # Pending decisions
play_history: List[int] # Play IDs
metadata: dict
```
### 3. Play Resolver (`core/play_resolver.py`)
**Responsibilities**:
- Roll dice and determine outcomes
- Apply league-specific result charts
- Calculate runner advancement
- Update game state based on outcome
**Key Methods**:
```python
class PlayResolver:
def __init__(self, config: LeagueConfig)
def roll_dice(self) -> int
def resolve_at_bat(
self,
batter: PdPlayer | SbaPlayer,
pitcher: PdPlayer | SbaPlayer,
defensive_positioning: str,
offensive_approach: str,
count: Dict[str, int]
) -> PlayOutcome
def advance_runners(
self,
hit_type: str,
runners_before: Dict[str, Optional[int]],
hit_location: Optional[str] = None
) -> Tuple[Dict[str, Optional[int]], int] # (runners_after, runs_scored)
```
### 4. Polymorphic Player Models (`models/player_models.py`)
**Class Hierarchy**:
```python
class BasePlayer(BaseModel, ABC):
id: int
name: str
@abstractmethod
def get_image_url(self) -> str:
pass
@abstractmethod
def get_display_data(self) -> dict:
pass
class SbaPlayer(BasePlayer):
image: str
team: Optional[str] = None
manager: Optional[str] = None
def get_image_url(self) -> str:
return self.image
class PdPlayer(BasePlayer):
image: str
scouting_data: dict
ratings: dict
probabilities: dict
def get_image_url(self) -> str:
return self.image
class Lineup(BaseModel):
id: int
game_id: str
card_id: int
position: str
batting_order: Optional[int] = None
player: Union[SbaPlayer, PdPlayer]
@classmethod
def from_api_data(cls, game_config: LeagueConfig, api_data: dict) -> 'Lineup':
player_data = api_data.pop('player')
if game_config.league_id == "sba":
player = SbaPlayer(**player_data)
elif game_config.league_id == "pd":
player = PdPlayer(**player_data)
else:
raise ValueError(f"Unknown league: {game_config.league_id}")
return cls(player=player, **api_data)
```
### 5. WebSocket Connection Manager (`websocket/connection_manager.py`)
**Responsibilities**:
- Manage Socket.io connections
- Handle room assignments (game rooms)
- Broadcast events to participants
- Handle disconnections and reconnections
**Key Methods**:
```python
class ConnectionManager:
def __init__(self, sio: socketio.AsyncServer)
async def connect(self, sid: str, user_id: str) -> None
async def disconnect(self, sid: str) -> None
async def join_game(self, sid: str, game_id: str, role: str) -> None
async def leave_game(self, sid: str, game_id: str) -> None
async def broadcast_to_game(self, game_id: str, event: str, data: dict) -> None
async def emit_to_user(self, sid: str, event: str, data: dict) -> None
def get_game_participants(self, game_id: str) -> List[str]
```
## Data Flow
### Game Action Processing
```
1. Client sends WebSocket event
2. handlers.py receives and validates
3. Verify user authorization
4. Load game state from StateManager
5. GameEngine processes action
6. PlayResolver determines outcome (if dice roll needed)
7. StateManager updates in-memory state
8. Database operations (async, non-blocking)
9. ConnectionManager broadcasts update to all clients
10. Clients update UI
```
### State Recovery on Reconnect
```
1. Client reconnects to WebSocket
2. Client sends join_game event
3. Check if state exists in StateManager
4. If not in memory:
a. Load game metadata from database
b. Load all plays from database
c. Replay plays through GameEngine
d. Store reconstructed state in StateManager
5. Send current state to client
6. Client resumes gameplay
```
## League Configuration System
### Config Loading
```python
# config/base_config.py
@dataclass
class BaseGameConfig:
innings: int = 9
outs_per_inning: int = 3
strikes_for_out: int = 3
balls_for_walk: int = 4
roster_size: int = 26
positions: List[str] = field(default_factory=lambda: ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'])
# config/league_configs.py
@dataclass
class SbaConfig(BaseGameConfig):
league_id: str = "sba"
league_name: str = "Super Baseball Alliance"
api_url: str = field(default_factory=lambda: os.getenv("SBA_API_URL"))
result_charts: dict = field(default_factory=lambda: load_sba_charts())
special_rules: dict = field(default_factory=dict)
@dataclass
class PdConfig(BaseGameConfig):
league_id: str = "pd"
league_name: str = "Paper Dynasty"
api_url: str = field(default_factory=lambda: os.getenv("PD_API_URL"))
result_charts: dict = field(default_factory=lambda: load_pd_charts())
special_rules: dict = field(default_factory=dict)
# config/loader.py
def get_league_config(league_id: str) -> BaseGameConfig:
configs = {
"sba": SbaConfig,
"pd": PdConfig
}
if league_id not in configs:
raise ValueError(f"Unknown league: {league_id}")
return configs[league_id]()
```
## Database Integration
### Async Operations
All database operations use async patterns to avoid blocking game logic:
```python
# database/operations.py
async def save_play(play: Play) -> int:
"""Save play to database asynchronously"""
async with get_session() as session:
db_play = DbPlay(**play.dict())
session.add(db_play)
await session.commit()
await session.refresh(db_play)
return db_play.id
async def load_game_plays(game_id: str) -> List[Play]:
"""Load all plays for a game"""
async with get_session() as session:
result = await session.execute(
select(DbPlay)
.where(DbPlay.game_id == game_id)
.order_by(DbPlay.play_number)
)
db_plays = result.scalars().all()
return [Play.from_orm(p) for p in db_plays]
```
## Security Considerations
### Authentication
- Discord OAuth tokens validated on REST endpoints
- WebSocket connections require valid JWT token
- Token expiration and refresh handled
### Authorization
- Verify user owns team before allowing actions
- Spectator permissions enforced (read-only)
- Rate limiting on API endpoints
### Game Integrity
- All rule validation server-side
- Cryptographically secure dice rolls
- Audit trail of all plays
- State validation before each action
## Performance Optimizations
### In-Memory State
- Active games kept in memory for fast access
- Idle games evicted after timeout (configurable)
- State recovery only when needed
### Async Database Writes
- Play persistence doesn't block game logic
- Connection pooling for efficiency
- Batch writes where appropriate
### Caching Layer (Optional)
- Cache team/roster data from league APIs
- Redis for distributed caching (if multi-server)
- TTL-based cache invalidation
## Logging Strategy
### Log Levels
- **DEBUG**: State transitions, action processing details
- **INFO**: Game started/completed, player joins/leaves
- **WARNING**: Invalid actions, timeouts, retry attempts
- **ERROR**: Database errors, API failures, state corruption
- **CRITICAL**: System failures requiring immediate attention
### Log Format
```python
import logging
logger = logging.getLogger(f'{__name__}.GameEngine')
# Usage
logger.info(f"Game {game_id} started", extra={
"game_id": game_id,
"league_id": league_id,
"home_team": home_team_id,
"away_team": away_team_id
})
```
## Error Handling
### Custom Exceptions
```python
class GameEngineError(Exception):
"""Base exception for game engine errors"""
pass
class InvalidActionError(GameEngineError):
"""Raised when action violates game rules"""
pass
class StateRecoveryError(GameEngineError):
"""Raised when state cannot be recovered"""
pass
class UnauthorizedActionError(GameEngineError):
"""Raised when user not authorized for action"""
pass
```
### Error Recovery
- Automatic reconnection for database issues
- State recovery from last known good state
- Graceful degradation when league API unavailable
- Transaction rollback on database errors
## Testing Strategy
### Unit Tests
- Test each core component in isolation
- Mock external dependencies (DB, APIs)
- Verify dice roll randomness and distribution
- Test all play resolution scenarios
### Integration Tests
- Test WebSocket event flow
- Test database persistence and recovery
- Test league config loading
- Test API client interactions
### Performance Tests
- Measure action processing time
- Test concurrent game handling
- Monitor memory usage under load
- Test state recovery speed
---
**Next Steps**: See [02-game-engine.md](./02-game-engine.md) for implementation details.

View File

@ -0,0 +1,687 @@
# Database Design
## Overview
PostgreSQL database for persistent storage of game data, play history, and session management. Designed for fast writes (async) and efficient recovery.
## Schema
### Games Table
Stores game metadata and current state.
```sql
CREATE TABLE games (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
league_id VARCHAR(50) NOT NULL,
home_team_id INTEGER NOT NULL,
away_team_id INTEGER NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
game_mode VARCHAR(20) NOT NULL,
visibility VARCHAR(20) NOT NULL,
current_inning INTEGER,
current_half VARCHAR(10),
home_score INTEGER DEFAULT 0,
away_score INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
started_at TIMESTAMP,
completed_at TIMESTAMP,
winner_team_id INTEGER,
metadata JSONB DEFAULT '{}'::jsonb
);
-- Indexes
CREATE INDEX idx_games_league ON games(league_id);
CREATE INDEX idx_games_status ON games(status);
CREATE INDEX idx_games_teams ON games(home_team_id, away_team_id);
CREATE INDEX idx_games_created ON games(created_at DESC);
-- Comments
COMMENT ON COLUMN games.status IS 'pending, active, completed, abandoned';
COMMENT ON COLUMN games.game_mode IS 'live, async, vs_ai';
COMMENT ON COLUMN games.visibility IS 'public, private';
COMMENT ON COLUMN games.metadata IS 'League-specific data, config version, etc.';
```
### Plays Table
Records every play that occurs in a game.
```sql
CREATE TABLE plays (
id SERIAL PRIMARY KEY,
game_id UUID NOT NULL REFERENCES games(id) ON DELETE CASCADE,
play_number INTEGER NOT NULL,
inning INTEGER NOT NULL,
half VARCHAR(10) NOT NULL,
outs_before INTEGER NOT NULL,
outs_recorded INTEGER NOT NULL,
batter_id INTEGER NOT NULL,
pitcher_id INTEGER NOT NULL,
runners_before JSONB,
runners_after JSONB,
balls INTEGER,
strikes INTEGER,
defensive_positioning VARCHAR(50),
offensive_approach VARCHAR(50),
dice_roll INTEGER,
hit_type VARCHAR(50),
result_description TEXT,
runs_scored INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
metadata JSONB DEFAULT '{}'::jsonb,
UNIQUE(game_id, play_number)
);
-- Indexes
CREATE INDEX idx_plays_game ON plays(game_id);
CREATE INDEX idx_plays_game_number ON plays(game_id, play_number);
CREATE INDEX idx_plays_created ON plays(created_at);
-- Comments
COMMENT ON COLUMN plays.half IS 'top or bottom';
COMMENT ON COLUMN plays.runners_before IS '{"first": card_id, "second": null, "third": card_id}';
COMMENT ON COLUMN plays.hit_type IS 'single, double, triple, homerun, out, walk, strikeout, etc.';
COMMENT ON COLUMN plays.metadata IS 'Additional play data, stolen base results, etc.';
```
### Lineups Table
Stores lineup information for each game.
```sql
CREATE TABLE lineups (
id SERIAL PRIMARY KEY,
game_id UUID NOT NULL REFERENCES games(id) ON DELETE CASCADE,
team_id INTEGER NOT NULL,
card_id INTEGER NOT NULL,
position VARCHAR(10) NOT NULL,
batting_order INTEGER,
is_starter BOOLEAN DEFAULT TRUE,
is_active BOOLEAN DEFAULT TRUE,
entered_inning INTEGER DEFAULT 1,
metadata JSONB DEFAULT '{}'::jsonb
);
-- Indexes
CREATE INDEX idx_lineups_game ON lineups(game_id);
CREATE INDEX idx_lineups_team ON lineups(team_id);
CREATE INDEX idx_lineups_active ON lineups(game_id, is_active);
CREATE INDEX idx_lineups_game_team ON lineups(game_id, team_id);
-- Comments
COMMENT ON COLUMN lineups.position IS 'P, C, 1B, 2B, 3B, SS, LF, CF, RF, DH';
COMMENT ON COLUMN lineups.is_active IS 'FALSE when substituted out';
COMMENT ON COLUMN lineups.entered_inning IS 'Inning when player entered game';
```
### Game Sessions Table
Tracks active WebSocket sessions and state snapshots.
```sql
CREATE TABLE game_sessions (
game_id UUID PRIMARY KEY REFERENCES games(id) ON DELETE CASCADE,
connected_users JSONB DEFAULT '{}'::jsonb,
last_action_at TIMESTAMP DEFAULT NOW(),
state_snapshot JSONB DEFAULT '{}'::jsonb,
updated_at TIMESTAMP DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_sessions_last_action ON game_sessions(last_action_at);
-- Comments
COMMENT ON COLUMN game_sessions.connected_users IS '{"user_id": {"role": "home_gm", "last_seen": "timestamp"}}';
COMMENT ON COLUMN game_sessions.state_snapshot IS 'Full game state for quick recovery';
```
### Users Table (Optional - if not using external auth exclusively)
```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
discord_id VARCHAR(50) UNIQUE NOT NULL,
discord_username VARCHAR(100),
discord_avatar VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
last_login TIMESTAMP,
metadata JSONB DEFAULT '{}'::jsonb
);
-- Indexes
CREATE INDEX idx_users_discord ON users(discord_id);
-- Comments
COMMENT ON TABLE users IS 'Optional user cache if needed for offline queries';
```
## Data Types & Constraints
### JSONB Fields
#### games.metadata
```json
{
"league_config_version": "1.0",
"home_gm_id": "discord_snowflake_123",
"away_gm_id": "discord_snowflake_456",
"home_gm_ready": true,
"away_gm_ready": false,
"starting_pitcher_home": 12345,
"starting_pitcher_away": 67890
}
```
#### plays.runners_before / runners_after
```json
{
"first": 12345, // card_id or null
"second": null,
"third": 67890
}
```
#### plays.metadata
```json
{
"stolen_base_attempts": {
"second": true,
"third": false
},
"stolen_base_results": {
"second": "safe"
},
"hit_location": "left_field",
"fielder": 54321
}
```
#### lineups.metadata
```json
{
"substitution_reason": "pinch_hitter",
"replaced_card_id": 11111,
"pitcher_stats": {
"innings_pitched": 6.0,
"hits_allowed": 4,
"runs_allowed": 1
}
}
```
#### game_sessions.state_snapshot
```json
{
"inning": 3,
"half": "top",
"outs": 2,
"balls": 2,
"strikes": 1,
"home_score": 2,
"away_score": 1,
"runners": {"first": null, "second": 12345, "third": null},
"current_batter_idx": 3,
"current_decisions": {}
}
```
## Queries
### Common Read Queries
#### Get Game with Latest Play
```sql
SELECT
g.*,
p.play_number as last_play,
p.result_description as last_play_description
FROM games g
LEFT JOIN LATERAL (
SELECT * FROM plays
WHERE game_id = g.id
ORDER BY play_number DESC
LIMIT 1
) p ON TRUE
WHERE g.id = $1;
```
#### Get Active Lineup for Team
```sql
SELECT
l.*,
l.batting_order,
l.position
FROM lineups l
WHERE l.game_id = $1
AND l.team_id = $2
AND l.is_active = TRUE
ORDER BY l.batting_order NULLS LAST;
```
#### Get Play History for Game
```sql
SELECT
play_number,
inning,
half,
outs_before,
batter_id,
pitcher_id,
dice_roll,
hit_type,
result_description,
runs_scored,
created_at
FROM plays
WHERE game_id = $1
ORDER BY play_number;
```
#### Get Active Games for User
```sql
SELECT
g.*,
CASE
WHEN g.home_team_id = ANY($2) THEN 'home'
WHEN g.away_team_id = ANY($2) THEN 'away'
END as role,
(SELECT COUNT(*) FROM plays WHERE game_id = g.id) as play_count
FROM games g
WHERE g.status IN ('pending', 'active')
AND (g.home_team_id = ANY($2) OR g.away_team_id = ANY($2))
ORDER BY g.created_at DESC;
-- $1 = user_id, $2 = array of team_ids user owns
```
#### Get Recent Completed Games
```sql
SELECT
g.id,
g.league_id,
g.home_team_id,
g.away_team_id,
g.home_score,
g.away_score,
g.winner_team_id,
g.completed_at,
EXTRACT(EPOCH FROM (g.completed_at - g.started_at))/60 as duration_minutes
FROM games g
WHERE g.status = 'completed'
AND g.league_id = $1
ORDER BY g.completed_at DESC
LIMIT 50;
```
### Common Write Queries
#### Create New Game
```sql
INSERT INTO games (
league_id,
home_team_id,
away_team_id,
status,
game_mode,
visibility,
metadata
) VALUES (
$1, $2, $3, 'pending', $4, $5, $6
)
RETURNING id;
```
#### Insert Play
```sql
INSERT INTO plays (
game_id,
play_number,
inning,
half,
outs_before,
outs_recorded,
batter_id,
pitcher_id,
runners_before,
runners_after,
balls,
strikes,
defensive_positioning,
offensive_approach,
dice_roll,
hit_type,
result_description,
runs_scored,
metadata
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18, $19
)
RETURNING id;
```
#### Update Game Score
```sql
UPDATE games
SET
current_inning = $2,
current_half = $3,
home_score = $4,
away_score = $5
WHERE id = $1;
```
#### Make Substitution
```sql
-- Deactivate old player
UPDATE lineups
SET is_active = FALSE
WHERE game_id = $1
AND card_id = $2;
-- Insert new player
INSERT INTO lineups (
game_id,
team_id,
card_id,
position,
batting_order,
is_starter,
is_active,
entered_inning
) VALUES (
$1, $3, $4, $5, $6, FALSE, TRUE, $7
);
```
#### Complete Game
```sql
UPDATE games
SET
status = 'completed',
completed_at = NOW(),
winner_team_id = $2
WHERE id = $1;
```
### State Recovery Query
```sql
-- Get all data needed to rebuild game state
WITH game_info AS (
SELECT * FROM games WHERE id = $1
),
lineup_data AS (
SELECT * FROM lineups WHERE game_id = $1 AND is_active = TRUE
),
play_data AS (
SELECT * FROM plays WHERE game_id = $1 ORDER BY play_number
)
SELECT
json_build_object(
'game', (SELECT row_to_json(g) FROM game_info g),
'lineups', (SELECT json_agg(l) FROM lineup_data l),
'plays', (SELECT json_agg(p) FROM play_data p)
) as game_state;
```
## Performance Optimization
### Index Strategy
**Games Table**:
- `idx_games_league`: Filter by league
- `idx_games_status`: Find active/completed games
- `idx_games_teams`: Find games by team
- `idx_games_created`: Recent games list
**Plays Table**:
- `idx_plays_game`: All plays for a game (most common)
- `idx_plays_game_number`: Specific play lookup
- `idx_plays_created`: Recent plays across all games
**Lineups Table**:
- `idx_lineups_game`: All lineups for a game
- `idx_lineups_team`: Team's lineup across games
- `idx_lineups_active`: Active players only
- `idx_lineups_game_team`: Combined filter (most efficient)
### Query Optimization Tips
1. **Use EXPLAIN ANALYZE** for slow queries
2. **Avoid SELECT \***: Specify needed columns
3. **Use LATERAL joins** for correlated subqueries
4. **Leverage JSONB indexes** if filtering on metadata fields frequently
5. **Partition plays table** if growing very large (by created_at)
### Connection Pooling
```python
# SQLAlchemy async engine with connection pooling
engine = create_async_engine(
database_url,
pool_size=20, # Number of persistent connections
max_overflow=10, # Additional connections when needed
pool_timeout=30, # Seconds to wait for connection
pool_recycle=3600, # Recycle connections after 1 hour
pool_pre_ping=True # Verify connection health before use
)
```
## Data Integrity
### Foreign Key Constraints
All foreign keys include `ON DELETE CASCADE` to ensure referential integrity:
- When a game is deleted, all plays and lineups are automatically deleted
- Prevents orphaned records
### Check Constraints
```sql
-- Add validation constraints
ALTER TABLE games
ADD CONSTRAINT check_status
CHECK (status IN ('pending', 'active', 'completed', 'abandoned'));
ALTER TABLE games
ADD CONSTRAINT check_game_mode
CHECK (game_mode IN ('live', 'async', 'vs_ai'));
ALTER TABLE games
ADD CONSTRAINT check_scores
CHECK (home_score >= 0 AND away_score >= 0);
ALTER TABLE plays
ADD CONSTRAINT check_half
CHECK (half IN ('top', 'bottom'));
ALTER TABLE plays
ADD CONSTRAINT check_outs
CHECK (outs_before >= 0 AND outs_before <= 2 AND outs_recorded >= 0);
ALTER TABLE lineups
ADD CONSTRAINT check_position
CHECK (position IN ('P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH'));
```
## Backup & Recovery
### Daily Backups
```bash
#!/bin/bash
# daily-backup.sh
DATE=$(date +%Y%m%d)
BACKUP_DIR="/backups/paperdynasty"
DB_NAME="paperdynasty"
# Create backup directory
mkdir -p $BACKUP_DIR
# Full database backup
pg_dump -U paperdynasty -F c -b -v -f "$BACKUP_DIR/full_backup_$DATE.dump" $DB_NAME
# Compress
gzip "$BACKUP_DIR/full_backup_$DATE.dump"
# Keep only last 30 days
find $BACKUP_DIR -name "full_backup_*.dump.gz" -mtime +30 -delete
echo "Backup completed: $DATE"
```
### Point-in-Time Recovery Setup
```sql
-- Enable WAL archiving (postgresql.conf)
wal_level = replica
archive_mode = on
archive_command = 'cp %p /var/lib/postgresql/wal_archive/%f'
```
### Recovery Procedure
```bash
# Restore from backup
pg_restore -U paperdynasty -d paperdynasty -c backup_file.dump
# Verify data
psql -U paperdynasty -d paperdynasty -c "SELECT COUNT(*) FROM games;"
```
## Migration Strategy
### Using Alembic
```bash
# Initialize Alembic
cd backend
alembic init alembic
# Create migration
alembic revision -m "create initial tables"
# Apply migration
alembic upgrade head
# Rollback
alembic downgrade -1
```
### Example Migration
```python
# alembic/versions/001_create_initial_tables.py
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID, JSONB
def upgrade():
op.create_table(
'games',
sa.Column('id', UUID(as_uuid=True), primary_key=True),
sa.Column('league_id', sa.String(50), nullable=False),
# ... other columns
)
# Create indexes
op.create_index('idx_games_league', 'games', ['league_id'])
op.create_index('idx_games_status', 'games', ['status'])
def downgrade():
op.drop_table('games')
```
## Monitoring Queries
### Active Connections
```sql
SELECT
count(*) as total_connections,
count(*) FILTER (WHERE state = 'active') as active_queries,
count(*) FILTER (WHERE state = 'idle') as idle_connections
FROM pg_stat_activity
WHERE datname = 'paperdynasty';
```
### Slow Queries
```sql
SELECT
query,
calls,
mean_exec_time,
max_exec_time,
total_exec_time
FROM pg_stat_statements
WHERE query LIKE '%games%'
ORDER BY mean_exec_time DESC
LIMIT 10;
```
### Table Sizes
```sql
SELECT
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
```
### Index Usage
```sql
SELECT
schemaname,
tablename,
indexname,
idx_scan as index_scans,
idx_tup_read as tuples_read,
idx_tup_fetch as tuples_fetched
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan DESC;
```
## Testing Data
### Seed Development Data
```sql
-- Insert test game
INSERT INTO games (id, league_id, home_team_id, away_team_id, status, game_mode, visibility)
VALUES (
'550e8400-e29b-41d4-a716-446655440000',
'sba',
1,
2,
'active',
'live',
'public'
);
-- Insert test lineups
INSERT INTO lineups (game_id, team_id, card_id, position, batting_order, is_starter, is_active)
VALUES
('550e8400-e29b-41d4-a716-446655440000', 1, 101, 'P', NULL, TRUE, TRUE),
('550e8400-e29b-41d4-a716-446655440000', 1, 102, 'C', 9, TRUE, TRUE),
('550e8400-e29b-41d4-a716-446655440000', 1, 103, 'CF', 1, TRUE, TRUE);
-- ... more players
-- Insert test play
INSERT INTO plays (
game_id, play_number, inning, half, outs_before, outs_recorded,
batter_id, pitcher_id, dice_roll, hit_type, result_description, runs_scored
) VALUES (
'550e8400-e29b-41d4-a716-446655440000',
1, 1, 'top', 0, 1, 103, 201, 5, 'groundout',
'Groundout to shortstop', 0
);
```
---
**Next Steps**: See [01-infrastructure.md](./01-infrastructure.md) for database setup instructions.

View File

@ -0,0 +1,779 @@
# Frontend Architecture
## Overview
Two separate Nuxt 3 applications (one per league) with maximum code reuse through a shared component library. Mobile-first design with real-time WebSocket updates.
## Directory Structure
### Per-League Frontend (`frontend-sba/` and `frontend-pd/`)
```
frontend-{league}/
├── assets/ # Static assets
│ ├── css/
│ │ └── tailwind.css # Tailwind imports
│ └── images/
│ ├── logo.png
│ └── field-bg.svg
├── components/ # League-specific components
│ ├── Branding/
│ │ ├── Header.vue
│ │ ├── Footer.vue
│ │ └── Logo.vue
│ └── League/
│ └── SpecialFeatures.vue # League-specific UI elements
├── composables/ # Vue composables
│ ├── useAuth.ts # Authentication state
│ ├── useWebSocket.ts # WebSocket connection
│ ├── useGameState.ts # Game state management
│ └── useLeagueConfig.ts # League-specific config
├── layouts/
│ ├── default.vue # Standard layout
│ ├── game.vue # Game view layout
│ └── auth.vue # Auth pages layout
├── pages/
│ ├── index.vue # Home/dashboard
│ ├── games/
│ │ ├── [id].vue # Game view
│ │ ├── create.vue # Create new game
│ │ └── history.vue # Completed games
│ ├── auth/
│ │ ├── login.vue
│ │ └── callback.vue # Discord OAuth callback
│ └── spectate/
│ └── [id].vue # Spectator view
├── plugins/
│ ├── socket.client.ts # Socket.io plugin
│ └── auth.ts # Auth plugin
├── store/ # Pinia stores
│ ├── auth.ts # Authentication state
│ ├── game.ts # Current game state
│ ├── games.ts # Games list
│ └── ui.ts # UI state (modals, toasts)
├── types/
│ ├── game.ts # Game-related types
│ ├── player.ts # Player types
│ ├── api.ts # API response types
│ └── websocket.ts # WebSocket event types
├── utils/
│ ├── api.ts # API client
│ ├── formatters.ts # Data formatting utilities
│ └── validators.ts # Input validation
├── middleware/
│ ├── auth.ts # Auth guard
│ └── game-access.ts # Game access validation
├── app.vue # Root component
├── nuxt.config.ts # Nuxt configuration
├── tailwind.config.js # Tailwind configuration
├── tsconfig.json # TypeScript configuration
└── package.json
```
### Shared Component Library (`shared-components/`)
```
shared-components/
├── src/
│ ├── components/
│ │ ├── Game/
│ │ │ ├── GameBoard.vue # Baseball diamond visualization
│ │ │ ├── ScoreBoard.vue # Score display
│ │ │ ├── PlayByPlay.vue # Play history feed
│ │ │ ├── CurrentSituation.vue # Current game context
│ │ │ └── BaseRunners.vue # Runner indicators
│ │ │
│ │ ├── Decisions/
│ │ │ ├── DefensivePositioning.vue
│ │ │ ├── StolenBaseAttempt.vue
│ │ │ ├── OffensiveApproach.vue
│ │ │ └── DecisionTimer.vue
│ │ │
│ │ ├── Actions/
│ │ │ ├── SubstitutionModal.vue
│ │ │ ├── PitchingChange.vue
│ │ │ └── ActionButton.vue
│ │ │
│ │ ├── Display/
│ │ │ ├── PlayerCard.vue # Player card display
│ │ │ ├── DiceRoll.vue # Dice animation
│ │ │ ├── PlayOutcome.vue # Play result display
│ │ │ └── ConnectionStatus.vue # WebSocket status
│ │ │
│ │ └── Common/
│ │ ├── Button.vue
│ │ ├── Modal.vue
│ │ ├── Toast.vue
│ │ └── Loading.vue
│ │
│ ├── composables/
│ │ ├── useGameActions.ts # Shared game actions
│ │ └── useGameDisplay.ts # Shared display logic
│ │
│ └── types/
│ └── index.ts # Shared TypeScript types
├── package.json
└── tsconfig.json
```
## Key Components
### 1. WebSocket Composable (`composables/useWebSocket.ts`)
**Responsibilities**:
- Establish and maintain WebSocket connection
- Handle reconnection logic
- Emit events to server
- Subscribe to server events
- Connection status monitoring
**Implementation**:
```typescript
import { io, Socket } from 'socket.io-client'
import { ref, onUnmounted } from 'vue'
export const useWebSocket = () => {
const socket = ref<Socket | null>(null)
const connected = ref(false)
const reconnecting = ref(false)
const connect = (token: string) => {
const config = useRuntimeConfig()
socket.value = io(config.public.wsUrl, {
auth: { token },
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5
})
socket.value.on('connect', () => {
connected.value = true
reconnecting.value = false
})
socket.value.on('disconnect', () => {
connected.value = false
})
socket.value.on('reconnecting', () => {
reconnecting.value = true
})
}
const disconnect = () => {
socket.value?.disconnect()
socket.value = null
connected.value = false
}
const emit = (event: string, data: any) => {
if (!socket.value?.connected) {
throw new Error('WebSocket not connected')
}
socket.value.emit(event, data)
}
const on = (event: string, handler: (...args: any[]) => void) => {
socket.value?.on(event, handler)
}
const off = (event: string, handler?: (...args: any[]) => void) => {
socket.value?.off(event, handler)
}
onUnmounted(() => {
disconnect()
})
return {
socket,
connected,
reconnecting,
connect,
disconnect,
emit,
on,
off
}
}
```
### 2. Game State Store (`store/game.ts`)
**Responsibilities**:
- Hold current game state
- Update state from WebSocket events
- Provide computed properties for UI
- Handle optimistic updates
**Implementation**:
```typescript
import { defineStore } from 'pinia'
import type { GameState, PlayOutcome } from '~/types/game'
export const useGameStore = defineStore('game', () => {
// State
const gameState = ref<GameState | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
const pendingAction = ref(false)
// Computed
const inning = computed(() => gameState.value?.inning ?? 1)
const half = computed(() => gameState.value?.half ?? 'top')
const outs = computed(() => gameState.value?.outs ?? 0)
const score = computed(() => ({
home: gameState.value?.home_score ?? 0,
away: gameState.value?.away_score ?? 0
}))
const runners = computed(() => gameState.value?.runners ?? {})
const currentBatter = computed(() => gameState.value?.current_batter)
const currentPitcher = computed(() => gameState.value?.current_pitcher)
const isMyTurn = computed(() => {
// Logic to determine if it's user's turn
return gameState.value?.decision_required?.user_id === useAuthStore().userId
})
// Actions
const setGameState = (state: GameState) => {
gameState.value = state
loading.value = false
error.value = null
}
const updateState = (updates: Partial<GameState>) => {
if (gameState.value) {
gameState.value = { ...gameState.value, ...updates }
}
}
const handlePlayCompleted = (outcome: PlayOutcome) => {
// Update state based on play outcome
if (gameState.value) {
updateState({
outs: outcome.outs_after,
runners: outcome.runners_after,
home_score: outcome.home_score,
away_score: outcome.away_score
})
}
pendingAction.value = false
}
const setError = (message: string) => {
error.value = message
loading.value = false
pendingAction.value = false
}
const reset = () => {
gameState.value = null
loading.value = false
error.value = null
pendingAction.value = false
}
return {
// State
gameState,
loading,
error,
pendingAction,
// Computed
inning,
half,
outs,
score,
runners,
currentBatter,
currentPitcher,
isMyTurn,
// Actions
setGameState,
updateState,
handlePlayCompleted,
setError,
reset
}
})
```
### 3. Game Board Component (`shared-components/Game/GameBoard.vue`)
**Responsibilities**:
- Visual baseball diamond
- Show runners on base
- Highlight active bases
- Responsive scaling
**Implementation**:
```vue
<template>
<div class="game-board relative w-full aspect-square max-w-md mx-auto">
<!-- Diamond SVG -->
<svg viewBox="0 0 100 100" class="w-full h-full">
<!-- Diamond shape -->
<path
d="M 50 10 L 90 50 L 50 90 L 10 50 Z"
class="fill-green-600 stroke-white stroke-2"
/>
<!-- Bases -->
<rect
v-for="base in bases"
:key="base.name"
:x="base.x"
:y="base.y"
width="8"
height="8"
:class="[
'stroke-white stroke-2',
hasRunner(base.name) ? 'fill-yellow-400' : 'fill-white'
]"
/>
<!-- Home plate -->
<polygon
points="50,88 48,90 52,90"
class="fill-white stroke-white"
/>
</svg>
<!-- Runner indicators -->
<div
v-for="(runner, base) in runners"
:key="base"
:class="getRunnerPositionClass(base)"
class="absolute"
>
<PlayerAvatar
v-if="runner"
:player-id="runner"
size="sm"
/>
</div>
<!-- Batter indicator -->
<div class="absolute bottom-2 left-1/2 -translate-x-1/2">
<span class="text-xs text-white font-bold">
{{ currentBatter?.name }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { Runners } from '~/types/game'
interface Props {
runners: Runners
currentBatter?: { name: string }
}
const props = defineProps<Props>()
const bases = [
{ name: 'first', x: 84, y: 46 },
{ name: 'second', x: 46, y: 6 },
{ name: 'third', x: 6, y: 46 }
]
const hasRunner = (base: string) => {
return props.runners[base] !== null
}
const getRunnerPositionClass = (base: string) => {
const positions = {
first: 'right-8 top-1/2 -translate-y-1/2',
second: 'top-8 left-1/2 -translate-x-1/2',
third: 'left-8 top-1/2 -translate-y-1/2'
}
return positions[base as keyof typeof positions]
}
</script>
```
### 4. Decision Flow Component (`shared-components/Decisions/DefensivePositioning.vue`)
**Responsibilities**:
- Present defensive positioning options
- Validate selection
- Submit decision via WebSocket
- Show loading state
**Implementation**:
```vue
<template>
<div class="decision-card bg-white rounded-lg shadow-lg p-6">
<h3 class="text-lg font-bold mb-4">Set Defensive Positioning</h3>
<div class="space-y-3">
<button
v-for="option in positioningOptions"
:key="option.value"
@click="selectPositioning(option.value)"
:disabled="submitting"
class="w-full py-3 px-4 rounded-lg border-2 transition-colors"
:class="[
selected === option.value
? 'border-blue-500 bg-blue-50'
: 'border-gray-300 hover:border-blue-300',
submitting && 'opacity-50 cursor-not-allowed'
]"
>
<div class="text-left">
<div class="font-semibold">{{ option.label }}</div>
<div class="text-sm text-gray-600">{{ option.description }}</div>
</div>
</button>
</div>
<div class="mt-6 flex justify-between items-center">
<DecisionTimer
v-if="timeoutSeconds"
:seconds="timeoutSeconds"
@timeout="handleTimeout"
/>
<button
@click="submitDecision"
:disabled="!selected || submitting"
class="btn-primary"
>
{{ submitting ? 'Submitting...' : 'Confirm' }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
timeoutSeconds?: number
}
const props = defineProps<Props>()
const emit = defineEmits<{
submit: [positioning: string]
timeout: []
}>()
const selected = ref<string | null>(null)
const submitting = ref(false)
const positioningOptions = [
{
value: 'standard',
label: 'Standard',
description: 'Normal defensive alignment'
},
{
value: 'infield_in',
label: 'Infield In',
description: 'Drawn in to prevent runs'
},
{
value: 'shift_left',
label: 'Shift Left',
description: 'Shifted for pull hitter'
},
{
value: 'shift_right',
label: 'Shift Right',
description: 'Shifted for opposite field'
}
]
const selectPositioning = (value: string) => {
if (!submitting.value) {
selected.value = value
}
}
const submitDecision = async () => {
if (!selected.value || submitting.value) return
submitting.value = true
emit('submit', selected.value)
}
const handleTimeout = () => {
emit('timeout')
}
</script>
```
## State Management Architecture
### Pinia Stores
**Auth Store**:
- User authentication state
- Discord profile data
- Team ownership information
- Token management
**Game Store**:
- Current game state
- Real-time updates from WebSocket
- Pending actions
- Error handling
**Games List Store**:
- Active games list
- Completed games history
- Game filtering
**UI Store**:
- Modal states
- Toast notifications
- Loading indicators
- Theme preferences
## Mobile-First Responsive Design
### Breakpoints
```javascript
// tailwind.config.js
module.exports = {
theme: {
screens: {
'xs': '375px', // Small phones
'sm': '640px', // Large phones
'md': '768px', // Tablets
'lg': '1024px', // Desktop
'xl': '1280px', // Large desktop
}
}
}
```
### Layout Strategy
**Mobile (< 768px)**:
- Single column layout
- Bottom sheet for decisions
- Sticky scoreboard at top
- Collapsible play-by-play
- Full-screen game board
**Tablet (768px - 1024px)**:
- Two column layout (game + sidebar)
- Larger game board
- Side panel for decisions
- Expanded play history
**Desktop (> 1024px)**:
- Three column layout (optional)
- Full game board center
- Decision panel right
- Stats panel left
## WebSocket Event Handling
### Event Listeners Setup
```typescript
// composables/useGameActions.ts
export const useGameActions = (gameId: string) => {
const { socket, emit, on, off } = useWebSocket()
const gameStore = useGameStore()
onMounted(() => {
// Join game room
emit('join_game', { game_id: gameId, role: 'player' })
// Listen for state updates
on('game_state_update', (data: GameState) => {
gameStore.setGameState(data)
})
on('play_completed', (data: PlayOutcome) => {
gameStore.handlePlayCompleted(data)
})
on('dice_rolled', (data: DiceRoll) => {
// Trigger dice animation
showDiceRoll(data.roll, data.animation_duration)
})
on('decision_required', (data: DecisionPrompt) => {
// Show decision UI
gameStore.setDecisionRequired(data)
})
on('invalid_action', (data: ErrorData) => {
gameStore.setError(data.message)
})
on('game_error', (data: ErrorData) => {
gameStore.setError(data.message)
})
})
onUnmounted(() => {
// Clean up listeners
off('game_state_update')
off('play_completed')
off('dice_rolled')
off('decision_required')
off('invalid_action')
off('game_error')
// Leave game room
emit('leave_game', { game_id: gameId })
})
// Action methods
const setDefense = (positioning: string) => {
emit('set_defense', { game_id: gameId, positioning })
}
const setStolenBase = (runners: string[]) => {
emit('set_stolen_base', { game_id: gameId, runners })
}
const setOffensiveApproach = (approach: string) => {
emit('set_offensive_approach', { game_id: gameId, approach })
}
return {
setDefense,
setStolenBase,
setOffensiveApproach
}
}
```
## League-Specific Customization
### Configuration
```typescript
// frontend-sba/nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
leagueId: 'sba',
leagueName: 'Super Baseball Alliance',
apiUrl: process.env.NUXT_PUBLIC_API_URL,
wsUrl: process.env.NUXT_PUBLIC_WS_URL,
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID,
primaryColor: '#1e40af', // Blue
secondaryColor: '#dc2626' // Red
}
}
})
// frontend-pd/nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
leagueId: 'pd',
leagueName: 'Paper Dynasty',
apiUrl: process.env.NUXT_PUBLIC_API_URL,
wsUrl: process.env.NUXT_PUBLIC_WS_URL,
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID,
primaryColor: '#16a34a', // Green
secondaryColor: '#ea580c' // Orange
}
}
})
```
### Theming
```typescript
// composables/useLeagueConfig.ts
export const useLeagueConfig = () => {
const config = useRuntimeConfig()
return {
leagueId: config.public.leagueId,
leagueName: config.public.leagueName,
colors: {
primary: config.public.primaryColor,
secondary: config.public.secondaryColor
},
features: {
showScoutingData: config.public.leagueId === 'pd',
useSimplePlayerCards: config.public.leagueId === 'sba'
}
}
}
```
## Performance Optimizations
### Code Splitting
- Lazy load game components
- Route-based code splitting
- Dynamic imports for heavy libraries
### Asset Optimization
- Image lazy loading
- SVG sprites for icons
- Optimized font loading
### State Updates
- Debounce non-critical updates
- Optimistic UI updates
- Efficient re-rendering with `v-memo`
## Error Handling
### Network Errors
```typescript
const handleNetworkError = (error: Error) => {
const uiStore = useUiStore()
if (error.message.includes('WebSocket')) {
uiStore.showToast({
type: 'error',
message: 'Connection lost. Reconnecting...'
})
} else {
uiStore.showToast({
type: 'error',
message: 'Network error. Please try again.'
})
}
}
```
### Game Errors
```typescript
const handleGameError = (error: GameError) => {
const uiStore = useUiStore()
uiStore.showModal({
title: 'Game Error',
message: error.message,
actions: [
{
label: 'Retry',
handler: () => retryLastAction()
},
{
label: 'Reload Game',
handler: () => reloadGameState()
}
]
})
}
```
---
**Next Steps**: See [03-gameplay-features.md](./03-gameplay-features.md) for gameplay implementation details.

View File

@ -0,0 +1,322 @@
# Testing Strategy
**Status**: Placeholder
**Cross-Cutting Concern**: All Phases, Critical in Phase 5
---
## Overview
Comprehensive testing approach covering unit, integration, E2E, load, and security testing to ensure production-ready quality.
## Testing Pyramid
```
/\
/ \ E2E Tests (10%)
/ \ - Full game flows
/------\ - Multi-user scenarios
/ \
/ Integ. \ Integration Tests (30%)
/ Tests \ - WebSocket flows
/ \ - Database operations
/----------------\
/ \ Unit Tests (60%)
/ Unit Tests \- Core logic
/______________________\- Pure functions
```
## Test Coverage Goals
- **Overall Coverage**: >80%
- **Core Game Logic**: >90%
- **WebSocket Handlers**: >85%
- **Database Operations**: >75%
- **Frontend Components**: >70%
## Testing Tools
### Backend
- **pytest**: Unit and integration tests
- **pytest-asyncio**: Async test support
- **pytest-cov**: Coverage reporting
- **httpx**: API client testing
- **python-socketio[client]**: WebSocket testing
- **factory_boy**: Test data generation
- **Faker**: Fake data generation
### Frontend
- **Vitest**: Unit testing (fast, Vite-native)
- **Testing Library**: Component testing
- **Cypress**: E2E testing
- **Playwright**: Alternative E2E testing
- **Lighthouse CI**: Performance testing
- **axe-core**: Accessibility testing
### Load Testing
- **Locust**: Distributed load testing
- **Artillery**: Alternative load testing
### Security Testing
- **Safety**: Dependency vulnerability scanning
- **Bandit**: Python security linting
- **OWASP ZAP**: Security scanning
- **npm audit**: Frontend dependency scanning
## Test Organization
### Backend Structure
```
backend/tests/
├── unit/
│ ├── test_game_engine.py
│ ├── test_state_manager.py
│ ├── test_play_resolver.py
│ ├── test_dice.py
│ └── test_validators.py
├── integration/
│ ├── test_websocket.py
│ ├── test_database.py
│ ├── test_api_client.py
│ └── test_full_turn.py
├── e2e/
│ ├── test_full_game.py
│ ├── test_reconnection.py
│ └── test_state_recovery.py
├── load/
│ └── locustfile.py
├── conftest.py # Shared fixtures
└── factories.py # Test data factories
```
### Frontend Structure
```
frontend-{league}/
├── tests/
│ ├── unit/
│ │ ├── composables/
│ │ └── utils/
│ └── components/
│ ├── Game/
│ ├── Decisions/
│ └── Display/
└── cypress/
├── e2e/
│ ├── game-flow.cy.ts
│ ├── auth.cy.ts
│ └── spectator.cy.ts
└── support/
```
## Key Test Scenarios
### Unit Tests
- Dice roll distribution (statistical validation)
- Play outcome resolution (all d20 results)
- State transitions
- Input validation
- Player model instantiation
### Integration Tests
- WebSocket event flow (connect → join → action → update)
- Database persistence and recovery
- API client responses
- Multi-turn game sequences
### E2E Tests
- Complete 9-inning game
- Game with substitutions
- AI opponent game
- Spectator joining active game
- Reconnection after disconnect
- Multiple concurrent games
### Load Tests
- 10 concurrent games
- 50 concurrent WebSocket connections
- Sustained load over 30 minutes
- Spike testing (sudden load increase)
### Security Tests
- SQL injection attempts
- XSS attempts
- CSRF protection
- Authorization bypass attempts
- Rate limit enforcement
## Example Test Cases
### Unit Test (Backend)
```python
# tests/unit/test_dice.py
import pytest
from app.core.dice import DiceRoller
def test_dice_roll_range():
"""Test that dice rolls are within valid range"""
roller = DiceRoller()
for _ in range(1000):
roll = roller.roll_d20()
assert 1 <= roll <= 20
def test_dice_distribution():
"""Test that dice rolls are reasonably distributed"""
roller = DiceRoller()
rolls = [roller.roll_d20() for _ in range(10000)]
# Each number should appear roughly 500 times (±10%)
for num in range(1, 21):
count = rolls.count(num)
assert 450 <= count <= 550
```
### Integration Test (Backend)
```python
# tests/integration/test_websocket.py
import pytest
import socketio
@pytest.mark.asyncio
async def test_game_action_flow(sio_client, test_game):
"""Test complete action flow through WebSocket"""
# Connect
await sio_client.connect('http://localhost:8000', auth={'token': 'valid-jwt'})
# Join game
await sio_client.emit('join_game', {'game_id': test_game.id, 'role': 'player'})
# Wait for game state
response = await sio_client.receive()
assert response[0] == 'game_state_update'
# Send defensive decision
await sio_client.emit('set_defense', {
'game_id': test_game.id,
'positioning': 'standard'
})
# Verify decision recorded
response = await sio_client.receive()
assert response[0] == 'decision_recorded'
```
### Component Test (Frontend)
```typescript
// tests/components/Game/GameBoard.spec.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import GameBoard from '@/components/Game/GameBoard.vue'
describe('GameBoard', () => {
it('displays runners on base correctly', () => {
const runners = {
first: null,
second: 12345,
third: null
}
const wrapper = mount(GameBoard, {
props: { runners }
})
expect(wrapper.find('[data-base="second"]').exists()).toBe(true)
expect(wrapper.find('[data-base="first"]').exists()).toBe(false)
})
})
```
### E2E Test (Frontend)
```typescript
// cypress/e2e/game-flow.cy.ts
describe('Complete Game Flow', () => {
it('can play through an at-bat', () => {
cy.login() // Custom command
cy.visit('/games/create')
cy.get('[data-test="create-game"]').click()
cy.get('[data-test="game-mode-live"]').click()
cy.get('[data-test="start-game"]').click()
// Wait for game to start
cy.get('[data-test="game-board"]').should('be.visible')
// Make defensive decision
cy.get('[data-test="defense-standard"]').click()
cy.get('[data-test="confirm-decision"]').click()
// Verify decision recorded
cy.get('[data-test="waiting-indicator"]').should('be.visible')
})
})
```
## Continuous Integration
### GitHub Actions Example
```yaml
name: Test Suite
on: [push, pull_request]
jobs:
backend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- run: pip install -r backend/requirements-dev.txt
- run: pytest backend/tests/ --cov --cov-report=xml
- uses: codecov/codecov-action@v3
frontend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run test:unit
- run: npm run test:e2e
```
## Test Data Management
### Fixtures
- **Shared fixtures** in `conftest.py`
- **Factory pattern** for creating test objects
- **Database seeding** for integration tests
- **Mock data** for external APIs
### Test Isolation
- Each test should be independent
- Database rollback after each test
- Clean in-memory state between tests
- No shared mutable state
## Performance Testing
### Metrics to Track
- Response time (p50, p95, p99)
- Throughput (requests/second)
- Error rate
- CPU and memory usage
- Database query time
### Load Test Scenarios
1. **Normal Load**: 5 concurrent games
2. **Peak Load**: 10 concurrent games
3. **Stress Test**: 20 concurrent games (breaking point)
4. **Spike Test**: 2 → 10 games instantly
## Reference Documents
- [PRD Lines 1063-1101](../prd-web-scorecard-1.1.md) - Testing strategy
- [05-testing-launch.md](./05-testing-launch.md) - Phase 5 testing details
---
**Note**: This is a placeholder to be expanded with specific test implementations during development.

View File

@ -0,0 +1,668 @@
# WebSocket Protocol Specification
## Overview
Real-time bidirectional communication protocol between game clients and backend server using Socket.io. All game state updates, player actions, and system events transmitted via WebSocket.
## Connection Lifecycle
### 1. Initial Connection
**Client → Server**
```typescript
import { io } from 'socket.io-client'
const socket = io('wss://api.paperdynasty.com', {
auth: {
token: 'jwt-token-here'
},
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5
})
```
**Server → Client**
```json
{
"event": "connected",
"data": {
"user_id": "123456789",
"connection_id": "abc123xyz"
}
}
```
### 2. Joining a Game
**Client → Server**
```json
{
"event": "join_game",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"role": "player" // "player" or "spectator"
}
}
```
**Server → Client** (Success)
```json
{
"event": "game_joined",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"role": "player",
"team_id": 42
}
}
```
**Server → All Participants** (User Joined Notification)
```json
{
"event": "user_connected",
"data": {
"user_id": "123456789",
"role": "player",
"team_id": 42,
"timestamp": "2025-10-21T19:45:23Z"
}
}
```
### 3. Receiving Game State
**Server → Client** (Full State on Join)
```json
{
"event": "game_state_update",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "active",
"inning": 3,
"half": "top",
"outs": 2,
"balls": 2,
"strikes": 1,
"home_score": 2,
"away_score": 1,
"runners": {
"first": null,
"second": 12345,
"third": null
},
"current_batter": {
"card_id": 67890,
"player_id": 999,
"name": "Mike Trout",
"position": "CF",
"batting_avg": 0.305,
"image": "https://..."
},
"current_pitcher": {
"card_id": 11111,
"player_id": 888,
"name": "Sandy Alcantara",
"position": "P",
"era": 2.45,
"image": "https://..."
},
"decision_required": {
"type": "set_defense",
"team_id": 42,
"user_id": "123456789",
"timeout_seconds": 30
},
"play_history": [
{
"play_number": 44,
"inning": 3,
"description": "Groundout to second base",
"runs_scored": 0
}
]
}
}
```
### 4. Heartbeat
**Client → Server** (Every 30 seconds)
```json
{
"event": "heartbeat"
}
```
**Server → Client**
```json
{
"event": "heartbeat_ack",
"data": {
"timestamp": "2025-10-21T19:45:23Z"
}
}
```
### 5. Disconnection
**Client → Server**
```json
{
"event": "leave_game",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
```
**Server → All Participants**
```json
{
"event": "user_disconnected",
"data": {
"user_id": "123456789",
"timestamp": "2025-10-21T19:46:00Z"
}
}
```
## Game Action Events
### 1. Set Defensive Positioning
**Client → Server**
```json
{
"event": "set_defense",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"positioning": "standard" // "standard", "infield_in", "shift_left", "shift_right"
}
}
```
**Server → Client** (Acknowledgment)
```json
{
"event": "decision_recorded",
"data": {
"type": "set_defense",
"positioning": "standard",
"timestamp": "2025-10-21T19:45:25Z"
}
}
```
**Server → All Participants** (Next Decision Required)
```json
{
"event": "decision_required",
"data": {
"type": "set_stolen_base",
"team_id": 42,
"user_id": "123456789",
"runners": ["second"],
"timeout_seconds": 20
}
}
```
### 2. Stolen Base Attempt
**Client → Server**
```json
{
"event": "set_stolen_base",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"attempts": {
"second": true, // Runner on second attempts
"third": false // No runner on third
}
}
}
```
### 3. Offensive Approach
**Client → Server**
```json
{
"event": "set_offensive_approach",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"approach": "swing_away" // "swing_away", "bunt", "hit_and_run"
}
}
```
### 4. Dice Roll & Play Resolution
**Server → All Participants** (Dice Roll Animation)
```json
{
"event": "dice_rolled",
"data": {
"roll": 14,
"animation_duration": 2000,
"timestamp": "2025-10-21T19:45:30Z"
}
}
```
**Server → All Participants** (Play Outcome)
```json
{
"event": "play_completed",
"data": {
"play_number": 45,
"inning": 3,
"half": "top",
"dice_roll": 14,
"result_type": "single",
"hit_location": "left_field",
"description": "Mike Trout singles to left field. Runner advances to third.",
"batter": {
"card_id": 67890,
"name": "Mike Trout"
},
"pitcher": {
"card_id": 11111,
"name": "Sandy Alcantara"
},
"outs_before": 2,
"outs_recorded": 0,
"outs_after": 2,
"runners_before": {
"first": null,
"second": 12345,
"third": null
},
"runners_after": {
"first": 67890,
"second": null,
"third": 12345
},
"runs_scored": 0,
"home_score": 2,
"away_score": 1,
"timestamp": "2025-10-21T19:45:32Z"
}
}
```
### 5. Play Result Selection
When multiple outcomes possible (e.g., hit location choices):
**Server → Offensive Player**
```json
{
"event": "select_play_result",
"data": {
"play_number": 45,
"options": [
{
"value": "single_left",
"label": "Single to Left",
"description": "Runner advances to third"
},
{
"value": "single_center",
"label": "Single to Center",
"description": "Runner scores"
}
],
"timeout_seconds": 15
}
}
```
**Client → Server**
```json
{
"event": "select_play_result",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"play_number": 45,
"selection": "single_center"
}
}
```
### 6. Substitution
**Client → Server**
```json
{
"event": "substitute_player",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"card_out": 67890,
"card_in": 55555,
"position": "CF",
"batting_order": 3
}
}
```
**Server → All Participants**
```json
{
"event": "substitution_made",
"data": {
"team_id": 42,
"player_out": {
"card_id": 67890,
"name": "Mike Trout"
},
"player_in": {
"card_id": 55555,
"name": "Byron Buxton",
"position": "CF",
"batting_order": 3
},
"inning": 3,
"timestamp": "2025-10-21T19:46:00Z"
}
}
```
### 7. Pitching Change
**Client → Server**
```json
{
"event": "change_pitcher",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"pitcher_out": 11111,
"pitcher_in": 22222
}
}
```
**Server → All Participants**
```json
{
"event": "pitcher_changed",
"data": {
"team_id": 42,
"pitcher_out": {
"card_id": 11111,
"name": "Sandy Alcantara",
"final_line": "6 IP, 4 H, 1 R, 1 ER, 2 BB, 8 K"
},
"pitcher_in": {
"card_id": 22222,
"name": "Edwin Diaz",
"position": "P"
},
"inning": 7,
"timestamp": "2025-10-21T19:47:00Z"
}
}
```
## System Events
### 1. Inning Change
**Server → All Participants**
```json
{
"event": "inning_change",
"data": {
"inning": 4,
"half": "top",
"home_score": 2,
"away_score": 1,
"timestamp": "2025-10-21T19:48:00Z"
}
}
```
### 2. Game Ended
**Server → All Participants**
```json
{
"event": "game_ended",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"winner_team_id": 42,
"final_score": {
"home": 5,
"away": 3
},
"innings": 9,
"duration_minutes": 87,
"mvp": {
"card_id": 67890,
"name": "Mike Trout",
"stats": "3-4, 2 R, 2 RBI, HR"
},
"completed_at": "2025-10-21T20:15:00Z"
}
}
```
### 3. Decision Timeout Warning
**Server → User**
```json
{
"event": "decision_timeout_warning",
"data": {
"decision_type": "set_defense",
"seconds_remaining": 10,
"default_action": "standard"
}
}
```
### 4. Auto-Decision Made
**Server → All Participants**
```json
{
"event": "auto_decision",
"data": {
"decision_type": "set_defense",
"team_id": 42,
"action_taken": "standard",
"reason": "timeout",
"timestamp": "2025-10-21T19:45:55Z"
}
}
```
## Error Events
### 1. Invalid Action
**Server → Client**
```json
{
"event": "invalid_action",
"data": {
"action": "set_defense",
"reason": "Not your turn",
"current_decision": {
"type": "set_offense",
"team_id": 99
},
"timestamp": "2025-10-21T19:46:00Z"
}
}
```
### 2. Connection Error
**Server → Client**
```json
{
"event": "connection_error",
"data": {
"code": "AUTH_FAILED",
"message": "Invalid or expired token",
"reconnect": false
}
}
```
### 3. Game Error
**Server → All Participants**
```json
{
"event": "game_error",
"data": {
"code": "STATE_RECOVERY_FAILED",
"message": "Unable to recover game state",
"severity": "critical",
"recovery_options": [
"reload_game",
"contact_support"
],
"timestamp": "2025-10-21T19:46:30Z"
}
}
```
## Rate Limiting
### Per-User Limits
- **Actions**: 10 per second
- **Heartbeats**: 1 per 10 seconds minimum
- **Invalid actions**: 5 per minute (after that, temporary ban)
### Response on Rate Limit
```json
{
"event": "rate_limit_exceeded",
"data": {
"action": "set_defense",
"limit": 10,
"window": "1 second",
"retry_after": 1000,
"timestamp": "2025-10-21T19:46:35Z"
}
}
```
## Reconnection Protocol
### Automatic Reconnection
1. Client detects disconnect
2. Client attempts reconnect with backoff (1s, 2s, 4s, 8s, 16s)
3. On successful reconnect, client sends `join_game` event
4. Server checks if game state exists in memory
5. If not, server recovers state from database
6. Server sends full `game_state_update` to client
7. Client resumes from current state
### Client Implementation
```typescript
socket.on('disconnect', () => {
console.log('Disconnected, will attempt reconnect')
// Socket.io handles reconnection automatically
})
socket.on('connect', () => {
console.log('Reconnected')
// Rejoin game room
socket.emit('join_game', { game_id: currentGameId, role: 'player' })
})
```
## Testing WebSocket Events
### Using Python Client
```python
import socketio
sio = socketio.Client()
@sio.event
def connect():
print('Connected')
sio.emit('join_game', {'game_id': 'test-game', 'role': 'player'})
@sio.event
def game_state_update(data):
print(f'Game state: {data}')
sio.connect('http://localhost:8000', auth={'token': 'jwt-token'})
sio.wait()
```
### Using Browser Console
```javascript
const socket = io('http://localhost:8000', {
auth: { token: 'jwt-token' }
})
socket.on('connect', () => {
console.log('Connected')
socket.emit('join_game', { game_id: 'test-game', role: 'player' })
})
socket.on('game_state_update', (data) => {
console.log('Game state:', data)
})
```
## Event Flow Diagrams
### Typical At-Bat Flow
```
1. [Server → All] decision_required (set_defense)
2. [Client → Server] set_defense
3. [Server → All] decision_recorded
4. [Server → All] decision_required (set_stolen_base) [if runners on base]
5. [Client → Server] set_stolen_base
6. [Server → All] decision_recorded
7. [Server → All] decision_required (set_offensive_approach)
8. [Client → Server] set_offensive_approach
9. [Server → All] decision_recorded
10. [Server → All] dice_rolled
11. [Server → All] play_completed
12. [Server → All] game_state_update
13. Loop to step 1 for next at-bat
```
### Substitution Flow
```
1. [Client → Server] substitute_player
2. [Server validates]
3. [Server → All] substitution_made
4. [Server → All] game_state_update (with new lineup)
```
## Security Considerations
### Authentication
- JWT token required for initial connection
- Token verified on every connection attempt
- Token refresh handled by HTTP API, not WebSocket
### Authorization
- User can only perform actions for their team
- Spectators receive read-only events
- Server validates all actions against game rules
### Data Validation
- All incoming events validated against Pydantic schemas
- Invalid events logged and rejected
- Repeated invalid events result in disconnect
---
**Implementation**: See [backend-architecture.md](./backend-architecture.md) for connection manager implementation.

77
.dockerignore Normal file
View File

@ -0,0 +1,77 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
.venv/
ENV/
env/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
tests/
# Logs
logs/
*.log
# Environment files
.env
.env.local
.env.*.local
# Data files
data/
*.json
# Git
.git/
.gitignore
# Docker
Dockerfile
docker-compose*
.dockerignore
# Documentation
.claude/
*.md
docs/
# CI/CD
.github/
.gitlab-ci.yml
.gitlab/
# OS
.DS_Store
Thumbs.db

47
.env.example Normal file
View File

@ -0,0 +1,47 @@
# Paper Dynasty Game Engine - Environment Variables
# Copy this file to .env and update with your actual values
# ============================================================================
# Application
# ============================================================================
APP_ENV=development
DEBUG=true
SECRET_KEY=your-secret-key-at-least-32-characters-long
# ============================================================================
# Database
# ============================================================================
# Update with your actual PostgreSQL server credentials
# Format: postgresql+asyncpg://username:password@hostname:port/database
DATABASE_URL=postgresql+asyncpg://paperdynasty:your-password@your-db-server:5432/paperdynasty_dev
# ============================================================================
# Discord OAuth
# ============================================================================
# Get these from Discord Developer Portal: https://discord.com/developers/applications
DISCORD_CLIENT_ID=your-discord-client-id
DISCORD_CLIENT_SECRET=your-discord-client-secret
DISCORD_REDIRECT_URI=http://localhost:3000/auth/callback
# ============================================================================
# League REST APIs
# ============================================================================
# SBA League API
SBA_API_URL=https://sba-api.example.com
SBA_API_KEY=your-sba-api-key
# PD League API
PD_API_URL=https://pd-api.example.com
PD_API_KEY=your-pd-api-key
# ============================================================================
# CORS Origins (comma-separated)
# ============================================================================
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
# ============================================================================
# Redis (optional - for caching)
# ============================================================================
# When using Docker Compose, Redis is automatically available at redis://redis:6379
# When running locally, use redis://localhost:6379
# REDIS_URL=redis://localhost:6379

43
.gitignore vendored
View File

@ -158,3 +158,46 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear # 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. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
lerna-debug.log*
# Nuxt
.nuxt/
.output/
.nitro/
dist/
# Logs
logs/
backend/logs/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Docker volumes
postgres_data/
redis_data/
# Database
*.db
*.sqlite
*.sqlite3
# Project-specific
.env.local
.env.*.local
.env.production

155
CLAUDE.md Normal file
View File

@ -0,0 +1,155 @@
# 🚨 CRITICAL: @ MENTION HANDLING 🚨
When ANY file is mentioned with @ syntax, you MUST IMMEDIATELY call Read tool on that file BEFORE responding.
You will see automatic loads of any @ mentioned filed, this is NOT ENOUGH, it only loads the file contents.
You MUST perform Read tool calls on the files directly, even if they were @ included.
This is NOT optional - it loads required CLAUDE.md context. along the file path.
See @./.claude/force-claude-reads.md for details.
---
# Paper Dynasty Real-Time Game Engine - Development Guide
## Project Overview
Web-based real-time multiplayer baseball simulation platform replacing legacy Google Sheets system. Consists of:
- **Shared Backend**: FastAPI (Python 3.11+) with WebSocket support, PostgreSQL persistence
- **Dual Frontends**: Separate Vue 3/Nuxt 3 apps per league (SBA and PD) with shared component library
**Critical Business Driver**: Legacy Google Sheets being deprecated - this is mission-critical replacement.
## Architecture Principles
### Backend Philosophy
- **Hybrid State Model**: In-memory game state for performance + PostgreSQL for persistence/recovery
- **League-Agnostic Core**: Polymorphic player models, config-driven league variations
- **Async-First**: All I/O operations use async/await patterns
- **Type Safety**: Pydantic models for validation, SQLAlchemy for ORM
### Frontend Philosophy
- **Mobile-First**: Primary design target is mobile portrait mode
- **Real-Time Updates**: WebSocket (Socket.io) for all game state changes
- **Shared Components**: Maximize reuse between league frontends
- **Type Safety**: TypeScript with strict mode
## Technology Stack
### Backend
- FastAPI + Socket.io (WebSocket)
- PostgreSQL 14+ with SQLAlchemy 2.0
- Pydantic for data validation
- pytest for testing
### Frontend (Per League)
- Vue 3 Composition API + Nuxt 3
- TypeScript (strict mode)
- Tailwind CSS
- Pinia for state management
- Socket.io-client
- @nuxtjs/auth-next (Discord OAuth)
## Key Technical Patterns
### Polymorphic Player Architecture
Use factory pattern for league-specific player types:
- `BasePlayer` (abstract base)
- `SbaPlayer` (simple model)
- `PdPlayer` (detailed scouting data)
- `Lineup.from_api_data(config, data)` factory method
### WebSocket Event Flow
1. Player action → WebSocket → Backend
2. Validate against in-memory state
3. Process + resolve outcome
4. Update in-memory state
5. Async write to PostgreSQL
6. Broadcast state update to all clients
### Game State Recovery
On reconnect: Load plays from DB → Replay to rebuild state → Send current state
## Project Structure
```
strat-gameplay-webapp/
├── backend/ # FastAPI game engine
│ ├── app/
│ │ ├── core/ # Game engine, dice, state management
│ │ ├── config/ # League configs and result charts
│ │ ├── websocket/ # Socket.io handlers
│ │ ├── models/ # Pydantic + SQLAlchemy models
│ │ └── api/ # REST endpoints
│ └── tests/
├── frontend-sba/ # SBA League Nuxt app
├── frontend-pd/ # PD League Nuxt app
└── shared-components/ # Shared Vue components (optional)
```
## Development Guidelines
### Code Quality
- **Python**: Dataclasses preferred, rotating loggers with `f'{__name__}.<className>'`
- **Error Handling**: "Raise or Return" pattern - no Optional unless required
- **Testing**: Run tests freely without asking permission
- **Imports**: Always verify imports during code review to prevent NameErrors
- **Git Commits**: Prefix with "CLAUDE: "
### Performance Targets
- Action response: < 500ms
- WebSocket delivery: < 200ms
- DB writes: < 100ms (async)
- State recovery: < 2 seconds
### Security Requirements
- Discord OAuth for authentication
- Server-side game logic only (zero client trust)
- Cryptographically secure dice rolls
- All rules enforced server-side
## Phase 1 MVP Scope (Weeks 1-13)
**Core Deliverables**:
1. Authentication (Discord OAuth)
2. Game creation & lobby
3. Complete turn-based gameplay with all strategic decisions
4. Real-time WebSocket updates
5. Game persistence & recovery
6. Spectator mode
7. Mobile-optimized UI
8. AI opponent support
**Explicitly Out of Scope for MVP**:
- Roster management
- Marketplace
- Tournaments
- Advanced analytics
## Critical References
- **Full PRD**: `/mnt/NV2/Development/strat-gameplay-webapp/prd-web-scorecard-1.1.md`
- **Player Model Architecture**: PRD lines 378-551
- **Database Schema**: PRD lines 553-628
- **WebSocket Events**: PRD lines 630-669
- **League Config System**: PRD lines 780-846
## League Differences
### SBA League
- Minimal player data (id, name, image)
- Simpler rules configuration
### PD League
- Detailed scouting data on players
- Complex probability calculations
- Additional analytics requirements
## Success Metrics
- 90% player migration within 1 month
- 99.5% uptime
- < 500ms average action latency
- 60%+ mobile usage
- Zero data corruption
---
**Note**: Subdirectories will have their own CLAUDE.md files with implementation-specific details to minimize context usage.

287
README.md Normal file
View File

@ -0,0 +1,287 @@
# Paper Dynasty Real-Time Game Engine
Web-based real-time multiplayer baseball simulation platform replacing the legacy Google Sheets system.
## Project Structure
```
strat-gameplay-webapp/
├── backend/ # FastAPI game engine
├── frontend-sba/ # SBA League Nuxt frontend
├── frontend-pd/ # PD League Nuxt frontend
├── .claude/ # Claude AI implementation guides
├── docker-compose.yml # Full stack orchestration
└── README.md # This file
```
## Two Development Workflows
### Option 1: Local Development (Recommended for Daily Work)
**Best for:** Fast hot-reload, quick iteration, debugging
**Services:**
- ✅ Backend runs locally (Python hot-reload)
- ✅ Frontends run locally (Nuxt hot-reload)
- ✅ Redis in Docker (lightweight)
- ✅ PostgreSQL on your existing server
**Setup:**
1. **Environment Setup**
```bash
# Copy environment template
cp .env.example .env
# Edit .env with your database credentials and API keys
```
2. **Start Redis** (in one terminal)
```bash
cd backend
docker-compose up
```
3. **Start Backend** (in another terminal)
```bash
cd backend
source venv/bin/activate # or 'venv\Scripts\activate' on Windows
python -m app.main
```
Backend will be available at http://localhost:8000
4. **Start SBA Frontend** (in another terminal)
```bash
cd frontend-sba
npm run dev
```
SBA frontend will be available at http://localhost:3000
5. **Start PD Frontend** (in another terminal)
```bash
cd frontend-pd
npm run dev
```
PD frontend will be available at http://localhost:3001
**Advantages:**
- ⚡ Instant hot-reload on code changes
- 🐛 Easy debugging (native debuggers work)
- 💨 Fast startup times
- 🔧 Simple to restart individual services
---
### Option 2: Full Docker Orchestration
**Best for:** Integration testing, demos, production-like environment
**Services:**
- ✅ Everything runs in containers
- ✅ Consistent environment
- ✅ One command to start everything
**Setup:**
1. **Environment Setup**
```bash
# Copy environment template
cp .env.example .env
# Edit .env with your database credentials and API keys
```
2. **Start Everything**
```bash
# From project root
docker-compose up
```
Or run in background:
```bash
docker-compose up -d
```
3. **View Logs**
```bash
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f backend
docker-compose logs -f frontend-sba
```
4. **Stop Everything**
```bash
docker-compose down
```
**Advantages:**
- 🎯 Production-like environment
- 🚀 One-command startup
- 🔄 Easy to share with team
- ✅ CI/CD ready
---
## Development Commands
### Backend
```bash
cd backend
# Activate virtual environment
source venv/bin/activate
# Install dependencies
pip install -r requirements-dev.txt
# Run server
python -m app.main
# Run tests
pytest tests/ -v
# Code formatting
black app/ tests/
# Linting
flake8 app/ tests/
# Type checking
mypy app/
```
### Frontend
```bash
cd frontend-sba # or frontend-pd
# Install dependencies
npm install
# Run dev server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
# Lint
npm run lint
# Type check
npm run type-check
```
## Database Setup
This project uses an existing PostgreSQL server. You need to manually create the database:
```sql
-- On your PostgreSQL server
CREATE DATABASE paperdynasty_dev;
CREATE USER paperdynasty WITH PASSWORD 'your-secure-password';
GRANT ALL PRIVILEGES ON DATABASE paperdynasty_dev TO paperdynasty;
```
Then update `DATABASE_URL` in `.env`:
```
DATABASE_URL=postgresql+asyncpg://paperdynasty:your-password@your-db-server:5432/paperdynasty_dev
```
## Environment Variables
Copy `.env.example` to `.env` and configure:
### Required
- `DATABASE_URL` - PostgreSQL connection string
- `SECRET_KEY` - Application secret key (at least 32 characters)
- `DISCORD_CLIENT_ID` - Discord OAuth client ID
- `DISCORD_CLIENT_SECRET` - Discord OAuth secret
- `SBA_API_URL` / `SBA_API_KEY` - SBA League API credentials
- `PD_API_URL` / `PD_API_KEY` - PD League API credentials
### Optional
- `REDIS_URL` - Redis connection (auto-configured in Docker)
- `CORS_ORIGINS` - Allowed origins (defaults to localhost:3000,3001)
## Available Services
When running, the following services are available:
| Service | URL | Description |
|---------|-----|-------------|
| Backend API | http://localhost:8000 | FastAPI REST API |
| Backend Docs | http://localhost:8000/docs | Interactive API documentation |
| SBA Frontend | http://localhost:3000 | SBA League web app |
| PD Frontend | http://localhost:3001 | PD League web app |
| Redis | localhost:6379 | Cache (not exposed via HTTP) |
## Health Checks
```bash
# Backend health
curl http://localhost:8000/api/health
# Or visit in browser
open http://localhost:8000/api/health
```
## Troubleshooting
### Backend won't start
- Check `DATABASE_URL` is correct in `.env`
- Verify PostgreSQL database exists
- Ensure Redis is running (`docker-compose up` in backend/)
- Check logs for specific errors
### Frontend won't connect to backend
- Verify backend is running at http://localhost:8000
- Check CORS settings in backend `.env`
- Clear browser cache and cookies
- Check browser console for errors
### Docker containers won't start
- Ensure `.env` file exists with all required variables
- Run `docker-compose down` then `docker-compose up` again
- Check `docker-compose logs` for specific errors
- Verify no port conflicts (8000, 3000, 3001, 6379)
### Database connection fails
- Verify PostgreSQL server is accessible
- Check firewall rules allow connection
- Confirm database and user exist
- Test connection with `psql` directly
## Documentation
- **Full PRD**: See `/prd-web-scorecard-1.1.md`
- **Implementation Guide**: See `.claude/implementation/00-index.md`
- **Architecture Docs**: See `.claude/implementation/` directory
## Tech Stack
### Backend
- **Framework**: FastAPI (Python 3.11+)
- **WebSocket**: Socket.io
- **Database**: PostgreSQL 14+ with SQLAlchemy
- **Cache**: Redis 7
- **Auth**: Discord OAuth with JWT
### Frontend
- **Framework**: Vue 3 + Nuxt 3
- **Language**: TypeScript
- **Styling**: Tailwind CSS
- **State**: Pinia
- **WebSocket**: Socket.io-client
## Contributing
See `.claude/implementation/` for detailed implementation guides and architecture documentation.
## License
Proprietary - Paper Dynasty League System

51
backend/.dockerignore Normal file
View File

@ -0,0 +1,51 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
*.egg-info/
dist/
build/
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Logs
logs/
*.log
# Environment
.env
.env.local
.env.*.local
# Docker
Dockerfile
.dockerignore
docker-compose.yml
# Git
.git/
.gitignore
# Documentation
*.md
docs/
# OS
.DS_Store
Thumbs.db

335
backend/CLAUDE.md Normal file
View File

@ -0,0 +1,335 @@
# Backend - Paper Dynasty Game Engine
## Overview
FastAPI-based real-time game backend handling WebSocket communication, game state management, and database persistence for both SBA and PD leagues.
## Technology Stack
- **Framework**: FastAPI (Python 3.11+)
- **WebSocket**: Socket.io (python-socketio)
- **Database**: PostgreSQL 14+ with SQLAlchemy 2.0 (async)
- **ORM**: SQLAlchemy with asyncpg driver
- **Validation**: Pydantic v2
- **Testing**: pytest with pytest-asyncio
- **Code Quality**: black, flake8, mypy
## Project Structure
```
backend/
├── app/
│ ├── main.py # FastAPI app + Socket.io initialization
│ ├── config.py # Settings with pydantic-settings
│ │
│ ├── core/ # Game logic (Phase 2+)
│ │ ├── game_engine.py # Main game simulation
│ │ ├── state_manager.py # In-memory state
│ │ ├── play_resolver.py # Play outcome resolution
│ │ ├── dice.py # Secure random rolls
│ │ └── validators.py # Rule validation
│ │
│ ├── config/ # League configurations (Phase 2+)
│ │ ├── base_config.py # Shared configuration
│ │ ├── league_configs.py # SBA/PD specific
│ │ └── result_charts.py # d20 outcome tables
│ │
│ ├── models/ # Data models
│ │ ├── db_models.py # SQLAlchemy ORM models
│ │ ├── game_models.py # Pydantic game state models (Phase 2+)
│ │ └── player_models.py # Polymorphic player models (Phase 2+)
│ │
│ ├── websocket/ # WebSocket handling
│ │ ├── connection_manager.py # Connection lifecycle
│ │ ├── handlers.py # Event handlers
│ │ └── events.py # Event definitions (Phase 2+)
│ │
│ ├── api/ # REST API
│ │ ├── routes/
│ │ │ ├── health.py # Health check endpoints
│ │ │ ├── auth.py # Discord OAuth (Phase 1)
│ │ │ └── games.py # Game CRUD (Phase 2+)
│ │ └── dependencies.py # FastAPI dependencies
│ │
│ ├── database/ # Database layer
│ │ ├── session.py # Async session management
│ │ └── operations.py # DB operations (Phase 2+)
│ │
│ ├── data/ # External data (Phase 2+)
│ │ ├── api_client.py # League REST API client
│ │ └── cache.py # Caching layer
│ │
│ └── utils/ # Utilities
│ ├── logging.py # Logging setup
│ └── auth.py # JWT utilities (Phase 1)
├── tests/
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests
│ └── e2e/ # End-to-end tests
├── logs/ # Application logs (gitignored)
├── venv/ # Virtual environment (gitignored)
├── .env # Environment variables (gitignored)
├── .env.example # Environment template
├── requirements.txt # Production dependencies
├── requirements-dev.txt # Dev dependencies
├── Dockerfile # Container definition
├── docker-compose.yml # Redis for local dev
└── pytest.ini # Pytest configuration
```
## Key Architectural Patterns
### 1. Hybrid State Management
- **In-Memory**: Active game states for fast access (<500ms response)
- **PostgreSQL**: Persistent storage for recovery and history
- **Pattern**: Write-through cache (update memory + async DB write)
### 2. Polymorphic Player Models
```python
# Base class with abstract methods
class BasePlayer(BaseModel, ABC):
@abstractmethod
def get_image_url(self) -> str: ...
# League-specific implementations
class SbaPlayer(BasePlayer): ...
class PdPlayer(BasePlayer): ...
# Factory pattern for instantiation
Lineup.from_api_data(config, data)
```
### 3. League-Agnostic Core
- Game engine works for any league
- League-specific logic in config classes
- Result charts loaded per league
### 4. Async-First
- All database operations use `async/await`
- Database writes don't block game logic
- Connection pooling for efficiency
## Development Workflow
### Daily Development
```bash
# Activate virtual environment
source venv/bin/activate
# Start Redis (in separate terminal)
docker-compose up
# Run backend with hot-reload
python -m app.main
# Backend available at http://localhost:8000
# API docs at http://localhost:8000/docs
```
### Testing
```bash
# Run all tests
pytest tests/ -v
# Run with coverage
pytest tests/ --cov=app --cov-report=html
# Run specific test file
pytest tests/unit/test_game_engine.py -v
# Type checking
mypy app/
# Code formatting
black app/ tests/
# Linting
flake8 app/ tests/
```
## Coding Standards
### Python Style
- **Formatting**: Black with default settings
- **Line Length**: 88 characters (black default)
- **Imports**: Group stdlib, third-party, local (isort compatible)
- **Type Hints**: Required for all public functions
- **Docstrings**: Google style for classes and public methods
### Logging Pattern
```python
import logging
logger = logging.getLogger(f'{__name__}.ClassName')
# Usage
logger.info(f"User {user_id} connected")
logger.error(f"Failed to process action: {error}", exc_info=True)
```
### Error Handling
- **Raise or Return**: Never return `Optional` unless specifically required
- **Custom Exceptions**: Use for domain-specific errors
- **Logging**: Always log exceptions with context
### Dataclasses
```python
from dataclasses import dataclass
@dataclass
class GameState:
game_id: str
inning: int
outs: int
# ... fields
```
## Database Patterns
### Async Session Usage
```python
from app.database.session import get_session
async def some_function():
async with get_session() as session:
result = await session.execute(query)
# session.commit() happens automatically
```
### Model Definitions
```python
from app.database.session import Base
from sqlalchemy import Column, String, Integer
class Game(Base):
__tablename__ = "games"
id = Column(UUID(as_uuid=True), primary_key=True)
# ... columns
```
## WebSocket Patterns
### Event Handler Registration
```python
@sio.event
async def some_event(sid, data):
"""Handle some_event from client"""
try:
# Validate data
# Process action
# Emit response
await sio.emit('response_event', result, room=sid)
except Exception as e:
logger.error(f"Error handling event: {e}")
await sio.emit('error', {'message': str(e)}, room=sid)
```
### Broadcasting
```python
# To specific game room
await connection_manager.broadcast_to_game(
game_id,
'game_state_update',
state_data
)
# To specific user
await connection_manager.emit_to_user(
sid,
'decision_required',
decision_data
)
```
## Environment Variables
Required in `.env`:
```bash
# Database
DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/dbname
# Application
SECRET_KEY=your-secret-key-at-least-32-chars
# Discord OAuth
DISCORD_CLIENT_ID=your-client-id
DISCORD_CLIENT_SECRET=your-client-secret
# League APIs
SBA_API_URL=https://sba-api.example.com
SBA_API_KEY=your-api-key
PD_API_URL=https://pd-api.example.com
PD_API_KEY=your-api-key
```
## Performance Targets
- **Action Response**: < 500ms from user action to state update
- **WebSocket Delivery**: < 200ms
- **Database Write**: < 100ms (async, non-blocking)
- **State Recovery**: < 2 seconds
- **Concurrent Games**: Support 10+ simultaneous games
- **Memory**: < 1GB with 10 active games
## Security Considerations
- **Authentication**: All WebSocket connections require valid JWT
- **Authorization**: Verify team ownership before allowing actions
- **Input Validation**: Pydantic models validate all inputs
- **SQL Injection**: Prevented by SQLAlchemy ORM
- **Dice Rolls**: Cryptographically secure random generation
- **Server-Side Logic**: All game rules enforced server-side
## Common Tasks
### Adding a New API Endpoint
1. Create route in `app/api/routes/`
2. Define Pydantic request/response models
3. Add dependency injection if needed
4. Register router in `app/main.py`
### Adding a New WebSocket Event
1. Define event handler in `app/websocket/handlers.py`
2. Register with `@sio.event` decorator
3. Validate data with Pydantic
4. Add corresponding client handling in frontend
### Adding a New Database Model
1. Define SQLAlchemy model in `app/models/db_models.py`
2. Create Alembic migration: `alembic revision --autogenerate -m "description"`
3. Apply migration: `alembic upgrade head`
## Troubleshooting
### Import Errors
- Ensure virtual environment is activated
- Check `PYTHONPATH` if using custom structure
- Verify all `__init__.py` files exist
### Database Connection Issues
- Verify `DATABASE_URL` in `.env` is correct
- Test connection: `psql $DATABASE_URL`
- Check firewall/network access
- Verify database exists
### WebSocket Not Connecting
- Check CORS settings in `config.py`
- Verify token is being sent from client
- Check logs for connection errors
- Ensure Socket.io versions match (client/server)
## References
- **Implementation Guide**: `../.claude/implementation/01-infrastructure.md`
- **Backend Architecture**: `../.claude/implementation/backend-architecture.md`
- **WebSocket Protocol**: `../.claude/implementation/websocket-protocol.md`
- **Database Design**: `../.claude/implementation/database-design.md`
- **Full PRD**: `../prd-web-scorecard-1.1.md`
---
**Current Phase**: Phase 1 - Core Infrastructure
**Next Phase**: Phase 2 - Game Engine Core

66
backend/Dockerfile Normal file
View File

@ -0,0 +1,66 @@
# Backend Dockerfile for Paper Dynasty Game Engine
# Multi-stage build for optimized production image
FROM python:3.11-slim as base
# Set environment variables
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Install system dependencies
RUN apt-get update && apt-get install -y \
curl \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Create app directory
WORKDIR /app
# Development stage
FROM base as development
# Copy requirements
COPY requirements.txt requirements-dev.txt ./
# Install Python dependencies
RUN pip install -r requirements-dev.txt
# Copy application code
COPY . .
# Expose port
EXPOSE 8000
# Run with uvicorn reload for development
CMD ["python", "-m", "uvicorn", "app.main:socket_app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
# Production stage
FROM base as production
# Copy requirements (production only)
COPY requirements.txt ./
# Install Python dependencies
RUN pip install -r requirements.txt
# Create non-root user
RUN useradd -m -u 1000 appuser && \
chown -R appuser:appuser /app
# Copy application code
COPY --chown=appuser:appuser . .
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/api/health || exit 1
# Run with production server
CMD ["python", "-m", "uvicorn", "app.main:socket_app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

121
docker-compose.yml Normal file
View File

@ -0,0 +1,121 @@
# Paper Dynasty Game Engine - Full Stack Orchestration
# Use this for integration testing, demos, or when you want everything containerized
# For daily dev, see README.md for local development workflow
version: '3.8'
services:
# Redis cache (shared dependency)
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
# FastAPI Game Backend
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
# Application
- APP_ENV=development
- DEBUG=true
- SECRET_KEY=${SECRET_KEY:-dev-secret-key-change-in-production}
# Database (using host machine's PostgreSQL)
- DATABASE_URL=${DATABASE_URL}
# Redis
- REDIS_URL=redis://redis:6379
# Discord OAuth
- DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
- DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET}
- DISCORD_REDIRECT_URI=${DISCORD_REDIRECT_URI:-http://localhost:3000/auth/callback}
# League APIs
- SBA_API_URL=${SBA_API_URL}
- SBA_API_KEY=${SBA_API_KEY}
- PD_API_URL=${PD_API_URL}
- PD_API_KEY=${PD_API_KEY}
# CORS
- CORS_ORIGINS=http://localhost:3000,http://localhost:3001
depends_on:
redis:
condition: service_healthy
volumes:
# Mount source code for hot-reload during development
- ./backend/app:/app/app:ro
- ./backend/logs:/app/logs
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
interval: 10s
timeout: 5s
retries: 5
# SBA League Frontend
frontend-sba:
build:
context: ./frontend-sba
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NUXT_PUBLIC_LEAGUE_ID=sba
- NUXT_PUBLIC_LEAGUE_NAME=Super Baseball Alliance
- NUXT_PUBLIC_API_URL=http://localhost:8000
- NUXT_PUBLIC_WS_URL=http://localhost:8000
- NUXT_PUBLIC_DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
- NUXT_PUBLIC_DISCORD_REDIRECT_URI=http://localhost:3000/auth/callback
depends_on:
backend:
condition: service_healthy
volumes:
# Mount source for hot-reload
- ./frontend-sba:/app
- /app/node_modules
- /app/.nuxt
restart: unless-stopped
# PD League Frontend
frontend-pd:
build:
context: ./frontend-pd
dockerfile: Dockerfile
ports:
- "3001:3001"
environment:
- NUXT_PUBLIC_LEAGUE_ID=pd
- NUXT_PUBLIC_LEAGUE_NAME=Paper Dynasty
- NUXT_PUBLIC_API_URL=http://localhost:8000
- NUXT_PUBLIC_WS_URL=http://localhost:8000
- NUXT_PUBLIC_DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
- NUXT_PUBLIC_DISCORD_REDIRECT_URI=http://localhost:3001/auth/callback
depends_on:
backend:
condition: service_healthy
volumes:
# Mount source for hot-reload
- ./frontend-pd:/app
- /app/node_modules
- /app/.nuxt
restart: unless-stopped
volumes:
redis_data:
networks:
default:
name: paperdynasty-network

52
frontend-pd/.dockerignore Normal file
View File

@ -0,0 +1,52 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build outputs
.output/
.nuxt/
dist/
# Environment
.env
.env.*
!.env.example
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Testing
coverage/
.nyc_output/
# Logs
logs/
*.log
# Docker
Dockerfile
.dockerignore
docker-compose.yml
# Git
.git/
.gitignore
# Documentation
*.md
docs/
# OS
.DS_Store
Thumbs.db
# Nuxt specific
.nuxt/
.output/
nuxt.d.ts

581
frontend-pd/CLAUDE.md Normal file
View File

@ -0,0 +1,581 @@
# Frontend PD - Paper Dynasty Web App
## Overview
Vue 3 + Nuxt 3 frontend for the PD (Paper Dynasty) league. Provides real-time game interface with WebSocket communication to the game backend.
## Technology Stack
- **Framework**: Nuxt 3 (Vue 3 Composition API)
- **Language**: TypeScript (strict mode)
- **Styling**: Tailwind CSS
- **State Management**: Pinia
- **WebSocket**: Socket.io-client
- **HTTP Client**: Axios (or Nuxt's built-in fetch)
- **Auth**: Discord OAuth with JWT
## League-Specific Characteristics
### PD League
- **Player Data**: Detailed model with scouting data, ratings, probabilities
- **Focus**: Advanced analytics and detailed player evaluation
- **Branding**: Green primary color (#16a34a)
- **API**: PD-specific REST API for team/player data with analytics
## Project Structure
```
frontend-pd/
├── assets/
│ ├── css/
│ │ └── tailwind.css # Tailwind imports
│ └── images/ # PD branding assets
├── components/
│ ├── Branding/ # PD-specific branding
│ │ ├── Header.vue
│ │ ├── Footer.vue
│ │ └── Logo.vue
│ └── League/ # PD-specific features
│ ├── PlayerCardDetailed.vue # Player cards with scouting
│ └── ScoutingPanel.vue # Scouting data display
├── composables/
│ ├── useAuth.ts # Authentication state
│ ├── useWebSocket.ts # WebSocket connection
│ ├── useGameState.ts # Game state management
│ └── useLeagueConfig.ts # PD-specific config
├── layouts/
│ ├── default.vue # Standard layout
│ ├── game.vue # Game view layout
│ └── auth.vue # Auth pages layout
├── pages/
│ ├── index.vue # Home/dashboard
│ ├── games/
│ │ ├── [id].vue # Game view
│ │ ├── create.vue # Create new game
│ │ └── history.vue # Completed games
│ ├── auth/
│ │ ├── login.vue
│ │ └── callback.vue # Discord OAuth callback
│ └── spectate/
│ └── [id].vue # Spectator view
├── plugins/
│ ├── socket.client.ts # Socket.io plugin
│ └── auth.ts # Auth plugin
├── store/ # Pinia stores
│ ├── auth.ts # Authentication state
│ ├── game.ts # Current game state
│ ├── games.ts # Games list
│ └── ui.ts # UI state (modals, toasts)
├── types/
│ ├── game.ts # Game-related types
│ ├── player.ts # PD player types (with scouting)
│ ├── api.ts # API response types
│ └── websocket.ts # WebSocket event types
├── utils/
│ ├── api.ts # API client
│ ├── formatters.ts # Data formatting utilities
│ └── validators.ts # Input validation
├── middleware/
│ ├── auth.ts # Auth guard
│ └── game-access.ts # Game access validation
├── public/ # Static assets
├── app.vue # Root component
├── nuxt.config.ts # Nuxt configuration
├── tailwind.config.js # Tailwind configuration
├── tsconfig.json # TypeScript configuration
└── package.json
```
## Shared Components
Many components are shared between SBA and PD frontends. These will be located in a shared component library:
**Shared**:
- Game board visualization
- Play-by-play feed
- Dice roll animations
- Decision input forms
- WebSocket connection status
**PD-Specific**:
- PD branding (header, footer, colors)
- Detailed player card display (with scouting data)
- Scouting data panels
- Advanced analytics views
## Development Workflow
### Daily Development
```bash
# Install dependencies (first time)
npm install
# Run dev server with hot-reload
npm run dev
# Frontend available at http://localhost:3001
```
### Building
```bash
# Build for production
npm run build
# Preview production build
npm run preview
# Generate static site (if needed)
npm run generate
```
### Code Quality
```bash
# Type checking
npm run type-check
# Linting
npm run lint
# Fix linting issues
npm run lint:fix
```
## Coding Standards
### Vue/TypeScript Style
- **Composition API**: Use `<script setup>` syntax
- **TypeScript**: Strict mode, explicit types for props/emits
- **Component Names**: PascalCase for components
- **File Names**: PascalCase for components, kebab-case for utilities
### Component Structure
```vue
<template>
<div class="component-wrapper">
<!-- Template content -->
</div>
</template>
<script setup lang="ts">
// Imports
import { ref, computed } from 'vue'
// Props/Emits with TypeScript
interface Props {
gameId: string
showScouting: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
update: [value: string]
close: []
}>()
// Reactive state
const localState = ref('')
// Computed properties
const displayMode = computed(() => {
return props.showScouting ? 'detailed' : 'simple'
})
// Methods
const handleClick = () => {
emit('update', localState.value)
}
</script>
<style scoped>
/* Component-specific styles */
.component-wrapper {
@apply p-4 bg-white rounded-lg;
}
</style>
```
### Composable Pattern
```typescript
// composables/useGameActions.ts
export const useGameActions = (gameId: string) => {
const { socket } = useWebSocket()
const gameStore = useGameStore()
const setDefense = (positioning: string) => {
if (!socket.value?.connected) {
throw new Error('Not connected')
}
socket.value.emit('set_defense', { game_id: gameId, positioning })
}
return {
setDefense,
// ... other actions
}
}
```
### Store Pattern (Pinia)
```typescript
// store/game.ts
export const useGameStore = defineStore('game', () => {
// State
const gameState = ref<GameState | null>(null)
const loading = ref(false)
// Computed
const currentInning = computed(() => gameState.value?.inning ?? 1)
// Actions
const setGameState = (state: GameState) => {
gameState.value = state
}
return {
// State
gameState,
loading,
// Computed
currentInning,
// Actions
setGameState,
}
})
```
## Configuration
### Environment Variables
Create `.env` file with:
```bash
NUXT_PUBLIC_LEAGUE_ID=pd
NUXT_PUBLIC_LEAGUE_NAME=Paper Dynasty
NUXT_PUBLIC_API_URL=http://localhost:8000
NUXT_PUBLIC_WS_URL=http://localhost:8000
NUXT_PUBLIC_DISCORD_CLIENT_ID=your-client-id
NUXT_PUBLIC_DISCORD_REDIRECT_URI=http://localhost:3001/auth/callback
```
### Nuxt Config
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
runtimeConfig: {
public: {
leagueId: 'pd',
leagueName: 'Paper Dynasty',
apiUrl: process.env.NUXT_PUBLIC_API_URL || 'http://localhost:8000',
// ... other config
}
},
css: ['~/assets/css/tailwind.css'],
typescript: {
strict: true,
typeCheck: true
}
})
```
### Tailwind Config (PD Theme)
```javascript
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#16a34a', // PD Green
50: '#f0fdf4',
100: '#dcfce7',
// ... other shades
},
secondary: {
DEFAULT: '#ea580c', // PD Orange
// ... other shades
}
}
}
}
}
```
## WebSocket Integration
### Connection Management
```typescript
// composables/useWebSocket.ts
const { $socket } = useNuxtApp()
const authStore = useAuthStore()
onMounted(() => {
if (authStore.token) {
$socket.connect(authStore.token)
}
})
onUnmounted(() => {
$socket.disconnect()
})
```
### Event Handling
```typescript
// composables/useGameEvents.ts
export const useGameEvents = () => {
const { socket } = useWebSocket()
const gameStore = useGameStore()
onMounted(() => {
socket.value?.on('game_state_update', (data: GameState) => {
gameStore.setGameState(data)
})
socket.value?.on('play_completed', (data: PlayOutcome) => {
gameStore.handlePlayCompleted(data)
})
})
onUnmounted(() => {
socket.value?.off('game_state_update')
socket.value?.off('play_completed')
})
}
```
## Type Definitions
### PD Player Type (with Scouting Data)
```typescript
// types/player.ts
export interface PdPlayer {
id: number
name: string
image: string
scouting_data: {
power: number
contact: number
speed: number
fielding: number
// ... other scouting metrics
}
ratings: {
overall: number
batting: number
pitching?: number
// ... other ratings
}
probabilities: {
single: number
double: number
triple: number
homerun: number
walk: number
strikeout: number
// ... other probabilities
}
}
export interface Lineup {
id: number
game_id: string
card_id: number
position: string
batting_order?: number
is_starter: boolean
is_active: boolean
player: PdPlayer
}
```
### Game State Type
```typescript
// types/game.ts
export interface GameState {
game_id: string
status: 'pending' | 'active' | 'completed'
inning: number
half: 'top' | 'bottom'
outs: number
balls: number
strikes: number
home_score: number
away_score: number
runners: {
first: number | null
second: number | null
third: number | null
}
current_batter: PdPlayer | null
current_pitcher: PdPlayer | null
}
```
## PD-Specific Features
### Scouting Data Display
```vue
<template>
<div class="scouting-panel">
<h3 class="text-lg font-bold mb-2">Scouting Report</h3>
<div class="grid grid-cols-2 gap-2">
<div v-for="(value, key) in player.scouting_data" :key="key">
<span class="text-sm text-gray-600">{{ formatLabel(key) }}</span>
<div class="flex items-center gap-2">
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div
class="bg-primary h-2 rounded-full"
:style="{ width: `${value}%` }"
/>
</div>
<span class="text-sm font-semibold">{{ value }}</span>
</div>
</div>
</div>
</div>
</template>
```
### Advanced Player Card
```vue
<template>
<div class="player-card">
<!-- Basic Info -->
<img :src="player.image" :alt="player.name" />
<h3>{{ player.name }}</h3>
<!-- Ratings -->
<div class="ratings">
<span>Overall: {{ player.ratings.overall }}</span>
<span>Batting: {{ player.ratings.batting }}</span>
</div>
<!-- Probabilities (collapsible on mobile) -->
<details class="md:open">
<summary>Probabilities</summary>
<div class="grid grid-cols-2 gap-1 text-sm">
<div v-for="(prob, outcome) in player.probabilities" :key="outcome">
{{ outcome }}: {{ (prob * 100).toFixed(1) }}%
</div>
</div>
</details>
</div>
</template>
```
## Mobile-First Design
### Responsive Breakpoints
- **xs**: 375px (Small phones)
- **sm**: 640px (Large phones)
- **md**: 768px (Tablets)
- **lg**: 1024px (Desktop)
### Mobile Layout Principles
- Single column layout on mobile
- Bottom sheet for decision inputs
- Sticky scoreboard at top
- Touch-friendly buttons (44x44px minimum)
- Swipe gestures for navigation
- Collapsible scouting data on mobile
### Example Responsive Component
```vue
<template>
<div class="game-view">
<!-- Sticky scoreboard -->
<div class="sticky top-0 z-10 bg-white shadow">
<ScoreBoard :score="score" />
</div>
<!-- Main content -->
<div class="container mx-auto p-4">
<!-- Mobile: stacked, Desktop: grid -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div class="lg:col-span-2">
<GameBoard :state="gameState" />
</div>
<div>
<ScoutingPanel :player="currentBatter" />
<PlayByPlay :plays="plays" />
</div>
</div>
</div>
</div>
</template>
```
## Common Tasks
### Adding a New Page
1. Create file in `pages/` directory
2. Use `<script setup>` with TypeScript
3. Add necessary composables (auth, websocket, etc.)
4. Define route meta if needed
### Adding a New Component
1. Create in appropriate `components/` subdirectory
2. Define Props/Emits interfaces
3. Use Tailwind for styling
4. Export for use in other components
### Adding a New Store
1. Create in `store/` directory
2. Use Composition API syntax
3. Define state, computed, and actions
4. Export with `defineStore`
## Performance Considerations
- **Code Splitting**: Auto by Nuxt routes
- **Lazy Loading**: Use `defineAsyncComponent` for heavy components (especially scouting panels)
- **Image Optimization**: Use Nuxt Image module
- **State Management**: Keep only necessary data in stores
- **WebSocket**: Throttle/debounce frequent updates
- **Scouting Data**: Lazy load detailed analytics
## Troubleshooting
### WebSocket Won't Connect
- Check backend is running at `NUXT_PUBLIC_WS_URL`
- Verify token is valid
- Check browser console for errors
- Ensure CORS is configured correctly on backend
### Type Errors
- Run `npm run type-check` to see all errors
- Ensure types are imported correctly
- Check for mismatched types in props/emits
- Verify PdPlayer type matches backend structure
### Hot Reload Not Working
- Restart dev server
- Clear `.nuxt` directory: `rm -rf .nuxt`
- Check for syntax errors in components
## References
- **Implementation Guide**: `../.claude/implementation/01-infrastructure.md`
- **Frontend Architecture**: `../.claude/implementation/frontend-architecture.md`
- **WebSocket Protocol**: `../.claude/implementation/websocket-protocol.md`
- **Full PRD**: `../prd-web-scorecard-1.1.md`
---
**League**: PD (Paper Dynasty)
**Port**: 3001
**Current Phase**: Phase 1 - Core Infrastructure

76
frontend-pd/Dockerfile Normal file
View File

@ -0,0 +1,76 @@
# Frontend Dockerfile for PD League
# Multi-stage build for optimized production image
FROM node:18-alpine as base
# Set working directory
WORKDIR /app
# Development stage
FROM base as development
# Copy package files
COPY package*.json ./
# Install all dependencies (including dev)
RUN npm ci
# Copy application code
COPY . .
# Expose port
EXPOSE 3000
# Set development environment
ENV NODE_ENV=development
# Run development server with hot-reload
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
# Build stage
FROM base as builder
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy application code
COPY . .
# Build application
RUN npm run build
# Production stage
FROM base as production
# Set production environment
ENV NODE_ENV=production
# Copy package files
COPY package*.json ./
# Install production dependencies only
RUN npm ci --omit=dev
# Copy built application from builder
COPY --from=builder /app/.output /app/.output
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nuxt -u 1001 && \
chown -R nuxt:nodejs /app
# Switch to non-root user
USER nuxt
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Run production server
CMD ["node", ".output/server/index.mjs"]

View File

@ -0,0 +1,52 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build outputs
.output/
.nuxt/
dist/
# Environment
.env
.env.*
!.env.example
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Testing
coverage/
.nyc_output/
# Logs
logs/
*.log
# Docker
Dockerfile
.dockerignore
docker-compose.yml
# Git
.git/
.gitignore
# Documentation
*.md
docs/
# OS
.DS_Store
Thumbs.db
# Nuxt specific
.nuxt/
.output/
nuxt.d.ts

499
frontend-sba/CLAUDE.md Normal file
View File

@ -0,0 +1,499 @@
# Frontend SBA - Strat-O-Matic Baseball Association Web App
## Overview
Vue 3 + Nuxt 3 frontend for the SBa (Strat-O-Matic Baseball Association) league. Provides real-time game interface with WebSocket communication to the game backend.
## Technology Stack
- **Framework**: Nuxt 3 (Vue 3 Composition API)
- **Language**: TypeScript (strict mode)
- **Styling**: Tailwind CSS
- **State Management**: Pinia
- **WebSocket**: Socket.io-client
- **HTTP Client**: Axios (or Nuxt's built-in fetch)
- **Auth**: Discord OAuth with JWT
## League-Specific Characteristics
### SBA League
- **Player Data**: Simple model (id, name, image)
- **Focus**: Straightforward card-based gameplay
- **Branding**: Blue primary color (#1e40af)
- **API**: SBA-specific REST API for team/player data
## Project Structure
```
frontend-sba/
├── assets/
│ ├── css/
│ │ └── tailwind.css # Tailwind imports
│ └── images/ # SBA branding assets
├── components/
│ ├── Branding/ # SBA-specific branding
│ │ ├── Header.vue
│ │ ├── Footer.vue
│ │ └── Logo.vue
│ └── League/ # SBA-specific features
│ └── PlayerCardSimple.vue # Simple player cards
├── composables/
│ ├── useAuth.ts # Authentication state
│ ├── useWebSocket.ts # WebSocket connection
│ ├── useGameState.ts # Game state management
│ └── useLeagueConfig.ts # SBA-specific config
├── layouts/
│ ├── default.vue # Standard layout
│ ├── game.vue # Game view layout
│ └── auth.vue # Auth pages layout
├── pages/
│ ├── index.vue # Home/dashboard
│ ├── games/
│ │ ├── [id].vue # Game view
│ │ ├── create.vue # Create new game
│ │ └── history.vue # Completed games
│ ├── auth/
│ │ ├── login.vue
│ │ └── callback.vue # Discord OAuth callback
│ └── spectate/
│ └── [id].vue # Spectator view
├── plugins/
│ ├── socket.client.ts # Socket.io plugin
│ └── auth.ts # Auth plugin
├── store/ # Pinia stores
│ ├── auth.ts # Authentication state
│ ├── game.ts # Current game state
│ ├── games.ts # Games list
│ └── ui.ts # UI state (modals, toasts)
├── types/
│ ├── game.ts # Game-related types
│ ├── player.ts # SBA player types
│ ├── api.ts # API response types
│ └── websocket.ts # WebSocket event types
├── utils/
│ ├── api.ts # API client
│ ├── formatters.ts # Data formatting utilities
│ └── validators.ts # Input validation
├── middleware/
│ ├── auth.ts # Auth guard
│ └── game-access.ts # Game access validation
├── public/ # Static assets
├── app.vue # Root component
├── nuxt.config.ts # Nuxt configuration
├── tailwind.config.js # Tailwind configuration
├── tsconfig.json # TypeScript configuration
└── package.json
```
## Shared Components
Many components are shared between SBA and PD frontends. These will be located in a shared component library:
**Shared**:
- Game board visualization
- Play-by-play feed
- Dice roll animations
- Decision input forms
- WebSocket connection status
**SBA-Specific**:
- SBA branding (header, footer, colors)
- Simple player card display (no scouting data)
- League-specific theming
## Development Workflow
### Daily Development
```bash
# Install dependencies (first time)
npm install
# Run dev server with hot-reload
npm run dev
# Frontend available at http://localhost:3000
```
### Building
```bash
# Build for production
npm run build
# Preview production build
npm run preview
# Generate static site (if needed)
npm run generate
```
### Code Quality
```bash
# Type checking
npm run type-check
# Linting
npm run lint
# Fix linting issues
npm run lint:fix
```
## Coding Standards
### Vue/TypeScript Style
- **Composition API**: Use `<script setup>` syntax
- **TypeScript**: Strict mode, explicit types for props/emits
- **Component Names**: PascalCase for components
- **File Names**: PascalCase for components, kebab-case for utilities
### Component Structure
```vue
<template>
<div class="component-wrapper">
<!-- Template content -->
</div>
</template>
<script setup lang="ts">
// Imports
import { ref, computed } from 'vue'
// Props/Emits with TypeScript
interface Props {
gameId: string
isActive: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
update: [value: string]
close: []
}>()
// Reactive state
const localState = ref('')
// Computed properties
const displayValue = computed(() => {
return props.isActive ? 'Active' : 'Inactive'
})
// Methods
const handleClick = () => {
emit('update', localState.value)
}
</script>
<style scoped>
/* Component-specific styles */
.component-wrapper {
@apply p-4 bg-white rounded-lg;
}
</style>
```
### Composable Pattern
```typescript
// composables/useGameActions.ts
export const useGameActions = (gameId: string) => {
const { socket } = useWebSocket()
const gameStore = useGameStore()
const setDefense = (positioning: string) => {
if (!socket.value?.connected) {
throw new Error('Not connected')
}
socket.value.emit('set_defense', { game_id: gameId, positioning })
}
return {
setDefense,
// ... other actions
}
}
```
### Store Pattern (Pinia)
```typescript
// store/game.ts
export const useGameStore = defineStore('game', () => {
// State
const gameState = ref<GameState | null>(null)
const loading = ref(false)
// Computed
const currentInning = computed(() => gameState.value?.inning ?? 1)
// Actions
const setGameState = (state: GameState) => {
gameState.value = state
}
return {
// State
gameState,
loading,
// Computed
currentInning,
// Actions
setGameState,
}
})
```
## Configuration
### Environment Variables
Create `.env` file with:
```bash
NUXT_PUBLIC_LEAGUE_ID=sba
NUXT_PUBLIC_LEAGUE_NAME=Super Baseball Alliance
NUXT_PUBLIC_API_URL=http://localhost:8000
NUXT_PUBLIC_WS_URL=http://localhost:8000
NUXT_PUBLIC_DISCORD_CLIENT_ID=your-client-id
NUXT_PUBLIC_DISCORD_REDIRECT_URI=http://localhost:3000/auth/callback
```
### Nuxt Config
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
runtimeConfig: {
public: {
leagueId: 'sba',
leagueName: 'Super Baseball Alliance',
apiUrl: process.env.NUXT_PUBLIC_API_URL || 'http://localhost:8000',
// ... other config
}
},
css: ['~/assets/css/tailwind.css'],
typescript: {
strict: true,
typeCheck: true
}
})
```
### Tailwind Config (SBA Theme)
```javascript
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#1e40af', // SBA Blue
50: '#eff6ff',
100: '#dbeafe',
// ... other shades
},
secondary: {
DEFAULT: '#dc2626', // SBA Red
// ... other shades
}
}
}
}
}
```
## WebSocket Integration
### Connection Management
```typescript
// composables/useWebSocket.ts
const { $socket } = useNuxtApp()
const authStore = useAuthStore()
onMounted(() => {
if (authStore.token) {
$socket.connect(authStore.token)
}
})
onUnmounted(() => {
$socket.disconnect()
})
```
### Event Handling
```typescript
// composables/useGameEvents.ts
export const useGameEvents = () => {
const { socket } = useWebSocket()
const gameStore = useGameStore()
onMounted(() => {
socket.value?.on('game_state_update', (data: GameState) => {
gameStore.setGameState(data)
})
socket.value?.on('play_completed', (data: PlayOutcome) => {
gameStore.handlePlayCompleted(data)
})
})
onUnmounted(() => {
socket.value?.off('game_state_update')
socket.value?.off('play_completed')
})
}
```
## Type Definitions
### SBA Player Type
```typescript
// types/player.ts
export interface SbaPlayer {
id: number
name: string
image: string
team?: string
manager?: string
}
export interface Lineup {
id: number
game_id: string
card_id: number
position: string
batting_order?: number
is_starter: boolean
is_active: boolean
player: SbaPlayer
}
```
### Game State Type
```typescript
// types/game.ts
export interface GameState {
game_id: string
status: 'pending' | 'active' | 'completed'
inning: number
half: 'top' | 'bottom'
outs: number
balls: number
strikes: number
home_score: number
away_score: number
runners: {
first: number | null
second: number | null
third: number | null
}
current_batter: SbaPlayer | null
current_pitcher: SbaPlayer | null
}
```
## Mobile-First Design
### Responsive Breakpoints
- **xs**: 375px (Small phones)
- **sm**: 640px (Large phones)
- **md**: 768px (Tablets)
- **lg**: 1024px (Desktop)
### Mobile Layout Principles
- Single column layout on mobile
- Bottom sheet for decision inputs
- Sticky scoreboard at top
- Touch-friendly buttons (44x44px minimum)
- Swipe gestures for navigation
### Example Responsive Component
```vue
<template>
<div class="game-view">
<!-- Sticky scoreboard -->
<div class="sticky top-0 z-10 bg-white shadow">
<ScoreBoard :score="score" />
</div>
<!-- Main content -->
<div class="container mx-auto p-4">
<!-- Mobile: stacked, Desktop: grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<GameBoard :state="gameState" />
<PlayByPlay :plays="plays" />
</div>
</div>
</div>
</template>
```
## Common Tasks
### Adding a New Page
1. Create file in `pages/` directory
2. Use `<script setup>` with TypeScript
3. Add necessary composables (auth, websocket, etc.)
4. Define route meta if needed
### Adding a New Component
1. Create in appropriate `components/` subdirectory
2. Define Props/Emits interfaces
3. Use Tailwind for styling
4. Export for use in other components
### Adding a New Store
1. Create in `store/` directory
2. Use Composition API syntax
3. Define state, computed, and actions
4. Export with `defineStore`
## Performance Considerations
- **Code Splitting**: Auto by Nuxt routes
- **Lazy Loading**: Use `defineAsyncComponent` for heavy components
- **Image Optimization**: Use Nuxt Image module
- **State Management**: Keep only necessary data in stores
- **WebSocket**: Throttle/debounce frequent updates
## Troubleshooting
### WebSocket Won't Connect
- Check backend is running at `NUXT_PUBLIC_WS_URL`
- Verify token is valid
- Check browser console for errors
- Ensure CORS is configured correctly on backend
### Type Errors
- Run `npm run type-check` to see all errors
- Ensure types are imported correctly
- Check for mismatched types in props/emits
### Hot Reload Not Working
- Restart dev server
- Clear `.nuxt` directory: `rm -rf .nuxt`
- Check for syntax errors in components
## References
- **Implementation Guide**: `../.claude/implementation/01-infrastructure.md`
- **Frontend Architecture**: `../.claude/implementation/frontend-architecture.md`
- **WebSocket Protocol**: `../.claude/implementation/websocket-protocol.md`
- **Full PRD**: `../prd-web-scorecard-1.1.md`
---
**League**: SBA (Super Baseball Alliance)
**Port**: 3000
**Current Phase**: Phase 1 - Core Infrastructure

76
frontend-sba/Dockerfile Normal file
View File

@ -0,0 +1,76 @@
# Frontend Dockerfile for SBA League
# Multi-stage build for optimized production image
FROM node:18-alpine as base
# Set working directory
WORKDIR /app
# Development stage
FROM base as development
# Copy package files
COPY package*.json ./
# Install all dependencies (including dev)
RUN npm ci
# Copy application code
COPY . .
# Expose port
EXPOSE 3000
# Set development environment
ENV NODE_ENV=development
# Run development server with hot-reload
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
# Build stage
FROM base as builder
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy application code
COPY . .
# Build application
RUN npm run build
# Production stage
FROM base as production
# Set production environment
ENV NODE_ENV=production
# Copy package files
COPY package*.json ./
# Install production dependencies only
RUN npm ci --omit=dev
# Copy built application from builder
COPY --from=builder /app/.output /app/.output
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nuxt -u 1001 && \
chown -R nuxt:nodejs /app
# Switch to non-root user
USER nuxt
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Run production server
CMD ["node", ".output/server/index.mjs"]

1436
prd-web-scorecard-1.1.md Normal file

File diff suppressed because it is too large Load Diff