strat-gameplay-webapp/backend/app/config/CLAUDE.md
Cal Corum 76e24ab22b CLAUDE: Refactor ManualOutcomeSubmission to use PlayOutcome enum + comprehensive documentation
## Refactoring
- Changed `ManualOutcomeSubmission.outcome` from `str` to `PlayOutcome` enum type
- Removed custom validator (Pydantic handles enum validation automatically)
- Added direct import of PlayOutcome (no circular dependency due to TYPE_CHECKING guard)
- Updated tests to use enum values while maintaining backward compatibility

Benefits:
- Better type safety with IDE autocomplete
- Cleaner code (removed 15 lines of validator boilerplate)
- Backward compatible (Pydantic auto-converts strings to enum)
- Access to helper methods (is_hit(), is_out(), etc.)

Files modified:
- app/models/game_models.py: Enum type + import
- tests/unit/config/test_result_charts.py: Updated 7 tests + added compatibility test

## Documentation
Created comprehensive CLAUDE.md files for all backend/app/ subdirectories to help future AI agents quickly understand and work with the code.

Added 8,799 lines of documentation covering:
- api/ (906 lines): FastAPI routes, health checks, auth patterns
- config/ (906 lines): League configs, PlayOutcome enum, result charts
- core/ (1,288 lines): GameEngine, StateManager, PlayResolver, dice system
- data/ (937 lines): API clients (planned), caching layer
- database/ (945 lines): Async sessions, operations, recovery
- models/ (1,270 lines): Pydantic/SQLAlchemy models, polymorphic patterns
- utils/ (959 lines): Logging, JWT auth, security
- websocket/ (1,588 lines): Socket.io handlers, real-time events
- tests/ (475 lines): Testing patterns and structure

Each CLAUDE.md includes:
- Purpose & architecture overview
- Key components with detailed explanations
- Patterns & conventions
- Integration points
- Common tasks (step-by-step guides)
- Troubleshooting with solutions
- Working code examples
- Testing guidance

Total changes: +9,294 lines / -24 lines
Tests: All passing (62/62 model tests, 7/7 ManualOutcomeSubmission tests)
2025-10-31 16:03:54 -05:00

24 KiB

Configuration System - League Rules & Play Outcomes

Purpose

The configuration system provides immutable, league-specific game rules and play outcome definitions for the Paper Dynasty game engine. It serves as the single source of truth for:

  • League-specific game rules (innings, outs, feature flags)
  • API endpoint configuration for external data sources
  • Universal play outcome definitions (hits, outs, walks, etc.)
  • Card-based resolution mechanics for both manual and auto modes
  • Hit location calculation for runner advancement logic

This system enables a league-agnostic game engine that adapts to SBA and PD league differences through configuration rather than conditional logic.

Architecture Overview

app/config/
├── __init__.py              # Public API exports
├── base_config.py           # Abstract base configuration
├── league_configs.py        # Concrete SBA/PD implementations
└── result_charts.py         # PlayOutcome enum + result chart abstractions

Design Principles

  1. Immutability: Configs are frozen Pydantic models (cannot be modified after creation)
  2. Registry Pattern: Pre-instantiated singletons in LEAGUE_CONFIGS dict
  3. Type Safety: Full Pydantic validation with abstract base class enforcement
  4. League Agnostic: Game engine uses BaseGameConfig interface, never concrete types

Key Components

1. BaseGameConfig (Abstract Base Class)

Location: base_config.py:13-77

Defines the interface all league configs must implement.

Common Fields:

  • league_id (str): League identifier ('sba' or 'pd')
  • version (str): Config version for compatibility tracking
  • innings (int): Standard innings per game (default 9)
  • outs_per_inning (int): Outs required per half-inning (default 3)

Abstract Methods (must be implemented by subclasses):

@abstractmethod
def get_result_chart_name(self) -> str:
    """Get name of result chart to use for this league."""

@abstractmethod
def supports_manual_result_selection(self) -> bool:
    """Whether players manually select results after dice roll."""

@abstractmethod
def supports_auto_mode(self) -> bool:
    """Whether this league supports auto-resolution of outcomes."""

@abstractmethod
def get_api_base_url(self) -> str:
    """Get base URL for league's external API."""

Configuration:

class Config:
    frozen = True  # Immutable - prevents accidental modification

2. League-Specific Configs

SbaConfig

Location: league_configs.py:17-46

Configuration for SBA League with manual result selection.

