strat-gameplay-webapp/backend/CLAUDE.md
Cal Corum 3c5055dbf6 CLAUDE: Implement polymorphic RosterLink for both PD and SBA leagues
Added league-agnostic roster tracking with single-table design:

Database Changes:
- Modified RosterLink model with surrogate primary key (id)
- Added nullable card_id (PD) and player_id (SBA) columns
- Added CHECK constraint ensuring exactly one ID populated (XOR logic)
- Added unique constraints for (game_id, card_id) and (game_id, player_id)
- Imported CheckConstraint and UniqueConstraint from SQLAlchemy

New Files:
- app/models/roster_models.py: Pydantic models for type safety
  - BaseRosterLinkData: Abstract base class
  - PdRosterLinkData: PD league card-based rosters
  - SbaRosterLinkData: SBA league player-based rosters
  - RosterLinkCreate: Request validation model

- tests/unit/models/test_roster_models.py: 24 unit tests (all passing)
  - Tests for PD/SBA roster link creation and validation
  - Tests for RosterLinkCreate XOR validation
  - Tests for polymorphic behavior

Database Operations:
- add_pd_roster_card(): Add PD card to game roster
- add_sba_roster_player(): Add SBA player to game roster
- get_pd_roster(): Get PD cards with optional team filter
- get_sba_roster(): Get SBA players with optional team filter
- remove_roster_entry(): Remove roster entry by ID

Tests:
- Added 12 integration tests for roster operations
- Fixed setup_database fixture scope (module → function)

Documentation:
- Updated backend/CLAUDE.md with RosterLink documentation
- Added usage examples and design rationale
- Updated Game model relationship description

Design Pattern:
Single table with application-layer type safety rather than SQLAlchemy
polymorphic inheritance. Simpler queries, database-enforced integrity,
and Pydantic type safety at application layer.

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

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

29 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: Roster tracking - cards (PD) or players (SBA) (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)

Tracks eligible cards (PD) or players (SBA) for a game.

Polymorphic Design: Single table supporting both leagues with application-layer type safety.

Key Fields:

  • id (Integer): Surrogate primary key (auto-increment)
  • game_id (UUID): Foreign key to games table
  • card_id (Integer, nullable): PD league - card identifier
  • player_id (Integer, nullable): SBA league - player identifier
  • team_id (Integer): Which team owns this entity in this game

Constraints:

  • roster_link_one_id_required: CHECK constraint ensures exactly one of card_id or player_id is populated (XOR logic)
  • uq_game_card: UNIQUE constraint on (game_id, card_id) for PD
  • uq_game_player: UNIQUE constraint on (game_id, player_id) for SBA

Usage Pattern:

# PD league - add card to roster
roster_data = await db_ops.add_pd_roster_card(
    game_id=game_id,
    card_id=123,
    team_id=1
)

# SBA league - add player to roster
roster_data = await db_ops.add_sba_roster_player(
    game_id=game_id,
    player_id=456,
    team_id=2
)

# Get roster (league-specific)
pd_roster = await db_ops.get_pd_roster(game_id, team_id=1)
sba_roster = await db_ops.get_sba_roster(game_id, team_id=2)

