strat-gameplay-webapp/.claude/implementation/backend-architecture.md
Cal Corum 5c75b935f0 CLAUDE: Initial project setup - documentation and infrastructure
Add comprehensive project documentation and Docker infrastructure for
Paper Dynasty Real-Time Game Engine - a web-based multiplayer baseball
simulation platform replacing the legacy Google Sheets system.

Documentation Added:
- Complete PRD (Product Requirements Document)
- Project README with dual development workflows
- Implementation guide with 5-phase roadmap
- Architecture docs (backend, frontend, database, WebSocket)
- CLAUDE.md context files for each major directory

Infrastructure Added:
- Root docker-compose.yml for full stack orchestration
- Dockerfiles for backend and both frontends (multi-stage builds)
- .dockerignore files for optimal build context
- .env.example with all required configuration
- Updated .gitignore for Python, Node, Nuxt, and Docker

Project Structure:
- backend/ - FastAPI + Socket.io game engine (Python 3.11+)
- frontend-sba/ - SBA League Nuxt 3 frontend
- frontend-pd/ - PD League Nuxt 3 frontend
- .claude/implementation/ - Detailed implementation guides

Supports two development workflows:
1. Local dev (recommended): Services run natively with hot-reload
2. Full Docker: One-command stack orchestration for testing/demos

Next: Phase 1 implementation (backend/frontend foundations)
2025-10-21 16:21:13 -05:00

484 lines
15 KiB
Markdown

# 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 = "Super Baseball Alliance"
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.