Features:

  • Manual result selection only (physical cards, not digitized)
  • Simple player data model
  • Standard baseball rules

Unique Fields:

  • player_selection_mode: "manual" (always manual selection)

Methods:

  • get_result_chart_name() → "sba_standard_v1"
  • supports_manual_result_selection() → True
  • supports_auto_mode() → False (cards not digitized)
  • get_api_base_url() → "https://api.sba.manticorum.com"

PdConfig

Location: league_configs.py:49-86

Configuration for Paper Dynasty League with flexible resolution modes.

Features:

  • Flexible result selection (manual OR auto via scouting)
  • Complex scouting data model (PdBattingRating/PdPitchingRating)
  • Cardset validation
  • Advanced analytics (WPA, RE24)

Unique Fields:

  • player_selection_mode: "flexible" (manual or auto)
  • use_scouting_model: True (use detailed ratings for auto)
  • cardset_validation: True (validate cards against approved sets)
  • detailed_analytics: True (track advanced stats)
  • wpa_calculation: True (calculate win probability added)

Methods:

  • get_result_chart_name() → "pd_standard_v1"
  • supports_manual_result_selection() → True (though auto is also available)
  • supports_auto_mode() → True (via digitized scouting data)
  • get_api_base_url() → "https://pd.manticorum.com"

3. Config Registry

Location: league_configs.py:88-115

Pre-instantiated singletons for O(1) lookup.

LEAGUE_CONFIGS: Dict[str, BaseGameConfig] = {
    "sba": SbaConfig(),
    "pd": PdConfig()
}

def get_league_config(league_id: str) -> BaseGameConfig:
    """Get configuration for specified league."""
    config = LEAGUE_CONFIGS.get(league_id)
    if not config:
        raise ValueError(f"Unknown league: {league_id}")
    return config

4. PlayOutcome Enum

Location: result_charts.py:38-197

Universal enum defining all possible play outcomes for both leagues.

Outcome Categories:

  1. Outs (9 types):

    • STRIKEOUT
    • GROUNDBALL_A / GROUNDBALL_B / GROUNDBALL_C (double play vs groundout)
    • FLYOUT_A / FLYOUT_B / FLYOUT_C (different trajectories/depths)
    • LINEOUT
    • POPOUT
  2. Hits (8 types):

    • SINGLE_1 / SINGLE_2 / SINGLE_UNCAPPED (standard vs enhanced vs decision tree)
    • DOUBLE_2 / DOUBLE_3 / DOUBLE_UNCAPPED (2nd base vs 3rd base vs decision tree)
    • TRIPLE
    • HOMERUN
  3. Walks/HBP (3 types):

    • WALK
    • HIT_BY_PITCH
    • INTENTIONAL_WALK
  4. Errors (1 type):

    • ERROR
  5. Interrupt Plays (6 types) - logged with pa=0:

    • WILD_PITCH (Play.wp = 1)
    • PASSED_BALL (Play.pb = 1)
    • STOLEN_BASE (Play.sb = 1)
    • CAUGHT_STEALING (Play.cs = 1)
    • BALK (Play.balk = 1)
    • PICK_OFF (Play.pick_off = 1)
  6. Ballpark Power (4 types) - PD league specific:

    • BP_HOMERUN (Play.bphr = 1)
    • BP_SINGLE (Play.bp1b = 1)
    • BP_FLYOUT (Play.bpfo = 1)
    • BP_LINEOUT (Play.bplo = 1)

Helper Methods:

outcome = PlayOutcome.SINGLE_UNCAPPED

# Categorization helpers
outcome.is_hit()              # True
outcome.is_out()              # False
outcome.is_walk()             # False
outcome.is_uncapped()         # True - requires advancement decision
outcome.is_interrupt()        # False
outcome.is_extra_base_hit()   # False

# Advancement logic
outcome.get_bases_advanced()  # 1
outcome.requires_hit_location()  # False (only groundballs/flyouts)

5. Hit Location Calculation

Location: result_charts.py:206-279

Calculates fielder positions for groundballs and flyouts based on batter handedness.

Function:

def calculate_hit_location(
    outcome: PlayOutcome,
    batter_handedness: str
) -> Optional[str]:
    """
    Calculate hit location based on outcome and batter handedness.

    Pull Rate Distribution:
        - 45% pull side (RHB left, LHB right)
        - 35% center
        - 20% opposite field

    Groundball Locations: P, C, 1B, 2B, SS, 3B (infield)
    Fly Ball Locations: LF, CF, RF (outfield)
    """

