strat-gameplay-webapp/backend/app/services/play_stat_calculator.py
Cal Corum a4b99ee53e CLAUDE: Replace black and flake8 with ruff for formatting and linting
Migrated to ruff for faster, modern code formatting and linting:

Configuration changes:
- pyproject.toml: Added ruff 0.8.6, removed black/flake8
- Configured ruff with black-compatible formatting (88 chars)
- Enabled comprehensive linting rules (pycodestyle, pyflakes, isort,
  pyupgrade, bugbear, comprehensions, simplify, return)
- Updated CLAUDE.md: Changed code quality commands to use ruff

Code improvements (490 auto-fixes):
- Modernized type hints: List[T] → list[T], Dict[K,V] → dict[K,V],
  Optional[T] → T | None
- Sorted all imports (isort integration)
- Removed unused imports
- Fixed whitespace issues
- Reformatted 38 files for consistency

Bug fixes:
- app/core/play_resolver.py: Fixed type hint bug (any → Any)
- tests/unit/core/test_runner_advancement.py: Removed obsolete random mock

Testing:
- All 739 unit tests passing (100%)
- No regressions introduced

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:33:21 -06:00

155 lines
4.7 KiB
Python

"""
Calculate statistical fields from PlayOutcome.
Called when recording a play to populate stat fields for materialized view aggregation.
"""
import logging
from app.config.result_charts import PlayOutcome
from app.core.play_resolver import PlayResult
from app.models.game_models import GameState
logger = logging.getLogger(f"{__name__}.PlayStatCalculator")
class PlayStatCalculator:
"""
Converts play outcome to statistical fields for database.
Stats are written to plays table and aggregated via materialized views.
Following legacy API pattern from major-domo /plays/batting and /plays/pitching.
"""
@staticmethod
def calculate_stats(
outcome: PlayOutcome,
result: PlayResult,
state_before: GameState,
state_after: GameState,
) -> dict[str, int]:
"""
Calculate all stat fields for a play.
Args:
outcome: The PlayOutcome enum value
result: PlayResult with detailed outcome info
state_before: GameState before play resolution
state_after: GameState after play resolution
Returns:
dict with stat fields (pa, ab, hit, run, etc.)
"""
stats: dict[str, int] = {
"pa": 0,
"ab": 0,
"hit": 0,
"run": 0,
"rbi": 0,
"double": 0,
"triple": 0,
"homerun": 0,
"bb": 0,
"so": 0,
"hbp": 0,
"sac": 0,
"ibb": 0,
"sb": 0,
"cs": 0,
"gidp": 0,
"error": 0,
"wild_pitch": 0,
"passed_ball": 0,
"balk": 0,
"pick_off": 0,
"outs_recorded": 0,
}
# Plate appearance (everything except non-PA events)
if outcome not in [
PlayOutcome.STOLEN_BASE,
PlayOutcome.CAUGHT_STEALING,
PlayOutcome.WILD_PITCH,
PlayOutcome.PASSED_BALL,
PlayOutcome.BALK,
PlayOutcome.PICK_OFF,
]:
stats["pa"] = 1
# At bat (PA minus walks, HBP, sac)
if stats["pa"] == 1 and outcome not in [
PlayOutcome.WALK,
PlayOutcome.HIT_BY_PITCH,
PlayOutcome.INTENTIONAL_WALK,
]:
stats["ab"] = 1
# Hits - check if outcome is a hit
if outcome.is_hit():
stats["hit"] = 1
# Determine hit type
if outcome in [
PlayOutcome.DOUBLE_2,
PlayOutcome.DOUBLE_3,
PlayOutcome.DOUBLE_UNCAPPED,
]:
stats["double"] = 1
elif outcome == PlayOutcome.TRIPLE:
stats["triple"] = 1
elif outcome == PlayOutcome.HOMERUN:
stats["homerun"] = 1
# Other batting outcomes
if outcome == PlayOutcome.WALK:
stats["bb"] = 1
elif outcome == PlayOutcome.INTENTIONAL_WALK:
stats["ibb"] = 1
stats["bb"] = 1 # IBB counts as BB too
elif outcome == PlayOutcome.STRIKEOUT:
stats["so"] = 1
elif outcome == PlayOutcome.HIT_BY_PITCH:
stats["hbp"] = 1
elif outcome == PlayOutcome.GROUNDBALL_A:
# GROUNDBALL_A is double play if possible
stats["gidp"] = 1
# Baserunning events (non-PA)
if outcome == PlayOutcome.STOLEN_BASE:
stats["sb"] = 1
elif outcome == PlayOutcome.CAUGHT_STEALING:
stats["cs"] = 1
# Pitching events (non-PA)
if outcome == PlayOutcome.WILD_PITCH:
stats["wild_pitch"] = 1
elif outcome == PlayOutcome.PASSED_BALL:
stats["passed_ball"] = 1
elif outcome == PlayOutcome.BALK:
stats["balk"] = 1
elif outcome == PlayOutcome.PICK_OFF:
stats["pick_off"] = 1
# Calculate runs scored from state change
# Check which team is batting to determine which score changed
if state_before.half == "top":
stats["run"] = state_after.away_score - state_before.away_score
else:
stats["run"] = state_after.home_score - state_before.home_score
# RBI logic - runs scored minus runs on errors
# If play had an error, don't credit RBI
if hasattr(result, "error_occurred") and result.error_occurred:
stats["rbi"] = 0
else:
stats["rbi"] = stats["run"]
# Outs recorded
stats["outs_recorded"] = state_after.outs - state_before.outs
# Error tracking (from X-Check plays)
if hasattr(result, "error_occurred") and result.error_occurred:
stats["error"] = 1
return stats