strat-gameplay-webapp/backend/CLAUDE.md
Cal Corum d8a43faa2e CLAUDE: Complete Phase 1 - Frontend Infrastructure Setup
Initialize both Nuxt 3 frontends (SBA and PD) with full configuration:

Frontend Setup:
- Initialized Nuxt 3 projects for both leagues (SBA and PD)
- Installed dependencies: Tailwind CSS, Pinia, Socket.io-client, Axios
- Configured league-specific settings in nuxt.config.ts
- Created WebSocket plugins for real-time communication
- Set up TypeScript with strict mode and type checking
- Configured Tailwind CSS for styling

Backend Updates:
- Updated database models documentation in backend/CLAUDE.md
- Enhanced db_models.py with additional relationship patterns

Documentation:
- Updated Phase 1 completion checklist (12/12 items - 100% complete)
- Marked all infrastructure objectives as complete

Running Services:
- Backend (FastAPI + Socket.io): http://localhost:8000
- Frontend SBA: http://localhost:3000
- Frontend PD: http://localhost:3001
- Redis: port 6379
- PostgreSQL: Connected to remote server

Phase 1 is now complete. Ready to proceed to Phase 2 (Game Engine Core).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 00:24:00 -05:00

21 KiB

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

# 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

# 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

# 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

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:

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

from dataclasses import dataclass

@dataclass
class GameState:
    game_id: str
    inning: int
    outs: int
    # ... fields

Database Models

Our database schema is designed based on the proven Discord game implementation, with enhancements for web real-time gameplay.

Core Tables

Game (games)

Primary game container with state tracking.

Key Fields:

  • id (UUID): Primary key, prevents ID collisions across distributed systems
  • league_id (String): 'sba' or 'pd', determines league-specific behavior
  • status (String): 'pending', 'active', 'completed'
  • game_mode (String): 'ranked', 'friendly', 'practice'
  • visibility (String): 'public', 'private'
  • current_inning, current_half: Current game state
  • home_score, away_score: Running scores

AI Support:

  • home_team_is_ai (Boolean): Home team controlled by AI
  • away_team_is_ai (Boolean): Away team controlled by AI
  • ai_difficulty (String): 'balanced', 'yolo', 'safe'

Relationships:

  • plays: All plays in the game (cascade delete)
  • lineups: All lineup entries (cascade delete)
  • cardset_links: PD only - approved cardsets (cascade delete)
  • roster_links: PD only - cards in use (cascade delete)
  • session: Real-time WebSocket session (cascade delete)

Play (plays)

Records every at-bat with full statistics and game state.

Game State Snapshot:

  • play_number: Sequential play counter
  • inning, half: Inning state
  • outs_before: Outs at start of play
  • batting_order: Current spot in order
  • away_score, home_score: Score at play start

Player References (FKs to Lineup):

  • batter_id, pitcher_id, catcher_id: Required players
  • defender_id, runner_id: Optional for specific plays
  • on_first_id, on_second_id, on_third_id: Base runners

Runner Outcomes:

  • on_first_final, on_second_final, on_third_final: Final base (None = out, 1-4 = base)
  • batter_final: Where batter ended up
  • on_base_code (Integer): Bit field for efficient queries (1=1st, 2=2nd, 4=3rd, 7=loaded)

Strategic Decisions:

  • defensive_choices (JSON): Alignment, holds, shifts
  • offensive_choices (JSON): Steal attempts, bunts, hit-and-run

Play Result:

  • dice_roll (String): Dice notation (e.g., "14+6")
  • hit_type (String): GB, FB, LD, etc.
  • result_description (Text): Human-readable result
  • outs_recorded, runs_scored: Play outcome
  • check_pos (String): Defensive position for X-check

Batting Statistics (25+ fields):

  • pa, ab, hit, double, triple, homerun
  • bb, so, hbp, rbi, sac, ibb, gidp
  • sb, cs: Base stealing
  • wild_pitch, passed_ball, pick_off, balk
  • bphr, bpfo, bp1b, bplo: Ballpark power events
  • run, e_run: Earned/unearned runs

Advanced Analytics:

  • wpa (Float): Win Probability Added
  • re24 (Float): Run Expectancy 24 base-out states

Game Situation Flags:

  • is_tied, is_go_ahead, is_new_inning: Context flags
  • in_pow: Pitcher over workload
  • complete, locked: Workflow state

Helper Properties:

@property
def ai_is_batting(self) -> bool:
    """True if batting team is AI-controlled"""
    return (self.half == 'top' and self.game.away_team_is_ai) or \
           (self.half == 'bot' and self.game.home_team_is_ai)

@property
def ai_is_fielding(self) -> bool:
    """True if fielding team is AI-controlled"""
    return not self.ai_is_batting

