strat-gameplay-webapp/backend/app/config/result_charts.py
Cal Corum 6880b6d5ad CLAUDE: Complete Week 6 - granular PlayOutcome integration and metadata support
- Renamed check_d20 → chaos_d20 throughout dice system
- Expanded PlayOutcome enum with granular variants (SINGLE_1/2, DOUBLE_2/3, GROUNDBALL_A/B/C, etc.)
- Integrated PlayOutcome from app.config into PlayResolver
- Added play_metadata support for uncapped hit tracking
- Updated all tests (139/140 passing)

Week 6: 100% Complete - Ready for Phase 3

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 20:29:06 -05:00

165 lines
5.9 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"
# Groundballs - 3 variants for different defensive outcomes
GROUNDBALL_A = "groundball_a" # Double play if possible, else groundout
GROUNDBALL_B = "groundball_b" # Standard groundout
GROUNDBALL_C = "groundball_c" # Standard groundout
# Flyouts - 3 variants for different trajectories/depths
FLYOUT_A = "flyout_a" # Flyout variant A
FLYOUT_B = "flyout_b" # Flyout variant B
FLYOUT_C = "flyout_c" # Flyout variant C
LINEOUT = "lineout"
POPOUT = "popout"
# ==================== Hits ====================
# Singles - variants for different advancement rules
SINGLE_1 = "single_1" # Single with standard advancement
SINGLE_2 = "single_2" # Single with enhanced advancement
SINGLE_UNCAPPED = "single_uncapped" # si(cf) - pitching card, decision tree
# Doubles - variants for batter advancement
DOUBLE_2 = "double_2" # Double to 2nd base
DOUBLE_3 = "double_3" # Double to 3rd base (extra advancement)
DOUBLE_UNCAPPED = "double_uncapped" # do(cf) - pitching card, decision tree
TRIPLE = "triple"
HOMERUN = "homerun"
# ==================== 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" # Play.balk = 1 / Logged during steal attempt
PICK_OFF = "pick_off" # Play.pick_off = 1 / Runner picked off
# ==================== Ballpark Power ====================
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_1, self.SINGLE_2, self.SINGLE_UNCAPPED,
self.DOUBLE_2, self.DOUBLE_3, self.DOUBLE_UNCAPPED,
self.TRIPLE, self.HOMERUN,
self.BP_HOMERUN, self.BP_SINGLE
}
def is_out(self) -> bool:
"""Check if outcome records an out."""
return self in {
self.STRIKEOUT,
self.GROUNDBALL_A, self.GROUNDBALL_B, self.GROUNDBALL_C,
self.FLYOUT_A, self.FLYOUT_B, self.FLYOUT_C,
self.LINEOUT, self.POPOUT,
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_2, self.DOUBLE_3, self.DOUBLE_UNCAPPED,
self.TRIPLE, self.HOMERUN, 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_1, self.SINGLE_2, self.SINGLE_UNCAPPED, self.BP_SINGLE}:
return 1
elif self in {self.DOUBLE_2, self.DOUBLE_3, self.DOUBLE_UNCAPPED}:
return 2
elif self == self.TRIPLE:
return 3
elif self in {self.HOMERUN, self.BP_HOMERUN}:
return 4
else:
return 0