Created centralized services for SBA player data fetching at lineup creation: Backend - New Services: - app/services/sba_api_client.py: REST client for SBA API (api.sba.manticorum.com) with batch player fetching and caching support - app/services/lineup_service.py: High-level service combining DB operations with API calls for complete lineup entries with player data Backend - Refactored Components: - app/core/game_engine.py: Replaced raw API calls with LineupService, reduced _prepare_next_play() from ~50 lines to ~15 lines - app/core/substitution_manager.py: Updated pinch_hit(), defensive_replace(), change_pitcher() to use lineup_service.get_sba_player_data() - app/models/game_models.py: Added player_name/player_image to LineupPlayerState - app/services/__init__.py: Exported new LineupService components - app/websocket/handlers.py: Enhanced lineup state handling Frontend - SBA League: - components/Game/CurrentSituation.vue: Restored player images with fallback badges (P/B letters) for both mobile and desktop layouts - components/Game/GameBoard.vue: Enhanced game board visualization - composables/useGameActions.ts: Updated game action handling - composables/useWebSocket.ts: Improved WebSocket state management - pages/games/[id].vue: Enhanced game page with better state handling - types/game.ts: Updated type definitions - types/websocket.ts: Added WebSocket type support Architecture Improvement: All SBA player data fetching now goes through LineupService: - Lineup creation: add_sba_player_to_lineup() - Lineup loading: load_team_lineup_with_player_data() - Substitutions: get_sba_player_data() All 739 unit tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
362 lines
7.8 KiB
TypeScript
362 lines
7.8 KiB
TypeScript
/**
|
|
* WebSocket Event Types
|
|
*
|
|
* TypeScript definitions for all Socket.io events between client and server.
|
|
* Ensures type safety for real-time communication.
|
|
*
|
|
* Backend Reference: app/websocket/handlers.py
|
|
*/
|
|
|
|
import type {
|
|
GameState,
|
|
PlayResult,
|
|
DecisionPrompt,
|
|
RollData,
|
|
DefensiveDecision,
|
|
OffensiveDecision,
|
|
ManualOutcomeSubmission,
|
|
} from './game'
|
|
|
|
import type {
|
|
SubstitutionRequest,
|
|
SubstitutionResult,
|
|
SubstitutionError,
|
|
LineupDataResponse,
|
|
} from './player'
|
|
|
|
/**
|
|
* WebSocket connection auth
|
|
*/
|
|
export interface SocketAuth {
|
|
token: string
|
|
}
|
|
|
|
/**
|
|
* Client → Server Events (what client can emit)
|
|
*/
|
|
export interface ClientToServerEvents {
|
|
// Connection management
|
|
join_game: (data: JoinGameRequest) => void
|
|
leave_game: (data: LeaveGameRequest) => void
|
|
heartbeat: () => void
|
|
|
|
// Strategic decisions
|
|
submit_defensive_decision: (data: DefensiveDecisionRequest) => void
|
|
submit_offensive_decision: (data: OffensiveDecisionRequest) => void
|
|
|
|
// Manual outcome workflow
|
|
roll_dice: (data: RollDiceRequest) => void
|
|
submit_manual_outcome: (data: SubmitManualOutcomeRequest) => void
|
|
|
|
// Substitutions
|
|
request_pinch_hitter: (data: PinchHitterRequest) => void
|
|
request_defensive_replacement: (data: DefensiveReplacementRequest) => void
|
|
request_pitching_change: (data: PitchingChangeRequest) => void
|
|
|
|
// Data requests
|
|
get_lineup: (data: GetLineupRequest) => void
|
|
get_box_score: (data: GetBoxScoreRequest) => void
|
|
request_game_state: (data: RequestGameStateRequest) => void
|
|
}
|
|
|
|
/**
|
|
* Server → Client Events (what client can receive)
|
|
*/
|
|
export interface ServerToClientEvents {
|
|
// Connection events
|
|
connected: (data: ConnectedEvent) => void
|
|
game_joined: (data: GameJoinedEvent) => void
|
|
user_connected: (data: UserConnectedEvent) => void
|
|
user_disconnected: (data: UserDisconnectedEvent) => void
|
|
heartbeat_ack: () => void
|
|
|
|
// Decision events
|
|
defensive_decision_submitted: (data: DefensiveDecisionSubmittedEvent) => void
|
|
offensive_decision_submitted: (data: OffensiveDecisionSubmittedEvent) => void
|
|
decision_required: (data: DecisionPrompt) => void
|
|
|
|
// Game state events
|
|
game_state: (data: GameState) => void
|
|
game_state_update: (data: GameState) => void
|
|
game_state_sync: (data: GameStateSyncEvent) => void
|
|
play_completed: (data: PlayResult) => void
|
|
inning_change: (data: InningChangeEvent) => void
|
|
game_ended: (data: GameEndedEvent) => void
|
|
|
|
// Manual workflow events
|
|
dice_rolled: (data: DiceRolledEvent) => void
|
|
outcome_accepted: (data: OutcomeAcceptedEvent) => void
|
|
|
|
// Substitution events
|
|
player_substituted: (data: SubstitutionResult) => void
|
|
substitution_confirmed: (data: SubstitutionConfirmedEvent) => void
|
|
|
|
// Data responses
|
|
lineup_data: (data: LineupDataResponse) => void
|
|
box_score_data: (data: BoxScoreDataEvent) => void
|
|
|
|
// Error events
|
|
error: (data: ErrorEvent) => void
|
|
outcome_rejected: (data: OutcomeRejectedEvent) => void
|
|
substitution_error: (data: SubstitutionError) => void
|
|
invalid_action: (data: InvalidActionEvent) => void
|
|
connection_error: (data: ConnectionErrorEvent) => void
|
|
}
|
|
|
|
// ============================================================================
|
|
// Client → Server Request Types
|
|
// ============================================================================
|
|
|
|
export interface JoinGameRequest {
|
|
game_id: string
|
|
role: 'player' | 'spectator'
|
|
}
|
|
|
|
export interface LeaveGameRequest {
|
|
game_id: string
|
|
}
|
|
|
|
export interface DefensiveDecisionRequest {
|
|
game_id: string
|
|
infield_depth: DefensiveDecision['infield_depth']
|
|
outfield_depth: DefensiveDecision['outfield_depth']
|
|
hold_runners: number[]
|
|
}
|
|
|
|
export interface OffensiveDecisionRequest {
|
|
game_id: string
|
|
action: OffensiveDecision['action']
|
|
steal_attempts: number[]
|
|
}
|
|
|
|
export interface RollDiceRequest {
|
|
game_id: string
|
|
}
|
|
|
|
export interface SubmitManualOutcomeRequest {
|
|
game_id: string
|
|
outcome: string
|
|
hit_location?: string
|
|
}
|
|
|
|
export interface PinchHitterRequest {
|
|
game_id: string
|
|
player_out_lineup_id: number
|
|
player_in_card_id: number
|
|
team_id: number
|
|
}
|
|
|
|
export interface DefensiveReplacementRequest {
|
|
game_id: string
|
|
player_out_lineup_id: number
|
|
player_in_card_id: number
|
|
new_position: string
|
|
team_id: number
|
|
}
|
|
|
|
export interface PitchingChangeRequest {
|
|
game_id: string
|
|
player_out_lineup_id: number
|
|
player_in_card_id: number
|
|
team_id: number
|
|
}
|
|
|
|
export interface GetLineupRequest {
|
|
game_id: string
|
|
team_id: number
|
|
}
|
|
|
|
export interface GetBoxScoreRequest {
|
|
game_id: string
|
|
}
|
|
|
|
export interface RequestGameStateRequest {
|
|
game_id: string
|
|
}
|
|
|
|
// ============================================================================
|
|
// Server → Client Event Types
|
|
// ============================================================================
|
|
|
|
export interface ConnectedEvent {
|
|
user_id: string
|
|
}
|
|
|
|
export interface GameJoinedEvent {
|
|
game_id: string
|
|
role: 'player' | 'spectator'
|
|
}
|
|
|
|
export interface UserConnectedEvent {
|
|
user_id: string
|
|
game_id: string
|
|
}
|
|
|
|
export interface UserDisconnectedEvent {
|
|
user_id: string
|
|
game_id: string
|
|
}
|
|
|
|
export interface DefensiveDecisionSubmittedEvent {
|
|
game_id: string
|
|
decision: DefensiveDecision
|
|
pending_decision: 'offensive' | 'resolution' | null
|
|
}
|
|
|
|
export interface OffensiveDecisionSubmittedEvent {
|
|
game_id: string
|
|
decision: OffensiveDecision
|
|
pending_decision: 'resolution' | null
|
|
}
|
|
|
|
export interface DiceRolledEvent {
|
|
game_id: string
|
|
roll_id: string
|
|
d6_one: number
|
|
d6_two_total: number
|
|
chaos_d20: number
|
|
resolution_d20: number
|
|
check_wild_pitch: boolean
|
|
check_passed_ball: boolean
|
|
timestamp: string
|
|
message: string
|
|
}
|
|
|
|
export interface OutcomeAcceptedEvent {
|
|
outcome: string
|
|
hit_location?: string
|
|
}
|
|
|
|
export interface InningChangeEvent {
|
|
inning: number
|
|
half: 'top' | 'bottom'
|
|
}
|
|
|
|
export interface GameEndedEvent {
|
|
game_id: string
|
|
winner_team_id: number
|
|
final_score: {
|
|
home: number
|
|
away: number
|
|
}
|
|
completed_at: string
|
|
}
|
|
|
|
export interface SubstitutionConfirmedEvent {
|
|
type: string
|
|
new_lineup_id: number
|
|
message: string
|
|
}
|
|
|
|
export interface BoxScoreDataEvent {
|
|
game_id: string
|
|
box_score: {
|
|
game_stats: {
|
|
home_runs: number
|
|
away_runs: number
|
|
home_hits: number
|
|
away_hits: number
|
|
home_errors: number
|
|
away_errors: number
|
|
linescore_home: number[]
|
|
linescore_away: number[]
|
|
}
|
|
batting_stats: BattingStatLine[]
|
|
pitching_stats: PitchingStatLine[]
|
|
}
|
|
}
|
|
|
|
export interface BattingStatLine {
|
|
lineup_id: number
|
|
player_card_id: number
|
|
pa: number
|
|
ab: number
|
|
run: number
|
|
hit: number
|
|
rbi: number
|
|
double: number
|
|
triple: number
|
|
hr: number
|
|
bb: number
|
|
so: number
|
|
hbp: number
|
|
sac: number
|
|
sb: number
|
|
cs: number
|
|
gidp: number
|
|
}
|
|
|
|
export interface PitchingStatLine {
|
|
lineup_id: number
|
|
player_card_id: number
|
|
batters_faced: number
|
|
ip: number
|
|
hit_allowed: number
|
|
run_allowed: number
|
|
erun: number
|
|
bb: number
|
|
so: number
|
|
hbp: number
|
|
hr_allowed: number
|
|
wp: number
|
|
}
|
|
|
|
export interface GameStateSyncEvent {
|
|
state: GameState
|
|
recent_plays: PlayResult[]
|
|
timestamp: string
|
|
}
|
|
|
|
export interface ErrorEvent {
|
|
message: string
|
|
code?: string
|
|
field?: string
|
|
hint?: string
|
|
}
|
|
|
|
export interface OutcomeRejectedEvent {
|
|
message: string
|
|
field: string
|
|
errors?: Array<{
|
|
loc: string[]
|
|
msg: string
|
|
type: string
|
|
}>
|
|
}
|
|
|
|
export interface InvalidActionEvent {
|
|
action: string
|
|
reason: string
|
|
}
|
|
|
|
export interface ConnectionErrorEvent {
|
|
error: string
|
|
details?: string
|
|
}
|
|
|
|
/**
|
|
* Type-safe Socket.io instance
|
|
*/
|
|
export interface TypedSocket {
|
|
on<K extends keyof ServerToClientEvents>(
|
|
event: K,
|
|
listener: ServerToClientEvents[K]
|
|
): void
|
|
|
|
emit<K extends keyof ClientToServerEvents>(
|
|
event: K,
|
|
...args: Parameters<ClientToServerEvents[K]>
|
|
): void
|
|
|
|
off<K extends keyof ServerToClientEvents>(
|
|
event: K,
|
|
listener?: ServerToClientEvents[K]
|
|
): void
|
|
|
|
connect(): void
|
|
disconnect(): void
|
|
|
|
readonly connected: boolean
|
|
readonly id: string
|
|
}
|