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

320 lines
9.4 KiB
Markdown

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