# Phase 3A: Data Models & Enums for X-Check System **Status**: Not Started **Estimated Effort**: 2-3 hours **Dependencies**: None ## Overview Add data models and enums to support X-Check play resolution. This includes: - PositionRating model for defensive ratings - XCheckResult intermediate state object - PlayOutcome.X_CHECK enum value - Updates to Play model documentation ## Tasks ### 1. Add PositionRating Model to player_models.py **File**: `backend/app/models/player_models.py` **Location**: After PdPitchingCard class (around line 289) ```python class PositionRating(BaseModel): """ Defensive rating for a player at a specific position. Used for X-Check play resolution. Ratings come from: - PD: API endpoint /api/v2/cardpositions/player/:player_id - SBA: Read from physical cards by players """ position: str = Field(..., description="Position code (SS, LF, CF, etc.)") innings: int = Field(..., description="Innings played at position") range: int = Field(..., ge=1, le=5, description="Defense range (1=best, 5=worst)") error: int = Field(..., ge=0, le=25, description="Error rating (0=best, 25=worst)") arm: Optional[int] = Field(None, description="Throwing arm rating") pb: Optional[int] = Field(None, description="Passed balls (catchers only)") overthrow: Optional[int] = Field(None, description="Overthrow risk") @classmethod def from_api_response(cls, data: Dict[str, Any]) -> "PositionRating": """ Create PositionRating from PD API response. Args: data: Single position dict from /api/v2/cardpositions response Returns: PositionRating instance """ return cls( position=data["position"], innings=data["innings"], range=data["range"], error=data["error"], arm=data.get("arm"), pb=data.get("pb"), overthrow=data.get("overthrow") ) ``` **Add to BasePlayer class** (around line 42): ```python class BasePlayer(BaseModel, ABC): # ... existing fields ... # Active position rating (loaded for current defensive position) active_position_rating: Optional['PositionRating'] = Field( None, description="Defensive rating for current position" ) ``` **Update imports** at top of file: ```python from typing import Optional, List, Dict, Any, TYPE_CHECKING if TYPE_CHECKING: from app.models.game_models import PositionRating # Forward reference ``` ### 2. Add XCheckResult Model to game_models.py **File**: `backend/app/models/game_models.py` **Location**: After PlayResult class (find it in the file) ```python from dataclasses import dataclass from typing import Optional from app.config.result_charts import PlayOutcome @dataclass class XCheckResult: """ Intermediate state for X-Check play resolution. Tracks all dice rolls, table lookups, and conversions from initial x-check through final outcome determination. Resolution Flow: 1. Roll 1d20 + 3d6 2. Look up base_result from defense table[d20][defender_range] 3. Apply SPD test if needed (base_result = 'SPD') 4. Apply G2#/G3# → SI2 conversion if conditions met 5. Look up error_result from error chart[error_rating][3d6] 6. Determine final_outcome (may be ERROR if out+error) Attributes: position: Position being checked (SS, LF, 3B, etc.) d20_roll: Defense range table row selector (1-20) d6_roll: Error chart lookup value (3-18, sum of 3d6) defender_range: Defender's range rating (1-5, adjusted for playing in) defender_error_rating: Defender's error rating (0-25) base_result: Initial result from defense table (G1, F2, SI2, SPD, etc.) converted_result: Result after SPD/G2#/G3# conversions (may equal base_result) error_result: Error type from error chart (NO, E1, E2, E3, RP) final_outcome: Final PlayOutcome after all conversions defender_id: Player ID of defender hit_type: Combined result string for Play.hit_type (e.g. 'single_2_plus_error_1') """ position: str d20_roll: int d6_roll: int defender_range: int defender_error_rating: int defender_id: int base_result: str converted_result: str error_result: str # 'NO', 'E1', 'E2', 'E3', 'RP' final_outcome: PlayOutcome hit_type: str # Optional: SPD test details if applicable spd_test_roll: Optional[int] = None spd_test_target: Optional[int] = None spd_test_passed: Optional[bool] = None def to_dict(self) -> dict: """Convert to dict for WebSocket transmission.""" return { 'position': self.position, 'd20_roll': self.d20_roll, 'd6_roll': self.d6_roll, 'defender_range': self.defender_range, 'defender_error_rating': self.defender_error_rating, 'defender_id': self.defender_id, 'base_result': self.base_result, 'converted_result': self.converted_result, 'error_result': self.error_result, 'final_outcome': self.final_outcome.value, 'hit_type': self.hit_type, 'spd_test': { 'roll': self.spd_test_roll, 'target': self.spd_test_target, 'passed': self.spd_test_passed } if self.spd_test_roll else None } ``` ### 3. Add X_CHECK to PlayOutcome Enum **File**: `backend/app/config/result_charts.py` **Location**: Line 89, after ERROR ```python class PlayOutcome(str, Enum): # ... existing outcomes ... # ==================== Errors ==================== ERROR = "error" # ==================== X-Check Plays ==================== # X-Check: Defense-dependent plays requiring range/error rolls # Resolution determines actual outcome (hit/out/error) X_CHECK = "x_check" # Play.check_pos contains position, resolve via tables # ==================== Interrupt Plays ==================== # ... rest of enums ... ``` **Add helper method** to PlayOutcome class (around line 199): ```python def is_x_check(self) -> bool: """Check if outcome requires x-check resolution.""" return self == self.X_CHECK ``` ### 4. Update PlayResult to Include XCheckResult **File**: `backend/app/models/game_models.py` **Location**: In PlayResult dataclass ```python @dataclass class PlayResult: """Result of resolving a single play.""" # ... existing fields ... # X-Check details (only populated for x-check plays) x_check_details: Optional[XCheckResult] = None ``` ### 5. Document Play.check_pos Field **File**: `backend/app/models/db_models.py` **Location**: Line 139, update check_pos field documentation ```python class Play(Base): # ... existing fields ... check_pos = Column( String(5), nullable=True, comment="Position checked for X-Check plays (SS, LF, 3B, etc.). " "Non-null indicates this was an X-Check play. " "Used only for X-Checks - all other plays leave this null." ) hit_type = Column( String(50), nullable=True, comment="Detailed hit/out type including errors. Examples: " "'single_2_plus_error_1', 'f2_plus_error_2', 'g1_no_error'. " "Used primarily for X-Check plays to preserve full resolution details." ) ``` ### 6. Add Redis Cache Key Constants **File**: `backend/app/core/cache.py` (create if doesn't exist) ```python """ Redis cache key patterns and helper functions. Author: Claude Date: 2025-11-01 """ def get_player_positions_cache_key(player_id: int) -> str: """ Get Redis cache key for player's position ratings. Args: player_id: Player ID Returns: Cache key string Example: >>> get_player_positions_cache_key(10932) 'player:10932:positions' """ return f"player:{player_id}:positions" def get_game_state_cache_key(game_id: int) -> str: """ Get Redis cache key for game state. Args: game_id: Game ID Returns: Cache key string """ return f"game:{game_id}:state" ``` ## Testing Requirements 1. **Unit Tests**: `tests/models/test_player_models.py` - Test PositionRating.from_api_response() - Test PositionRating field validation (range 1-5, error 0-25) 2. **Unit Tests**: `tests/models/test_game_models.py` - Test XCheckResult.to_dict() - Test XCheckResult with and without SPD test 3. **Integration Tests**: `tests/test_x_check_models.py` - Test PlayResult with x_check_details populated - Test Play record with check_pos and hit_type ## Acceptance Criteria - [ ] PositionRating model added with validation - [ ] BasePlayer has active_position_rating field - [ ] XCheckResult dataclass complete with to_dict() - [ ] PlayOutcome.X_CHECK enum added - [ ] PlayOutcome.is_x_check() helper method added - [ ] PlayResult.x_check_details field added - [ ] Play.check_pos and Play.hit_type documented - [ ] Redis cache key helpers created - [ ] All unit tests pass - [ ] No import errors (verify imports during code review) ## Notes - PositionRating will be loaded from PD API at lineup creation (Phase 3E) - For SBA games, position ratings come from manual input (semi-auto mode) - XCheckResult preserves all resolution steps for debugging and UI display - hit_type field allows us to track complex results like "g2_converted_single_2_plus_error_1" ## Next Phase After completion, proceed to **Phase 3B: League Config Tables**