CLAUDE: Add configurable regulation_innings and outs_per_inning

Issue #8 from code review - hardcoded inning limit (9) and outs (3)
prevented custom game modes like 7-inning doubleheaders.

Changes:
- Added regulation_innings: int = 9 to GameState (default standard)
- Added outs_per_inning: int = 3 to GameState (default standard)
- Updated validators.py: is_game_over(), can_continue_inning(), shallow outfield
- Updated game_models.py: is_game_over() uses state.regulation_innings
- Updated game_engine.py: _finalize_play() uses state.outs_per_inning

Now supports custom game modes:
- 7-inning doubleheaders
- 6-inning youth/minor league games
- Alternative out rules (2-out innings, etc.)

All 739 unit tests passing.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-11-19 20:06:57 -06:00
parent 86f671ba0c
commit 2521833afb
4 changed files with 36 additions and 24 deletions

View File

@ -295,9 +295,12 @@ Some methods seem designed for testing but are public API.
- Validation exists in validators.py: hold_runners validates against occupied bases (lines 71-77)
- Validation exists in validators.py: steal_attempts validates against runner state (lines 156-165)
- Validators called in submit_defensive_decision (line 225) and submit_offensive_decision (line 263)
- [x] High issue #8 deferred to next sprint (2025-11-19)
- Hardcoded inning limit (9) used in validators.py and game_models.py
- Requires config system changes - appropriate for technical debt phase
- [x] High issue #8 fixed (2025-11-19)
- Added `regulation_innings: int = 9` and `outs_per_inning: int = 3` to GameState
- Updated validators.py: `is_game_over()`, `can_continue_inning()`, shallow outfield check
- Updated game_models.py: `is_game_over()` method
- Updated game_engine.py: `_finalize_play()` inning change checks
- Now supports 7-inning doubleheaders, 6-inning youth games, etc.
- [x] High issue #9 already fixed (part of Issue #3)
- `_cleanup_game_resources()` called in `end_game()` at line 1109
- [x] High issue #10 acknowledged (2025-11-19)

View File

@ -474,7 +474,7 @@ class GameEngine:
logger.debug("Skipped game state update - no changes to persist")
# Check for inning change
if state.outs >= 3:
if state.outs >= state.outs_per_inning:
await self._advance_inning(state, game_id)
# Update DB again after inning change
await self.db_ops.update_game_state(
@ -497,7 +497,7 @@ class GameEngine:
raise
# Batch save rolls at half-inning boundary (separate transaction - audit data)
if state.outs >= 3:
if state.outs >= state.outs_per_inning:
await self._batch_save_inning_rolls(game_id)
# Prepare next play or clean up if game completed

View File

@ -85,11 +85,11 @@ class GameValidator:
if decision.outfield_depth == 'shallow':
# Walk-off conditions:
# 1. Home team batting (bottom of inning)
# 2. Bottom of 9th or later
# 2. Bottom of final inning or later
# 3. Tied or trailing
# 4. Runner on base
is_home_batting = (state.half == 'bottom')
is_late_inning = (state.inning >= 9)
is_late_inning = (state.inning >= state.regulation_innings)
if is_home_batting:
is_close_game = (state.home_score <= state.away_score)
@ -100,8 +100,8 @@ class GameValidator:
if not (is_home_batting and is_late_inning and is_close_game and has_runners):
raise ValidationError(
"Shallow outfield only allowed in walk-off situations "
"(home team batting, bottom 9th+ inning, tied/trailing, runner on base)"
f"Shallow outfield only allowed in walk-off situations "
f"(home team batting, bottom {state.regulation_innings}th+ inning, tied/trailing, runner on base)"
)
logger.debug("Defensive decision validated")
@ -202,21 +202,23 @@ class GameValidator:
@staticmethod
def can_continue_inning(state: GameState) -> bool:
"""Check if inning can continue"""
return state.outs < 3
"""Check if inning can continue using state's outs_per_inning"""
return state.outs < state.outs_per_inning
@staticmethod
def is_game_over(state: GameState) -> bool:
"""Check if game is complete"""
# Game over after 9 innings if score not tied
if state.inning >= 9 and state.half == "bottom":
"""Check if game is complete using state's regulation_innings"""
reg = state.regulation_innings
# Game over after regulation innings if score not tied
if state.inning >= reg and state.half == "bottom":
if state.home_score != state.away_score:
return True
# Home team wins if ahead in bottom of 9th
# Home team wins if ahead in bottom of final inning
if state.home_score > state.away_score:
return True
# Also check if we're in extras and bottom team is ahead
if state.inning > 9 and state.half == "bottom":
if state.inning > reg and state.half == "bottom":
if state.home_score > state.away_score:
return True
return False

View File

@ -374,6 +374,10 @@ class GameState(BaseModel):
# Resolution mode
auto_mode: bool = False # True = auto-generate outcomes (PD only), False = manual submissions
# Game rules (configurable per game)
regulation_innings: int = Field(default=9, ge=1) # Standard 9, doubleheader 7, etc.
outs_per_inning: int = Field(default=3, ge=1) # Standard 3
# Game state
status: str = "pending" # pending, active, paused, completed
inning: int = Field(default=1, ge=1)
@ -646,24 +650,27 @@ class GameState(BaseModel):
"""
Check if game is over.
Game ends after 9 innings if home team is ahead or tied,
or immediately if home team takes lead in bottom of 9th or later.
Game ends after regulation innings if home team is ahead or tied,
or immediately if home team takes lead in bottom of final inning or later.
Uses self.regulation_innings (default 9) for game length.
"""
if self.inning < 9:
reg = self.regulation_innings
if self.inning < reg:
return False
if self.inning == 9 and self.half == "bottom":
# Bottom of 9th - game ends if home team ahead
if self.inning == reg and self.half == "bottom":
# Bottom of final regulation inning - game ends if home team ahead
if self.home_score > self.away_score:
return True
if self.inning > 9 and self.half == "bottom":
if self.inning > reg and self.half == "bottom":
# Extra innings, bottom half - walk-off possible
if self.home_score > self.away_score:
return True
if self.inning >= 9 and self.half == "top" and self.outs >= 3:
# Top of 9th or later just ended
if self.inning >= reg and self.half == "top" and self.outs >= self.outs_per_inning:
# Top of final inning or later just ended
if self.home_score != self.away_score:
return True