strat-gameplay-webapp/.claude/implementation/backend-architecture.md
2025-10-22 11:22:15 -05:00

15 KiB

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:

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:

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:

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

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:

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:

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

# 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 = "Stratomatic Baseball Association"
    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:

# 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

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

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 for implementation details.