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>
548 lines
13 KiB
TypeScript
548 lines
13 KiB
TypeScript
/**
|
|
* Game State and Play Result Types
|
|
*
|
|
* TypeScript definitions matching backend Pydantic models exactly.
|
|
* These types ensure type safety between frontend and backend.
|
|
*
|
|
* Backend Reference: app/models/game_models.py
|
|
*/
|
|
|
|
/**
|
|
* Game status enumeration
|
|
*/
|
|
export type GameStatus = 'pending' | 'active' | 'paused' | 'completed' | 'abandoned'
|
|
|
|
/**
|
|
* Inning half
|
|
*/
|
|
export type InningHalf = 'top' | 'bottom'
|
|
|
|
/**
|
|
* Game mode
|
|
*/
|
|
export type GameMode = 'live' | 'async' | 'vs_ai'
|
|
|
|
/**
|
|
* Game visibility
|
|
*/
|
|
export type GameVisibility = 'public' | 'private'
|
|
|
|
/**
|
|
* League identifier
|
|
*/
|
|
export type LeagueId = 'sba' | 'pd'
|
|
|
|
/**
|
|
* Decision phase in the play workflow
|
|
*
|
|
* Standardized naming (2025-01-21): Uses backend convention 'awaiting_*'
|
|
* for clarity about what action is pending.
|
|
*/
|
|
export type DecisionPhase =
|
|
| 'awaiting_defensive'
|
|
| 'awaiting_stolen_base'
|
|
| 'awaiting_offensive'
|
|
| 'resolution'
|
|
| 'complete'
|
|
// Interactive x-check workflow phases
|
|
| 'awaiting_x_check_result'
|
|
| 'awaiting_decide_advance'
|
|
| 'awaiting_decide_throw'
|
|
| 'awaiting_decide_result'
|
|
// Uncapped hit decision tree phases
|
|
| 'awaiting_uncapped_lead_advance'
|
|
| 'awaiting_uncapped_defensive_throw'
|
|
| 'awaiting_uncapped_trail_advance'
|
|
| 'awaiting_uncapped_throw_target'
|
|
| 'awaiting_uncapped_safe_out'
|
|
|
|
/**
|
|
* Lineup player state - represents a player in the game
|
|
* Backend: LineupPlayerState
|
|
*/
|
|
export interface LineupPlayerState {
|
|
lineup_id: number
|
|
card_id: number
|
|
position: string
|
|
batting_order: number | null
|
|
is_active: boolean
|
|
position_rating?: {
|
|
range: number
|
|
error: number
|
|
innings: number
|
|
} | null
|
|
}
|
|
|
|
/**
|
|
* Core game state - complete representation of active game
|
|
* Backend: GameState
|
|
*/
|
|
export interface GameState {
|
|
// Identity
|
|
game_id: string
|
|
league_id: LeagueId
|
|
|
|
// Teams
|
|
home_team_id: number
|
|
away_team_id: number
|
|
home_team_is_ai: boolean
|
|
away_team_is_ai: boolean
|
|
|
|
// Team display info (from game_metadata, stored at creation time)
|
|
home_team_name?: string | null // Full name: "Chicago Cyclones"
|
|
home_team_abbrev?: string | null // Abbreviation: "CHC"
|
|
home_team_color?: string | null // Hex color without #: "ff5349"
|
|
home_team_dice_color?: string | null // Dice color hex without #, default "cc0000"
|
|
home_team_thumbnail?: string | null // Team logo URL
|
|
away_team_name?: string | null
|
|
away_team_abbrev?: string | null
|
|
away_team_color?: string | null
|
|
away_team_dice_color?: string | null // Dice color hex without #, default "cc0000"
|
|
away_team_thumbnail?: string | null
|
|
|
|
// Creator (for demo/testing - creator can control home team)
|
|
creator_discord_id: string | null
|
|
|
|
// Resolution mode
|
|
auto_mode: boolean
|
|
|
|
// Game state
|
|
status: GameStatus
|
|
inning: number
|
|
half: InningHalf
|
|
outs: number
|
|
balls: number
|
|
strikes: number
|
|
home_score: number
|
|
away_score: number
|
|
|
|
// Runners (direct references)
|
|
on_first: LineupPlayerState | null
|
|
on_second: LineupPlayerState | null
|
|
on_third: LineupPlayerState | null
|
|
|
|
// Current players
|
|
current_batter: LineupPlayerState
|
|
current_pitcher: LineupPlayerState | null
|
|
current_catcher: LineupPlayerState | null
|
|
current_on_base_code: number
|
|
|
|
// Batting order tracking
|
|
away_team_batter_idx: number // 0-8
|
|
home_team_batter_idx: number // 0-8
|
|
|
|
// Decision tracking
|
|
pending_decision: DecisionPhase | null
|
|
decision_phase: DecisionPhase
|
|
decisions_this_play: Record<string, boolean>
|
|
pending_defensive_decision: DefensiveDecision | null
|
|
pending_offensive_decision: OffensiveDecision | null
|
|
|
|
// Manual mode
|
|
pending_manual_roll: RollData | null
|
|
|
|
// Interactive x-check workflow
|
|
pending_x_check: PendingXCheck | null
|
|
|
|
// Uncapped hit decision tree
|
|
pending_uncapped_hit: PendingUncappedHit | null
|
|
|
|
// Play history
|
|
play_count: number
|
|
last_play_result: string | null
|
|
|
|
// Timestamps
|
|
created_at: string
|
|
started_at: string | null
|
|
completed_at: string | null
|
|
}
|
|
|
|
/**
|
|
* Defensive strategic decision
|
|
* Backend: DefensiveDecision
|
|
*/
|
|
export interface DefensiveDecision {
|
|
infield_depth: 'infield_in' | 'normal' | 'corners_in'
|
|
outfield_depth: 'normal' | 'shallow'
|
|
hold_runners: number[] // Bases to hold (e.g., [1, 3])
|
|
}
|
|
|
|
/**
|
|
* Offensive strategic decision
|
|
* Backend: OffensiveDecision
|
|
*
|
|
* Session 2 Update (2025-01-14): Replaced approach/hit_and_run/bunt_attempt with action field.
|
|
*/
|
|
export interface OffensiveDecision {
|
|
action: 'swing_away' | 'steal' | 'check_jump' | 'hit_and_run' | 'sac_bunt' | 'squeeze_bunt'
|
|
steal_attempts: number[] // Bases to steal (2, 3, or 4) - only used when action="steal"
|
|
}
|
|
|
|
/**
|
|
* Dice roll data from server
|
|
* Backend: AbRoll (simplified for frontend)
|
|
*/
|
|
export interface RollData {
|
|
roll_id: string
|
|
d6_one: number
|
|
d6_two_a: number
|
|
d6_two_b: number
|
|
d6_two_total: number
|
|
chaos_d20: number
|
|
resolution_d20: number
|
|
check_wild_pitch: boolean
|
|
check_passed_ball: boolean
|
|
chaos_check_skipped: boolean // True when no runners on base (WP/PB irrelevant)
|
|
timestamp: string
|
|
}
|
|
|
|
/**
|
|
* Play outcome enumeration (matches backend PlayOutcome enum values)
|
|
* Note: These are the STRING VALUES, not enum names
|
|
*/
|
|
export type PlayOutcome =
|
|
// Standard outs
|
|
| 'strikeout'
|
|
| 'groundball_a'
|
|
| 'groundball_b'
|
|
| 'groundball_c'
|
|
| 'flyout_a'
|
|
| 'flyout_b'
|
|
| 'flyout_bq'
|
|
| 'flyout_c'
|
|
| 'lineout'
|
|
| 'popout'
|
|
|
|
// Walks and hits by pitch
|
|
| 'walk'
|
|
| 'intentional_walk'
|
|
| 'hbp'
|
|
|
|
// Hits (capped - specific bases)
|
|
| 'single_1'
|
|
| 'single_2'
|
|
| 'single_uncapped'
|
|
| 'double_2'
|
|
| 'double_3'
|
|
| 'double_uncapped'
|
|
| 'triple'
|
|
| 'homerun'
|
|
|
|
// Interrupt plays (baserunning events)
|
|
| 'stolen_base'
|
|
| 'caught_stealing'
|
|
| 'wild_pitch'
|
|
| 'passed_ball'
|
|
| 'balk'
|
|
| 'pick_off'
|
|
|
|
// Errors
|
|
| 'error'
|
|
| 'x_check'
|
|
|
|
// Ballpark power (PD specific)
|
|
| 'bp_homerun'
|
|
| 'bp_flyout'
|
|
| 'bp_single'
|
|
| 'bp_lineout'
|
|
|
|
/**
|
|
* Runner advancement during a play
|
|
*/
|
|
export interface RunnerAdvancement {
|
|
from: number // 0=batter, 1-3=bases
|
|
to: number // 1-4=bases (4=home/scored)
|
|
lineup_id: number // Player's lineup ID for name lookup
|
|
is_out?: boolean // Runner was out during advancement (renamed from 'out')
|
|
}
|
|
|
|
/**
|
|
* Play resolution result from server
|
|
* Backend: PlayResult
|
|
*/
|
|
export interface PlayResult {
|
|
// Play identification
|
|
play_number: number
|
|
inning?: number // Inning when play occurred
|
|
half?: InningHalf // 'top' or 'bottom'
|
|
outcome: PlayOutcome
|
|
|
|
// Play description
|
|
description: string
|
|
hit_type?: string
|
|
|
|
// Results
|
|
outs_recorded: number
|
|
runs_scored: number
|
|
|
|
// Player identification for display
|
|
batter_lineup_id?: number // Batter's lineup ID for name lookup
|
|
|
|
// Runner advancement
|
|
runners_advanced: RunnerAdvancement[]
|
|
batter_result: number | null // Where batter ended up (1-4, null=out)
|
|
|
|
// Updated state snapshot
|
|
new_state: Partial<GameState>
|
|
|
|
// Categorization helpers
|
|
is_hit: boolean
|
|
is_out: boolean
|
|
is_walk: boolean
|
|
is_strikeout: boolean
|
|
|
|
// Roll reference (if manual mode)
|
|
roll_id?: string
|
|
|
|
// X-Check details (if defensive play)
|
|
x_check_details?: XCheckResult
|
|
}
|
|
|
|
/**
|
|
* X-Check resolution audit trail
|
|
*/
|
|
export interface XCheckResult {
|
|
check_position: string
|
|
defense_range: number
|
|
error_rating: number
|
|
d20_roll: number
|
|
error_3d6: number
|
|
base_result: string
|
|
error_result: string
|
|
final_outcome: PlayOutcome
|
|
held_runners: string[]
|
|
}
|
|
|
|
/**
|
|
* Decision prompt from server
|
|
* Backend: DecisionPrompt (WebSocket event)
|
|
*/
|
|
export interface DecisionPrompt {
|
|
phase: DecisionPhase
|
|
role: 'home' | 'away'
|
|
timeout_seconds: number
|
|
options?: string[]
|
|
message?: string
|
|
data?: Record<string, unknown>
|
|
}
|
|
|
|
/**
|
|
* Manual outcome submission to server
|
|
* Backend: ManualOutcomeSubmission
|
|
*/
|
|
export interface ManualOutcomeSubmission {
|
|
outcome: PlayOutcome
|
|
hit_location?: string // Required for groundballs/flyballs
|
|
}
|
|
|
|
/**
|
|
* Game list item for active/completed games
|
|
* Note: Field names match backend GameListItem response
|
|
*/
|
|
export interface GameListItem {
|
|
game_id: string
|
|
league_id: LeagueId
|
|
home_team_id: number
|
|
away_team_id: number
|
|
status: GameStatus
|
|
// Enriched team info from backend
|
|
home_team_name?: string
|
|
away_team_name?: string
|
|
home_team_abbrev?: string
|
|
away_team_abbrev?: string
|
|
home_score: number
|
|
away_score: number
|
|
inning?: number
|
|
half?: InningHalf
|
|
// External schedule reference (for linking to SBA/PD schedule systems)
|
|
schedule_game_id?: number
|
|
}
|
|
|
|
/**
|
|
* Game creation request
|
|
*/
|
|
export interface CreateGameRequest {
|
|
league_id: LeagueId
|
|
home_team_id: number
|
|
away_team_id: number
|
|
game_mode: GameMode
|
|
visibility: GameVisibility
|
|
}
|
|
|
|
/**
|
|
* Game creation response
|
|
*/
|
|
export interface CreateGameResponse {
|
|
game_id: string
|
|
status: GameStatus
|
|
created_at: string
|
|
}
|
|
|
|
/**
|
|
* Interactive X-Check Data
|
|
* Sent with decision_required event when x-check is initiated
|
|
*/
|
|
export interface XCheckData {
|
|
position: string
|
|
d20_roll: number
|
|
d6_total: number
|
|
d6_individual: readonly number[] | number[]
|
|
chart_row: readonly string[] | string[] // 5 values: ["G1", "G2", "G3#", "SI1", "SI2"]
|
|
chart_type: 'infield' | 'outfield' | 'catcher'
|
|
spd_d20: number | null // Pre-rolled, shown on click-to-reveal
|
|
defender_lineup_id: number
|
|
active_team_id: number // Which team can interact (transparency model)
|
|
}
|
|
|
|
/**
|
|
* DECIDE Advance Data
|
|
* Sent when offensive player must decide if runner advances
|
|
*/
|
|
export interface DecideAdvanceData {
|
|
runner_base: number // 1, 2, or 3
|
|
target_base: number // 2, 3, or 4
|
|
runner_lineup_id: number
|
|
active_team_id: number
|
|
}
|
|
|
|
/**
|
|
* DECIDE Throw Data
|
|
* Sent when defensive player chooses throw target
|
|
*/
|
|
export interface DecideThrowData {
|
|
runner_base: number
|
|
target_base: number
|
|
runner_lineup_id: number
|
|
active_team_id: number
|
|
}
|
|
|
|
/**
|
|
* DECIDE Speed Check Data
|
|
* Sent when offensive player must resolve speed check
|
|
*/
|
|
export interface DecideSpeedCheckData {
|
|
d20_roll: number
|
|
runner_lineup_id: number
|
|
runner_base: number
|
|
target_base: number
|
|
active_team_id: number
|
|
}
|
|
|
|
/**
|
|
* Pending X-Check State (on GameState)
|
|
* Persisted for reconnection recovery
|
|
*/
|
|
export interface PendingXCheck {
|
|
position: string
|
|
ab_roll_id: string
|
|
d20_roll: number
|
|
d6_total: number
|
|
d6_individual: number[]
|
|
chart_row: string[]
|
|
chart_type: string
|
|
spd_d20: number | null
|
|
defender_lineup_id: number
|
|
selected_result: string | null
|
|
error_result: string | null
|
|
decide_runner_base: number | null
|
|
decide_target_base: number | null
|
|
decide_advance: boolean | null
|
|
decide_throw: string | null
|
|
decide_d20: number | null
|
|
}
|
|
|
|
/**
|
|
* Pending Uncapped Hit State (on GameState)
|
|
* Persisted for reconnection recovery.
|
|
* Backend: PendingUncappedHit (game_models.py)
|
|
*/
|
|
export interface PendingUncappedHit {
|
|
hit_type: string
|
|
hit_location: string
|
|
lead_runner_base: number
|
|
lead_runner_lineup_id: number
|
|
lead_target_base: number
|
|
auto_runners: number[][]
|
|
lead_advance: boolean | null
|
|
will_throw: boolean | null
|
|
trail_runner_base: number | null
|
|
trail_runner_lineup_id: number | null
|
|
trail_target_base: number | null
|
|
trail_advance: boolean | null
|
|
throw_target: string | null
|
|
d20_roll: number | null
|
|
safe_out_result: string | null
|
|
}
|
|
|
|
/**
|
|
* Phase 1: Lead runner advance decision data
|
|
* Sent with awaiting_uncapped_lead_advance
|
|
*/
|
|
export interface UncappedLeadAdvanceData {
|
|
hit_type: string
|
|
hit_location: string
|
|
lead_runner_base: number
|
|
lead_runner_lineup_id: number
|
|
lead_target_base: number
|
|
auto_runners: number[][]
|
|
}
|
|
|
|
/**
|
|
* Phase 2: Defensive throw decision data
|
|
* Sent with awaiting_uncapped_defensive_throw
|
|
*/
|
|
export interface UncappedDefensiveThrowData {
|
|
lead_runner_base: number
|
|
lead_target_base: number
|
|
lead_runner_lineup_id: number
|
|
hit_location: string
|
|
}
|
|
|
|
/**
|
|
* Phase 3: Trail runner advance decision data
|
|
* Sent with awaiting_uncapped_trail_advance
|
|
*/
|
|
export interface UncappedTrailAdvanceData {
|
|
trail_runner_base: number
|
|
trail_target_base: number
|
|
trail_runner_lineup_id: number
|
|
hit_location: string
|
|
}
|
|
|
|
/**
|
|
* Phase 4: Throw target selection data
|
|
* Sent with awaiting_uncapped_throw_target
|
|
*/
|
|
export interface UncappedThrowTargetData {
|
|
lead_runner_base: number
|
|
lead_target_base: number
|
|
lead_runner_lineup_id: number
|
|
trail_runner_base: number
|
|
trail_target_base: number
|
|
trail_runner_lineup_id: number
|
|
hit_location: string
|
|
}
|
|
|
|
/**
|
|
* Phase 5: Safe/out resolution data
|
|
* Sent with awaiting_uncapped_safe_out
|
|
*/
|
|
export interface UncappedSafeOutData {
|
|
d20_roll: number
|
|
runner: string
|
|
runner_base: number
|
|
target_base: number
|
|
runner_lineup_id: number
|
|
hit_location: string
|
|
}
|
|
|
|
/**
|
|
* Union of all uncapped hit phase data types
|
|
*/
|
|
export type UncappedHitData =
|
|
| UncappedLeadAdvanceData
|
|
| UncappedDefensiveThrowData
|
|
| UncappedTrailAdvanceData
|
|
| UncappedThrowTargetData
|
|
| UncappedSafeOutData
|