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>
24 KiB
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 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:
- Player Models (see separate documentation above)
- League Configuration System (this file)
- Result Chart System (this file)
- 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 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.
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:
- Create
backend/app/models/player_models.py - Implement BasePlayer abstract class
- Implement SbaPlayer (simple)
- Implement PdPlayer with scouting grades
- Implement LineupFactory
- 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.
# 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
# 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:
- Create
backend/app/config/base_config.py - Create
backend/app/config/league_configs.py - Implement SbaConfig
- Implement PdConfig
- 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.
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:
- Create
backend/app/config/result_charts.py - Define ChartOutcome enum
- Create SBA chart (placeholder)
- Create PD chart (placeholder)
- Write tests
4. League API Client (backend/app/data/api_client.py)
HTTP client for fetching team and player data from league APIs.
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:
- Create
backend/app/data/__init__.py - Create
backend/app/data/api_client.py - Implement LeagueApiClient with httpx
- Add error handling and retries
- 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.
# 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
# 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
Phase 2 Status: Planning Complete (2025-10-22) Ready to Begin: Week 4 - State Management