strat-gameplay-webapp/frontend-sba/store/game.ts
Cal Corum 646878c572 CLAUDE: Optimize play history sync to O(1) with setPlayHistory
Replaced O(n²) deduplication check per play with O(1) array replacement:
- Added setPlayHistory() for game_state_sync (replaces entire array)
- Simplified addPlayToHistory() for live play_resolved events (just push)

This separates sync operations (replace) from live events (append),
eliminating the need for deduplication checks during gameplay.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 12:41:50 -06:00

437 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
}
}
/**
* 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<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,
setPlayHistory,
addPlayToHistory,
setDecisionPrompt,
clearDecisionPrompt,
setPendingRoll,
clearPendingRoll,
setLastPlayResult,
clearLastPlayResult,
setConnected,
setLoading,
setError,
setPendingDefensiveSetup,
setPendingOffensiveDecision,
setPendingStealAttempts,
addDecisionToHistory,
clearPendingDecisions,
resetGame,
getActiveLineup,
getBenchPlayers,
findPlayerInLineup,
}
})