Usage:

from app.config import calculate_hit_location, PlayOutcome

# Calculate location for groundball
location = calculate_hit_location(PlayOutcome.GROUNDBALL_A, 'R')  # '3B', 'SS', etc.

# Only works for groundballs/flyouts
location = calculate_hit_location(PlayOutcome.HOMERUN, 'R')  # None

6. ResultChart Abstraction (Future)

Location: result_charts.py:285-588

Abstract base class for result chart implementations. Currently defines interface for future auto-mode implementation.

Classes:

  • ResultChart (ABC): Abstract interface
  • ManualResultChart: Placeholder (not used - manual outcomes come via WebSocket)
  • PdAutoResultChart: Auto-resolution for PD league using digitized card data

Note: Manual mode doesn't use result charts - outcomes come directly from WebSocket handlers.

Patterns & Conventions

1. Immutable Configuration

All configs are frozen after instantiation to prevent accidental modification.

# ✅ CORRECT - Read-only access
config = get_league_config("sba")
api_url = config.get_api_base_url()
chart_name = config.get_result_chart_name()

# ❌ WRONG - Raises ValidationError
config.innings = 7  # ValidationError: "Game" object is immutable

2. Registry Pattern

Configs are pre-instantiated singletons in the registry, not created per-request.

# ✅ CORRECT - Use registry
from app.config import get_league_config
config = get_league_config(league_id)

# ❌ WRONG - Don't instantiate directly
from app.config import SbaConfig
config = SbaConfig()  # Creates unnecessary instance

3. League-Agnostic Code

Game engine uses BaseGameConfig interface, never concrete types.

# ✅ CORRECT - Works for any league
def resolve_play(state: GameState, config: BaseGameConfig):
    if config.supports_auto_mode():
        # Auto-resolve
        pass
    else:
        # Wait for manual input
        pass

# ❌ WRONG - Hard-coded league logic
def resolve_play(state: GameState):
    if state.league_id == "sba":
        # SBA-specific logic
        pass
    elif state.league_id == "pd":
        # PD-specific logic
        pass

4. Enum Helper Methods

Use PlayOutcome helper methods instead of duplicate logic.

# ✅ CORRECT - Use helper methods
if outcome.is_hit():
    record_hit()
elif outcome.is_walk():
    record_walk()
elif outcome.is_interrupt():
    log_interrupt_play()

# ❌ WRONG - Duplicate categorization logic
if outcome in {PlayOutcome.SINGLE_1, PlayOutcome.SINGLE_2, PlayOutcome.HOMERUN, ...}:
    record_hit()

5. Type Safety

Always use type hints with BaseGameConfig for league-agnostic code.

# ✅ CORRECT - Type-safe
from app.config import BaseGameConfig

def process_game(config: BaseGameConfig) -> None:
    # Works for SBA or PD
    pass

# ❌ WRONG - No type safety
def process_game(config) -> None:
    # Could be anything
    pass

Integration Points

With Game Engine

from app.config import get_league_config, PlayOutcome
from app.models import GameState

async def resolve_play(state: GameState, outcome: PlayOutcome):
    # Get league-specific config
    config = get_league_config(state.league_id)

    # Handle based on outcome type
    if outcome.is_uncapped() and state.on_base_code > 0:
        # Uncapped hit with runners - need advancement decision
        await request_advancement_decision(state)
    elif outcome.is_interrupt():
        # Interrupt play - logged with pa=0
        await log_interrupt_play(state, outcome)
    elif outcome.is_hit():
        # Standard hit - advance runners
        bases = outcome.get_bases_advanced()
        await advance_batter(state, bases)
    elif outcome.is_out():
        # Record out
        state.outs += 1

With Database Models

from app.config import PlayOutcome
from app.models import Play

async def save_play(outcome: PlayOutcome, state: GameState):
    play = Play(
        game_id=state.game_id,
        outcome=outcome.value,  # Store enum value as string
        pa=0 if outcome.is_interrupt() else 1,
        ab=1 if not outcome.is_walk() and not outcome.is_interrupt() else 0,
        hit=1 if outcome.is_hit() else 0,
        # ... other fields
    )
    await db_ops.save_play(play)

With WebSocket Handlers

from app.config import get_league_config, PlayOutcome