Lineup (lineups)

Tracks player assignments and substitutions.

Key Fields:

  • game_id, team_id, card_id: Links to game/team/card
  • position (String): P, C, 1B, 2B, 3B, SS, LF, CF, RF, DH
  • batting_order (Integer): 1-9

Substitution Tracking:

  • is_starter (Boolean): Original lineup vs substitute
  • is_active (Boolean): Currently in game
  • entered_inning (Integer): When player entered
  • replacing_id (Integer): Lineup ID of replaced player
  • after_play (Integer): Exact play number of substitution

Pitcher Management:

  • is_fatigued (Boolean): Triggers bullpen decisions

PD league only - defines legal cardsets for a game.

Key Fields:

  • game_id, cardset_id: Composite primary key
  • priority (Integer): 1 = primary, 2+ = backup

Usage:

  • SBA games: Empty (no cardset restrictions)
  • PD games: Required (validates card eligibility)

PD league only - tracks which cards each team is using.

Key Fields:

  • game_id, card_id: Composite primary key
  • team_id: Which team owns this card in this game

GameSession (game_sessions)

Real-time WebSocket state tracking.

Key Fields:

  • game_id (UUID): Primary key, one-to-one with Game
  • connected_users (JSON): Active WebSocket connections
  • last_action_at (DateTime): Last activity timestamp
  • state_snapshot (JSON): In-memory game state cache

Database Patterns

Async Session Usage

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

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

Relationship Patterns

Using Lazy Loading:

# In Play model - common players loaded automatically
batter = relationship("Lineup", foreign_keys=[batter_id], lazy="joined")
pitcher = relationship("Lineup", foreign_keys=[pitcher_id], lazy="joined")

# Rare players loaded on demand
defender = relationship("Lineup", foreign_keys=[defender_id])  # lazy="select" (default)

Querying with Relationships:

from sqlalchemy.orm import joinedload

# Efficient loading for broadcasting
play = await session.execute(
    select(Play)
    .options(
        joinedload(Play.batter),
        joinedload(Play.pitcher),
        joinedload(Play.on_first),
        joinedload(Play.on_second),
        joinedload(Play.on_third)
    )
    .where(Play.id == play_id)
)

Common Query Patterns

Find plays with bases loaded:

# Using on_base_code bit field
bases_loaded_plays = await session.execute(
    select(Play).where(Play.on_base_code == 7)  # 1+2+4 = 7
)

Get active pitcher for team:

pitcher = await session.execute(
    select(Lineup)
    .where(
        Lineup.game_id == game_id,
        Lineup.team_id == team_id,
        Lineup.position == 'P',
        Lineup.is_active == True
    )
)

Calculate box score stats:

# Player batting stats for game
stats = await session.execute(
    select(
        func.sum(Play.ab).label('ab'),
        func.sum(Play.hit).label('hits'),
        func.sum(Play.homerun).label('hr'),
        func.sum(Play.rbi).label('rbi')
    )
    .where(
        Play.game_id == game_id,
        Play.batter_id == lineup_id
    )
)

WebSocket Patterns

Event Handler Registration

@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

# 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:

# 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):

# 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)
    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:

# 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:

# ❌ 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:

# 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

# 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 Models Updated: 2025-10-21 (Discord parity achieved) Python Version: 3.13.3 Database Server: 10.10.0.42:5432

Database Model Updates (2025-10-21)

Enhanced all database models based on proven Discord game implementation:

Changes from Initial Design:

  • Added GameCardsetLink and RosterLink tables for PD league cardset management
  • Enhanced Game model with AI opponent support (home_team_is_ai, away_team_is_ai, ai_difficulty)
  • Added 25+ statistic fields to Play model (pa, ab, hit, hr, rbi, sb, wpa, re24, etc.)
  • Added player reference FKs to Play (batter, pitcher, catcher, defender, runner)
  • Added base runner tracking with on_base_code bit field for efficient queries
  • Added game situation flags to Play (is_tied, is_go_ahead, is_new_inning, in_pow)
  • Added play workflow flags (complete, locked)
  • Enhanced Lineup with substitution tracking (replacing_id, after_play, is_fatigued)
  • Changed strategic decisions to JSON (defensive_choices, offensive_choices)
  • Added helper properties for AI decision-making (ai_is_batting, ai_is_fielding)

Design Decisions:

  • UUID vs BigInteger: Kept UUIDs for Game primary key (better for distributed systems)
  • AI Tracking: Per-team booleans instead of single ai_team field (supports AI vs AI simulations)
  • Runner Tracking: Removed JSON fields, using FKs + on_base_code for type safety
  • Cardsets: Optional relationships - empty for SBA, required for PD
  • Relationships: Using SQLAlchemy relationships with strategic lazy loading