Frontend: Full 5-phase interactive wizard for uncapped hit decisions (lead advance, defensive throw, trail advance, throw target, safe/out) with mobile-first design, offense/defense role switching, and auto- clearing on workflow completion. Backend fixes: - Remove nested asyncio.Lock acquisition causing deadlocks in all submit_uncapped_* methods and initiate_uncapped_hit (non-re-entrant) - Preserve pending_manual_roll during interactive workflows - Add league_id to all dice_system.roll_d20() calls - Extract D20Roll.roll int for state serialization - Fix batter-runner not advancing when non-targeted in throw - Fix rollback_plays not recalculating scores from remaining plays Files: 10 modified, 1 new (UncappedHitWizard.vue) Tests: 2481/2481 passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
429 lines
9.6 KiB
TypeScript
429 lines
9.6 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,
|
|
XCheckData,
|
|
DecideAdvanceData,
|
|
DecideThrowData,
|
|
DecideSpeedCheckData,
|
|
} 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
|
|
|
|
// Interactive x-check workflow
|
|
submit_x_check_result: (data: SubmitXCheckResultRequest) => void
|
|
submit_decide_advance: (data: SubmitDecideAdvanceRequest) => void
|
|
submit_decide_throw: (data: SubmitDecideThrowRequest) => void
|
|
submit_decide_result: (data: SubmitDecideResultRequest) => void
|
|
|
|
// Uncapped hit decision tree
|
|
submit_uncapped_lead_advance: (data: SubmitUncappedLeadAdvanceRequest) => void
|
|
submit_uncapped_defensive_throw: (data: SubmitUncappedDefensiveThrowRequest) => void
|
|
submit_uncapped_trail_advance: (data: SubmitUncappedTrailAdvanceRequest) => void
|
|
submit_uncapped_throw_target: (data: SubmitUncappedThrowTargetRequest) => void
|
|
submit_uncapped_safe_out: (data: SubmitUncappedSafeOutRequest) => 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
|
|
}
|
|
|
|
/**
|
|
* Interactive X-Check Request Types
|
|
*/
|
|
|
|
export interface SubmitXCheckResultRequest {
|
|
game_id: string
|
|
result_code: string // G1, G2, SI2, F1, etc.
|
|
error_result: string // NO, E1, E2, E3, RP
|
|
}
|
|
|
|
export interface SubmitDecideAdvanceRequest {
|
|
game_id: string
|
|
advance: boolean
|
|
}
|
|
|
|
export interface SubmitDecideThrowRequest {
|
|
game_id: string
|
|
target: 'runner' | 'first'
|
|
}
|
|
|
|
export interface SubmitDecideResultRequest {
|
|
game_id: string
|
|
outcome: 'safe' | 'out'
|
|
}
|
|
|
|
export interface SubmitUncappedLeadAdvanceRequest {
|
|
game_id: string
|
|
advance: boolean
|
|
}
|
|
|
|
export interface SubmitUncappedDefensiveThrowRequest {
|
|
game_id: string
|
|
will_throw: boolean
|
|
}
|
|
|
|
export interface SubmitUncappedTrailAdvanceRequest {
|
|
game_id: string
|
|
advance: boolean
|
|
}
|
|
|
|
export interface SubmitUncappedThrowTargetRequest {
|
|
game_id: string
|
|
target: 'lead' | 'trail'
|
|
}
|
|
|
|
export interface SubmitUncappedSafeOutRequest {
|
|
game_id: string
|
|
result: 'safe' | 'out'
|
|
}
|