""" Custom exceptions for the game engine. Provides a hierarchy of specific exception types to replace broad `except Exception` patterns, improving debugging and error handling precision. Exception Hierarchy: GameEngineError (base) ├── GameNotFoundError - Game doesn't exist in state manager ├── InvalidGameStateError - Game in wrong state for operation ├── SubstitutionError - Invalid player substitution ├── AuthorizationError - User lacks permission ├── DecisionTimeoutError - Decision not submitted in time └── ExternalAPIError - External service call failed └── PlayerDataError - Failed to fetch player data Author: Claude Date: 2025-11-27 """ from uuid import UUID class GameEngineError(Exception): """ Base exception for all game engine errors. All game-specific exceptions should inherit from this class to allow catching game errors separately from system errors. """ pass class GameNotFoundError(GameEngineError): """ Raised when a game doesn't exist in state manager. Attributes: game_id: The UUID of the missing game """ def __init__(self, game_id: UUID | str): self.game_id = game_id super().__init__(f"Game not found: {game_id}") class InvalidGameStateError(GameEngineError): """ Raised when game is in invalid state for requested operation. Examples: - Trying to submit decision when game is completed - Rolling dice before both decisions submitted - Starting an already-started game Attributes: message: Description of the state violation current_state: Optional current state value for debugging expected_state: Optional expected state value for debugging """ def __init__( self, message: str, current_state: str | None = None, expected_state: str | None = None, ): self.current_state = current_state self.expected_state = expected_state super().__init__(message) class SubstitutionError(GameEngineError): """ Raised when a player substitution is invalid. Used for substitution rule violations, not found errors. Attributes: message: Description of the substitution error error_code: Machine-readable error code for frontend handling """ def __init__(self, message: str, error_code: str = "SUBSTITUTION_ERROR"): self.error_code = error_code super().__init__(message) class AuthorizationError(GameEngineError): """ Raised when user lacks permission for an operation. Note: Currently deferred (task 001) but defined for future use. Attributes: message: Description of the authorization failure user_id: Optional user ID for logging resource: Optional resource being accessed """ def __init__( self, message: str, user_id: int | None = None, resource: str | None = None, ): self.user_id = user_id self.resource = resource super().__init__(message) class DecisionTimeoutError(GameEngineError): """ Raised when a decision is not submitted within the timeout period. The game engine uses default decisions when this occurs, so this exception is informational rather than fatal. Attributes: game_id: Game where timeout occurred decision_type: Type of decision that timed out ('defensive' or 'offensive') timeout_seconds: How long we waited """ def __init__( self, game_id: UUID | str, decision_type: str, timeout_seconds: int, ): self.game_id = game_id self.decision_type = decision_type self.timeout_seconds = timeout_seconds super().__init__( f"Decision timeout for game {game_id}: " f"{decision_type} decision not received within {timeout_seconds}s" ) class ExternalAPIError(GameEngineError): """ Raised when an external API call fails. Base class for external service errors. Attributes: service: Name of the external service message: Error description status_code: Optional HTTP status code """ def __init__( self, service: str, message: str, status_code: int | None = None, ): self.service = service self.status_code = status_code super().__init__(f"{service} API error: {message}") class PlayerDataError(ExternalAPIError): """ Raised when player data cannot be fetched from external API. Specialized error for player data lookups (SBA API, PD API). Attributes: player_id: ID of the player that couldn't be fetched service: Which API was called """ def __init__(self, player_id: int, service: str = "SBA API"): self.player_id = player_id super().__init__( service=service, message=f"Failed to fetch player data for ID {player_id}", ) class DatabaseError(GameEngineError): """ Raised when a database operation fails. Wraps SQLAlchemy exceptions with game context. Attributes: operation: What operation was attempted (e.g., 'save_play', 'create_substitution') original_error: The underlying database exception """ def __init__(self, operation: str, original_error: Exception | None = None): self.operation = operation self.original_error = original_error message = f"Database error during {operation}" if original_error: message += f": {original_error}" super().__init__(message) class LineupError(GameEngineError): """ Raised when lineup validation or lookup fails. Attributes: team_id: Team with the lineup issue message: Description of the error """ def __init__(self, team_id: int, message: str): self.team_id = team_id super().__init__(f"Lineup error for team {team_id}: {message}")