strat-gameplay-webapp/backend/tests/unit/config/test_league_configs.py
Cal Corum 5d5c13f2b8 CLAUDE: Implement Week 6 league configuration and play outcome systems
Week 6 Progress: 75% Complete

## Components Implemented

### 1. League Configuration System 
- Created BaseGameConfig abstract class for league-agnostic rules
- Implemented SbaConfig and PdConfig with league-specific settings
- Immutable configs (frozen=True) with singleton registry
- 28 unit tests, all passing

Files:
- backend/app/config/base_config.py
- backend/app/config/league_configs.py
- backend/tests/unit/config/test_league_configs.py

### 2. PlayOutcome Enum 
- Universal enum for all play outcomes (both SBA and PD)
- Helper methods: is_hit(), is_out(), is_uncapped(), is_interrupt()
- Supports standard hits, uncapped hits, interrupt plays, ballpark power
- 30 unit tests, all passing

Files:
- backend/app/config/result_charts.py
- backend/tests/unit/config/test_play_outcome.py

### 3. Player Model Refinements 
- Fixed PdPlayer.id field mapping (player_id → id)
- Improved field docstrings for image types
- Fixed position checking logic in SBA helper methods
- Added safety checks for missing image data

Files:
- backend/app/models/player_models.py (updated)

### 4. Documentation 
- Updated backend/CLAUDE.md with Week 6 section
- Documented card-based resolution mechanics
- Detailed config system and PlayOutcome usage

## Architecture Decisions

1. **Card-Based Resolution**: Both SBA and PD use same mechanics
   - 1d6 (column) + 2d6 (row) + 1d20 (split resolution)
   - PD: Digitized cards with auto-resolution
   - SBA: Manual entry from physical cards

2. **Immutable Configs**: Prevent accidental modification using Pydantic frozen

3. **Universal PlayOutcome**: Single enum for both leagues reduces duplication

## Testing
- Total: 58 tests, all passing
- Config tests: 28
- PlayOutcome tests: 30

## Remaining Work (25%)
- Update dice system (check_d20 → chaos_d20)
- Integrate PlayOutcome into PlayResolver
- Add Play.metadata support for uncapped hits

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

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

211 lines
6.9 KiB
Python

