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

```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.