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>
152 lines
5.1 KiB
Python
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
|