CLAUDE: Fix pitcher/catcher recovery and lineup data format
Backend fixes: - state_manager: Properly recover current_pitcher and current_catcher from fielding team during game state recovery (fixes pitcher badge not showing) - handlers: Add headshot field to lineup data, use lineup_service for proper player data loading on cache miss - lineup_service: Minor adjustments for headshot support Frontend fixes: - player.ts: Update Lineup type to match WebSocket event format - lineup_id (was 'id'), card_id fields - player.headshot for UI circles - Optional fields for event variations - CurrentSituation.vue: Adapt to updated type structure - Substitution selectors: Use updated Lineup type fields This fixes the issue where pitcher badge wouldn't show after game recovery because current_pitcher was being set from batting team instead of fielding team. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9546d2a370
commit
1a562a75d2
@ -272,43 +272,70 @@ class StateManager:
|
||||
is_active=lineup.get('is_active', True)
|
||||
)
|
||||
|
||||
# Get placeholder current_batter (required field)
|
||||
# _prepare_next_play() will set the correct batter after recovery
|
||||
# Determine fielding team based on current half
|
||||
current_half = game.get('current_half', 'top')
|
||||
home_team_id = game['home_team_id']
|
||||
away_team_id = game['away_team_id']
|
||||
|
||||
if current_half == 'top':
|
||||
# Top of inning: away team batting, home team fielding
|
||||
batting_team_id = away_team_id
|
||||
fielding_team_id = home_team_id
|
||||
else:
|
||||
# Bottom of inning: home team batting, away team fielding
|
||||
batting_team_id = home_team_id
|
||||
fielding_team_id = away_team_id
|
||||
|
||||
# Get current batter from batting team (player with batting_order 1 as placeholder)
|
||||
current_batter_placeholder = None
|
||||
for lineup in lineups:
|
||||
if lineup.get('batting_order') == 1 and lineup.get('is_active'):
|
||||
if (lineup.get('team_id') == batting_team_id and
|
||||
lineup.get('batting_order') == 1 and
|
||||
lineup.get('is_active')):
|
||||
current_batter_placeholder = get_lineup_player(lineup['id'])
|
||||
break
|
||||
|
||||
# If no batter found, use first available lineup
|
||||
if not current_batter_placeholder and lineups:
|
||||
first_lineup = lineups[0]
|
||||
current_batter_placeholder = LineupPlayerState(
|
||||
lineup_id=first_lineup['id'],
|
||||
card_id=first_lineup.get('card_id') or 0,
|
||||
position=first_lineup['position'],
|
||||
batting_order=first_lineup.get('batting_order'),
|
||||
is_active=True
|
||||
)
|
||||
# If no batter found, use first available lineup from batting team
|
||||
if not current_batter_placeholder:
|
||||
for lineup in lineups:
|
||||
if lineup.get('team_id') == batting_team_id and lineup.get('is_active'):
|
||||
current_batter_placeholder = get_lineup_player(lineup['id'])
|
||||
break
|
||||
|
||||
# If still no batter (no lineups at all), raise error - game is in invalid state
|
||||
if not current_batter_placeholder:
|
||||
raise ValueError(f"Cannot recover game {game['id']}: No lineups found")
|
||||
raise ValueError(f"Cannot recover game {game['id']}: No lineups found for batting team")
|
||||
|
||||
# Get current pitcher and catcher from fielding team
|
||||
current_pitcher = None
|
||||
current_catcher = None
|
||||
for lineup in lineups:
|
||||
if lineup.get('team_id') == fielding_team_id and lineup.get('is_active'):
|
||||
if lineup.get('position') == 'P' and not current_pitcher:
|
||||
current_pitcher = get_lineup_player(lineup['id'])
|
||||
elif lineup.get('position') == 'C' and not current_catcher:
|
||||
current_catcher = get_lineup_player(lineup['id'])
|
||||
|
||||
# Stop if we found both
|
||||
if current_pitcher and current_catcher:
|
||||
break
|
||||
|
||||
state = GameState(
|
||||
game_id=game['id'],
|
||||
league_id=game['league_id'],
|
||||
home_team_id=game['home_team_id'],
|
||||
away_team_id=game['away_team_id'],
|
||||
home_team_id=home_team_id,
|
||||
away_team_id=away_team_id,
|
||||
home_team_is_ai=game.get('home_team_is_ai', False),
|
||||
away_team_is_ai=game.get('away_team_is_ai', False),
|
||||
status=game['status'],
|
||||
inning=game.get('current_inning', 1),
|
||||
half=game.get('current_half', 'top'),
|
||||
half=current_half,
|
||||
home_score=game.get('home_score', 0),
|
||||
away_score=game.get('away_score', 0),
|
||||
play_count=len(game_data.get('plays', [])),
|
||||
current_batter=current_batter_placeholder # Placeholder - corrected by _prepare_next_play()
|
||||
current_batter=current_batter_placeholder,
|
||||
current_pitcher=current_pitcher,
|
||||
current_catcher=current_catcher
|
||||
)
|
||||
|
||||
# Get last completed play to recover runner state and batter indices
|
||||
|
||||
@ -46,7 +46,8 @@ class LineupPlayerState(BaseModel):
|
||||
|
||||
# Player data (loaded at game start from SBA/PD API)
|
||||
player_name: Optional[str] = None
|
||||
player_image: Optional[str] = None
|
||||
player_image: Optional[str] = None # Card image
|
||||
player_headshot: Optional[str] = None # Headshot for UI circles
|
||||
|
||||
# Phase 3E-Main: Position rating (loaded at game start for PD league)
|
||||
position_rating: Optional['PositionRating'] = None
|
||||
|
||||
@ -30,7 +30,8 @@ class LineupEntryWithPlayer:
|
||||
is_active: bool
|
||||
# Player data from API
|
||||
player_name: str
|
||||
player_image: str
|
||||
player_image: str # Card image
|
||||
player_headshot: str # Headshot for UI circles
|
||||
|
||||
|
||||
class LineupService:
|
||||
@ -88,11 +89,13 @@ class LineupService:
|
||||
# Step 2: Fetch player data from SBA API
|
||||
player_name = f"Player #{player_id}"
|
||||
player_image = ""
|
||||
player_headshot = ""
|
||||
|
||||
try:
|
||||
player = await sba_api_client.get_player(player_id)
|
||||
player_name = player.name
|
||||
player_image = player.get_image_url()
|
||||
player_headshot = player.headshot or ""
|
||||
logger.info(f"Loaded player data for {player_id}: {player_name}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch player data for {player_id}: {e}")
|
||||
@ -107,7 +110,8 @@ class LineupService:
|
||||
is_starter=is_starter,
|
||||
is_active=True,
|
||||
player_name=player_name,
|
||||
player_image=player_image
|
||||
player_image=player_image,
|
||||
player_headshot=player_headshot
|
||||
)
|
||||
|
||||
async def load_team_lineup_with_player_data(
|
||||
@ -152,11 +156,13 @@ class LineupService:
|
||||
for p in lineup_entries:
|
||||
player_name = None
|
||||
player_image = None
|
||||
player_headshot = None
|
||||
|
||||
if league_id == 'sba' and p.player_id and player_data.get(p.player_id): # type: ignore[arg-type]
|
||||
player = player_data.get(p.player_id) # type: ignore[arg-type]
|
||||
player_name = player.name
|
||||
player_image = player.get_image_url()
|
||||
player_headshot = player.headshot
|
||||
|
||||
players.append(LineupPlayerState(
|
||||
lineup_id=p.id, # type: ignore[arg-type]
|
||||
@ -166,7 +172,8 @@ class LineupService:
|
||||
is_active=p.is_active, # type: ignore[arg-type]
|
||||
is_starter=p.is_starter, # type: ignore[arg-type]
|
||||
player_name=player_name,
|
||||
player_image=player_image
|
||||
player_image=player_image,
|
||||
player_headshot=player_headshot
|
||||
))
|
||||
|
||||
return TeamLineupState(team_id=team_id, players=players)
|
||||
|
||||
@ -15,6 +15,7 @@ from app.core.validators import ValidationError as GameValidationError
|
||||
from app.config.result_charts import PlayOutcome
|
||||
from app.database.operations import DatabaseOperations
|
||||
from app.services.sba_api_client import sba_api_client
|
||||
from app.services.lineup_service import lineup_service
|
||||
|
||||
logger = logging.getLogger(f'{__name__}.handlers')
|
||||
|
||||
@ -1022,7 +1023,8 @@ def register_handlers(sio: AsyncServer, manager: ConnectionManager) -> None:
|
||||
"player": {
|
||||
"id": p.card_id,
|
||||
"name": p.player_name or f"Player #{p.card_id}",
|
||||
"image": p.player_image or ""
|
||||
"image": p.player_image or "",
|
||||
"headshot": p.player_headshot or ""
|
||||
}
|
||||
}
|
||||
for p in lineup.players if p.is_active
|
||||
@ -1031,13 +1033,22 @@ def register_handlers(sio: AsyncServer, manager: ConnectionManager) -> None:
|
||||
)
|
||||
logger.info(f"Lineup data sent for game {game_id}, team {team_id}")
|
||||
else:
|
||||
# Lineup not in cache - try to load from database
|
||||
db_ops = DatabaseOperations()
|
||||
lineup_entries = await db_ops.get_active_lineup(game_id, team_id)
|
||||
# Lineup not in cache - try to load from database with player data
|
||||
# Get league_id from game state or database
|
||||
state = state_manager.get_state(game_id)
|
||||
league_id = state.league_id if state else "sba"
|
||||
|
||||
if lineup_entries:
|
||||
# Note: Player data not available when loading directly from DB
|
||||
# This is a cache miss scenario - normally lineups are cached with player data
|
||||
lineup_state = await lineup_service.load_team_lineup_with_player_data(
|
||||
game_id=game_id,
|
||||
team_id=team_id,
|
||||
league_id=league_id
|
||||
)
|
||||
|
||||
if lineup_state:
|
||||
# Cache the lineup for future requests
|
||||
state_manager.set_lineup(game_id, team_id, lineup_state)
|
||||
|
||||
# Send lineup data with player info
|
||||
await manager.emit_to_user(
|
||||
sid,
|
||||
"lineup_data",
|
||||
@ -1046,23 +1057,23 @@ def register_handlers(sio: AsyncServer, manager: ConnectionManager) -> None:
|
||||
"team_id": team_id,
|
||||
"players": [
|
||||
{
|
||||
"lineup_id": entry.id, # type: ignore[assignment]
|
||||
"card_id": entry.card_id or entry.player_id, # type: ignore[assignment]
|
||||
"position": entry.position, # type: ignore[assignment]
|
||||
"batting_order": entry.batting_order, # type: ignore[assignment]
|
||||
"is_active": entry.is_active, # type: ignore[assignment]
|
||||
"is_starter": entry.is_starter, # type: ignore[assignment]
|
||||
"lineup_id": p.lineup_id,
|
||||
"card_id": p.card_id,
|
||||
"position": p.position,
|
||||
"batting_order": p.batting_order,
|
||||
"is_active": p.is_active,
|
||||
"is_starter": p.is_starter,
|
||||
"player": {
|
||||
"id": entry.card_id or entry.player_id, # type: ignore[assignment]
|
||||
"name": f"Player #{entry.card_id or entry.player_id}", # type: ignore[assignment]
|
||||
"image": ""
|
||||
"id": p.card_id,
|
||||
"name": p.player_name or f"Player #{p.card_id}",
|
||||
"image": p.player_image or ""
|
||||
}
|
||||
}
|
||||
for entry in lineup_entries
|
||||
for p in lineup_state.players if p.is_active
|
||||
]
|
||||
}
|
||||
)
|
||||
logger.info(f"Lineup data loaded from DB for game {game_id}, team {team_id} (without player details)")
|
||||
logger.info(f"Lineup data loaded from DB with player data for game {game_id}, team {team_id}")
|
||||
else:
|
||||
await manager.emit_to_user(
|
||||
sid,
|
||||
|
||||
@ -11,8 +11,8 @@
|
||||
<!-- Pitcher Image/Badge -->
|
||||
<div class="w-12 h-12 rounded-full flex items-center justify-center shadow-lg flex-shrink-0 overflow-hidden">
|
||||
<img
|
||||
v-if="pitcherPlayer?.image"
|
||||
:src="pitcherPlayer.image"
|
||||
v-if="pitcherPlayer?.headshot"
|
||||
:src="pitcherPlayer.headshot"
|
||||
:alt="pitcherName"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
@ -52,8 +52,8 @@
|
||||
<!-- Batter Image/Badge -->
|
||||
<div class="w-12 h-12 rounded-full flex items-center justify-center shadow-lg flex-shrink-0 overflow-hidden">
|
||||
<img
|
||||
v-if="batterPlayer?.image"
|
||||
:src="batterPlayer.image"
|
||||
v-if="batterPlayer?.headshot"
|
||||
:src="batterPlayer.headshot"
|
||||
:alt="batterName"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
@ -90,8 +90,8 @@
|
||||
<!-- Pitcher Image/Badge -->
|
||||
<div class="w-16 h-16 rounded-full flex items-center justify-center shadow-xl flex-shrink-0 overflow-hidden">
|
||||
<img
|
||||
v-if="pitcherPlayer?.image"
|
||||
:src="pitcherPlayer.image"
|
||||
v-if="pitcherPlayer?.headshot"
|
||||
:src="pitcherPlayer.headshot"
|
||||
:alt="pitcherName"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
@ -124,8 +124,8 @@
|
||||
<!-- Batter Image/Badge -->
|
||||
<div class="w-16 h-16 rounded-full flex items-center justify-center shadow-xl flex-shrink-0 overflow-hidden">
|
||||
<img
|
||||
v-if="batterPlayer?.image"
|
||||
:src="batterPlayer.image"
|
||||
v-if="batterPlayer?.headshot"
|
||||
:src="batterPlayer.headshot"
|
||||
:alt="batterName"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
<div v-else class="bench-grid">
|
||||
<button
|
||||
v-for="player in filteredBenchPlayers"
|
||||
:key="player.id"
|
||||
:key="player.lineup_id"
|
||||
:class="[
|
||||
'bench-player-card',
|
||||
selectedPlayerId === player.player.id ? 'bench-player-selected' : 'bench-player-default'
|
||||
@ -194,7 +194,7 @@ const handleSubmit = () => {
|
||||
}
|
||||
|
||||
emit('submit', {
|
||||
playerOutLineupId: props.playerOut.id,
|
||||
playerOutLineupId: props.playerOut.lineup_id,
|
||||
playerInCardId: selectedPlayer.value.player.id,
|
||||
newPosition: selectedPosition.value,
|
||||
teamId: props.teamId,
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
<div v-else class="bench-grid">
|
||||
<button
|
||||
v-for="player in availableBenchPlayers"
|
||||
:key="player.id"
|
||||
:key="player.lineup_id"
|
||||
:class="[
|
||||
'bench-player-card',
|
||||
selectedPlayerId === player.player.id ? 'bench-player-selected' : 'bench-player-default'
|
||||
@ -138,7 +138,7 @@ const handleSubmit = () => {
|
||||
if (!canSubmit.value || !props.playerOut || !selectedPlayer.value) return
|
||||
|
||||
emit('submit', {
|
||||
playerOutLineupId: props.playerOut.id,
|
||||
playerOutLineupId: props.playerOut.lineup_id,
|
||||
playerInCardId: selectedPlayer.value.player.id,
|
||||
teamId: props.teamId,
|
||||
})
|
||||
|
||||
@ -171,7 +171,7 @@ const handleSubmit = () => {
|
||||
}
|
||||
|
||||
emit('submit', {
|
||||
playerOutLineupId: props.currentPitcher.id,
|
||||
playerOutLineupId: props.currentPitcher.lineup_id,
|
||||
playerInCardId: selectedPitcher.value.player.id,
|
||||
teamId: props.teamId,
|
||||
})
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
<div class="player-grid">
|
||||
<button
|
||||
v-for="player in activeFielders"
|
||||
:key="player.id"
|
||||
:key="player.lineup_id"
|
||||
class="player-button"
|
||||
@click="selectDefensivePlayer(player)"
|
||||
>
|
||||
|
||||
@ -335,7 +335,7 @@ export const useGameStore = defineStore('game', () => {
|
||||
*/
|
||||
function findPlayerInLineup(lineupId: number): Lineup | undefined {
|
||||
return [...homeLineup.value, ...awayLineup.value].find(
|
||||
p => p.id === lineupId
|
||||
p => p.lineup_id === lineupId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -48,26 +48,31 @@ export interface SbaPlayer {
|
||||
|
||||
/**
|
||||
* Lineup entry - player assignment in a game
|
||||
* Backend: Lineup (db model)
|
||||
* Backend: Lineup (db model) via WebSocket lineup_data event
|
||||
*/
|
||||
export interface Lineup {
|
||||
id: number
|
||||
game_id: string
|
||||
team_id: number
|
||||
player_id: number
|
||||
lineup_id: number // Unique lineup entry ID (was 'id')
|
||||
card_id: number // Player/card ID
|
||||
game_id?: string // Optional - not always sent in events
|
||||
team_id?: number // Optional - not always sent in events
|
||||
position: string
|
||||
batting_order: number | null
|
||||
is_starter: boolean
|
||||
is_active: boolean
|
||||
entered_inning: number
|
||||
entered_inning?: number // Optional - not always sent in events
|
||||
|
||||
// Substitution tracking
|
||||
replacing_id: number | null
|
||||
after_play: number | null
|
||||
is_fatigued: boolean
|
||||
// Substitution tracking (optional - not always sent in events)
|
||||
replacing_id?: number | null
|
||||
after_play?: number | null
|
||||
is_fatigued?: boolean
|
||||
|
||||
// Player data (embedded)
|
||||
player: SbaPlayer
|
||||
// Player data (embedded from SBA API)
|
||||
player: {
|
||||
id: number
|
||||
name: string
|
||||
image: string // Card image
|
||||
headshot: string // Headshot for UI circles
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user