@sio.event
async def submit_manual_outcome(sid: str, data: dict):
    """Handle manual outcome submission from player."""
    # Validate league supports manual mode
    config = get_league_config(data['league_id'])
    if not config.supports_manual_result_selection():
        raise ValueError("Manual selection not supported for this league")

    # Parse outcome
    outcome = PlayOutcome(data['outcome'])

    # Process play
    await process_play_outcome(data['game_id'], outcome)

With Player Models

from app.config import calculate_hit_location, PlayOutcome
from app.models import PdPlayer

def resolve_groundball(batter: PdPlayer, outcome: PlayOutcome):
    # Get batter handedness
    handedness = batter.batting_card.hand if batter.batting_card else 'R'

    # Calculate hit location
    location = calculate_hit_location(outcome, handedness)

    # Use location for advancement logic
    if location in ['1B', '2B']:
        # Right side groundball - slower to turn double play
        pass
    elif location in ['SS', '3B']:
        # Left side groundball - easier double play
        pass

Common Tasks

Adding a New League Config

  1. Create config class in league_configs.py:
class NewLeagueConfig(BaseGameConfig):
    """Configuration for New League."""

    league_id: str = "new_league"

    # New league-specific features
    custom_feature: bool = True

    def get_result_chart_name(self) -> str:
        return "new_league_standard_v1"

    def supports_manual_result_selection(self) -> bool:
        return True

    def supports_auto_mode(self) -> bool:
        return False

    def get_api_base_url(self) -> str:
        return "https://api.newleague.com"
  1. Register in LEAGUE_CONFIGS:
LEAGUE_CONFIGS: Dict[str, BaseGameConfig] = {
    "sba": SbaConfig(),
    "pd": PdConfig(),
    "new_league": NewLeagueConfig()  # Add here
}
  1. Write tests in tests/unit/config/test_league_configs.py:
def test_new_league_config():
    config = get_league_config("new_league")
    assert config.league_id == "new_league"
    assert config.get_result_chart_name() == "new_league_standard_v1"
    assert config.supports_manual_result_selection() is True
    assert config.supports_auto_mode() is False

Adding a New PlayOutcome

  1. Add to enum in result_charts.py:
class PlayOutcome(str, Enum):
    # ... existing outcomes

    # New outcome
    BUNT_SINGLE = "bunt_single"  # New bunt result
  1. Update helper methods if needed:
def is_hit(self) -> bool:
    return self in {
        self.SINGLE_1, self.SINGLE_2, self.SINGLE_UNCAPPED,
        # ... existing hits
        self.BUNT_SINGLE  # Add to hit category
    }
  1. Write tests in tests/unit/config/test_play_outcome.py:
def test_bunt_single_categorization():
    outcome = PlayOutcome.BUNT_SINGLE
    assert outcome.is_hit()
    assert not outcome.is_out()
    assert outcome.get_bases_advanced() == 1

Modifying Existing Config

DON'T: Configs are immutable by design.

DO: Create new version if rules change:

# Old version (keep for compatibility)
class SbaConfigV1(BaseGameConfig):
    league_id: str = "sba"
    version: str = "1.0.0"
    innings: int = 9

# New version (different rules)
class SbaConfigV2(BaseGameConfig):
    league_id: str = "sba"
    version: str = "2.0.0"
    innings: int = 7  # New: 7-inning games

# Registry supports versioning
LEAGUE_CONFIGS = {
    "sba:v1": SbaConfigV1(),
    "sba:v2": SbaConfigV2(),
    "sba": SbaConfigV2()  # Default to latest
}

Checking League Capabilities

from app.config import get_league_config

def can_use_auto_mode(league_id: str) -> bool:
    """Check if league supports auto-resolution."""
    config = get_league_config(league_id)
    return config.supports_auto_mode()

def requires_cardset_validation(league_id: str) -> bool:
    """Check if league requires cardset validation."""
    config = get_league_config(league_id)
    # PD-specific check
    return hasattr(config, 'cardset_validation') and config.cardset_validation

Troubleshooting

Problem: "Unknown league" error

Symptom:

ValueError: Unknown league: xyz. Valid leagues: ['sba', 'pd']

Cause: League ID not in registry

Solution:

# Check valid leagues
from app.config import LEAGUE_CONFIGS
print(LEAGUE_CONFIGS.keys())  # ['sba', 'pd']

# Use correct league ID
config = get_league_config("sba")  # ✅
config = get_league_config("xyz")  # ❌ ValueError

Problem: Cannot modify config

Symptom:

ValidationError: "SbaConfig" object is immutable

Cause: Configs are frozen Pydantic models

Solution: Don't modify configs. They are immutable by design.

