Implements schedule viewing from SBA production API with week navigation and game creation from scheduled matchups. Groups games by team matchup horizontally with games stacked vertically for space efficiency. Backend: - Add schedule routes (/api/schedule/current, /api/schedule/games) - Add SBA API client methods for schedule data - Fix multi-worker state isolation (single worker for in-memory state) - Add Redis migration TODO for future scalability - Support custom team IDs in quick-create endpoint Frontend: - Add Schedule tab as default on home page - Week navigation with prev/next and "Current Week" jump - Horizontal group layout (2-6 columns responsive) - Completed games show score + "Final" badge (no Play button) - Incomplete games show "Play" button to create webapp game Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
704 lines
25 KiB
Python
704 lines
25 KiB
Python
import logging
|
|
from uuid import UUID, uuid4
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
from app.api.dependencies import get_current_user_optional
|
|
from app.core.game_engine import game_engine
|
|
from app.core.state_manager import state_manager
|
|
from app.database.operations import DatabaseOperations
|
|
from app.services.lineup_service import lineup_service
|
|
from app.services.sba_api_client import sba_api_client
|
|
|
|
logger = logging.getLogger(f"{__name__}.games")
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class GameListItem(BaseModel):
|
|
"""Game list item model with enriched team and game state info"""
|
|
|
|
game_id: str
|
|
league_id: str
|
|
status: str
|
|
home_team_id: int
|
|
away_team_id: int
|
|
# Enriched fields
|
|
home_team_name: str | None = None
|
|
away_team_name: str | None = None
|
|
home_team_abbrev: str | None = None
|
|
away_team_abbrev: str | None = None
|
|
home_score: int = 0
|
|
away_score: int = 0
|
|
inning: int | None = None
|
|
half: str | None = None # 'top' or 'bottom'
|
|
|
|
|
|
class CreateGameRequest(BaseModel):
|
|
"""Request model for creating a new game"""
|
|
|
|
name: str = Field(..., description="Game name")
|
|
home_team_id: int = Field(..., description="Home team ID")
|
|
away_team_id: int = Field(..., description="Away team ID")
|
|
is_ai_opponent: bool = Field(default=False, description="Is AI opponent")
|
|
season: int = Field(default=3, description="Season number")
|
|
league_id: str = Field(default="sba", description="League ID (sba or pd)")
|
|
|
|
|
|
class CreateGameResponse(BaseModel):
|
|
"""Response model for game creation"""
|
|
|
|
game_id: str
|
|
message: str
|
|
status: str
|
|
|
|
|
|
class QuickCreateRequest(BaseModel):
|
|
"""Optional request model for quick-create with custom teams"""
|
|
|
|
home_team_id: int | None = Field(None, description="Home team ID (uses default if not provided)")
|
|
away_team_id: int | None = Field(None, description="Away team ID (uses default if not provided)")
|
|
|
|
|
|
class LineupPlayerRequest(BaseModel):
|
|
"""Single player in lineup request"""
|
|
|
|
player_id: int = Field(..., description="SBA player ID")
|
|
position: str = Field(..., description="Defensive position (P, C, 1B, etc.)")
|
|
batting_order: int | None = Field(
|
|
None, ge=1, le=9, description="Batting order (1-9), null for pitcher in DH lineup"
|
|
)
|
|
|
|
@field_validator("position")
|
|
@classmethod
|
|
def validate_position(cls, v: str) -> str:
|
|
"""Ensure position is valid"""
|
|
valid = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "DH"]
|
|
if v not in valid:
|
|
raise ValueError(f"Position must be one of {valid}")
|
|
return v
|
|
|
|
|
|
class SubmitLineupsRequest(BaseModel):
|
|
"""
|
|
Request model for submitting lineups for both teams.
|
|
|
|
Supports both 9-player (pitcher bats) and 10-player (DH) configurations:
|
|
- 9 players: All have batting orders 1-9
|
|
- 10 players: Pitcher has batting_order=null, others have 1-9
|
|
"""
|
|
|
|
home_lineup: list[LineupPlayerRequest] = Field(
|
|
..., min_length=9, max_length=10, description="Home team starting lineup (9-10 players)"
|
|
)
|
|
away_lineup: list[LineupPlayerRequest] = Field(
|
|
..., min_length=9, max_length=10, description="Away team starting lineup (9-10 players)"
|
|
)
|
|
|
|
@field_validator("home_lineup", "away_lineup")
|
|
@classmethod
|
|
def validate_lineup(cls, v: list[LineupPlayerRequest]) -> list[LineupPlayerRequest]:
|
|
"""
|
|
Validate lineup structure for 9-player or 10-player (DH) configurations.
|
|
|
|
Rules:
|
|
- 9 players: All must have batting orders 1-9
|
|
- 10 players: Exactly 9 have batting orders 1-9, pitcher has null
|
|
"""
|
|
lineup_size = len(v)
|
|
|
|
# Check batting orders
|
|
batters = [p for p in v if p.batting_order is not None]
|
|
batting_orders = [p.batting_order for p in batters]
|
|
|
|
if len(batting_orders) != 9:
|
|
raise ValueError(f"Must have exactly 9 batters with batting orders, got {len(batting_orders)}")
|
|
|
|
if set(batting_orders) != {1, 2, 3, 4, 5, 6, 7, 8, 9}:
|
|
raise ValueError("Batting orders must be exactly 1-9 with no duplicates")
|
|
|
|
# Check positions
|
|
positions = [p.position for p in v]
|
|
required_positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"]
|
|
|
|
for req_pos in required_positions:
|
|
if req_pos not in positions:
|
|
raise ValueError(f"Missing required position: {req_pos}")
|
|
|
|
# For 10-player lineup, must have DH and pitcher must not bat
|
|
if lineup_size == 10:
|
|
if "DH" not in positions:
|
|
raise ValueError("10-player lineup must include DH position")
|
|
|
|
pitchers = [p for p in v if p.position == "P"]
|
|
if len(pitchers) != 1:
|
|
raise ValueError("Must have exactly 1 pitcher")
|
|
|
|
if pitchers[0].batting_order is not None:
|
|
raise ValueError("Pitcher cannot have batting order in DH lineup")
|
|
|
|
# For 9-player lineup, pitcher must bat (no DH)
|
|
elif lineup_size == 9:
|
|
if "DH" in positions:
|
|
raise ValueError("9-player lineup cannot include DH")
|
|
|
|
pitchers = [p for p in v if p.position == "P"]
|
|
if len(pitchers) != 1:
|
|
raise ValueError("Must have exactly 1 pitcher")
|
|
|
|
if pitchers[0].batting_order is None:
|
|
raise ValueError("Pitcher must have batting order when no DH")
|
|
|
|
# Check player uniqueness
|
|
player_ids = [p.player_id for p in v]
|
|
if len(set(player_ids)) != len(player_ids):
|
|
raise ValueError("Players cannot be duplicated in lineup")
|
|
|
|
return v
|
|
|
|
|
|
class SubmitLineupsResponse(BaseModel):
|
|
"""Response model for lineup submission"""
|
|
|
|
game_id: str
|
|
message: str
|
|
home_lineup_count: int
|
|
away_lineup_count: int
|
|
|
|
|
|
@router.get("/", response_model=list[GameListItem])
|
|
async def list_games():
|
|
"""
|
|
List all games from the database with enriched team and game state info.
|
|
|
|
Returns game information including team names, scores, and current inning.
|
|
TODO: Add user filtering, pagination, and more sophisticated queries
|
|
"""
|
|
try:
|
|
logger.info("Fetching games list from database")
|
|
|
|
# Get all games from database (for now - later we can add filters)
|
|
from app.database.session import AsyncSessionLocal
|
|
from app.models.db_models import Game
|
|
|
|
async with AsyncSessionLocal() as session:
|
|
from sqlalchemy import select
|
|
result = await session.execute(
|
|
select(Game).order_by(Game.created_at.desc())
|
|
)
|
|
games = result.scalars().all()
|
|
|
|
# Collect unique team IDs for batch lookup
|
|
team_ids = set()
|
|
for game in games:
|
|
team_ids.add(game.home_team_id)
|
|
team_ids.add(game.away_team_id)
|
|
|
|
# Fetch team data (uses cache)
|
|
teams_data = await sba_api_client.get_teams_by_ids(list(team_ids))
|
|
|
|
# Convert to response model with enriched data
|
|
game_list = []
|
|
for game in games:
|
|
home_team = teams_data.get(game.home_team_id, {})
|
|
away_team = teams_data.get(game.away_team_id, {})
|
|
|
|
game_list.append(
|
|
GameListItem(
|
|
game_id=str(game.id),
|
|
league_id=game.league_id,
|
|
status=game.status,
|
|
home_team_id=game.home_team_id,
|
|
away_team_id=game.away_team_id,
|
|
home_team_name=home_team.get("lname"),
|
|
away_team_name=away_team.get("lname"),
|
|
home_team_abbrev=home_team.get("abbrev"),
|
|
away_team_abbrev=away_team.get("abbrev"),
|
|
home_score=game.home_score or 0,
|
|
away_score=game.away_score or 0,
|
|
inning=game.current_inning,
|
|
half=game.current_half,
|
|
)
|
|
)
|
|
|
|
logger.info(f"Retrieved {len(game_list)} games from database")
|
|
return game_list
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Failed to list games: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to list games: {str(e)}")
|
|
|
|
|
|
@router.get("/{game_id}")
|
|
async def get_game(game_id: str):
|
|
"""
|
|
Get game details including team IDs and status.
|
|
|
|
Args:
|
|
game_id: Game identifier
|
|
|
|
Returns:
|
|
Game information including home/away team IDs
|
|
|
|
Raises:
|
|
400: Invalid game_id format
|
|
404: Game not found
|
|
"""
|
|
try:
|
|
# Validate game_id format
|
|
try:
|
|
game_uuid = UUID(game_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid game_id format")
|
|
|
|
logger.info(f"Fetching game details for {game_id}")
|
|
|
|
# Get game from state manager
|
|
if game_uuid not in state_manager._states:
|
|
raise HTTPException(status_code=404, detail=f"Game {game_id} not found")
|
|
|
|
state = state_manager._states[game_uuid]
|
|
|
|
return {
|
|
"game_id": game_id,
|
|
"status": state.status,
|
|
"home_team_id": state.home_team_id,
|
|
"away_team_id": state.away_team_id,
|
|
"league_id": state.league_id,
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.exception(f"Failed to fetch game {game_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to fetch game: {str(e)}")
|
|
|
|
|
|
@router.post("/", response_model=CreateGameResponse)
|
|
async def create_game(request: CreateGameRequest):
|
|
"""
|
|
Create new game
|
|
|
|
Creates a new game in the state manager and database.
|
|
Note: Lineups must be added separately before the game can be started.
|
|
"""
|
|
try:
|
|
# Generate game ID
|
|
game_id = uuid4()
|
|
logger.info(
|
|
f"Creating game {game_id}: {request.home_team_id} vs {request.away_team_id}"
|
|
)
|
|
|
|
# Validate teams are different
|
|
if request.home_team_id == request.away_team_id:
|
|
raise HTTPException(
|
|
status_code=400, detail="Home and away teams must be different"
|
|
)
|
|
|
|
# Create game in state manager (in-memory)
|
|
state = await state_manager.create_game(
|
|
game_id=game_id,
|
|
league_id=request.league_id,
|
|
home_team_id=request.home_team_id,
|
|
away_team_id=request.away_team_id,
|
|
)
|
|
|
|
# Save to database
|
|
db_ops = DatabaseOperations()
|
|
await db_ops.create_game(
|
|
game_id=game_id,
|
|
league_id=request.league_id,
|
|
home_team_id=request.home_team_id,
|
|
away_team_id=request.away_team_id,
|
|
game_mode="friendly" if not request.is_ai_opponent else "ai",
|
|
visibility="public",
|
|
)
|
|
|
|
logger.info(
|
|
f"Game {game_id} created successfully - status: {state.status}"
|
|
)
|
|
|
|
return CreateGameResponse(
|
|
game_id=str(game_id),
|
|
message=f"Game '{request.name}' created successfully. Add lineups to start the game.",
|
|
status=state.status,
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.exception(f"Failed to create game: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to create game: {str(e)}")
|
|
|
|
|
|
@router.post("/quick-create", response_model=CreateGameResponse)
|
|
async def quick_create_game(
|
|
request: QuickCreateRequest | None = None,
|
|
user: dict | None = Depends(get_current_user_optional),
|
|
):
|
|
"""
|
|
Quick-create endpoint for testing - creates a game with auto-generated lineups.
|
|
|
|
If home_team_id and away_team_id are provided, fetches rosters from SBA API
|
|
and auto-generates lineups. Otherwise uses default teams (35 vs 38) with
|
|
pre-configured lineups.
|
|
|
|
The authenticated user's discord_id is stored as creator_discord_id, allowing
|
|
them to control teams regardless of actual team ownership.
|
|
|
|
Args:
|
|
request: Optional request body with custom team IDs
|
|
|
|
Returns:
|
|
CreateGameResponse with game_id
|
|
"""
|
|
try:
|
|
# Generate game ID
|
|
game_id = uuid4()
|
|
league_id = "sba"
|
|
|
|
# Determine team IDs
|
|
use_custom_teams = (
|
|
request is not None
|
|
and request.home_team_id is not None
|
|
and request.away_team_id is not None
|
|
)
|
|
|
|
if use_custom_teams:
|
|
home_team_id = request.home_team_id
|
|
away_team_id = request.away_team_id
|
|
else:
|
|
# Default demo teams
|
|
home_team_id = 35
|
|
away_team_id = 38
|
|
|
|
# Get creator's discord_id from authenticated user
|
|
creator_discord_id = user.get("discord_id") if user else None
|
|
|
|
logger.info(
|
|
f"Quick-creating game {game_id}: {home_team_id} vs {away_team_id} "
|
|
f"(creator: {creator_discord_id}, custom_teams: {use_custom_teams})"
|
|
)
|
|
|
|
# Create game in state manager
|
|
state = await state_manager.create_game(
|
|
game_id=game_id,
|
|
league_id=league_id,
|
|
home_team_id=home_team_id,
|
|
away_team_id=away_team_id,
|
|
creator_discord_id=creator_discord_id,
|
|
)
|
|
|
|
# Save to database
|
|
db_ops = DatabaseOperations()
|
|
await db_ops.create_game(
|
|
game_id=game_id,
|
|
league_id=league_id,
|
|
home_team_id=home_team_id,
|
|
away_team_id=away_team_id,
|
|
game_mode="friendly",
|
|
visibility="public",
|
|
)
|
|
|
|
if use_custom_teams:
|
|
# Fetch rosters from SBA API and build lineups dynamically
|
|
await _build_lineup_from_roster(game_id, home_team_id, season=13)
|
|
await _build_lineup_from_roster(game_id, away_team_id, season=13)
|
|
else:
|
|
# Use pre-configured demo lineups
|
|
# Submit home lineup (Team 35)
|
|
home_lineup_data = [
|
|
{"player_id": 1417, "position": "C", "batting_order": 1},
|
|
{"player_id": 1186, "position": "1B", "batting_order": 2},
|
|
{"player_id": 1381, "position": "2B", "batting_order": 3},
|
|
{"player_id": 1576, "position": "3B", "batting_order": 4},
|
|
{"player_id": 1242, "position": "SS", "batting_order": 5},
|
|
{"player_id": 1600, "position": "LF", "batting_order": 6},
|
|
{"player_id": 1675, "position": "CF", "batting_order": 7},
|
|
{"player_id": 1700, "position": "RF", "batting_order": 8},
|
|
{"player_id": 1759, "position": "DH", "batting_order": 9},
|
|
{"player_id": 1948, "position": "P", "batting_order": None},
|
|
]
|
|
|
|
for player in home_lineup_data:
|
|
await lineup_service.add_sba_player_to_lineup(
|
|
game_id=game_id,
|
|
team_id=home_team_id,
|
|
player_id=player["player_id"],
|
|
position=player["position"],
|
|
batting_order=player["batting_order"],
|
|
is_starter=True,
|
|
)
|
|
|
|
# Submit away lineup (Team 38)
|
|
away_lineup_data = [
|
|
{"player_id": 1080, "position": "C", "batting_order": 1},
|
|
{"player_id": 1148, "position": "1B", "batting_order": 2},
|
|
{"player_id": 1166, "position": "2B", "batting_order": 3},
|
|
{"player_id": 1513, "position": "3B", "batting_order": 4},
|
|
{"player_id": 1209, "position": "SS", "batting_order": 5},
|
|
{"player_id": 1735, "position": "LF", "batting_order": 6},
|
|
{"player_id": 1665, "position": "CF", "batting_order": 7},
|
|
{"player_id": 1961, "position": "RF", "batting_order": 8},
|
|
{"player_id": 1980, "position": "DH", "batting_order": 9},
|
|
{"player_id": 2005, "position": "P", "batting_order": None},
|
|
]
|
|
|
|
for player in away_lineup_data:
|
|
await lineup_service.add_sba_player_to_lineup(
|
|
game_id=game_id,
|
|
team_id=away_team_id,
|
|
player_id=player["player_id"],
|
|
position=player["position"],
|
|
batting_order=player["batting_order"],
|
|
is_starter=True,
|
|
)
|
|
|
|
# Start the game
|
|
await game_engine.start_game(game_id)
|
|
|
|
logger.info(f"Quick-created game {game_id} and started successfully")
|
|
|
|
return CreateGameResponse(
|
|
game_id=str(game_id),
|
|
message=f"Game quick-created with Team {home_team_id} vs Team {away_team_id}. Ready to play!",
|
|
status="active",
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Failed to quick-create game: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to quick-create game: {str(e)}")
|
|
|
|
|
|
async def _build_lineup_from_roster(game_id: UUID, team_id: int, season: int = 13) -> None:
|
|
"""
|
|
Build a lineup from SBA API roster data.
|
|
|
|
Fetches the team roster and assigns players to positions based on their
|
|
listed positions. Uses a simple algorithm to fill all positions:
|
|
- First player at each position gets assigned
|
|
- DH filled with extra player if available
|
|
- Batting order assigned by position priority
|
|
|
|
Args:
|
|
game_id: Game UUID
|
|
team_id: Team ID to fetch roster for
|
|
season: Season number (default 13)
|
|
"""
|
|
# Fetch roster from SBA API
|
|
roster = await sba_api_client.get_roster(team_id=team_id, season=season)
|
|
|
|
if not roster:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"No roster found for team {team_id} in season {season}",
|
|
)
|
|
|
|
# Position priority for lineup building
|
|
# Standard DH lineup: 9 fielders + DH, pitcher doesn't bat
|
|
positions_needed = ["C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "P"]
|
|
lineup_positions: dict[str, dict] = {}
|
|
used_player_ids: set[int] = set()
|
|
|
|
# First pass: assign players to their primary positions
|
|
for player in roster:
|
|
player_id = player.get("id")
|
|
player_positions = player.get("position", "").split("/")
|
|
|
|
if not player_id or player_id in used_player_ids:
|
|
continue
|
|
|
|
# Try to assign to first matching needed position
|
|
for pos in player_positions:
|
|
pos = pos.strip().upper()
|
|
if pos in positions_needed and pos not in lineup_positions:
|
|
lineup_positions[pos] = {
|
|
"player_id": player_id,
|
|
"position": pos,
|
|
}
|
|
used_player_ids.add(player_id)
|
|
break
|
|
|
|
# Check we have all required positions
|
|
missing = [p for p in positions_needed if p not in lineup_positions]
|
|
if missing:
|
|
# Second pass: try to fill missing positions with any available player
|
|
for pos in missing:
|
|
for player in roster:
|
|
player_id = player.get("id")
|
|
if player_id and player_id not in used_player_ids:
|
|
lineup_positions[pos] = {
|
|
"player_id": player_id,
|
|
"position": pos,
|
|
}
|
|
used_player_ids.add(player_id)
|
|
break
|
|
|
|
# Still missing? Raise error
|
|
still_missing = [p for p in positions_needed if p not in lineup_positions]
|
|
if still_missing:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Could not fill positions {still_missing} for team {team_id}",
|
|
)
|
|
|
|
# Find DH - use next available player not in lineup
|
|
dh_player = None
|
|
for player in roster:
|
|
player_id = player.get("id")
|
|
if player_id and player_id not in used_player_ids:
|
|
dh_player = {"player_id": player_id, "position": "DH"}
|
|
used_player_ids.add(player_id)
|
|
break
|
|
|
|
if not dh_player:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Not enough players for DH on team {team_id}",
|
|
)
|
|
|
|
# Build final lineup with batting order
|
|
# Batting order: C, 1B, 2B, 3B, SS, LF, CF, RF, DH
|
|
batting_order_positions = ["C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "DH"]
|
|
|
|
for order, pos in enumerate(batting_order_positions, start=1):
|
|
if pos == "DH":
|
|
player_data = dh_player
|
|
else:
|
|
player_data = lineup_positions[pos]
|
|
|
|
await lineup_service.add_sba_player_to_lineup(
|
|
game_id=game_id,
|
|
team_id=team_id,
|
|
player_id=player_data["player_id"],
|
|
position=player_data["position"],
|
|
batting_order=order,
|
|
is_starter=True,
|
|
)
|
|
|
|
# Add pitcher (no batting order in DH lineup)
|
|
pitcher_data = lineup_positions["P"]
|
|
await lineup_service.add_sba_player_to_lineup(
|
|
game_id=game_id,
|
|
team_id=team_id,
|
|
player_id=pitcher_data["player_id"],
|
|
position="P",
|
|
batting_order=None,
|
|
is_starter=True,
|
|
)
|
|
|
|
logger.info(f"Built lineup for team {team_id} with {len(batting_order_positions) + 1} players")
|
|
|
|
|
|
@router.post("/{game_id}/lineups", response_model=SubmitLineupsResponse)
|
|
async def submit_lineups(game_id: str, request: SubmitLineupsRequest):
|
|
"""
|
|
Submit lineups for both teams.
|
|
|
|
Accepts complete lineups for home and away teams. Supports both:
|
|
- 9-player lineups (pitcher bats, no DH)
|
|
- 10-player lineups (universal DH, pitcher doesn't bat)
|
|
|
|
Args:
|
|
game_id: Game identifier
|
|
request: Lineup data for both teams
|
|
|
|
Returns:
|
|
Confirmation with lineup counts
|
|
|
|
Raises:
|
|
400: Invalid lineup structure or validation error
|
|
404: Game not found
|
|
500: Database or API error
|
|
"""
|
|
try:
|
|
# Validate game_id format
|
|
try:
|
|
game_uuid = UUID(game_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid game_id format")
|
|
|
|
logger.info(f"Submitting lineups for game {game_id}")
|
|
|
|
# Get game state from memory or load basic info from database
|
|
state = state_manager.get_state(game_uuid)
|
|
if not state:
|
|
logger.info(f"Game {game_id} not in memory, loading from database")
|
|
|
|
# Load basic game info from database
|
|
db_ops = DatabaseOperations()
|
|
game_data = await db_ops.load_game_state(game_uuid)
|
|
|
|
if not game_data:
|
|
raise HTTPException(status_code=404, detail=f"Game {game_id} not found")
|
|
|
|
game_info = game_data['game']
|
|
|
|
# Recreate game state in memory (without lineups - we're about to add them)
|
|
state = await state_manager.create_game(
|
|
game_id=game_uuid,
|
|
league_id=game_info['league_id'],
|
|
home_team_id=game_info['home_team_id'],
|
|
away_team_id=game_info['away_team_id'],
|
|
home_team_is_ai=game_info.get('home_team_is_ai', False),
|
|
away_team_is_ai=game_info.get('away_team_is_ai', False),
|
|
auto_mode=game_info.get('auto_mode', False)
|
|
)
|
|
logger.info(f"Recreated game {game_id} in memory from database")
|
|
|
|
# Process home team lineup
|
|
home_count = 0
|
|
for player in request.home_lineup:
|
|
await lineup_service.add_sba_player_to_lineup(
|
|
game_id=game_uuid,
|
|
team_id=state.home_team_id,
|
|
player_id=player.player_id,
|
|
position=player.position,
|
|
batting_order=player.batting_order,
|
|
is_starter=True,
|
|
)
|
|
home_count += 1
|
|
|
|
logger.info(f"Added {home_count} players to home team lineup")
|
|
|
|
# Process away team lineup
|
|
away_count = 0
|
|
for player in request.away_lineup:
|
|
await lineup_service.add_sba_player_to_lineup(
|
|
game_id=game_uuid,
|
|
team_id=state.away_team_id,
|
|
player_id=player.player_id,
|
|
position=player.position,
|
|
batting_order=player.batting_order,
|
|
is_starter=True,
|
|
)
|
|
away_count += 1
|
|
|
|
logger.info(f"Added {away_count} players to away team lineup")
|
|
|
|
# Automatically start the game after lineups are submitted
|
|
from app.core.game_engine import game_engine
|
|
try:
|
|
await game_engine.start_game(game_uuid)
|
|
logger.info(f"Game {game_id} started successfully after lineup submission")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to auto-start game {game_id}: {e}")
|
|
# Don't fail the lineup submission if game start fails
|
|
# User can manually start if needed
|
|
|
|
return SubmitLineupsResponse(
|
|
game_id=game_id,
|
|
message=f"Lineups submitted successfully. Home: {home_count} players, Away: {away_count} players.",
|
|
home_lineup_count=home_count,
|
|
away_lineup_count=away_count,
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.exception(f"Failed to submit lineups for game {game_id}: {e}")
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Failed to submit lineups: {str(e)}"
|
|
)
|