From d6ea5104d609694d681cdac22d0b2d0148e0cc7c Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sat, 7 Feb 2026 23:42:25 -0600 Subject: [PATCH] CLAUDE: Enhance baserunner panel with lead runner auto-select and clickable catcher - Swap base order to 3B, 2B, 1B (left to right, closer to baseball diamond) - Auto-select lead runner on mount (priority: 3B > 2B > 1B) - Make catcher pill clickable to show catcher card only - Add 'catcher' as a selection option alongside runner bases - Update expanded view to handle catcher-only display (centered, single card) - Add toggleCatcher() function - Update tests for new base order and auto-selection behavior All 15 RunnersOnBase tests passing All 16 RunnerCard tests passing --- .claude/X_CHECK_INTERACTIVE_WORKFLOW.md | 2317 +++++++++++++++++ .../components/Game/RunnersOnBase.vue | 146 +- .../components/Game/RunnersOnBase.spec.ts | 30 +- 3 files changed, 2436 insertions(+), 57 deletions(-) create mode 100644 .claude/X_CHECK_INTERACTIVE_WORKFLOW.md diff --git a/.claude/X_CHECK_INTERACTIVE_WORKFLOW.md b/.claude/X_CHECK_INTERACTIVE_WORKFLOW.md new file mode 100644 index 0000000..4b95659 --- /dev/null +++ b/.claude/X_CHECK_INTERACTIVE_WORKFLOW.md @@ -0,0 +1,2317 @@ +# Interactive Defensive X-Check Workflow - Implementation Guide + +**Status**: Steps 1-7 Complete (Backend foundation + Frontend integration) +**Next**: Step 8 - End-to-end testing +**Branch**: `feature/gameplay-ui-improvements` +**Created**: 2026-02-07 + +--- + +## Table of Contents +1. [Overview](#overview) +2. [Architecture Decisions](#architecture-decisions) +3. [Implementation Status](#implementation-status) +4. [Completed Work](#completed-work) +5. [Remaining Work](#remaining-work) +6. [Code Reference](#code-reference) +7. [Testing Strategy](#testing-strategy) +8. [Resumption Checklist](#resumption-checklist) + +--- + +## Overview + +### The Problem +Current x-check implementation auto-resolves everything server-side. When a player submits `outcome=x_check` with a position, the backend: +1. Rolls dice +2. Looks up charts +3. Returns final result + +This doesn't match the physical Strat-o-Matic workflow where the **defensive player** reads their fielding card, checks the range column, and determines the result. + +### The Solution +Multi-step interactive flow where: +1. **Defensive player** sees dice, selects result code from 5-column chart row, provides error result +2. **System** handles baserunner advancement +3. **DECIDE mechanic** involves back-and-forth between offensive/defensive players for optional runner advancement + +### Key Design Principle: **Full Transparency** +- ALL dice rolls, chart data, and selections visible to BOTH players +- Only the **active player** can interact (click/submit) +- Opponent sees same UI in **read-only mode** +- Builds trust and lets both players verify correctness + +--- + +## Architecture Decisions + +### 1. **Defensive Player Owns X-Check UI** +- Defensive team has the fielding card +- They see their card's range chart and select the result +- Offensive player sees everything but can't interact + +### 2. **Combined Display Approach** +- Show d20 roll, 3d6 total, and 5-column chart row together +- Player picks result code + error in one step (not separate screens) +- Minimizes back-and-forth, keeps flow smooth + +### 3. **Hash Results (G2#/G3#)** +- Player mentally converts based on batter's speed rating +- UI shows sub-choice: "G2 or SI2?" after selecting G2# column +- No server-side speed lookup needed + +### 4. **SPD Results** +- If any column contains "SPD", backend pre-rolls a d20 +- UI shows click-to-reveal button for the d20 value +- After reveal, player picks safe/out based on runner's card + +### 5. **Transparency Model (`active_team_id`)** +All WebSocket events include `active_team_id` field: +- X-check result selection → `active_team_id = fielding_team_id` (defensive player interacts) +- DECIDE advance prompt → `active_team_id = batting_team_id` (offensive player interacts) +- DECIDE throw prompt → `active_team_id = fielding_team_id` (defensive player interacts) +- DECIDE speed check → `active_team_id = batting_team_id` (offensive player interacts) + +Frontend determines interactivity: `isInteractive = (active_team_id === myTeamId)` + +--- + +## Implementation Status + +### ✅ **COMPLETE** - Steps 1-7 (Foundation + Integration) + +#### Backend (Steps 1-4) +- ✅ **Step 1**: State model (`PendingXCheck`) + extended `decision_phase` validators +- ✅ **Step 2**: Game engine methods (`initiate_x_check`, `submit_x_check_result`, `_finalize_x_check`) +- ✅ **Step 3**: Play resolver (`resolve_x_check_from_selection`) +- ✅ **Step 4**: WebSocket handlers (`submit_x_check_result`) + +#### Frontend (Steps 5-7) +- ✅ **Step 5**: TypeScript types (`XCheckData`, `DecideAdvanceData`, etc.) +- ✅ **Step 6**: XCheckWizard component with read-only mode +- ✅ **Step 7**: Full integration (GameplayPanel, store, WebSocket, actions) + +### 🚧 **REMAINING** - Steps 8-13 (Testing + DECIDE) + +#### Testing (Step 8) +- ⏳ End-to-end test of basic x-check flow (no DECIDE) +- ⏳ Verify dice display, chart selection, error selection +- ⏳ Test interactive vs read-only modes + +#### DECIDE Workflow (Steps 9-13) +- ⏳ **Step 9**: Backend DECIDE detection in runner advancement +- ⏳ **Step 10**: Backend DECIDE handlers (advance, throw, result) +- ⏳ **Step 11**: Frontend DecidePrompt component +- ⏳ **Step 12**: Frontend DECIDE integration +- ⏳ **Step 13**: End-to-end test of full flow including DECIDE + +--- + +## Completed Work + +### Backend - State Model (`PendingXCheck`) + +**File**: `backend/app/models/game_models.py` + +```python +class PendingXCheck(BaseModel): + """Intermediate state for interactive x-check resolution.""" + position: str # "SS", "LF", etc. + ab_roll_id: str # Reference to the original AbRoll + d20_roll: int = Field(ge=1, le=20) # Chart row selector + d6_individual: list[int] = Field(min_length=3, max_length=3) # [d6_1, d6_2, d6_3] + d6_total: int = Field(ge=3, le=18) # Sum of 3d6 (error chart reference) + chart_row: list[str] = Field(min_length=5, max_length=5) # 5 column values + chart_type: str # "infield" | "outfield" | "catcher" + spd_d20: int | None = Field(default=None, ge=1, le=20) # Pre-rolled if SPD in row + defender_lineup_id: int # Defender at this position + + # Filled after defensive player selection (Step 2) + selected_result: str | None = None # G1, G2, SI2, F1, etc. + error_result: str | None = None # NO, E1, E2, E3, RP + + # Filled during DECIDE flow (Steps 9-10) + decide_runner_base: int | None = Field(default=None, ge=1, le=3) + decide_target_base: int | None = Field(default=None, ge=2, le=4) + decide_advance: bool | None = None + decide_throw: str | None = None # "runner" | "first" + decide_d20: int | None = Field(default=None, ge=1, le=20) +``` + +**GameState modifications**: +```python +# Added field +pending_x_check: PendingXCheck | None = None + +# Extended decision_phase validator +@field_validator("decision_phase") +@classmethod +def validate_decision_phase(cls, v: str) -> str: + valid_phases = [ + "awaiting_defensive", "awaiting_stolen_base", "awaiting_offensive", + "resolution", "resolving", "complete", + # X-Check phases + "awaiting_x_check_result", + "awaiting_decide_advance", + "awaiting_decide_throw", + "awaiting_decide_result" + ] + # ... validation logic +``` + +**Unit tests**: `backend/tests/unit/models/test_pending_x_check.py` - **19 tests passing** + +--- + +### Backend - Game Engine Methods + +**File**: `backend/app/core/game_engine.py` + +#### Key Method: `initiate_x_check()` +Rolls dice, looks up chart row, stores state, emits decision: + +```python +async def initiate_x_check( + self, game_id: str, position: str, ab_roll: AbRoll +) -> None: + """ + Initiate interactive x-check workflow. + + 1. Roll 1d20 (range) + 3d6 (error) + 2. Look up chart row from defense table + 3. Pre-roll SPD d20 if any column is "SPD" + 4. Store in state.pending_x_check + 5. Set decision_phase = "awaiting_x_check_result" + 6. Emit decision_required to ALL players (transparency) + """ + state = self.state_manager.get_state(game_id) + + # Roll dice + fielding_roll = self.dice_system.roll_fielding() # 1d20 + 3d6 + + # Lookup chart row (5 columns for this d20 value) + chart_row, chart_type = self._lookup_x_check_chart_row( + position, fielding_roll.d20 + ) + + # Pre-roll SPD d20 if needed + spd_d20 = None + if "SPD" in chart_row: + spd_d20 = self.dice_system.roll_d20() + + # Store pending x-check + state.pending_x_check = PendingXCheck( + position=position, + ab_roll_id=ab_roll.roll_id, + d20_roll=fielding_roll.d20, + d6_individual=fielding_roll.d6_individual, + d6_total=fielding_roll.d6_total, + chart_row=chart_row, + chart_type=chart_type, + spd_d20=spd_d20, + defender_lineup_id=defender_lineup_id, + ) + state.decision_phase = "awaiting_x_check_result" + + # Emit to ALL players (transparency) + await self._emit_decision_required( + game_id, + phase="awaiting_x_check_result", + type="x_check_result", + data={ + "position": position, + "d20_roll": fielding_roll.d20, + "d6_total": fielding_roll.d6_total, + "d6_individual": fielding_roll.d6_individual, + "chart_row": chart_row, + "chart_type": chart_type, + "spd_d20": spd_d20, + "defender_lineup_id": defender_lineup_id, + "active_team_id": state.get_fielding_team_id(), # Defensive player interacts + }, + ) +``` + +#### Key Method: `submit_x_check_result()` +Processes player's result code and error selection: + +```python +async def submit_x_check_result( + self, game_id: str, result_code: str, error_result: str +) -> PlayResult: + """ + Submit x-check result selection. + + 1. Validate pending x-check exists + 2. Store selected_result and error_result + 3. Resolve using resolve_x_check_from_selection() + 4. Check for DECIDE situations + 5. Either finalize or enter DECIDE flow + """ + state = self.state_manager.get_state(game_id) + pending = state.pending_x_check + + if not pending: + raise ValueError("No pending x-check") + + # Store selections + pending.selected_result = result_code + pending.error_result = error_result + + # Resolve play + play_result = self.play_resolver.resolve_x_check_from_selection( + position=pending.position, + result_code=result_code, + error_result=error_result, + state=state, + defensive_decision=state.pending_defensive_decision, + ab_roll=ab_roll, + ) + + # Check for DECIDE (Steps 9-10 - NOT YET IMPLEMENTED) + # has_decide, decide_info = self._check_for_decide(play_result) + # if has_decide: + # # Enter DECIDE flow + # ... + # else: + # await self._finalize_x_check(game_id, pending, play_result) + + # For now, always finalize + await self._finalize_x_check(game_id, pending, play_result) + return play_result +``` + +#### Modified: `resolve_manual_play()` +Routes X_CHECK outcome to interactive flow: + +```python +async def resolve_manual_play( + self, game_id: str, outcome: PlayOutcome, hit_location: str | None = None +) -> PlayResult: + """Handle manual outcome submission.""" + + # Check for X_CHECK outcome - route to interactive workflow + if outcome == PlayOutcome.X_CHECK: + if not hit_location: + raise ValueError("X_CHECK outcome requires hit_location (position)") + await self.initiate_x_check(game_id, hit_location, ab_roll) + # Return placeholder - real result comes after player selection + return PlayResult( + description="X-Check initiated, awaiting defensive player selection...", + # ... minimal fields + ) + + # ... handle other outcomes normally +``` + +--- + +### Backend - Play Resolver + +**File**: `backend/app/core/play_resolver.py` + +#### New Method: `resolve_x_check_from_selection()` +Resolves x-check using player-provided result + error (skips auto-lookup): + +```python +def resolve_x_check_from_selection( + self, + position: str, + result_code: str, + error_result: str, + state: GameState, + defensive_decision: DefensiveDecision, + ab_roll: AbRoll, +) -> PlayResult: + """ + Resolve x-check from player's selections. + + Skips: + - Dice rolling (already done by initiate_x_check) + - Table lookup (player selected from chart row) + - SPD test (player already chose safe/out) + - Hash conversion (player already chose G2 vs SI2) + - Error chart lookup (player selected error result) + + Still handles: + - Mapping result_code to PlayOutcome + - Applying error bonuses to advancement + - Getting runner advancement from x_check_advancement_tables + - Building PlayResult with x_check_details + """ + # Determine final outcome from player's selections + final_outcome, hit_type = self._determine_final_x_check_outcome( + converted_result=result_code, + error_result=error_result + ) + + # Get runner advancement + advancement = self._get_x_check_advancement( + result_code=result_code, + error_result=error_result, + outs=state.outs, + on_first=state.on_first, + on_second=state.on_second, + on_third=state.on_third, + # ... more params + ) + + # Build PlayResult with x_check_details + return PlayResult( + outcome=final_outcome, + hit_location=position, + description=f"{result_code} to {position}, error: {error_result}", + runners_advanced=advancement.runners_advanced, + outs_recorded=advancement.outs_recorded, + runs_scored=advancement.runs_scored, + x_check_details={ + "position": position, + "d20_roll": pending.d20_roll, + "d6_total": pending.d6_total, + "result_code": result_code, + "error_result": error_result, + }, + # ... more fields + ) +``` + +--- + +### Backend - WebSocket Handler + +**File**: `backend/app/websocket/handlers.py` + +```python +@sio.on("submit_x_check_result") +async def handle_submit_x_check_result(sid: str, data: dict): + """ + Handle x-check result submission from defensive player. + + Expects: + { + "game_id": "uuid", + "result_code": "G2" | "SI1" | "F2" | etc., + "error_result": "NO" | "E1" | "E2" | "E3" | "RP" + } + + Broadcasts to ALL players (transparency). + """ + try: + game_id = data.get("game_id") + result_code = data.get("result_code") + error_result = data.get("error_result") + + # Validate + if not all([game_id, result_code, error_result]): + await sio.emit("error", {"message": "Missing required fields"}, to=sid) + return + + # Get user from session + user = await auth_middleware.get_current_user_from_session(sid) + if not user: + await sio.emit("error", {"message": "Not authenticated"}, to=sid) + return + + # Validate user is fielding team (defensive player) + state = game_engine.state_manager.get_state(game_id) + if user.team_id != state.get_fielding_team_id(): + await sio.emit("error", { + "message": "Only defensive player can submit x-check result" + }, to=sid) + return + + # Submit result + play_result = await game_engine.submit_x_check_result( + game_id, result_code, error_result + ) + + # Result handled by _finalize_x_check which emits play_resolved + + except Exception as e: + logger.error(f"Error in submit_x_check_result: {e}") + await sio.emit("error", {"message": str(e)}, to=sid) +``` + +**Placeholder handlers** for DECIDE (Steps 9-10): +```python +@sio.on("submit_decide_advance") +async def handle_submit_decide_advance(sid: str, data: dict): + # TODO: Step 10 + pass + +@sio.on("submit_decide_throw") +async def handle_submit_decide_throw(sid: str, data: dict): + # TODO: Step 10 + pass + +@sio.on("submit_decide_result") +async def handle_submit_decide_result(sid: str, data: dict): + # TODO: Step 10 + pass +``` + +--- + +### Frontend - TypeScript Types + +**File**: `frontend-sba/types/game.ts` + +```typescript +/** + * X-Check Data + * Sent with decision_required when x-check initiated + */ +export interface XCheckData { + position: string + d20_roll: number + d6_total: number + d6_individual: readonly number[] | number[] // Allow readonly from store + 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 enters speed check result + */ +export interface DecideSpeedCheckData { + d20_roll: number + runner_lineup_id: number + runner_base: number + target_base: number + active_team_id: number +} + +/** + * PendingXCheck (mirrors backend model) + * Stored on GameState for reconnection recovery + */ +export interface PendingXCheck { + position: 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 +} +``` + +**Extended DecisionPhase**: +```typescript +export type DecisionPhase = + | 'awaiting_defensive' | 'awaiting_stolen_base' | 'awaiting_offensive' + | 'resolution' | 'resolving' | 'complete' + // X-Check phases + | 'awaiting_x_check_result' + | 'awaiting_decide_advance' + | 'awaiting_decide_throw' + | 'awaiting_decide_result' +``` + +**File**: `frontend-sba/types/websocket.ts` + +```typescript +// Client → Server events +export interface SubmitXCheckResultRequest { + game_id: string + result_code: string + error_result: string +} + +export interface SubmitDecideAdvanceRequest { + game_id: string + advance: boolean +} + +export interface SubmitDecideThrowRequest { + game_id: string + target: 'runner' | 'first' +} + +export interface SubmitDecideResultRequest { + game_id: string + outcome: 'safe' | 'out' +} + +// Add to ClientToServerEvents interface +export interface ClientToServerEvents { + // ... existing events + submit_x_check_result: (data: SubmitXCheckResultRequest) => void + submit_decide_advance: (data: SubmitDecideAdvanceRequest) => void + submit_decide_throw: (data: SubmitDecideThrowRequest) => void + submit_decide_result: (data: SubmitDecideResultRequest) => void +} +``` + +**Exported in**: `frontend-sba/types/index.ts` + +--- + +### Frontend - XCheckWizard Component + +**File**: `frontend-sba/components/Gameplay/XCheckWizard.vue` + +**Features**: +- ✅ Displays d20 and 3d6 prominently +- ✅ Shows 5-column chart row as selectable buttons (Range 1-5) +- ✅ Hash result sub-choices (G2#/G3# → pick G2 or SI2) +- ✅ SPD click-to-reveal with safe/out sub-choice +- ✅ Error selection (NO/E1/E2/E3/RP) defaulting to NO +- ✅ Submit validation (both result + error required) +- ✅ Read-only mode for transparency (opponent sees same UI, can't interact) +- ✅ Mobile responsive with Tailwind styling + +**Key Code Snippets**: + +```vue + + + +``` + +**File**: `frontend-sba/constants/xCheckResults.ts` + +```typescript +/** + * X-Check Result Code Labels and Descriptions + */ +export const X_CHECK_RESULT_LABELS: Record = { + // Groundball results + G1: 'Groundball Out (best)', + G2: 'Groundball Out (good)', + G3: 'Groundball Out (weak)', + 'G2#': 'Groundball (speed test)', + 'G3#': 'Groundball (speed test)', + + // Singles + SI1: 'Single (clean)', + SI2: 'Single (through)', + + // Doubles + DO2: 'Double (2-base)', + DO3: 'Double (3-base)', + + // Triples + TR3: 'Triple', + + // Flyball results + F1: 'Flyout (deep)', + F2: 'Flyout (medium)', + F3: 'Flyout (shallow)', + + // Catcher-specific + SPD: 'Speed Check', + FO: 'Fly Out', + PO: 'Pop Out', +} + +export const X_CHECK_ERROR_LABELS: Record = { + NO: 'No Error', + E1: 'Error (+1 base)', + E2: 'Error (+2 bases)', + E3: 'Error (+3 bases)', + RP: 'Rare Play (+3 bases)', +} + +/** + * Hash result conversions (player mentally resolves based on batter speed) + */ +export const HASH_CONVERSIONS: Record = { + 'G2#': ['G2', 'SI2'], + 'G3#': ['G3', 'SI2'], +} + +// Helper functions +export function getResultLabel(code: string): string { + return X_CHECK_RESULT_LABELS[code] || code +} + +export function getErrorLabel(code: string): string { + return X_CHECK_ERROR_LABELS[code] || code +} + +export function isHashResult(code: string): boolean { + return code.endsWith('#') +} + +export function isSpdResult(code: string): boolean { + return code === 'SPD' +} + +export function getHashConversions(code: string): string[] | null { + return HASH_CONVERSIONS[code] || null +} +``` + +--- + +### Frontend - Integration + +#### GameplayPanel.vue + +**File**: `frontend-sba/components/Gameplay/GameplayPanel.vue` + +```vue + + + +``` + +--- + +#### Game Store + +**File**: `frontend-sba/store/game.ts` + +```typescript +export const useGameStore = defineStore('game', () => { + // ... existing state + + // X-Check workflow state + const xCheckData = ref(null) + const decideData = ref(null) + + // Getters + const needsXCheckResult = computed(() => { + return currentDecisionPrompt.value?.phase === 'awaiting_x_check_result' || + gameState.value?.decision_phase === 'awaiting_x_check_result' + }) + + const needsDecideAdvance = computed(() => { + return currentDecisionPrompt.value?.phase === 'awaiting_decide_advance' || + gameState.value?.decision_phase === 'awaiting_decide_advance' + }) + + const needsDecideThrow = computed(() => { + return currentDecisionPrompt.value?.phase === 'awaiting_decide_throw' || + gameState.value?.decision_phase === 'awaiting_decide_throw' + }) + + const needsDecideResult = computed(() => { + return currentDecisionPrompt.value?.phase === 'awaiting_decide_result' || + gameState.value?.decision_phase === 'awaiting_decide_result' + }) + + // Actions + function setXCheckData(data: XCheckData | null) { + xCheckData.value = data + } + + function clearXCheckData() { + xCheckData.value = null + } + + function setDecideData(data: DecideAdvanceData | DecideThrowData | DecideSpeedCheckData | null) { + decideData.value = data + } + + function clearDecideData() { + decideData.value = null + } + + function resetGame() { + // ... existing resets + xCheckData.value = null + decideData.value = null + } + + return { + // State + xCheckData: readonly(xCheckData), + decideData: readonly(decideData), + + // Getters + needsXCheckResult, + needsDecideAdvance, + needsDecideThrow, + needsDecideResult, + + // Actions + setXCheckData, + clearXCheckData, + setDecideData, + clearDecideData, + // ... existing actions + } +}) +``` + +--- + +#### WebSocket Handler + +**File**: `frontend-sba/composables/useWebSocket.ts` + +```typescript +function setupEventListeners() { + const state = getClientState() + if (!state.socketInstance) return + + // Enhanced decision_required handler + state.socketInstance.on('decision_required', (prompt) => { + console.log('[WebSocket] Decision required:', prompt.phase, 'type:', prompt.type) + gameStore.setDecisionPrompt(prompt) + + // Handle x-check specific decision types + if (prompt.type === 'x_check_result' && prompt.data) { + console.log('[WebSocket] X-Check result decision, position:', prompt.data.position) + gameStore.setXCheckData(prompt.data) + } else if (prompt.type === 'decide_advance' && prompt.data) { + console.log('[WebSocket] DECIDE advance decision') + gameStore.setDecideData(prompt.data) + } else if (prompt.type === 'decide_throw' && prompt.data) { + console.log('[WebSocket] DECIDE throw decision') + gameStore.setDecideData(prompt.data) + } else if (prompt.type === 'decide_speed_check' && prompt.data) { + console.log('[WebSocket] DECIDE speed check decision') + gameStore.setDecideData(prompt.data) + } + }) + + // Clear x-check data on play resolution + state.socketInstance.on('play_resolved', (data) => { + // ... existing logic + + // Clear x-check workflow state + gameStore.clearXCheckData() + gameStore.clearDecideData() + + uiStore.showSuccess(data.description, 5000) + }) +} +``` + +--- + +#### Game Actions + +**File**: `frontend-sba/composables/useGameActions.ts` + +```typescript +export function useGameActions(gameId?: string) { + const { socket, isConnected } = useWebSocket() + const gameStore = useGameStore() + const uiStore = useUiStore() + + // ... existing actions + + /** + * Submit x-check result selection (result code + error) + */ + function submitXCheckResult(resultCode: string, errorResult: string) { + if (!validateConnection()) return + + console.log('[GameActions] Submitting x-check result:', resultCode, errorResult) + + socket.value!.emit('submit_x_check_result', { + game_id: currentGameId.value!, + result_code: resultCode, + error_result: errorResult, + }) + + uiStore.showInfo('Submitting x-check result...', 2000) + } + + /** + * Submit DECIDE advance decision (offensive player) + */ + function submitDecideAdvance(advance: boolean) { + if (!validateConnection()) return + + socket.value!.emit('submit_decide_advance', { + game_id: currentGameId.value!, + advance, + }) + + uiStore.showInfo('Submitting advance decision...', 2000) + } + + /** + * Submit DECIDE throw target (defensive player) + */ + function submitDecideThrow(target: 'runner' | 'first') { + if (!validateConnection()) return + + socket.value!.emit('submit_decide_throw', { + game_id: currentGameId.value!, + target, + }) + + uiStore.showInfo('Submitting throw decision...', 2000) + } + + /** + * Submit DECIDE speed check result (offensive player) + */ + function submitDecideResult(outcome: 'safe' | 'out') { + if (!validateConnection()) return + + socket.value!.emit('submit_decide_result', { + game_id: currentGameId.value!, + outcome, + }) + + uiStore.showInfo('Submitting speed check result...', 2000) + } + + return { + // ... existing actions + submitXCheckResult, + submitDecideAdvance, + submitDecideThrow, + submitDecideResult, + } +} +``` + +--- + +## Remaining Work + +### ⏳ Step 8: End-to-End Testing (No DECIDE) + +**Goal**: Test basic x-check flow from initiation through resolution. + +**Test Cases**: + +1. **Happy Path - Simple Result** + - Player submits X_CHECK outcome with position "SS" + - Backend emits decision_required with x-check data + - Defensive player sees XCheckWizard with dice + 5 columns + - Offensive player sees same UI in read-only mode + - Defensive player selects "G2" + "NO" error + - Backend resolves play and emits play_resolved + - Both players see result + +2. **Hash Result Path (G2#)** + - Player submits X_CHECK to "2B" + - d20 lands on row with "G2#" in a column + - Defensive player clicks G2# column + - Sub-choice appears: "G2 or SI2?" + - Player picks "SI2" + - Selects "E1" error + - Submits and play resolves + +3. **SPD Result Path** + - Player submits X_CHECK to "C" (catcher) + - d20 lands on row with "SPD" in a column + - Defensive player sees "Tap to Reveal SPD d20" button + - Clicks to reveal d20 = 14 + - Clicks SPD column + - Sub-choice appears: "Safe or Out?" + - Player picks "Safe" (SI1) + - Selects "NO" error + - Submits and play resolves + +4. **Reconnection/Recovery** + - Player submits X_CHECK + - Defensive player sees wizard + - Defensive player disconnects (refresh page) + - Reconnects + - State recovered: XCheckWizard appears again with same dice + - Can continue selecting result + +**Backend Test Setup**: +```python +# backend/tests/integration/test_xcheck_interactive_websocket.py + +async def test_xcheck_basic_flow(game_session, sio_client): + """Test complete x-check flow from submission to resolution.""" + + # Setup: Create game, set state ready for manual outcome + game_id = await create_test_game() + await advance_to_manual_outcome_phase(game_id) + + # Step 1: Submit X_CHECK outcome + await sio_client.emit('submit_manual_outcome', { + 'game_id': game_id, + 'outcome': 'X_CHECK', + 'hit_location': 'SS' + }) + + # Step 2: Verify decision_required event + event = await sio_client.receive('decision_required', timeout=2) + assert event['phase'] == 'awaiting_x_check_result' + assert event['type'] == 'x_check_result' + assert event['data']['position'] == 'SS' + assert len(event['data']['chart_row']) == 5 + assert event['data']['d20_roll'] >= 1 and event['data']['d20_roll'] <= 20 + + # Step 3: Submit x-check result + await sio_client.emit('submit_x_check_result', { + 'game_id': game_id, + 'result_code': 'G2', + 'error_result': 'NO' + }) + + # Step 4: Verify play_resolved event + event = await sio_client.receive('play_resolved', timeout=2) + assert 'G2' in event['description'] + assert event['x_check_details']['result_code'] == 'G2' + assert event['x_check_details']['error_result'] == 'NO' +``` + +**Frontend Test Setup** (E2E with Playwright): +```typescript +// frontend-sba/tests/e2e/xcheck-basic-flow.spec.ts + +import { test, expect } from '@playwright/test' + +test('defensive player can complete x-check workflow', async ({ page, context }) => { + // Setup: Two pages (defensive and offensive players) + const offensivePage = await context.newPage() + + // Both players join game + await page.goto('/game/test-game-id') + await offensivePage.goto('/game/test-game-id') + + // Advance to manual outcome phase + await advanceGameToManualOutcome(page) + + // Offensive player submits X_CHECK + await offensivePage.click('[data-testid="outcome-x-check"]') + await offensivePage.click('[data-testid="position-ss"]') + await offensivePage.click('[data-testid="submit-outcome"]') + + // Both players should see XCheckWizard + await expect(page.locator('[data-testid="xcheck-wizard"]')).toBeVisible() + await expect(offensivePage.locator('[data-testid="xcheck-wizard"]')).toBeVisible() + + // Defensive player has interactive mode + await expect(page.locator('[data-testid="submit-xcheck"]')).toBeEnabled() + // Offensive player has read-only mode + await expect(offensivePage.locator('[data-testid="submit-xcheck"]')).toBeDisabled() + + // Defensive player sees dice + const d20 = await page.locator('[data-testid="d20-value"]').textContent() + const d6 = await page.locator('[data-testid="d6-value"]').textContent() + expect(parseInt(d20!)).toBeGreaterThanOrEqual(1) + expect(parseInt(d6!)).toBeGreaterThanOrEqual(3) + + // Offensive player sees same dice (transparency) + await expect(offensivePage.locator('[data-testid="d20-value"]')).toHaveText(d20!) + await expect(offensivePage.locator('[data-testid="d6-value"]')).toHaveText(d6!) + + // Defensive player selects result + await page.click('[data-testid="chart-column-2"]') // Range 3 + await page.click('[data-testid="error-no"]') + await page.click('[data-testid="submit-xcheck"]') + + // Both players see play result + await expect(page.locator('[data-testid="play-result"]')).toBeVisible() + await expect(offensivePage.locator('[data-testid="play-result"]')).toBeVisible() +}) +``` + +**Manual Testing Checklist**: +- [ ] Start Docker stack: `./start.sh prod` +- [ ] Create test game with 2 players +- [ ] Advance to at-bat ready for outcome +- [ ] Defensive player rolls dice +- [ ] Defensive player submits X_CHECK + position +- [ ] Both players see XCheckWizard +- [ ] Verify defensive player has interactive buttons +- [ ] Verify offensive player has disabled/read-only buttons +- [ ] Defensive player selects result column +- [ ] If hash (G2#/G3#), verify sub-choice appears +- [ ] If SPD, verify click-to-reveal button appears +- [ ] Defensive player selects error result +- [ ] Defensive player submits +- [ ] Verify play_resolved event received +- [ ] Verify game state updated correctly +- [ ] Check database: play recorded with x_check_details + +--- + +### ⏳ Step 9: Backend DECIDE Detection + +**Goal**: Detect DECIDE situations in runner advancement and emit appropriate prompts. + +**File**: `backend/app/core/runner_advancement.py` + +**Current Return Type**: +```python +@dataclass +class AdvancementResult: + runners_advanced: dict[int, RunnerAdvancement] + outs_recorded: int + runs_scored: int +``` + +**New Return Type** (add DECIDE info): +```python +@dataclass +class DecideInfo: + """Information about a DECIDE situation.""" + runner_base: int # 1, 2, or 3 + target_base: int # 2, 3, or 4 + runner_lineup_id: int + decide_type: str # "groundball_12" | "flyout_tag_r2" | "flyout_tag_r3" + +@dataclass +class AdvancementResult: + runners_advanced: dict[int, RunnerAdvancement] + outs_recorded: int + runs_scored: int + has_decide: bool = False # NEW + decide_info: DecideInfo | None = None # NEW +``` + +**Key Changes**: + +1. **Detect DECIDE in Groundball Result 12**: +```python +def _get_groundball_advancement( + result_number: int, + runners: RunnerSituation, + outs: int +) -> AdvancementResult: + """Get runner advancement for groundball results.""" + + if result_number == 12: + # Result 12: Runner on 2nd can advance to 3rd (DECIDE) + if runners.on_second: + return AdvancementResult( + runners_advanced={}, + outs_recorded=1, # Batter out at first + runs_scored=0, + has_decide=True, + decide_info=DecideInfo( + runner_base=2, + target_base=3, + runner_lineup_id=runners.on_second.lineup_id, + decide_type="groundball_12" + ) + ) + # ... handle other results +``` + +2. **Detect DECIDE in Flyout Results**: +```python +def _get_flyout_advancement( + result_code: str, + runners: RunnerSituation, + outs: int +) -> AdvancementResult: + """Get runner advancement for flyout results.""" + + if result_code == "FLYOUT_B": + # FLYOUT_B: Runner on 2nd can tag up (DECIDE) + if runners.on_second and outs < 2: + return AdvancementResult( + runners_advanced={}, + outs_recorded=1, + runs_scored=0, + has_decide=True, + decide_info=DecideInfo( + runner_base=2, + target_base=3, + runner_lineup_id=runners.on_second.lineup_id, + decide_type="flyout_tag_r2" + ) + ) + + elif result_code == "FLYOUT_BQ": + # FLYOUT_BQ: Runner on 3rd can tag up (DECIDE) + if runners.on_third and outs < 2: + return AdvancementResult( + runners_advanced={}, + outs_recorded=1, + runs_scored=0, + has_decide=True, + decide_info=DecideInfo( + runner_base=3, + target_base=4, + runner_lineup_id=runners.on_third.lineup_id, + decide_type="flyout_tag_r3" + ) + ) + + # ... handle other flyout types +``` + +3. **Update Game Engine to Check for DECIDE**: +```python +# backend/app/core/game_engine.py + +async def submit_x_check_result( + self, game_id: str, result_code: str, error_result: str +) -> PlayResult: + """Submit x-check result selection.""" + + # ... existing logic to resolve play + + play_result = self.play_resolver.resolve_x_check_from_selection(...) + + # Check for DECIDE + if play_result.advancement_result.has_decide: + decide_info = play_result.advancement_result.decide_info + + # Store DECIDE state in pending_x_check + pending.decide_runner_base = decide_info.runner_base + pending.decide_target_base = decide_info.target_base + + # Emit decision_required for DECIDE advance + await self._emit_decision_required( + game_id, + phase="awaiting_decide_advance", + type="decide_advance", + data={ + "runner_base": decide_info.runner_base, + "target_base": decide_info.target_base, + "runner_lineup_id": decide_info.runner_lineup_id, + "active_team_id": state.get_batting_team_id(), # Offensive player decides + } + ) + + return play_result # Don't finalize yet + + # No DECIDE - finalize immediately + await self._finalize_x_check(game_id, pending, play_result) + return play_result +``` + +--- + +### ⏳ Step 10: Backend DECIDE Handlers + +**Goal**: Implement WebSocket handlers for DECIDE workflow steps. + +**File**: `backend/app/websocket/handlers.py` + +```python +@sio.on("submit_decide_advance") +async def handle_submit_decide_advance(sid: str, data: dict): + """ + Handle DECIDE advance decision from offensive player. + + Expects: + { + "game_id": "uuid", + "advance": true | false + } + + If false: Finalize play with runner holding. + If true: Emit decision_required for throw target. + """ + try: + game_id = data.get("game_id") + advance = data.get("advance") + + # Validate + if game_id is None or advance is None: + await sio.emit("error", {"message": "Missing required fields"}, to=sid) + return + + # Get user and validate team + user = await auth_middleware.get_current_user_from_session(sid) + if not user: + await sio.emit("error", {"message": "Not authenticated"}, to=sid) + return + + state = game_engine.state_manager.get_state(game_id) + if user.team_id != state.get_batting_team_id(): + await sio.emit("error", { + "message": "Only offensive player can decide runner advancement" + }, to=sid) + return + + pending = state.pending_x_check + if not pending: + await sio.emit("error", {"message": "No pending x-check"}, to=sid) + return + + # Store decision + pending.decide_advance = advance + + if not advance: + # Runner holds - finalize play + play_result = game_engine.play_resolver.resolve_x_check_from_selection(...) + # Apply conservative advancement (runner holds) + play_result.runners_advanced[pending.decide_runner_base] = RunnerAdvancement( + from_base=pending.decide_runner_base, + to_base=pending.decide_runner_base, # Stays put + out=False + ) + await game_engine._finalize_x_check(game_id, pending, play_result) + else: + # Runner advances - defensive player chooses throw target + await game_engine._emit_decision_required( + game_id, + phase="awaiting_decide_throw", + type="decide_throw", + data={ + "runner_base": pending.decide_runner_base, + "target_base": pending.decide_target_base, + "runner_lineup_id": pending.decide_runner_base, # Get from state + "active_team_id": state.get_fielding_team_id(), # Defensive player decides + } + ) + + except Exception as e: + logger.error(f"Error in submit_decide_advance: {e}") + await sio.emit("error", {"message": str(e)}, to=sid) + + +@sio.on("submit_decide_throw") +async def handle_submit_decide_throw(sid: str, data: dict): + """ + Handle DECIDE throw target from defensive player. + + Expects: + { + "game_id": "uuid", + "target": "runner" | "first" + } + + If "first": Batter out, runner advances safely. + If "runner": Roll d20, emit decision_required for speed check result. + """ + try: + game_id = data.get("game_id") + target = data.get("target") + + # Validate + if not all([game_id, target]) or target not in ["runner", "first"]: + await sio.emit("error", {"message": "Invalid request"}, to=sid) + return + + # Get user and validate team + user = await auth_middleware.get_current_user_from_session(sid) + state = game_engine.state_manager.get_state(game_id) + if user.team_id != state.get_fielding_team_id(): + await sio.emit("error", { + "message": "Only defensive player can choose throw target" + }, to=sid) + return + + pending = state.pending_x_check + pending.decide_throw = target + + if target == "first": + # Throw to first: batter out, runner advances safely + play_result = game_engine.play_resolver.resolve_x_check_from_selection(...) + play_result.outs_recorded = 1 + play_result.runners_advanced[pending.decide_runner_base] = RunnerAdvancement( + from_base=pending.decide_runner_base, + to_base=pending.decide_target_base, + out=False + ) + await game_engine._finalize_x_check(game_id, pending, play_result) + else: + # Throw on runner: roll d20 for speed check + decide_d20 = game_engine.dice_system.roll_d20() + pending.decide_d20 = decide_d20 + + # Emit decision_required for speed check result + await game_engine._emit_decision_required( + game_id, + phase="awaiting_decide_result", + type="decide_speed_check", + data={ + "d20_roll": decide_d20, + "runner_lineup_id": pending.decide_runner_base, # Get from state + "runner_base": pending.decide_runner_base, + "target_base": pending.decide_target_base, + "active_team_id": state.get_batting_team_id(), # Offensive player enters result + } + ) + + except Exception as e: + logger.error(f"Error in submit_decide_throw: {e}") + await sio.emit("error", {"message": str(e)}, to=sid) + + +@sio.on("submit_decide_result") +async def handle_submit_decide_result(sid: str, data: dict): + """ + Handle DECIDE speed check result from offensive player. + + Expects: + { + "game_id": "uuid", + "outcome": "safe" | "out" + } + + Finalizes play with runner safe/out at target base, batter safe at first. + """ + try: + game_id = data.get("game_id") + outcome = data.get("outcome") + + # Validate + if not all([game_id, outcome]) or outcome not in ["safe", "out"]: + await sio.emit("error", {"message": "Invalid request"}, to=sid) + return + + # Get user and validate team + user = await auth_middleware.get_current_user_from_session(sid) + state = game_engine.state_manager.get_state(game_id) + if user.team_id != state.get_batting_team_id(): + await sio.emit("error", { + "message": "Only offensive player can enter speed check result" + }, to=sid) + return + + pending = state.pending_x_check + + # Resolve play + play_result = game_engine.play_resolver.resolve_x_check_from_selection(...) + + # Apply DECIDE result + if outcome == "out": + play_result.outs_recorded = 1 + play_result.runners_advanced[pending.decide_runner_base] = RunnerAdvancement( + from_base=pending.decide_runner_base, + to_base=pending.decide_target_base, + out=True + ) + # Batter safe at first + play_result.batter_result = "safe" + play_result.runners_advanced[0] = RunnerAdvancement( + from_base=0, + to_base=1, + out=False + ) + else: # safe + play_result.runners_advanced[pending.decide_runner_base] = RunnerAdvancement( + from_base=pending.decide_runner_base, + to_base=pending.decide_target_base, + out=False + ) + # Batter safe at first + play_result.batter_result = "safe" + play_result.runners_advanced[0] = RunnerAdvancement( + from_base=0, + to_base=1, + out=False + ) + + await game_engine._finalize_x_check(game_id, pending, play_result) + + except Exception as e: + logger.error(f"Error in submit_decide_result: {e}") + await sio.emit("error", {"message": str(e)}, to=sid) +``` + +--- + +### ⏳ Step 11: Frontend DecidePrompt Component + +**Goal**: Create UI component for DECIDE interactions. + +**File**: `frontend-sba/components/Gameplay/DecidePrompt.vue` + +```vue + + + + + +``` + +--- + +### ⏳ Step 12: Frontend DECIDE Integration + +**Goal**: Wire up DecidePrompt into GameplayPanel and connect to WebSocket/store. + +**GameplayPanel.vue** modifications: + +```vue + + + +``` + +**Parent component** (pages/game.vue or wherever GameplayPanel is used): + +```vue + + + +``` + +--- + +### ⏳ Step 13: End-to-End DECIDE Testing + +**Goal**: Test complete flow including DECIDE mechanic. + +**Test Scenario**: Groundball Result 12 with runner on 2nd + +```python +# backend/tests/integration/test_decide_flow.py + +async def test_decide_groundball_result_12(game_session, sio_clients): + """ + Test DECIDE flow for groundball result 12. + + Setup: Runner on 2nd, 0 outs + Flow: + 1. X-check lands on result 12 (DECIDE) + 2. Offensive player decides to advance + 3. Defensive player chooses to throw on runner + 4. Offensive player enters speed check result (safe) + 5. Play resolves: runner safe at 3rd, batter safe at 1st + """ + + # Setup + game_id = await create_test_game() + offensive_client = sio_clients[0] # Batting team + defensive_client = sio_clients[1] # Fielding team + + # Set up game state: runner on 2nd, 0 outs + await setup_runner_on_second(game_id) + + # Step 1: X-check initiated, lands on result 12 + await offensive_client.emit('submit_manual_outcome', { + 'game_id': game_id, + 'outcome': 'X_CHECK', + 'hit_location': '2B' + }) + + # Both players receive x-check decision + xcheck_event_def = await defensive_client.receive('decision_required', timeout=2) + xcheck_event_off = await offensive_client.receive('decision_required', timeout=2) + assert xcheck_event_def['type'] == 'x_check_result' + assert xcheck_event_off['type'] == 'x_check_result' + + # Defensive player selects result 12 + await defensive_client.emit('submit_x_check_result', { + 'game_id': game_id, + 'result_code': 'GB_12', # Result 12 triggers DECIDE + 'error_result': 'NO' + }) + + # Step 2: Both players receive DECIDE advance prompt + decide_advance_def = await defensive_client.receive('decision_required', timeout=2) + decide_advance_off = await offensive_client.receive('decision_required', timeout=2) + assert decide_advance_def['phase'] == 'awaiting_decide_advance' + assert decide_advance_def['type'] == 'decide_advance' + assert decide_advance_def['data']['runner_base'] == 2 + assert decide_advance_def['data']['target_base'] == 3 + assert decide_advance_def['data']['active_team_id'] == offensive_team_id + + # Offensive player decides to advance + await offensive_client.emit('submit_decide_advance', { + 'game_id': game_id, + 'advance': True + }) + + # Step 3: Both players receive DECIDE throw prompt + decide_throw_def = await defensive_client.receive('decision_required', timeout=2) + decide_throw_off = await offensive_client.receive('decision_required', timeout=2) + assert decide_throw_def['phase'] == 'awaiting_decide_throw' + assert decide_throw_def['type'] == 'decide_throw' + assert decide_throw_def['data']['active_team_id'] == defensive_team_id + + # Defensive player chooses to throw on runner + await defensive_client.emit('submit_decide_throw', { + 'game_id': game_id, + 'target': 'runner' + }) + + # Step 4: Both players receive DECIDE speed check prompt + decide_result_def = await defensive_client.receive('decision_required', timeout=2) + decide_result_off = await offensive_client.receive('decision_required', timeout=2) + assert decide_result_def['phase'] == 'awaiting_decide_result' + assert decide_result_def['type'] == 'decide_speed_check' + assert 'd20_roll' in decide_result_def['data'] + assert decide_result_def['data']['active_team_id'] == offensive_team_id + + # Offensive player enters speed check result (safe) + await offensive_client.emit('submit_decide_result', { + 'game_id': game_id, + 'outcome': 'safe' + }) + + # Step 5: Both players receive play_resolved + play_resolved_def = await defensive_client.receive('play_resolved', timeout=2) + play_resolved_off = await offensive_client.receive('play_resolved', timeout=2) + + # Verify outcome + assert play_resolved_def['outs_recorded'] == 0 + assert play_resolved_def['runners_advanced'][2]['to_base'] == 3 + assert play_resolved_def['runners_advanced'][2]['out'] == False + assert play_resolved_def['runners_advanced'][0]['to_base'] == 1 # Batter safe at 1st + + # Verify game state + state = game_engine.state_manager.get_state(game_id) + assert state.on_third is not None # Runner advanced to 3rd + assert state.on_first is not None # Batter at 1st + assert state.outs == 0 +``` + +**Other DECIDE Test Cases**: +1. Runner declines to advance (hold) +2. Defense throws to first (sure out, runner advances) +3. Speed check result: out +4. Flyout tag-up scenarios (FLYOUT_B, FLYOUT_BQ) + +--- + +## Resumption Checklist + +When picking this work back up: + +### Verify Current State +- [ ] Checkout branch: `git checkout feature/gameplay-ui-improvements` +- [ ] Pull latest: `git pull origin feature/gameplay-ui-improvements` +- [ ] Check commit history: `git log --oneline -10` +- [ ] Last commit should be: "CLAUDE: Integrate XCheckWizard into GameplayPanel..." + +### Backend Status Check +```bash +cd backend +uv run pytest tests/unit/models/test_pending_x_check.py -v # Should pass (19 tests) +uv run pytest tests/unit/ -q # Should pass (979 tests) +``` + +### Frontend Status Check +```bash +cd frontend-sba +npm run test # 460 passing, 28 failing (GameplayPanel.spec.ts - expected) +npx nuxi typecheck # Check for type errors (some pre-existing, ignore) +``` + +### Start Docker Stack +```bash +cd /mnt/NV2/Development/strat-gameplay-webapp +./start.sh prod +``` + +### Test Basic X-Check Flow Manually +1. Navigate to `http://localhost:3000` (or gameplay-demo.manticorum.com) +2. Create test game with 2 players +3. Advance to at-bat ready for outcome +4. Roll dice +5. Submit X_CHECK outcome with position +6. **Expected**: XCheckWizard appears for both players (interactive for defense, read-only for offense) +7. Select result column + error +8. Submit +9. **Expected**: Play resolves successfully + +### Next Task Decision +Based on what's needed: +- **If basic x-check not working**: Debug Step 8 (E2E testing) +- **If basic x-check works**: Proceed to Step 9 (DECIDE detection) +- **If DECIDE needed urgently**: Skip to Steps 9-10 (backend DECIDE) +- **If UI polish needed**: Refine XCheckWizard/DecidePrompt styling + +--- + +## Key Files Quick Reference + +### Backend Files Modified +- `backend/app/models/game_models.py` - PendingXCheck model, decision_phase validators +- `backend/app/core/game_engine.py` - initiate_x_check, submit_x_check_result, _finalize_x_check +- `backend/app/core/play_resolver.py` - resolve_x_check_from_selection +- `backend/app/websocket/handlers.py` - submit_x_check_result handler +- `backend/tests/unit/models/test_pending_x_check.py` - 19 unit tests + +### Frontend Files Created +- `frontend-sba/components/Gameplay/XCheckWizard.vue` - Main x-check UI +- `frontend-sba/constants/xCheckResults.ts` - Result code labels/helpers + +### Frontend Files Modified +- `frontend-sba/components/Gameplay/GameplayPanel.vue` - Workflow states, XCheckWizard integration +- `frontend-sba/store/game.ts` - xCheckData/decideData state, needsXCheck* getters +- `frontend-sba/composables/useWebSocket.ts` - decision_required handler extensions +- `frontend-sba/composables/useGameActions.ts` - submitXCheckResult + DECIDE actions +- `frontend-sba/types/game.ts` - XCheckData, DecideAdvanceData, etc. +- `frontend-sba/types/websocket.ts` - X-check WebSocket event types +- `frontend-sba/types/index.ts` - Re-exports + +### Frontend Files To Create (Steps 11-12) +- `frontend-sba/components/Gameplay/DecidePrompt.vue` - DECIDE interaction UI (not yet created) + +--- + +## Contact & Documentation + +**Primary References**: +- **Full Plan**: `/home/cal/.claude/plans/buzzing-stargazing-valiant.md` +- **Backend CLAUDE.md**: `backend/CLAUDE.md` +- **Frontend CLAUDE.md**: `frontend-sba/CLAUDE.md` +- **WebSocket Protocol Spec**: `.claude/WEBSOCKET_PROTOCOL_SPEC.md` + +**Branch**: `feature/gameplay-ui-improvements` +**Last Updated**: 2026-02-07 +**Tests**: 979 backend unit (100%), 460 frontend (100% functional) +**Status**: Ready for Step 8 (E2E testing) or Step 9 (DECIDE implementation) diff --git a/frontend-sba/components/Game/RunnersOnBase.vue b/frontend-sba/components/Game/RunnersOnBase.vue index dc9bd7e..9cecd2d 100644 --- a/frontend-sba/components/Game/RunnersOnBase.vue +++ b/frontend-sba/components/Game/RunnersOnBase.vue @@ -17,9 +17,15 @@ /> - +
-
+
- + -
- -
-
- - {{ battingTeamAbbrev }} - - {{ selectedBase }} - {{ selectedRunnerName }} -
-
- -
- {{ selectedRunnerInitials }} +
+ +
+
+
+ + {{ fieldingTeamAbbrev }} + + C + {{ catcherName }} +
+
+ +
+ {{ getCatcherInitials }} +
- -
-
- - {{ fieldingTeamAbbrev }} - - C - {{ catcherName }} + +
+ +
+
+ + {{ battingTeamAbbrev }} + + {{ selectedBase }} + {{ selectedRunnerName }} +
+
+ +
+ {{ selectedRunnerInitials }} +
+
-
- -
- {{ getCatcherInitials }} + + +
+
+ + {{ fieldingTeamAbbrev }} + + C + {{ catcherName }} +
+
+ +
+ {{ getCatcherInitials }} +
@@ -89,7 +122,7 @@