strat-gameplay-webapp/.claude/implementation/02-week6-league-features.md
Cal Corum 64aa800672 CLAUDE: Update implementation plans for next session
Updated week 6 status and created comprehensive next session plan.

## Changes

### Implementation Status Updates
- Updated 00-index.md with Week 6 progress (75% complete)
- Updated 02-week6-league-features.md status and goals
- Added new components to status table:
  - League Configs:  Complete
  - PlayOutcome Enum:  Complete
  - PlayResolver Integration: 🟡 Partial

### Next Session Plan (NEW)
Created NEXT_SESSION.md with detailed task breakdown:

**Task 1: Update Dice System** (30 min)
- Rename check_d20 → chaos_d20
- Update docstrings for chaos die purpose
- Update all references

**Task 2: Integrate PlayOutcome** (60 min)
- Replace old local enum with universal enum
- Update SimplifiedResultChart
- Add uncapped hit handling
- Update all outcome references

**Task 3: Add Play.metadata** (30 min)
- Add JSONB metadata field to Play model
- Log uncapped hits in metadata
- Update game_engine._save_play_to_db()

## Quick Context for Next Session

**What's Complete**: Config system, PlayOutcome enum, 58 tests passing
**What's Remaining**: Dice system update, PlayResolver integration, metadata support
**Estimated Time**: 2 hours
**Success Criteria**: All tests passing, uncapped hits tracked, terminal client works

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 22:58:56 -05:00

860 lines
24 KiB
Markdown