# ❌ WRONG - Trying to modify
config = get_league_config("sba")
config.innings = 7  # ValidationError

# ✅ CORRECT - Create new state with different value
state.innings = 7  # Modify game state, not config

Problem: PlayOutcome validation error

Symptom:

ValueError: 'invalid_outcome' is not a valid PlayOutcome

Cause: String doesn't match any enum value

Solution:

# ❌ WRONG - Invalid string
outcome = PlayOutcome("invalid_outcome")  # ValueError

# ✅ CORRECT - Use enum member
outcome = PlayOutcome.SINGLE_1

# ✅ CORRECT - Parse from valid string
outcome = PlayOutcome("single_1")

# ✅ CORRECT - Check if valid
try:
    outcome = PlayOutcome(user_input)
except ValueError:
    # Handle invalid input
    pass

Problem: Result chart not found

Symptom:

KeyError: 'sba_standard_v1'

Cause: Result chart registry not implemented yet

Solution: Result charts are future implementation. Manual mode receives outcomes via WebSocket, not chart lookups.

# ❌ WRONG - Trying to lookup chart directly
chart = RESULT_CHARTS[config.get_result_chart_name()]

# ✅ CORRECT - Manual outcomes come via WebSocket
@sio.event
async def submit_manual_outcome(sid: str, data: dict):
    outcome = PlayOutcome(data['outcome'])
    await process_outcome(outcome)

Problem: Missing import

Symptom:

ImportError: cannot import name 'PlayOutcome' from 'app.config'

Cause: Not imported in __init__.py

Solution:

# ✅ CORRECT - Import from package
from app.config import PlayOutcome, get_league_config, BaseGameConfig

# ❌ WRONG - Direct module import
from app.config.result_charts import PlayOutcome  # Don't do this

Examples

Example 1: Basic Config Usage

from app.config import get_league_config

# Get config for SBA league
sba_config = get_league_config("sba")

print(f"League: {sba_config.league_id}")
print(f"Innings: {sba_config.innings}")
print(f"API: {sba_config.get_api_base_url()}")
print(f"Chart: {sba_config.get_result_chart_name()}")
print(f"Manual mode: {sba_config.supports_manual_result_selection()}")
print(f"Auto mode: {sba_config.supports_auto_mode()}")

# Output:
# League: sba
# Innings: 9
# API: https://api.sba.manticorum.com
# Chart: sba_standard_v1
# Manual mode: True
# Auto mode: False

Example 2: PlayOutcome Categorization

from app.config import PlayOutcome

outcomes = [
    PlayOutcome.SINGLE_1,
    PlayOutcome.STRIKEOUT,
    PlayOutcome.WALK,
    PlayOutcome.SINGLE_UNCAPPED,
    PlayOutcome.WILD_PITCH
]

for outcome in outcomes:
    categories = []
    if outcome.is_hit():
        categories.append("HIT")
    if outcome.is_out():
        categories.append("OUT")
    if outcome.is_walk():
        categories.append("WALK")
    if outcome.is_uncapped():
        categories.append("UNCAPPED")
    if outcome.is_interrupt():
        categories.append("INTERRUPT")

    print(f"{outcome.value}: {', '.join(categories) or 'OTHER'}")

# Output:
# single_1: HIT
# strikeout: OUT
# walk: WALK
# single_uncapped: HIT, UNCAPPED
# wild_pitch: INTERRUPT

Example 3: Hit Location Calculation

from app.config import calculate_hit_location, PlayOutcome

# Simulate 10 groundballs for right-handed batter
print("Right-handed batter groundballs:")
for _ in range(10):
    location = calculate_hit_location(PlayOutcome.GROUNDBALL_A, 'R')
    print(f"  Hit to: {location}")

# Output (random, but follows pull rate):
# Right-handed batter groundballs:
#   Hit to: 3B   (pull side)
#   Hit to: SS   (pull side)
#   Hit to: 2B   (center)
#   Hit to: P    (center)
#   Hit to: 3B   (pull side)
#   Hit to: 1B   (opposite)
#   Hit to: SS   (pull side)
#   Hit to: 2B   (center)
#   Hit to: 3B   (pull side)
#   Hit to: 2B   (opposite)

Example 4: League-Agnostic Game Logic

from app.config import get_league_config, PlayOutcome
from app.models import GameState

