/** * Game Store * * Manages active game state, synchronized with backend via WebSocket. * This is the central state container for real-time gameplay. */ import { defineStore } from 'pinia' import { ref, computed } from 'vue' import type { GameState, PlayResult, DecisionPrompt, DefensiveDecision, OffensiveDecision, RollData, Lineup, } from '~/types' export const useGameStore = defineStore('game', () => { // ============================================================================ // State // ============================================================================ const gameState = ref(null) const homeLineup = ref([]) const awayLineup = ref([]) const playHistory = ref([]) const currentDecisionPrompt = ref(null) const pendingRoll = ref(null) const lastPlayResult = ref(null) const isConnected = ref(false) const isLoading = ref(false) const error = ref(null) // Decision state (local pending decisions before submission) const pendingDefensiveSetup = ref(null) const pendingOffensiveDecision = ref | null>(null) const pendingStealAttempts = ref([]) const decisionHistory = ref>([]) // ============================================================================ // Getters // ============================================================================ const gameId = computed(() => gameState.value?.game_id ?? null) const leagueId = computed(() => gameState.value?.league_id ?? null) const currentInning = computed(() => gameState.value?.inning ?? 1) const currentHalf = computed(() => gameState.value?.half ?? 'top') const outs = computed(() => gameState.value?.outs ?? 0) const balls = computed(() => gameState.value?.balls ?? 0) const strikes = computed(() => gameState.value?.strikes ?? 0) const homeScore = computed(() => gameState.value?.home_score ?? 0) const awayScore = computed(() => gameState.value?.away_score ?? 0) const gameStatus = computed(() => gameState.value?.status ?? 'pending') const isGameActive = computed(() => gameStatus.value === 'active') const isGameComplete = computed(() => gameStatus.value === 'completed') const currentBatter = computed(() => gameState.value?.current_batter ?? null) const currentPitcher = computed(() => gameState.value?.current_pitcher ?? null) const currentCatcher = computed(() => gameState.value?.current_catcher ?? null) const runnersOnBase = computed(() => { const runners: number[] = [] if (gameState.value?.on_first) runners.push(1) if (gameState.value?.on_second) runners.push(2) if (gameState.value?.on_third) runners.push(3) return runners }) const basesLoaded = computed(() => runnersOnBase.value.length === 3) const basesEmpty = computed(() => runnersOnBase.value.length === 0) const runnerInScoringPosition = computed(() => runnersOnBase.value.includes(2) || runnersOnBase.value.includes(3) ) const battingTeamId = computed(() => { if (!gameState.value) return null return gameState.value.half === 'top' ? gameState.value.away_team_id : gameState.value.home_team_id }) const fieldingTeamId = computed(() => { if (!gameState.value) return null return gameState.value.half === 'top' ? gameState.value.home_team_id : gameState.value.away_team_id }) const isBattingTeamAI = computed(() => { if (!gameState.value) return false return gameState.value.half === 'top' ? gameState.value.away_team_is_ai : gameState.value.home_team_is_ai }) const isFieldingTeamAI = computed(() => { if (!gameState.value) return false return gameState.value.half === 'top' ? gameState.value.home_team_is_ai : gameState.value.away_team_is_ai }) const needsDefensiveDecision = computed(() => { // Standardized naming (2025-01-21): Both backend and frontend now use 'awaiting_defensive' return currentDecisionPrompt.value?.phase === 'awaiting_defensive' || gameState.value?.decision_phase === 'awaiting_defensive' }) const needsOffensiveDecision = computed(() => { // Standardized naming (2025-01-21): Both backend and frontend now use 'awaiting_offensive' return currentDecisionPrompt.value?.phase === 'awaiting_offensive' || gameState.value?.decision_phase === 'awaiting_offensive' }) const needsStolenBaseDecision = computed(() => { // Standardized naming (2025-01-21): Both backend and frontend now use 'awaiting_stolen_base' return currentDecisionPrompt.value?.phase === 'awaiting_stolen_base' || gameState.value?.decision_phase === 'awaiting_stolen_base' }) const canRollDice = computed(() => { return gameState.value?.decision_phase === 'resolution' && !pendingRoll.value }) const canSubmitOutcome = computed(() => { return pendingRoll.value !== null }) const recentPlays = computed(() => { return playHistory.value.slice(-10).reverse() }) // ============================================================================ // Actions // ============================================================================ /** * Set complete game state (from server) */ function setGameState(state: GameState) { const oldBatter = gameState.value?.current_batter const newBatter = state?.current_batter const oldBatterInfo = oldBatter ? `lineup_id=${oldBatter.lineup_id}, batting_order=${oldBatter.batting_order}` : 'None' const newBatterInfo = newBatter ? `lineup_id=${newBatter.lineup_id}, batting_order=${newBatter.batting_order}` : 'None' console.log('[GameStore] setGameState - current_batter:', oldBatterInfo, '->', newBatterInfo) gameState.value = state error.value = null } /** * Update partial game state */ function updateGameState(updates: Partial) { if (gameState.value) { gameState.value = { ...gameState.value, ...updates } } } /** * Set lineups for both teams */ function setLineups(home: Lineup[], away: Lineup[]) { homeLineup.value = home awayLineup.value = away } /** * Update lineup for a specific team */ function updateLineup(teamId: number, lineup: Lineup[]) { if (teamId === gameState.value?.home_team_id) { homeLineup.value = lineup } else if (teamId === gameState.value?.away_team_id) { awayLineup.value = lineup } } /** * Set play history (replaces entire array - used for initial sync) * O(1) operation - no deduplication needed */ function setPlayHistory(plays: PlayResult[]) { playHistory.value = plays } /** * Add play to history (used for live play_resolved events) * O(1) operation - assumes no duplicates from live events */ function addPlayToHistory(play: PlayResult) { playHistory.value.push(play) // Update game state from play result if provided if (play.new_state) { updateGameState(play.new_state) } } /** * Set current decision prompt */ function setDecisionPrompt(prompt: DecisionPrompt | null) { currentDecisionPrompt.value = prompt } /** * Clear decision prompt after submission */ function clearDecisionPrompt() { currentDecisionPrompt.value = null } /** * Set pending dice roll */ function setPendingRoll(roll: RollData | null) { pendingRoll.value = roll } /** * Clear pending roll after outcome submission */ function clearPendingRoll() { pendingRoll.value = null } /** * Set last play result */ function setLastPlayResult(result: PlayResult | null) { lastPlayResult.value = result } /** * Clear last play result */ function clearLastPlayResult() { lastPlayResult.value = null } /** * Set connection status */ function setConnected(connected: boolean) { isConnected.value = connected } /** * Set loading state */ function setLoading(loading: boolean) { isLoading.value = loading } /** * Set error */ function setError(message: string | null) { error.value = message } /** * Set pending defensive setup (before submission) */ function setPendingDefensiveSetup(setup: DefensiveDecision | null) { pendingDefensiveSetup.value = setup } /** * Set pending offensive decision (before submission) */ function setPendingOffensiveDecision(decision: Omit | null) { pendingOffensiveDecision.value = decision } /** * Set pending steal attempts (before submission) */ function setPendingStealAttempts(attempts: number[]) { pendingStealAttempts.value = attempts } /** * Add decision to history */ function addDecisionToHistory(type: 'Defensive' | 'Offensive', summary: string) { decisionHistory.value.unshift({ type, summary, timestamp: new Date().toLocaleTimeString(), }) // Keep only last 10 decisions if (decisionHistory.value.length > 10) { decisionHistory.value = decisionHistory.value.slice(0, 10) } } /** * Clear all pending decisions */ function clearPendingDecisions() { pendingDefensiveSetup.value = null pendingOffensiveDecision.value = null pendingStealAttempts.value = [] } /** * Reset game store (when leaving game) */ function resetGame() { gameState.value = null homeLineup.value = [] awayLineup.value = [] playHistory.value = [] currentDecisionPrompt.value = null pendingRoll.value = null isConnected.value = false isLoading.value = false error.value = null pendingDefensiveSetup.value = null pendingOffensiveDecision.value = null pendingStealAttempts.value = [] decisionHistory.value = [] } /** * Get active lineup for a team */ function getActiveLineup(teamId: number): Lineup[] { const lineup = teamId === gameState.value?.home_team_id ? homeLineup.value : awayLineup.value return lineup.filter(p => p.is_active) } /** * Get bench players for a team */ function getBenchPlayers(teamId: number): Lineup[] { const lineup = teamId === gameState.value?.home_team_id ? homeLineup.value : awayLineup.value return lineup.filter(p => !p.is_active) } /** * Find player in lineup by lineup_id */ function findPlayerInLineup(lineupId: number): Lineup | undefined { return [...homeLineup.value, ...awayLineup.value].find( p => p.lineup_id === lineupId ) } // ============================================================================ // Return Store API // ============================================================================ return { // State gameState: readonly(gameState), homeLineup: readonly(homeLineup), awayLineup: readonly(awayLineup), playHistory: readonly(playHistory), currentDecisionPrompt: readonly(currentDecisionPrompt), pendingRoll: readonly(pendingRoll), lastPlayResult: readonly(lastPlayResult), isConnected: readonly(isConnected), isLoading: readonly(isLoading), error: readonly(error), pendingDefensiveSetup: readonly(pendingDefensiveSetup), pendingOffensiveDecision: readonly(pendingOffensiveDecision), pendingStealAttempts: readonly(pendingStealAttempts), decisionHistory: readonly(decisionHistory), // Getters gameId, leagueId, currentInning, currentHalf, outs, balls, strikes, homeScore, awayScore, gameStatus, isGameActive, isGameComplete, currentBatter, currentPitcher, currentCatcher, runnersOnBase, basesLoaded, basesEmpty, runnerInScoringPosition, battingTeamId, fieldingTeamId, isBattingTeamAI, isFieldingTeamAI, needsDefensiveDecision, needsOffensiveDecision, needsStolenBaseDecision, canRollDice, canSubmitOutcome, recentPlays, // Actions setGameState, updateGameState, setLineups, updateLineup, setPlayHistory, addPlayToHistory, setDecisionPrompt, clearDecisionPrompt, setPendingRoll, clearPendingRoll, setLastPlayResult, clearLastPlayResult, setConnected, setLoading, setError, setPendingDefensiveSetup, setPendingOffensiveDecision, setPendingStealAttempts, addDecisionToHistory, clearPendingDecisions, resetGame, getActiveLineup, getBenchPlayers, findPlayerInLineup, } })