# Week 6: League Features & Integration
**Duration**: Week 6 of Phase 2
**Prerequisites**: Week 5 Complete (Game engine working)
**Focus**: League-specific features, configurations, and API integration
**Status**: 75% Complete (2025-10-28)
**Last Session**: Config system and PlayOutcome implemented
**Next Session**: Dice system update and PlayResolver integration
---
## ⚠️ IMPORTANT: Player Models Documentation
Player model implementation is now documented separately due to complexity:
**→ See [02-week6-player-models-overview.md](./02-week6-player-models-overview.md) for complete player model specifications**
This includes:
- Two-layer architecture (API models + Game models)
- Real API data structures for PD and SBA
- Mapper layer for transformations
- Complete implementation guide
**This file focuses on configs and result charts only.**
---
## Overview
Complete Phase 2 by implementing:
1. **Player Models** (see separate documentation above)
2. **League Configuration System** (this file)
3. **Result Chart System** (this file)
4. **PlayResolver Integration** (this file)
## Goals
By end of Week 6:
- ✅ Player models (polymorphic with factory methods) - **COMPLETE 2025-10-28**
- ✅ League configuration framework (BaseConfig → SbaConfig/PdConfig) - **COMPLETE 2025-10-28**
- ✅ PlayOutcome enum (universal for both leagues) - **COMPLETE 2025-10-28**
- ⏳ Dice system update (check_d20 → chaos_d20) - **PENDING**
- ⏳ PlayResolver updated to use PlayOutcome and configs - **PENDING**
- ⏳ Play.metadata support for uncapped hits - **PENDING**
- ⏳ Full test coverage for integration - **PENDING**
## Architecture
```
┌──────────────────────────────────────────────────────────┐
│ League-Agnostic Core │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ GameEngine │ │ StateManager │ │ PlayResolver│ │
│ └─────────────┘ └──────────────┘ └────────────┘ │
│ ↓ │
│ LeagueConfig │
│ ↓ ↓ │
│ SbaConfig PdConfig │
│ ↓ ↓ │
│ SbaPlayer PdPlayer (see separate docs) │
└──────────────────────────────────────────────────────────┘
```
## Components to Build
### 1. Player Models
**→ MOVED TO SEPARATE DOCUMENTATION**
See [02-week6-player-models-overview.md](./02-week6-player-models-overview.md) and the `player-model-specs/` directory for:
- API models (exact API responses)
- Game models (gameplay-optimized)
- Mappers and factories
- API client
- Complete testing strategy
Abstract player model with league-specific implementations.
```python
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any, List
from pydantic import BaseModel, Field
class BasePlayer(BaseModel, ABC):
"""
Abstract base player model
All players have basic identification fields.
League-specific data is added in subclasses.
"""
card_id: int
name: str
team_id: int
@abstractmethod
def get_image_url(self) -> str:
"""Get player image URL"""
pass
@abstractmethod
def get_display_name(self) -> str:
"""Get display name for UI"""
pass
class Config:
# Allow subclassing with Pydantic
orm_mode = True
class SbaPlayer(BasePlayer):
"""
SBA League player model
Minimal data model - most stats on backend via API
"""
player_id: int # SBA player ID
position: str
image_url: Optional[str] = None
def get_image_url(self) -> str:
"""Get image URL"""
return self.image_url or f"https://sba-api.example.com/images/{self.player_id}.png"
def get_display_name(self) -> str:
"""Display name"""
return f"{self.name} ({self.position})"
class Config:
json_schema_extra = {
"example": {
"card_id": 123,
"player_id": 456,
"name": "John Smith",
"team_id": 1,
"position": "CF",
"image_url": "https://example.com/image.png"
}
}
class ScoutingGrades(BaseModel):
"""PD scouting grades for detailed probability calculations"""
contact: int = Field(ge=1, le=10) # 1-10 scale
gap_power: int = Field(ge=1, le=10)
raw_power: int = Field(ge=1, le=10)
eye: int = Field(ge=1, le=10)
avoid_k: int = Field(ge=1, le=10)
speed: int = Field(ge=1, le=10)
stealing: Optional[int] = Field(default=None, ge=1, le=10)
# Fielding (position-specific)
fielding: Optional[int] = Field(default=None, ge=1, le=10)
arm: Optional[int] = Field(default=None, ge=1, le=10)
# Pitching (pitchers only)
stuff: Optional[int] = Field(default=None, ge=1, le=10)
movement: Optional[int] = Field(default=None, ge=1, le=10)
control: Optional[int] = Field(default=None, ge=1, le=10)
class PdPlayer(BasePlayer):
"""
Paper Dynasty player model
Includes detailed scouting grades for probability calculations
"""
player_id: int # PD player ID
position: str
cardset_id: int
image_url: Optional[str] = None
# Scouting model
scouting: ScoutingGrades
# Metadata
is_pitcher: bool = False
handedness: str = "R" # R, L, S
def get_image_url(self) -> str:
"""Get image URL"""
return self.image_url or f"https://pd-api.example.com/images/{self.player_id}.png"
def get_display_name(self) -> str:
"""Display name with handedness"""
hand_symbol = {"R": "⟩", "L": "⟨", "S": "⟨⟩"}.get(self.handedness, "")
return f"{self.name} {hand_symbol} ({self.position})"
def calculate_contact_probability(self, pitcher_stuff: int) -> float:
"""
Calculate contact probability based on scouting grades
This is where PD's detailed model comes into play.
Real implementation will use complex formulas.
"""
# Simplified placeholder
batter_skill = (self.scouting.contact + self.scouting.eye) / 2
pitcher_skill = pitcher_stuff
base_prob = 0.5 + (batter_skill - pitcher_skill) * 0.05
return max(0.1, min(0.9, base_prob))
class Config:
json_schema_extra = {
"example": {
"card_id": 789,
"player_id": 101,
"name": "Jane Doe",
"team_id": 2,
"position": "SS",
"cardset_id": 5,
"handedness": "R",
"scouting": {
"contact": 8,
"gap_power": 6,
"raw_power": 5,
"eye": 7,
"avoid_k": 8,
"speed": 9,
"stealing": 8,
"fielding": 9,
"arm": 8
}
}
}
class LineupFactory:
"""Factory for creating league-specific lineup instances"""
@staticmethod
def create_player(league_id: str, data: Dict[str, Any]) -> BasePlayer:
"""
Create player instance based on league
Args:
league_id: 'sba' or 'pd'
data: Player data from API or database
Returns:
SbaPlayer or PdPlayer instance
"""
if league_id == "sba":
return SbaPlayer(**data)
elif league_id == "pd":
return PdPlayer(**data)
else:
raise ValueError(f"Unknown league: {league_id}")
@staticmethod
def create_lineup(
league_id: str,
team_data: List[Dict[str, Any]]
) -> List[BasePlayer]:
"""Create full lineup from team data"""
return [
LineupFactory.create_player(league_id, player_data)
for player_data in team_data
]
```
**Implementation Steps:**
1. Create `backend/app/models/player_models.py`
2. Implement BasePlayer abstract class
3. Implement SbaPlayer (simple)
4. Implement PdPlayer with scouting grades
5. Implement LineupFactory
6. Write unit tests
**Tests:**
- `tests/unit/models/test_player_models.py`
- Test SbaPlayer instantiation
- Test PdPlayer with scouting
- Test factory pattern
- Test type guards
---
### 2. League Configuration System (`backend/app/config/`)
Configuration classes for league-specific game rules.
```python
# backend/app/config/base_config.py
from abc import ABC, abstractmethod
from typing import Dict, Any
from pydantic import BaseModel
class BaseGameConfig(BaseModel, ABC):
"""Base configuration for all leagues"""
league_id: str
version: str = "1.0.0"
# Basic rules
innings: int = 9
outs_per_inning: int = 3
strikes_for_out: int = 3
balls_for_walk: int = 4
@abstractmethod
def get_result_chart_name(self) -> str:
"""Get name of result chart to use"""
pass
@abstractmethod
def supports_result_selection(self) -> bool:
"""Whether players manually select results"""
pass
@abstractmethod
def get_api_base_url(self) -> str:
"""Get base URL for league API"""
pass
class Config:
frozen = True # Immutable config
```
```python
# backend/app/config/league_configs.py
from app.config.base_config import BaseGameConfig
class SbaConfig(BaseGameConfig):
"""SBA League configuration"""
league_id: str = "sba"
# SBA-specific rules
player_selection_mode: str = "manual" # Players select from chart after dice
def get_result_chart_name(self) -> str:
return "sba_standard_v1"
def supports_result_selection(self) -> bool:
return True # SBA players see dice, then pick result
def get_api_base_url(self) -> str:
return "https://sba-api.example.com"
class PdConfig(BaseGameConfig):
"""Paper Dynasty League configuration"""
league_id: str = "pd"
# PD-specific rules
player_selection_mode: str = "flexible" # Manual or auto via scouting model
use_scouting_model: bool = True
cardset_validation: bool = True # Validate cards against approved cardsets
# Advanced features
detailed_analytics: bool = True
wpa_calculation: bool = True
def get_result_chart_name(self) -> str:
return "pd_standard_v1"
def supports_result_selection(self) -> bool:
return True # Can be manual or auto
def get_api_base_url(self) -> str:
return "https://pd-api.example.com"
# Config registry
LEAGUE_CONFIGS = {
"sba": SbaConfig(),
"pd": PdConfig()
}
def get_league_config(league_id: str) -> BaseGameConfig:
"""Get configuration for league"""
config = LEAGUE_CONFIGS.get(league_id)
if not config:
raise ValueError(f"Unknown league: {league_id}")
return config
```
**Implementation Steps:**
1. Create `backend/app/config/base_config.py`
2. Create `backend/app/config/league_configs.py`
3. Implement SbaConfig
4. Implement PdConfig
5. Create config registry
**Tests:**
- `tests/unit/config/test_league_configs.py`
- Test config loading
- Test config immutability
- Test config registry
---
### 3. Result Charts (`backend/app/config/result_charts.py`)
Result chart definitions for each league.
```python
from typing import Dict, List
from enum import Enum
class ChartOutcome(str, Enum):
"""Standardized outcome types"""
STRIKEOUT = "strikeout"
GROUNDOUT = "groundout"
FLYOUT = "flyout"
LINEOUT = "lineout"
SINGLE = "single"
DOUBLE = "double"
TRIPLE = "triple"
HOMERUN = "homerun"
WALK = "walk"
HBP = "hbp"
ERROR = "error"
class ResultChart:
"""Base result chart"""
def __init__(self, name: str, outcomes: Dict[int, List[ChartOutcome]]):
self.name = name
self.outcomes = outcomes
def get_outcomes(self, roll: int) -> List[ChartOutcome]:
"""Get available outcomes for dice roll"""
return self.outcomes.get(roll, [])
# SBA Standard Chart (simplified placeholder)
SBA_STANDARD_CHART = ResultChart(
name="sba_standard_v1",
outcomes={
1: [ChartOutcome.STRIKEOUT],
2: [ChartOutcome.STRIKEOUT, ChartOutcome.GROUNDOUT],
3: [ChartOutcome.GROUNDOUT],
4: [ChartOutcome.GROUNDOUT, ChartOutcome.FLYOUT],
5: [ChartOutcome.FLYOUT],
6: [ChartOutcome.FLYOUT, ChartOutcome.LINEOUT],
7: [ChartOutcome.LINEOUT],
8: [ChartOutcome.GROUNDOUT],
9: [ChartOutcome.FLYOUT],
10: [ChartOutcome.SINGLE],
11: [ChartOutcome.SINGLE],
12: [ChartOutcome.SINGLE, ChartOutcome.ERROR],
13: [ChartOutcome.WALK],
14: [ChartOutcome.SINGLE],
15: [ChartOutcome.SINGLE, ChartOutcome.DOUBLE],
16: [ChartOutcome.DOUBLE],
17: [ChartOutcome.DOUBLE, ChartOutcome.TRIPLE],
18: [ChartOutcome.TRIPLE],
19: [ChartOutcome.TRIPLE, ChartOutcome.HOMERUN],
20: [ChartOutcome.HOMERUN]
}
)
# PD Standard Chart (simplified placeholder)
PD_STANDARD_CHART = ResultChart(
name="pd_standard_v1",
outcomes={
# Similar structure but auto-selectable based on scouting model
1: [ChartOutcome.STRIKEOUT],
2: [ChartOutcome.GROUNDOUT],
3: [ChartOutcome.GROUNDOUT],
4: [ChartOutcome.FLYOUT],
5: [ChartOutcome.FLYOUT],
6: [ChartOutcome.LINEOUT],
7: [ChartOutcome.GROUNDOUT],
8: [ChartOutcome.FLYOUT],
9: [ChartOutcome.SINGLE],
10: [ChartOutcome.SINGLE],
11: [ChartOutcome.SINGLE],
12: [ChartOutcome.WALK],
13: [ChartOutcome.SINGLE],
14: [ChartOutcome.SINGLE],
15: [ChartOutcome.DOUBLE],
16: [ChartOutcome.DOUBLE],
17: [ChartOutcome.TRIPLE],
18: [ChartOutcome.TRIPLE],
19: [ChartOutcome.HOMERUN],
20: [ChartOutcome.HOMERUN]
}
)
# Chart registry
RESULT_CHARTS = {
"sba_standard_v1": SBA_STANDARD_CHART,
"pd_standard_v1": PD_STANDARD_CHART
}
def get_result_chart(chart_name: str) -> ResultChart:
"""Get result chart by name"""
chart = RESULT_CHARTS.get(chart_name)
if not chart:
raise ValueError(f"Unknown chart: {chart_name}")
return chart
```
**Implementation Steps:**
1. Create `backend/app/config/result_charts.py`
2. Define ChartOutcome enum
3. Create SBA chart (placeholder)
4. Create PD chart (placeholder)
5. Write tests
---
### 4. League API Client (`backend/app/data/api_client.py`)
HTTP client for fetching team and player data from league APIs.
```python
import logging
from typing import Dict, List, Optional
import httpx
from app.config.league_configs import get_league_config
logger = logging.getLogger(f'{__name__}.LeagueApiClient')
class LeagueApiClient:
"""Client for league REST APIs"""
def __init__(self, league_id: str):
self.league_id = league_id
self.config = get_league_config(league_id)
self.base_url = self.config.get_api_base_url()
self.client = httpx.AsyncClient(
base_url=self.base_url,
timeout=10.0
)
async def get_team(self, team_id: int) -> Dict:
"""Fetch team data"""
try:
response = await self.client.get(f"/teams/{team_id}")
response.raise_for_status()
data = response.json()
logger.info(f"Fetched team {team_id} from {self.league_id} API")
return data
except httpx.HTTPError as e:
logger.error(f"Failed to fetch team {team_id}: {e}")
raise
async def get_roster(self, team_id: int) -> List[Dict]:
"""Fetch team roster"""
try:
response = await self.client.get(f"/teams/{team_id}/roster")
response.raise_for_status()
data = response.json()
logger.info(f"Fetched roster for team {team_id}")
return data
except httpx.HTTPError as e:
logger.error(f"Failed to fetch roster: {e}")
raise
async def get_player(self, player_id: int) -> Dict:
"""Fetch player/card data"""
try:
response = await self.client.get(f"/players/{player_id}")
response.raise_for_status()
data = response.json()
logger.debug(f"Fetched player {player_id}")
return data
except httpx.HTTPError as e:
logger.error(f"Failed to fetch player: {e}")
raise
async def close(self):
"""Close HTTP client"""
await self.client.aclose()
class LeagueApiClientFactory:
"""Factory for creating API clients"""
@staticmethod
def create(league_id: str) -> LeagueApiClient:
"""Create API client for league"""
return LeagueApiClient(league_id)
```
**Implementation Steps:**
1. Create `backend/app/data/__init__.py`
2. Create `backend/app/data/api_client.py`
3. Implement LeagueApiClient with httpx
4. Add error handling and retries
5. Write integration tests with mocked API
**Tests:**
- `tests/integration/data/test_api_client.py`
- Mock API responses
- Test team fetching
- Test roster fetching
- Test error handling
---
### 5. Integration - Update Play Resolver
Update play resolver to use league configs and result charts.
```python
# backend/app/core/play_resolver.py (updates)
from app.config.league_configs import get_league_config
from app.config.result_charts import get_result_chart
class PlayResolver:
"""Updated to use league configs"""
def __init__(self, league_id: str):
self.league_id = league_id
self.config = get_league_config(league_id)
self.result_chart = get_result_chart(self.config.get_result_chart_name())
self.dice = DiceSystem()
def resolve_play(
self,
state: GameState,
defensive_decision: DefensiveDecision,
offensive_decision: OffensiveDecision,
selected_outcome: Optional[str] = None # For manual selection
) -> PlayResult:
"""
Resolve play with league-specific logic
Args:
selected_outcome: For SBA/manual mode, player's outcome choice
"""
# Roll dice
dice_roll = self.dice.roll_d20()
# Get available outcomes
available_outcomes = self.result_chart.get_outcomes(dice_roll.roll)
# Determine final outcome
if selected_outcome:
# Manual selection (SBA)
outcome = ChartOutcome(selected_outcome)
if outcome not in available_outcomes:
raise ValueError(f"Invalid outcome selection: {outcome}")
else:
# Auto-select (PD with scouting model or simplified)
outcome = self._auto_select_outcome(
available_outcomes,
state,
defensive_decision,
offensive_decision
)
# Rest of resolution logic...
return self._resolve_outcome(outcome, state, dice_roll)
def _auto_select_outcome(
self,
available: List[ChartOutcome],
state: GameState,
def_decision: DefensiveDecision,
off_decision: OffensiveDecision
) -> ChartOutcome:
"""Auto-select outcome for PD or fallback"""
# Simplified: just pick first available
# TODO: Use scouting model for PD
return available[0]
```
---
## Week 6 Deliverables
### Code Files
-`backend/app/models/player_models.py` - Polymorphic players
-`backend/app/config/base_config.py` - Base config
-`backend/app/config/league_configs.py` - SBA/PD configs
-`backend/app/config/result_charts.py` - Result charts
-`backend/app/data/api_client.py` - League API client
- ✅ Updated `backend/app/core/play_resolver.py` - League-aware resolution
### Tests
-`tests/unit/models/test_player_models.py`
-`tests/unit/config/test_league_configs.py`
-`tests/unit/config/test_result_charts.py`
-`tests/integration/data/test_api_client.py`
-`tests/integration/test_sba_game_flow.py` - Full SBA at-bat
-`tests/integration/test_pd_game_flow.py` - Full PD at-bat
### Integration Test Scripts
```python
# tests/integration/test_sba_game_flow.py
import pytest
from uuid import uuid4
from app.core.state_manager import state_manager
from app.core.game_engine import GameEngine
from app.models.game_models import DefensiveDecision, OffensiveDecision
@pytest.mark.asyncio
async def test_complete_sba_at_bat():
"""Test complete SBA at-bat with manual result selection"""
game_id = uuid4()
# Create SBA game
state = await state_manager.create_game(
game_id=game_id,
league_id="sba",
home_team_id=1,
away_team_id=2
)
engine = GameEngine(league_id="sba")
# Start game
await engine.start_game(game_id)
# Submit decisions
await engine.submit_defensive_decision(
game_id,
DefensiveDecision(alignment="normal")
)
await engine.submit_offensive_decision(
game_id,
OffensiveDecision(approach="normal")
)
# Resolve with manual selection
result = await engine.resolve_play(
game_id,
selected_outcome="single" # Player chose single from available outcomes
)
assert result.outcome == "single"
assert result.dice_roll is not None
# Verify persistence
final_state = state_manager.get_state(game_id)
assert final_state.play_count == 1
@pytest.mark.asyncio
async def test_complete_pd_at_bat():
"""Test complete PD at-bat with auto-resolution"""
game_id = uuid4()
# Create PD game
state = await state_manager.create_game(
game_id=game_id,
league_id="pd",
home_team_id=1,
away_team_id=2
)
engine = GameEngine(league_id="pd")
# Start game
await engine.start_game(game_id)
# Submit decisions
await engine.submit_defensive_decision(
game_id,
DefensiveDecision(alignment="normal")
)
await engine.submit_offensive_decision(
game_id,
OffensiveDecision(approach="normal")
)
# Resolve with auto-selection (no manual choice)
result = await engine.resolve_play(game_id)
assert result.outcome is not None
assert result.dice_roll is not None
# Verify persistence
final_state = state_manager.get_state(game_id)
assert final_state.play_count == 1
```
---
## Phase 2 Final Deliverables
### Complete File Structure
```
backend/app/
├── core/
│ ├── __init__.py
│ ├── game_engine.py ✅ Week 5
│ ├── state_manager.py ✅ Week 4
│ ├── play_resolver.py ✅ Week 5, updated Week 6
│ ├── dice.py ✅ Week 5
│ └── validators.py ✅ Week 5
├── config/
│ ├── __init__.py
│ ├── base_config.py ✅ Week 6
│ ├── league_configs.py ✅ Week 6
│ └── result_charts.py ✅ Week 6
├── models/
│ ├── game_models.py ✅ Week 4
│ ├── player_models.py ✅ Week 6
│ └── db_models.py ✅ Phase 1
├── database/
│ ├── operations.py ✅ Week 4
│ └── session.py ✅ Phase 1
└── data/
├── __init__.py
└── api_client.py ✅ Week 6
```
### Success Criteria - Phase 2 Complete
- [ ] Can create game for SBA league
- [ ] Can create game for PD league
- [ ] Complete at-bat works for SBA (with manual result selection)
- [ ] Complete at-bat works for PD (with auto-resolution)
- [ ] State persists to database
- [ ] State recovers from database
- [ ] All unit tests pass (90%+ coverage)
- [ ] All integration tests pass
- [ ] Dice distribution verified as uniform
- [ ] Action response time < 500ms
- [ ] Database writes complete in < 100ms
---
## Next Phase
After completing Phase 2, move to **Phase 3: Gameplay Features** which will add:
- Advanced strategic decisions (stolen bases, substitutions)
- Complete result charts with all edge cases
- PD scouting model probability calculations
- Full WebSocket integration
- UI testing
Detailed plan: [03-gameplay-features.md](./03-gameplay-features.md)
---
**Phase 2 Status**: Planning Complete (2025-10-22)
**Ready to Begin**: Week 4 - State Management