# 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**
