strat-gameplay-webapp/backend/app/config/result_charts.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

152 lines
5.1 KiB
Python

"""
Play outcome definitions for card-based resolution system.
Both SBA and PD leagues use the same resolution mechanics:
1. Roll 1d6 → determines column (1-3: batter card, 4-6: pitcher card)
2. Roll 2d6 → selects row 2-12 on that card
3. Roll 1d20 → resolves split results if needed
4. Outcome from card is a PlayOutcome enum value
League Differences:
- PD: Card data digitized in PdBattingRating/PdPitchingRating
Can auto-resolve using probabilities OR manual selection
- SBA: Physical cards only (not digitized)
Players read physical cards and manually enter outcomes
This module defines the universal PlayOutcome enum used by both leagues.
Author: Claude
Date: 2025-10-28
"""
import logging
from enum import Enum
logger = logging.getLogger(f'{__name__}.PlayOutcome')
class PlayOutcome(str, Enum):
"""
Universal play outcome types for both SBA and PD leagues.
These represent all possible results that can appear on player cards.
Each outcome triggers specific game logic in the PlayResolver.
Usage:
- PD: Outcomes determined from PdBattingRating/PdPitchingRating data
- SBA: Outcomes manually entered by players reading physical cards
"""
# ==================== Outs ====================
STRIKEOUT = "strikeout"
GROUNDOUT = "groundout"
FLYOUT = "flyout"
LINEOUT = "lineout"
POPOUT = "popout"
DOUBLE_PLAY = "double_play"
# ==================== Hits ====================
SINGLE = "single"
DOUBLE = "double"
TRIPLE = "triple"
HOMERUN = "homerun"
# Uncapped hits (only on pitching cards)
# Trigger decision tree for advancing runners when on_base_code > 0
SINGLE_UNCAPPED = "single_uncapped" # si(cf) on card
DOUBLE_UNCAPPED = "double_uncapped" # do(cf) on card
# ==================== Walks/HBP ====================
WALK = "walk"
HIT_BY_PITCH = "hbp"
INTENTIONAL_WALK = "intentional_walk"
# ==================== Errors ====================
ERROR = "error"
# ==================== Interrupt Plays ====================
# These are logged as separate plays with Play.pa = 0
WILD_PITCH = "wild_pitch" # Play.wp = 1
PASSED_BALL = "passed_ball" # Play.pb = 1
STOLEN_BASE = "stolen_base" # Play.sb = 1
CAUGHT_STEALING = "caught_stealing" # Play.cs = 1
BALK = "balk" # Logged during steal attempt
PICK_OFF = "pick_off" # Runner picked off
# ==================== Ballpark Power (PD specific) ====================
# Special PD outcomes for ballpark factors
BP_HOMERUN = "bp_homerun" # Ballpark HR (Play.bphr = 1)
BP_SINGLE = "bp_single" # Ballpark single (Play.bp1b = 1)
BP_FLYOUT = "bp_flyout" # Ballpark flyout (Play.bpfo = 1)
BP_LINEOUT = "bp_lineout" # Ballpark lineout (Play.bplo = 1)
# ==================== Helper Methods ====================
def is_hit(self) -> bool:
"""Check if outcome is a hit (counts toward batting average)."""
return self in {
self.SINGLE, self.DOUBLE, self.TRIPLE, self.HOMERUN,
self.SINGLE_UNCAPPED, self.DOUBLE_UNCAPPED,
self.BP_HOMERUN, self.BP_SINGLE
}
def is_out(self) -> bool:
"""Check if outcome records an out."""
return self in {
self.STRIKEOUT, self.GROUNDOUT, self.FLYOUT,
self.LINEOUT, self.POPOUT, self.DOUBLE_PLAY,
self.CAUGHT_STEALING, self.PICK_OFF,
self.BP_FLYOUT, self.BP_LINEOUT
}
def is_walk(self) -> bool:
"""Check if outcome is a walk."""
return self in {self.WALK, self.INTENTIONAL_WALK}
def is_uncapped(self) -> bool:
"""
Check if outcome is uncapped (requires advancement decision).
Uncapped hits only trigger decisions when on_base_code > 0.
"""
return self in {self.SINGLE_UNCAPPED, self.DOUBLE_UNCAPPED}
def is_interrupt(self) -> bool:
"""
Check if outcome is an interrupt play (logged with pa=0).
Interrupt plays don't change the batter, only advance runners.
"""
return self in {
self.WILD_PITCH, self.PASSED_BALL,
self.STOLEN_BASE, self.CAUGHT_STEALING,
self.BALK, self.PICK_OFF
}
def is_extra_base_hit(self) -> bool:
"""Check if outcome is an extra-base hit (2B, 3B, HR)."""
return self in {
self.DOUBLE, self.TRIPLE, self.HOMERUN,
self.DOUBLE_UNCAPPED, self.BP_HOMERUN
}
def get_bases_advanced(self) -> int:
"""
Get number of bases batter advances (for standard outcomes).
Returns:
0 for outs/walks, 1-4 for hits
Note: Uncapped hits return base value; actual advancement
determined by decision tree.
"""
if self in {self.SINGLE, self.SINGLE_UNCAPPED, self.BP_SINGLE}:
return 1
elif self in {self.DOUBLE, self.DOUBLE_UNCAPPED}:
return 2
elif self == self.TRIPLE:
return 3
elif self in {self.HOMERUN, self.BP_HOMERUN}:
return 4
else:
return 0