519 lines
14 KiB
TypeScript
519 lines
14 KiB
TypeScript
/**
|
|
* Game Store Tests
|
|
*
|
|
* Tests for game state management, play history, and computed properties.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { setActivePinia, createPinia } from 'pinia'
|
|
import { useGameStore } from '~/store/game'
|
|
import type { GameState, PlayResult, DecisionPrompt, RollData, Lineup } from '~/types'
|
|
|
|
// Mock game state factory
|
|
const createMockGameState = (overrides?: Partial<GameState>): GameState => ({
|
|
game_id: 'game-123',
|
|
league_id: 'sba',
|
|
status: 'active',
|
|
inning: 1,
|
|
half: 'top',
|
|
outs: 0,
|
|
balls: 0,
|
|
strikes: 0,
|
|
home_score: 0,
|
|
away_score: 0,
|
|
home_team_id: 1,
|
|
away_team_id: 2,
|
|
home_team_is_ai: false,
|
|
away_team_is_ai: false,
|
|
on_first: null,
|
|
on_second: null,
|
|
on_third: null,
|
|
current_batter: null,
|
|
current_pitcher: null,
|
|
current_catcher: null,
|
|
decision_phase: 'awaiting_defensive',
|
|
...overrides,
|
|
})
|
|
|
|
describe('useGameStore', () => {
|
|
beforeEach(() => {
|
|
// Ensure process.env exists before creating Pinia
|
|
if (!process.env) {
|
|
(process as any).env = {}
|
|
}
|
|
process.env.NODE_ENV = 'test'
|
|
|
|
setActivePinia(createPinia())
|
|
})
|
|
|
|
describe('initialization', () => {
|
|
it('initializes with null/empty state', () => {
|
|
const store = useGameStore()
|
|
|
|
expect(store.gameState).toBeNull()
|
|
expect(store.homeLineup).toEqual([])
|
|
expect(store.awayLineup).toEqual([])
|
|
expect(store.playHistory).toEqual([])
|
|
expect(store.currentDecisionPrompt).toBeNull()
|
|
expect(store.pendingRoll).toBeNull()
|
|
expect(store.isConnected).toBe(false)
|
|
expect(store.isLoading).toBe(false)
|
|
expect(store.error).toBeNull()
|
|
})
|
|
|
|
it('has null computed properties on init', () => {
|
|
const store = useGameStore()
|
|
|
|
expect(store.gameId).toBeNull()
|
|
expect(store.leagueId).toBeNull()
|
|
expect(store.currentBatter).toBeNull()
|
|
expect(store.currentPitcher).toBeNull()
|
|
})
|
|
|
|
it('has default computed values on init', () => {
|
|
const store = useGameStore()
|
|
|
|
expect(store.currentInning).toBe(1)
|
|
expect(store.currentHalf).toBe('top')
|
|
expect(store.outs).toBe(0)
|
|
expect(store.homeScore).toBe(0)
|
|
expect(store.awayScore).toBe(0)
|
|
expect(store.gameStatus).toBe('pending')
|
|
})
|
|
})
|
|
|
|
describe('setting game state', () => {
|
|
it('sets complete game state', () => {
|
|
const store = useGameStore()
|
|
const mockState = createMockGameState()
|
|
|
|
store.setGameState(mockState)
|
|
|
|
expect(store.gameState).toEqual(mockState)
|
|
expect(store.gameId).toBe('game-123')
|
|
expect(store.leagueId).toBe('sba')
|
|
expect(store.error).toBeNull()
|
|
})
|
|
|
|
it('updates partial game state', () => {
|
|
const store = useGameStore()
|
|
const mockState = createMockGameState()
|
|
|
|
store.setGameState(mockState)
|
|
store.updateGameState({ outs: 2, strikes: 2 })
|
|
|
|
expect(store.outs).toBe(2)
|
|
expect(store.strikes).toBe(2)
|
|
expect(store.gameId).toBe('game-123') // Other fields unchanged
|
|
})
|
|
|
|
it('does not update if no game state exists', () => {
|
|
const store = useGameStore()
|
|
|
|
// Should not crash
|
|
expect(() => store.updateGameState({ outs: 1 })).not.toThrow()
|
|
expect(store.gameState).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('computed properties', () => {
|
|
it('computes game status flags', () => {
|
|
const store = useGameStore()
|
|
|
|
store.setGameState(createMockGameState({ status: 'pending' }))
|
|
expect(store.isGameActive).toBe(false)
|
|
expect(store.isGameComplete).toBe(false)
|
|
|
|
store.setGameState(createMockGameState({ status: 'active' }))
|
|
expect(store.isGameActive).toBe(true)
|
|
expect(store.isGameComplete).toBe(false)
|
|
|
|
store.setGameState(createMockGameState({ status: 'completed' }))
|
|
expect(store.isGameActive).toBe(false)
|
|
expect(store.isGameComplete).toBe(true)
|
|
})
|
|
|
|
it('computes runners on base correctly', () => {
|
|
const store = useGameStore()
|
|
|
|
// No runners
|
|
store.setGameState(createMockGameState())
|
|
expect(store.runnersOnBase).toEqual([])
|
|
expect(store.basesLoaded).toBe(false)
|
|
expect(store.runnerInScoringPosition).toBe(false)
|
|
|
|
// Runner on first
|
|
store.setGameState(createMockGameState({ on_first: 123 }))
|
|
expect(store.runnersOnBase).toEqual([1])
|
|
expect(store.basesLoaded).toBe(false)
|
|
expect(store.runnerInScoringPosition).toBe(false)
|
|
|
|
// Runner in scoring position (second)
|
|
store.setGameState(createMockGameState({ on_second: 456 }))
|
|
expect(store.runnersOnBase).toEqual([2])
|
|
expect(store.runnerInScoringPosition).toBe(true)
|
|
|
|
// Bases loaded
|
|
store.setGameState(createMockGameState({
|
|
on_first: 123,
|
|
on_second: 456,
|
|
on_third: 789,
|
|
}))
|
|
expect(store.runnersOnBase).toEqual([1, 2, 3])
|
|
expect(store.basesLoaded).toBe(true)
|
|
expect(store.runnerInScoringPosition).toBe(true)
|
|
})
|
|
|
|
it('computes batting and fielding team IDs', () => {
|
|
const store = useGameStore()
|
|
|
|
// Top of inning: away bats, home fields
|
|
store.setGameState(createMockGameState({ half: 'top' }))
|
|
expect(store.battingTeamId).toBe(2) // away
|
|
expect(store.fieldingTeamId).toBe(1) // home
|
|
|
|
// Bottom of inning: home bats, away fields
|
|
store.setGameState(createMockGameState({ half: 'bottom' }))
|
|
expect(store.battingTeamId).toBe(1) // home
|
|
expect(store.fieldingTeamId).toBe(2) // away
|
|
})
|
|
|
|
it('computes AI team flags', () => {
|
|
const store = useGameStore()
|
|
|
|
// Top of inning with away team AI
|
|
store.setGameState(createMockGameState({
|
|
half: 'top',
|
|
away_team_is_ai: true,
|
|
home_team_is_ai: false,
|
|
}))
|
|
expect(store.isBattingTeamAI).toBe(true)
|
|
expect(store.isFieldingTeamAI).toBe(false)
|
|
|
|
// Bottom of inning with home team AI
|
|
store.setGameState(createMockGameState({
|
|
half: 'bottom',
|
|
home_team_is_ai: true,
|
|
away_team_is_ai: false,
|
|
}))
|
|
expect(store.isBattingTeamAI).toBe(true)
|
|
expect(store.isFieldingTeamAI).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('play history management', () => {
|
|
it('adds play to history', () => {
|
|
const store = useGameStore()
|
|
const play: PlayResult = {
|
|
play_number: 1,
|
|
outcome: 'SINGLE_1',
|
|
description: 'Single to center field',
|
|
runs_scored: 0,
|
|
outs_recorded: 0,
|
|
new_state: {},
|
|
}
|
|
|
|
store.addPlayToHistory(play)
|
|
|
|
expect(store.playHistory.length).toBe(1)
|
|
expect(store.playHistory[0]).toEqual(play)
|
|
})
|
|
|
|
it('updates game state from play result', () => {
|
|
const store = useGameStore()
|
|
store.setGameState(createMockGameState())
|
|
|
|
const play: PlayResult = {
|
|
play_number: 1,
|
|
outcome: 'STRIKEOUT',
|
|
description: 'Struck out swinging',
|
|
runs_scored: 0,
|
|
outs_recorded: 1,
|
|
new_state: { outs: 1 },
|
|
}
|
|
|
|
store.addPlayToHistory(play)
|
|
|
|
expect(store.outs).toBe(1)
|
|
})
|
|
|
|
it('returns recent plays in reverse order', () => {
|
|
const store = useGameStore()
|
|
|
|
// Add 15 plays
|
|
for (let i = 1; i <= 15; i++) {
|
|
store.addPlayToHistory({
|
|
play_number: i,
|
|
outcome: 'TEST',
|
|
description: `Play ${i}`,
|
|
runs_scored: 0,
|
|
outs_recorded: 0,
|
|
new_state: {},
|
|
})
|
|
}
|
|
|
|
const recent = store.recentPlays
|
|
|
|
// Should return last 10 plays in reverse order
|
|
expect(recent.length).toBe(10)
|
|
expect(recent[0].play_number).toBe(15) // Most recent first
|
|
expect(recent[9].play_number).toBe(6) // 10th most recent
|
|
})
|
|
})
|
|
|
|
describe('decision prompt management', () => {
|
|
it('sets decision prompt', () => {
|
|
const store = useGameStore()
|
|
const prompt: DecisionPrompt = {
|
|
phase: 'awaiting_defensive',
|
|
role: 'home',
|
|
timeout_seconds: 30,
|
|
}
|
|
|
|
store.setDecisionPrompt(prompt)
|
|
|
|
expect(store.currentDecisionPrompt).toEqual(prompt)
|
|
expect(store.needsDefensiveDecision).toBe(true)
|
|
})
|
|
|
|
it('identifies defensive decision need', () => {
|
|
const store = useGameStore()
|
|
|
|
store.setDecisionPrompt({ phase: 'awaiting_defensive', role: 'home', timeout_seconds: 30 })
|
|
expect(store.needsDefensiveDecision).toBe(true)
|
|
expect(store.needsOffensiveDecision).toBe(false)
|
|
})
|
|
|
|
it('identifies offensive decision need', () => {
|
|
const store = useGameStore()
|
|
|
|
store.setDecisionPrompt({ phase: 'awaiting_offensive', role: 'away', timeout_seconds: 30 })
|
|
expect(store.needsOffensiveDecision).toBe(true)
|
|
expect(store.needsDefensiveDecision).toBe(false)
|
|
})
|
|
|
|
it('identifies stolen base decision need', () => {
|
|
const store = useGameStore()
|
|
|
|
store.setDecisionPrompt({ phase: 'awaiting_stolen_base', role: 'away', timeout_seconds: 30 })
|
|
expect(store.needsStolenBaseDecision).toBe(true)
|
|
})
|
|
|
|
it('clears decision prompt', () => {
|
|
const store = useGameStore()
|
|
|
|
store.setDecisionPrompt({ phase: 'defense', role: 'home', timeout_seconds: 30 })
|
|
expect(store.currentDecisionPrompt).not.toBeNull()
|
|
|
|
store.clearDecisionPrompt()
|
|
expect(store.currentDecisionPrompt).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('dice roll management', () => {
|
|
it('sets pending roll', () => {
|
|
const store = useGameStore()
|
|
const roll: RollData = {
|
|
roll_id: 'roll-123',
|
|
d6_one: 3,
|
|
d6_two_a: 4,
|
|
d6_two_b: 2,
|
|
chaos_d20: 15,
|
|
resolution_d20: 8,
|
|
}
|
|
|
|
store.setPendingRoll(roll)
|
|
|
|
expect(store.pendingRoll).toEqual(roll)
|
|
})
|
|
|
|
it('computes canRollDice correctly', () => {
|
|
const store = useGameStore()
|
|
|
|
// No game state
|
|
expect(store.canRollDice).toBe(false)
|
|
|
|
// In resolution phase, no pending roll
|
|
store.setGameState(createMockGameState({ decision_phase: 'resolution' }))
|
|
expect(store.canRollDice).toBe(true)
|
|
|
|
// Has pending roll
|
|
store.setPendingRoll({
|
|
roll_id: 'roll-123',
|
|
d6_one: 3,
|
|
d6_two_a: 4,
|
|
d6_two_b: 2,
|
|
chaos_d20: 15,
|
|
resolution_d20: 8,
|
|
})
|
|
expect(store.canRollDice).toBe(false)
|
|
})
|
|
|
|
it('computes canSubmitOutcome correctly', () => {
|
|
const store = useGameStore()
|
|
|
|
expect(store.canSubmitOutcome).toBe(false)
|
|
|
|
store.setPendingRoll({
|
|
roll_id: 'roll-123',
|
|
d6_one: 3,
|
|
d6_two_a: 4,
|
|
d6_two_b: 2,
|
|
chaos_d20: 15,
|
|
resolution_d20: 8,
|
|
})
|
|
|
|
expect(store.canSubmitOutcome).toBe(true)
|
|
})
|
|
|
|
it('clears pending roll', () => {
|
|
const store = useGameStore()
|
|
|
|
store.setPendingRoll({
|
|
roll_id: 'roll-123',
|
|
d6_one: 3,
|
|
d6_two_a: 4,
|
|
d6_two_b: 2,
|
|
chaos_d20: 15,
|
|
resolution_d20: 8,
|
|
})
|
|
expect(store.pendingRoll).not.toBeNull()
|
|
|
|
store.clearPendingRoll()
|
|
expect(store.pendingRoll).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('connection and loading state', () => {
|
|
it('sets connection status', () => {
|
|
const store = useGameStore()
|
|
|
|
expect(store.isConnected).toBe(false)
|
|
|
|
store.setConnected(true)
|
|
expect(store.isConnected).toBe(true)
|
|
|
|
store.setConnected(false)
|
|
expect(store.isConnected).toBe(false)
|
|
})
|
|
|
|
it('sets loading state', () => {
|
|
const store = useGameStore()
|
|
|
|
expect(store.isLoading).toBe(false)
|
|
|
|
store.setLoading(true)
|
|
expect(store.isLoading).toBe(true)
|
|
|
|
store.setLoading(false)
|
|
expect(store.isLoading).toBe(false)
|
|
})
|
|
|
|
it('sets error message', () => {
|
|
const store = useGameStore()
|
|
|
|
expect(store.error).toBeNull()
|
|
|
|
store.setError('Connection failed')
|
|
expect(store.error).toBe('Connection failed')
|
|
|
|
store.setError(null)
|
|
expect(store.error).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('lineup management', () => {
|
|
it('sets both lineups', () => {
|
|
const store = useGameStore()
|
|
const homeLineup: Lineup[] = [
|
|
{
|
|
id: 1,
|
|
game_id: 'game-123',
|
|
team_id: 1,
|
|
card_id: 100,
|
|
position: 'SS',
|
|
batting_order: 1,
|
|
is_starter: true,
|
|
is_active: true,
|
|
player: { id: 100, name: 'Player 1', image: '' },
|
|
},
|
|
]
|
|
const awayLineup: Lineup[] = [
|
|
{
|
|
id: 2,
|
|
game_id: 'game-123',
|
|
team_id: 2,
|
|
card_id: 200,
|
|
position: 'CF',
|
|
batting_order: 1,
|
|
is_starter: true,
|
|
is_active: true,
|
|
player: { id: 200, name: 'Player 2', image: '' },
|
|
},
|
|
]
|
|
|
|
store.setLineups(homeLineup, awayLineup)
|
|
|
|
expect(store.homeLineup).toEqual(homeLineup)
|
|
expect(store.awayLineup).toEqual(awayLineup)
|
|
})
|
|
|
|
it('updates lineup for specific team', () => {
|
|
const store = useGameStore()
|
|
store.setGameState(createMockGameState())
|
|
|
|
const updatedLineup: Lineup[] = [
|
|
{
|
|
id: 3,
|
|
game_id: 'game-123',
|
|
team_id: 1,
|
|
card_id: 300,
|
|
position: 'P',
|
|
batting_order: null,
|
|
is_starter: false,
|
|
is_active: true,
|
|
player: { id: 300, name: 'Relief Pitcher', image: '' },
|
|
},
|
|
]
|
|
|
|
store.updateLineup(1, updatedLineup) // Update home team
|
|
|
|
expect(store.homeLineup).toEqual(updatedLineup)
|
|
})
|
|
})
|
|
|
|
describe('reset functionality', () => {
|
|
it('resets all game state', () => {
|
|
const store = useGameStore()
|
|
|
|
// Set up some state
|
|
store.setGameState(createMockGameState())
|
|
store.addPlayToHistory({
|
|
play_number: 1,
|
|
outcome: 'SINGLE_1',
|
|
description: 'Single',
|
|
runs_scored: 0,
|
|
outs_recorded: 0,
|
|
new_state: {},
|
|
})
|
|
store.setConnected(true)
|
|
store.setLoading(true)
|
|
store.setError('Error')
|
|
|
|
// Reset
|
|
store.resetGame()
|
|
|
|
// Verify everything is reset
|
|
expect(store.gameState).toBeNull()
|
|
expect(store.homeLineup).toEqual([])
|
|
expect(store.awayLineup).toEqual([])
|
|
expect(store.playHistory).toEqual([])
|
|
expect(store.currentDecisionPrompt).toBeNull()
|
|
expect(store.pendingRoll).toBeNull()
|
|
expect(store.isConnected).toBe(false)
|
|
expect(store.isLoading).toBe(false)
|
|
expect(store.error).toBeNull()
|
|
})
|
|
})
|
|
})
|