strat-gameplay-webapp/.claude/implementation/phase-3a-data-models.md
Cal Corum a1f42a93b8 CLAUDE: Implement Phase 3A - X-Check data models and enums
Add foundational data structures for X-Check play resolution system:

Models Added:
- PositionRating: Defensive ratings (range 1-5, error 0-88) for X-Check resolution
- XCheckResult: Dataclass tracking complete X-Check resolution flow with dice rolls,
  conversions (SPD test, G2#/G3#→SI2), error results, and final outcomes
- BasePlayer.active_position_rating: Optional field for current defensive position

Enums Extended:
- PlayOutcome.X_CHECK: New outcome type requiring special resolution
- PlayOutcome.is_x_check(): Helper method for type checking

Documentation Enhanced:
- Play.check_pos: Documented as X-Check position identifier
- Play.hit_type: Documented with examples (single_2_plus_error_1, etc.)

Utilities Added:
- app/core/cache.py: Redis cache key helpers for player positions and game state

Implementation Planning:
- Complete 6-phase implementation plan (3A-3F) documented in .claude/implementation/
- Phase 3A complete with all acceptance criteria met
- Zero breaking changes, all existing tests passing

Next: Phase 3B will add defense tables, error charts, and advancement logic

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 15:32:09 -05:00

9.4 KiB

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)

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):

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:

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)

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

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):

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

@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

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)

"""
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