Backend changes: - Modified request_game_state handler to fetch plays from database - Convert Play DB models to frontend-compatible PlayResult dicts - Emit game_state_sync event with state + recent_plays array Frontend changes: - Added deduplication by play_number in addPlayToHistory() - Prevents duplicate plays when game_state_sync is received Field mapping from Play model: - hit_type -> outcome - result_description -> description - batter_id -> batter_lineup_id - batter_final -> batter_result 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
433 lines
12 KiB
TypeScript
433 lines
12 KiB
TypeScript
/**
|
|
* 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<GameState | null>(null)
|
|
const homeLineup = ref<Lineup[]>([])
|
|
const awayLineup = ref<Lineup[]>([])
|
|
const playHistory = ref<PlayResult[]>([])
|
|
const currentDecisionPrompt = ref<DecisionPrompt | null>(null)
|
|
const pendingRoll = ref<RollData | null>(null)
|
|
const lastPlayResult = ref<PlayResult | null>(null)
|
|
const isConnected = ref(false)
|
|
const isLoading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
// Decision state (local pending decisions before submission)
|
|
const pendingDefensiveSetup = ref<DefensiveDecision | null>(null)
|
|
const pendingOffensiveDecision = ref<Omit<OffensiveDecision, 'steal_attempts'> | null>(null)
|
|
const pendingStealAttempts = ref<number[]>([])
|
|
const decisionHistory = ref<Array<{
|
|
type: 'Defensive' | 'Offensive'
|
|
summary: string
|
|
timestamp: string
|
|
}>>([])
|
|
|
|
// ============================================================================
|
|
// 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) {
|
|
gameState.value = state
|
|
error.value = null
|
|
}
|
|
|
|
/**
|
|
* Update partial game state
|
|
*/
|
|
function updateGameState(updates: Partial<GameState>) {
|
|
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
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add play to history (with deduplication by play_number)
|
|
*/
|
|
function addPlayToHistory(play: PlayResult) {
|
|
// Check if play already exists (deduplicate by play_number)
|
|
const exists = playHistory.value.some(p => p.play_number === play.play_number)
|
|
if (exists) {
|
|
return
|
|
}
|
|
|
|
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<OffensiveDecision, 'steal_attempts'> | 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,
|
|
addPlayToHistory,
|
|
setDecisionPrompt,
|
|
clearDecisionPrompt,
|
|
setPendingRoll,
|
|
clearPendingRoll,
|
|
setLastPlayResult,
|
|
clearLastPlayResult,
|
|
setConnected,
|
|
setLoading,
|
|
setError,
|
|
setPendingDefensiveSetup,
|
|
setPendingOffensiveDecision,
|
|
setPendingStealAttempts,
|
|
addDecisionToHistory,
|
|
clearPendingDecisions,
|
|
resetGame,
|
|
getActiveLineup,
|
|
getBenchPlayers,
|
|
findPlayerInLineup,
|
|
}
|
|
})
|