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>
860 lines
24 KiB
Markdown
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 |