Implemented full FastAPI backend with WebSocket support, database models, and comprehensive documentation for the Paper Dynasty game engine. Backend Implementation: - FastAPI application with Socket.io WebSocket server - SQLAlchemy async database models (Game, Play, Lineup, GameSession) - PostgreSQL connection to dev server (10.10.0.42:5432) - Connection manager for WebSocket lifecycle - JWT authentication utilities - Health check and stub API endpoints - Rotating file logger with Pendulum datetime handling - Redis via Docker Compose for caching Technical Details: - Python 3.13 with updated package versions - Pendulum 3.0 for all datetime operations - Greenlet for SQLAlchemy async support - Fixed SQLAlchemy reserved column names (metadata -> *_metadata) - Pydantic Settings with JSON array format for lists - Docker Compose V2 commands Documentation: - Updated backend/CLAUDE.md with environment-specific details - Created .claude/ENVIRONMENT.md for gotchas and quirks - Created QUICKSTART.md for developer onboarding - Documented all critical learnings and troubleshooting steps Database: - Tables created: games, plays, lineups, game_sessions - All indexes and foreign keys configured - Successfully tested connection and health checks Verified: - Server starts at http://localhost:8000 - Health endpoints responding - Database connection working - WebSocket infrastructure functional - Hot-reload working 🎯 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
463 lines
13 KiB
Markdown
463 lines
13 KiB
Markdown
# 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.13)
|
|
- **WebSocket**: Socket.io (python-socketio)
|
|
- **Database**: PostgreSQL 14+ with SQLAlchemy 2.0 (async)
|
|
- **ORM**: SQLAlchemy with asyncpg driver
|
|
- **Validation**: Pydantic v2
|
|
- **DateTime**: Pendulum 3.0 (replaces Python's datetime module)
|
|
- **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 or use -d for detached)
|
|
docker compose up -d
|
|
|
|
# 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)
|
|
```
|
|
|
|
### DateTime Handling
|
|
**ALWAYS use Pendulum, NEVER use Python's datetime module:**
|
|
```python
|
|
import pendulum
|
|
|
|
# Get current UTC time
|
|
now = pendulum.now('UTC')
|
|
|
|
# Format for display
|
|
formatted = now.format('YYYY-MM-DD HH:mm:ss')
|
|
formatted_iso = now.to_iso8601_string()
|
|
|
|
# Parse dates
|
|
parsed = pendulum.parse('2025-10-21')
|
|
|
|
# Timezones
|
|
eastern = pendulum.now('America/New_York')
|
|
utc = eastern.in_timezone('UTC')
|
|
|
|
# Database defaults (in models)
|
|
created_at = Column(DateTime, default=lambda: pendulum.now('UTC'))
|
|
```
|
|
|
|
### 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)
|
|
|
|
## Environment-Specific Configuration
|
|
|
|
### Database Connection
|
|
- **Dev Server**: PostgreSQL at `10.10.0.42:5432`
|
|
- **Database Name**: `paperdynasty_dev`
|
|
- **User**: `paperdynasty`
|
|
- **Connection String Format**: `postgresql+asyncpg://paperdynasty:PASSWORD@10.10.0.42:5432/paperdynasty_dev`
|
|
|
|
### Docker Compose Commands
|
|
This system uses **Docker Compose V2** (not legacy docker-compose):
|
|
```bash
|
|
# Correct (with space)
|
|
docker compose up -d
|
|
docker compose down
|
|
docker compose logs
|
|
|
|
# Incorrect (will not work)
|
|
docker-compose up -d # Old command not available
|
|
```
|
|
|
|
### Python Environment
|
|
- **Version**: Python 3.13.3 (not 3.11 as originally planned)
|
|
- **Virtual Environment**: Located at `backend/venv/`
|
|
- **Activation**: `source venv/bin/activate` (from backend directory)
|
|
|
|
### Critical Dependencies
|
|
- **greenlet**: Required for SQLAlchemy async support (must be explicitly installed)
|
|
- **Pendulum**: Used for ALL datetime operations (replaces Python's datetime module)
|
|
```python
|
|
import pendulum
|
|
|
|
# Always use Pendulum
|
|
now = pendulum.now('UTC')
|
|
|
|
# Never use
|
|
from datetime import datetime # ❌ Don't import this
|
|
```
|
|
|
|
### Environment Variable Format
|
|
|
|
**IMPORTANT**: Pydantic Settings requires specific formats for complex types:
|
|
|
|
```bash
|
|
# Lists must be JSON arrays
|
|
CORS_ORIGINS=["http://localhost:3000", "http://localhost:3001"] # ✅ Correct
|
|
|
|
# NOT comma-separated strings
|
|
CORS_ORIGINS=http://localhost:3000,http://localhost:3001 # ❌ Will fail
|
|
```
|
|
|
|
### SQLAlchemy Reserved Names
|
|
|
|
The following column names are **reserved** in SQLAlchemy and will cause errors:
|
|
|
|
```python
|
|
# ❌ NEVER use these as column names
|
|
metadata = Column(JSON) # RESERVED - will fail
|
|
|
|
# ✅ Use descriptive alternatives
|
|
game_metadata = Column(JSON)
|
|
play_metadata = Column(JSON)
|
|
lineup_metadata = Column(JSON)
|
|
```
|
|
|
|
**Other reserved names to avoid**: `metadata`, `registry`, `__tablename__`, `__mapper__`, `__table__`
|
|
|
|
### Package Version Notes
|
|
|
|
Due to Python 3.13 compatibility, we use newer versions than originally planned:
|
|
|
|
```txt
|
|
# Updated versions (from requirements.txt)
|
|
fastapi==0.115.6 # (was 0.104.1)
|
|
uvicorn==0.34.0 # (was 0.24.0)
|
|
pydantic==2.10.6 # (was 2.5.0)
|
|
sqlalchemy==2.0.36 # (was 2.0.23)
|
|
asyncpg==0.30.0 # (was 0.29.0)
|
|
pendulum==3.0.0 # (new addition)
|
|
```
|
|
|
|
### Server Startup
|
|
|
|
```bash
|
|
# From backend directory with venv activated
|
|
python -m app.main
|
|
|
|
# Server runs at:
|
|
# - Main API: http://localhost:8000
|
|
# - Swagger UI: http://localhost:8000/docs
|
|
# - ReDoc: http://localhost:8000/redoc
|
|
|
|
# With hot-reload enabled by default
|
|
```
|
|
|
|
### Logs Directory
|
|
- Auto-created at `backend/logs/`
|
|
- Daily rotating logs: `app_YYYYMMDD.log`
|
|
- 10MB max size, 5 backup files
|
|
- Gitignored
|
|
|
|
## 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 (✅ Complete)
|
|
**Next Phase**: Phase 2 - Game Engine Core
|
|
|
|
**Setup Completed**: 2025-10-21
|
|
**Python Version**: 3.13.3
|
|
**Database Server**: 10.10.0.42:5432 |