Design Rationale:

  • Single table avoids complex joins and simplifies queries
  • Nullable columns with CHECK constraint ensures data integrity at database level
  • Pydantic models (PdRosterLinkData, SbaRosterLinkData) provide type safety at application layer
  • Surrogate key allows nullable columns (can't use nullable columns in composite PK)

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

Week 4 Implementation (Phase 2 - State Management)

Completed: 2025-10-22 Status: COMPLETE - All deliverables achieved

Components Implemented

1. Pydantic Game State Models (app/models/game_models.py)

Purpose: Type-safe models for in-memory game state representation

Key Models:

  • GameState: Core game state (inning, score, runners, decisions)
  • RunnerState: Base runner tracking
  • LineupPlayerState / TeamLineupState: Lineup management
  • DefensiveDecision / OffensiveDecision: Strategic decisions

Features:

  • Full Pydantic v2 validation with field validators
  • 20+ helper methods on GameState
  • Optimized for fast serialization (WebSocket broadcasts)
  • Game over logic, runner advancement, scoring

Usage Example:

from app.models.game_models import GameState, RunnerState

# Create game state
state = GameState(
    game_id=uuid4(),
    league_id="sba",
    home_team_id=1,
    away_team_id=2
)

# Add runner
state.add_runner(lineup_id=1, card_id=101, base=1)

# Advance runner and score
state.advance_runner(from_base=1, to_base=4)  # Scores run

2. State Manager (app/core/state_manager.py)

Purpose: In-memory state management with O(1) lookups

Key Features:

  • Dictionary-based storage: _states: Dict[UUID, GameState]
  • Lineup caching per game
  • Last access tracking with Pendulum timestamps
  • Idle game eviction (configurable timeout)
  • State recovery from database
  • Statistics tracking

Usage Example:

from app.core.state_manager import state_manager

# Create game
state = await state_manager.create_game(
    game_id=game_id,
    league_id="sba",
    home_team_id=1,
    away_team_id=2
)

# Get state (fast O(1) lookup)
state = state_manager.get_state(game_id)

# Update state
state.inning = 5
state_manager.update_state(game_id, state)

# Recover from database
recovered = await state_manager.recover_game(game_id)

Performance:

  • State access: O(1) via dictionary lookup
  • Memory per game: ~1KB (just state, no player data yet)
  • Target response time: <500ms

3. Database Operations (app/database/operations.py)

Purpose: Async PostgreSQL persistence layer

Key Methods:

  • create_game(): Create game in database
  • update_game_state(): Update inning, score, status
  • create_lineup_entry() / get_active_lineup(): Lineup persistence
  • save_play() / get_plays(): Play recording
  • load_game_state(): Complete state loading for recovery
  • create_game_session() / update_session_snapshot(): WebSocket state

Usage Example:

from app.database.operations import DatabaseOperations

db_ops = DatabaseOperations()

# Create game
await db_ops.create_game(
    game_id=game_id,
    league_id="sba",
    home_team_id=1,
    away_team_id=2,
    game_mode="friendly",
    visibility="public"
)

# Update state (async, non-blocking)
await db_ops.update_game_state(
    game_id=game_id,
    inning=5,
    half="bottom",
    home_score=3,
    away_score=2
)

# Load for recovery
game_data = await db_ops.load_game_state(game_id)

Pattern: All operations use async/await with proper error handling

Testing

Unit Tests (86 tests, 100% passing):

  • tests/unit/models/test_game_models.py (60 tests)
  • tests/unit/core/test_state_manager.py (26 tests)

Integration Tests (29 tests written):

  • tests/integration/database/test_operations.py (21 tests)
  • tests/integration/test_state_persistence.py (8 tests)

Run Tests:

# All unit tests
pytest tests/unit/ -v

# Integration tests (requires database)
pytest tests/integration/ -v -m integration

# Specific file
pytest tests/unit/models/test_game_models.py -v

Patterns Established

1. Pydantic Field Validation

@field_validator('position')
@classmethod
def validate_position(cls, v: str) -> str:
    valid_positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH']
    if v not in valid_positions:
        raise ValueError(f"Position must be one of {valid_positions}")
    return v

2. Async Database Session Management

async with AsyncSessionLocal() as session:
    try:
        # database operations
        await session.commit()
    except Exception as e:
        await session.rollback()
        logger.error(f"Error: {e}")
        raise

3. State Recovery Pattern

# 1. Load from database
game_data = await db_ops.load_game_state(game_id)

# 2. Rebuild in-memory state
state = await state_manager._rebuild_state_from_data(game_data)

# 3. Cache in memory
state_manager._states[game_id] = state

4. Helper Methods on Models

# GameState helper methods
def get_batting_team_id(self) -> int:
    return self.away_team_id if self.half == "top" else self.home_team_id

def is_runner_on_first(self) -> bool:
    return any(r.on_base == 1 for r in self.runners)

def advance_runner(self, from_base: int, to_base: int) -> None:
    # Auto-score when to_base == 4
    if to_base == 4:
        if self.half == "top":
            self.away_score += 1
        else:
            self.home_score += 1

Configuration

pytest.ini (created):

[pytest]
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function

Fixed Warnings:

  • Pydantic v2 config deprecation (migrated to model_config = ConfigDict())
  • pytest-asyncio loop scope configuration

Key Files

app/models/game_models.py      (492 lines) - Pydantic state models
app/core/state_manager.py      (296 lines) - In-memory state management
app/database/operations.py     (362 lines) - Async database operations
tests/unit/models/test_game_models.py      (788 lines, 60 tests)
tests/unit/core/test_state_manager.py      (447 lines, 26 tests)
tests/integration/database/test_operations.py      (438 lines, 21 tests)
tests/integration/test_state_persistence.py        (290 lines, 8 tests)
pytest.ini                     (23 lines)  - Test configuration

Next Phase (Week 5)

Week 5 will build game logic on top of this state management foundation:

  1. Dice system (cryptographic d20 rolls)
  2. Play resolver (result charts and outcome determination)
  3. Game engine (orchestrate complete at-bat flow)
  4. Rule validators (enforce baseball rules)
  5. Enhanced state recovery (replay plays to rebuild complete state)

References

  • Implementation Guide: ../.claude/implementation/01-infrastructure.md
  • Backend Architecture: ../.claude/implementation/backend-architecture.md
  • Week 4 Plan: ../.claude/implementation/02-week4-state-management.md
  • Week 5 Plan: ../.claude/implementation/02-week5-game-logic.md
  • Player Data Catalog: ../.claude/implementation/player-data-catalog.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 2 - Game Engine Core (Week 4 Complete) Next Phase: Phase 2 - Game Logic (Week 5)

Phase 1 Completed: 2025-10-21 Week 4 Completed: 2025-10-22 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