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>
211 lines
6.9 KiB
Python
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')
|