async def handle_play_outcome(state: GameState, outcome: PlayOutcome):
    """Process play outcome in league-agnostic way."""
    # Get league config
    config = get_league_config(state.league_id)

    # Different handling based on outcome type
    if outcome.is_interrupt():
        # Interrupt plays don't change batter
        print(f"Interrupt play: {outcome.value}")
        await log_interrupt_play(state, outcome)

    elif outcome.is_uncapped() and state.on_base_code > 0:
        # Uncapped hit with runners - need decision
        print(f"Uncapped hit: {outcome.value} - requesting advancement decision")
        if config.supports_auto_mode() and state.auto_mode_enabled:
            # Auto-resolve advancement
            await auto_resolve_advancement(state, outcome)
        else:
            # Request manual decision
            await request_advancement_decision(state, outcome)

    elif outcome.is_hit():
        # Standard hit - advance batter
        bases = outcome.get_bases_advanced()
        print(f"Hit: {outcome.value} - batter to base {bases}")
        await advance_batter(state, bases)

    elif outcome.is_walk():
        # Walk - advance batter to first
        print(f"Walk: {outcome.value}")
        await walk_batter(state)

    elif outcome.is_out():
        # Out - increment out counter
        print(f"Out: {outcome.value}")
        state.outs += 1
        await check_inning_over(state)

Example 5: Config-Driven Feature Flags

from app.config import get_league_config

def should_calculate_wpa(league_id: str) -> bool:
    """Check if league tracks win probability added."""
    config = get_league_config(league_id)

    # PD-specific feature
    if hasattr(config, 'wpa_calculation'):
        return config.wpa_calculation

    return False

def requires_cardset_validation(league_id: str) -> bool:
    """Check if league requires cardset validation."""
    config = get_league_config(league_id)

    # PD-specific feature
    if hasattr(config, 'cardset_validation'):
        return config.cardset_validation

    return False

# Usage
if should_calculate_wpa(state.league_id):
    wpa = calculate_win_probability_added(state, outcome)
    play.wpa = wpa

if requires_cardset_validation(state.league_id):
    validate_cardsets(game_id, card_id)

Testing

Unit Tests

Location: tests/unit/config/

Test Coverage:

  • test_league_configs.py (28 tests): Config registry, implementations, immutability
  • test_play_outcome.py (30 tests): Enum helpers, categorization, edge cases

Run Tests:

# All config tests
pytest tests/unit/config/ -v

# Specific file
pytest tests/unit/config/test_league_configs.py -v

# Specific test
pytest tests/unit/config/test_play_outcome.py::test_is_hit -v

Test Examples

# Test config retrieval
def test_get_sba_config():
    config = get_league_config("sba")
    assert config.league_id == "sba"
    assert isinstance(config, SbaConfig)

# Test immutability
def test_config_immutable():
    config = get_league_config("sba")
    with pytest.raises(ValidationError):
        config.innings = 7

# Test PlayOutcome helpers
def test_single_uncapped_is_hit():
    outcome = PlayOutcome.SINGLE_UNCAPPED
    assert outcome.is_hit()
    assert outcome.is_uncapped()
    assert not outcome.is_out()
    assert outcome.get_bases_advanced() == 1

Source Files

  • app/config/base_config.py - Abstract base configuration
  • app/config/league_configs.py - Concrete implementations
  • app/config/result_charts.py - PlayOutcome enum
  • app/config/__init__.py - Public API

Test Files

  • tests/unit/config/test_league_configs.py - Config system tests
  • tests/unit/config/test_play_outcome.py - PlayOutcome tests

Integration Points

  • app/core/game_engine.py - Uses configs for league-specific rules
  • app/core/play_resolver.py - Uses PlayOutcome for resolution logic
  • app/models/game_models.py - GameState uses league_id
  • app/models/player_models.py - Player models use handedness for hit location
  • app/websocket/handlers.py - Validates league capabilities

Key Takeaways

  1. Immutability: Configs are frozen and cannot be modified after creation
  2. Registry: Use get_league_config() to access pre-instantiated singletons
  3. Type Safety: Always use BaseGameConfig for league-agnostic code
  4. Helper Methods: Use PlayOutcome helpers instead of duplicate categorization logic
  5. No Static Charts: Result charts come from card data (PD) or manual entry (SBA)
  6. League Agnostic: Game engine adapts to leagues via config, not conditionals

References

  • Parent backend documentation: ../CLAUDE.md
  • Week 6 implementation: ../../../../.claude/implementation/02-week6-player-models.md
  • PlayResolver integration: ../core/play_resolver.py
  • Game engine usage: ../core/game_engine.py