"""
Unit tests for league configuration system.
Tests:
- Config loading and registry
- Config immutability
- Abstract method implementations
- League-specific settings
"""
import pytest
from pydantic import ValidationError
from app.config import (
BaseGameConfig,
SbaConfig,
PdConfig,
LEAGUE_CONFIGS,
get_league_config
)
class TestBaseGameConfig:
"""Tests for BaseGameConfig abstract class."""
def test_cannot_instantiate_abstract_base(self):
"""BaseGameConfig cannot be instantiated directly."""
with pytest.raises(TypeError):
BaseGameConfig(league_id="test")
class TestSbaConfig:
"""Tests for SBA league configuration."""
def test_sba_config_creation(self):
"""Can create SBA config instance."""
config = SbaConfig()
assert config.league_id == "sba"
assert config.version == "1.0.0"
def test_sba_basic_rules(self):
"""SBA uses standard baseball rules."""
config = SbaConfig()
assert config.innings == 9
assert config.outs_per_inning == 3
assert config.strikes_for_out == 3
assert config.balls_for_walk == 4
def test_sba_result_chart_name(self):
"""SBA uses sba_standard_v1 chart."""
config = SbaConfig()
assert config.get_result_chart_name() == "sba_standard_v1"
def test_sba_supports_manual_selection(self):
"""SBA supports manual result selection."""
config = SbaConfig()
assert config.supports_manual_result_selection() is True
def test_sba_api_url(self):
"""SBA API URL is correct."""
config = SbaConfig()
assert config.get_api_base_url() == "https://api.sba.manticorum.com"
def test_sba_player_selection_mode(self):
"""SBA uses manual player selection."""
config = SbaConfig()
assert config.player_selection_mode == "manual"
def test_sba_config_is_immutable(self):
"""Config cannot be modified after creation."""
config = SbaConfig()
with pytest.raises(ValidationError):
config.innings = 7
class TestPdConfig:
"""Tests for PD league configuration."""
def test_pd_config_creation(self):
"""Can create PD config instance."""
config = PdConfig()
assert config.league_id == "pd"
assert config.version == "1.0.0"
def test_pd_basic_rules(self):
"""PD uses standard baseball rules."""
config = PdConfig()
assert config.innings == 9
assert config.outs_per_inning == 3
assert config.strikes_for_out == 3
assert config.balls_for_walk == 4
def test_pd_result_chart_name(self):
"""PD uses pd_standard_v1 chart."""
config = PdConfig()
assert config.get_result_chart_name() == "pd_standard_v1"
def test_pd_supports_manual_selection(self):
"""PD supports manual result selection (though auto is also available)."""
config = PdConfig()
assert config.supports_manual_result_selection() is True
def test_pd_api_url(self):
"""PD API URL is correct."""
config = PdConfig()
assert config.get_api_base_url() == "https://pd.manticorum.com"
def test_pd_player_selection_mode(self):
"""PD uses flexible player selection."""
config = PdConfig()
assert config.player_selection_mode == "flexible"
def test_pd_scouting_model_enabled(self):
"""PD has scouting model enabled."""
config = PdConfig()
assert config.use_scouting_model is True
def test_pd_cardset_validation_enabled(self):
"""PD has cardset validation enabled."""
config = PdConfig()
assert config.cardset_validation is True
def test_pd_advanced_analytics_enabled(self):
"""PD has advanced analytics enabled."""
config = PdConfig()
assert config.detailed_analytics is True
assert config.wpa_calculation is True
def test_pd_config_is_immutable(self):
"""Config cannot be modified after creation."""
config = PdConfig()
with pytest.raises(ValidationError):
config.use_scouting_model = False
class TestLeagueRegistry:
"""Tests for league config registry and getter."""
def test_registry_contains_both_leagues(self):
"""Registry has both SBA and PD configs."""
assert "sba" in LEAGUE_CONFIGS
assert "pd" in LEAGUE_CONFIGS
assert len(LEAGUE_CONFIGS) == 2
def test_registry_configs_are_correct_type(self):
"""Registry contains correct config types."""
assert isinstance(LEAGUE_CONFIGS["sba"], SbaConfig)
assert isinstance(LEAGUE_CONFIGS["pd"], PdConfig)
def test_get_sba_config(self):
"""Can get SBA config via getter."""
config = get_league_config("sba")
assert isinstance(config, SbaConfig)
assert config.league_id == "sba"
def test_get_pd_config(self):
"""Can get PD config via getter."""
config = get_league_config("pd")
assert isinstance(config, PdConfig)
assert config.league_id == "pd"
def test_get_invalid_league_raises(self):
"""Getting invalid league raises ValueError."""
with pytest.raises(ValueError) as exc_info:
get_league_config("invalid")
assert "Unknown league: invalid" in str(exc_info.value)
assert "sba" in str(exc_info.value)
assert "pd" in str(exc_info.value)
def test_registry_returns_same_instance(self):
"""Registry returns singleton instances."""
config1 = get_league_config("sba")
config2 = get_league_config("sba")
assert config1 is config2
class TestConfigDifferences:
"""Tests for differences between league configs."""
def test_different_result_charts(self):
"""SBA and PD use different result charts."""
sba = get_league_config("sba")
pd = get_league_config("pd")
assert sba.get_result_chart_name() != pd.get_result_chart_name()
def test_different_api_urls(self):
"""SBA and PD have different API URLs."""
sba = get_league_config("sba")
pd = get_league_config("pd")
assert sba.get_api_base_url() != pd.get_api_base_url()
def test_different_player_selection_modes(self):
"""SBA and PD have different selection modes."""
sba = get_league_config("sba")
pd = get_league_config("pd")
assert sba.player_selection_mode == "manual"
assert pd.player_selection_mode == "flexible"
def test_pd_has_extra_features(self):
"""PD has features that SBA doesn't."""
pd = get_league_config("pd")
sba = get_league_config("sba")
# PD-specific attributes
assert hasattr(pd, 'use_scouting_model')
assert hasattr(pd, 'cardset_validation')
assert hasattr(pd, 'detailed_analytics')
# SBA doesn't have these (or they're False/None)
assert not hasattr(sba, 'use_scouting_model')
assert not hasattr(sba, 'cardset_validation')