CLAUDE: Implement forced outcome feature for terminal client testing

Add ability to force specific play outcomes instead of random dice rolls,
enabling targeted testing of specific game scenarios.

Changes:
- play_resolver.resolve_play(): Add forced_outcome parameter, bypass dice
  rolls when provided, create dummy AbRoll with placeholder values
- game_engine.resolve_play(): Accept and pass through forced_outcome param
- terminal_client/commands.py: Pass forced_outcome to game engine

Testing:
- Verified TRIPLE, HOMERUN, and STRIKEOUT outcomes work correctly
- Dummy AbRoll properly constructed with all required fields
- Game state updates correctly with forced outcomes

Example usage in REPL:
  resolve_with triple
  resolve_with homerun

Fixes terminal client testing workflow to allow controlled scenarios.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-10-30 15:39:35 -05:00
parent 16ba30b351
commit 8ecce0f5ad
3 changed files with 42 additions and 17 deletions

View File

@ -324,7 +324,7 @@ class GameEngine:
logger.warning(f"Offensive decision timeout for game {state.game_id}, using default") logger.warning(f"Offensive decision timeout for game {state.game_id}, using default")
return OffensiveDecision() # All defaults return OffensiveDecision() # All defaults
async def resolve_play(self, game_id: UUID) -> PlayResult: async def resolve_play(self, game_id: UUID, forced_outcome: Optional[PlayOutcome] = None) -> PlayResult:
""" """
Resolve the current play with dice roll Resolve the current play with dice roll
@ -335,6 +335,13 @@ class GameEngine:
4. Update game state in DB 4. Update game state in DB
5. Check for inning change (outs >= 3) 5. Check for inning change (outs >= 3)
6. Prepare next play (always last step) 6. Prepare next play (always last step)
Args:
game_id: Game to resolve
forced_outcome: If provided, use this outcome instead of rolling dice (for testing)
Returns:
PlayResult with complete outcome
""" """
state = state_manager.get_state(game_id) state = state_manager.get_state(game_id)
if not state: if not state:
@ -347,7 +354,7 @@ class GameEngine:
offensive_decision = OffensiveDecision(**state.decisions_this_play.get('offensive', {})) offensive_decision = OffensiveDecision(**state.decisions_this_play.get('offensive', {}))
# STEP 1: Resolve play (this internally calls dice_system.roll_ab) # STEP 1: Resolve play (this internally calls dice_system.roll_ab)
result = play_resolver.resolve_play(state, defensive_decision, offensive_decision) result = play_resolver.resolve_play(state, defensive_decision, offensive_decision, forced_outcome)
# Track roll for batch saving at end of inning # Track roll for batch saving at end of inning
if game_id not in self._rolls_this_inning: if game_id not in self._rolls_this_inning:

View File

@ -11,9 +11,10 @@ Updated: 2025-10-29 - Integrated universal PlayOutcome enum
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, List from typing import Optional, List
import pendulum
from app.core.dice import dice_system from app.core.dice import dice_system
from app.core.roll_types import AbRoll from app.core.roll_types import AbRoll, RollType
from app.models.game_models import GameState, DefensiveDecision, OffensiveDecision from app.models.game_models import GameState, DefensiveDecision, OffensiveDecision
from app.config import PlayOutcome from app.config import PlayOutcome
@ -135,7 +136,8 @@ class PlayResolver:
self, self,
state: GameState, state: GameState,
defensive_decision: DefensiveDecision, defensive_decision: DefensiveDecision,
offensive_decision: OffensiveDecision offensive_decision: OffensiveDecision,
forced_outcome: Optional[PlayOutcome] = None
) -> PlayResult: ) -> PlayResult:
""" """
Resolve a complete play Resolve a complete play
@ -144,22 +146,41 @@ class PlayResolver:
state: Current game state state: Current game state
defensive_decision: Defensive team's choices defensive_decision: Defensive team's choices
offensive_decision: Offensive team's choices offensive_decision: Offensive team's choices
forced_outcome: If provided, use this outcome instead of rolling dice (for testing)
Returns: Returns:
PlayResult with complete outcome PlayResult with complete outcome
""" """
logger.info(f"Resolving play - Inning {state.inning} {state.half}, {state.outs} outs") logger.info(f"Resolving play - Inning {state.inning} {state.half}, {state.outs} outs")
# Roll dice using our advanced AbRoll system if forced_outcome:
ab_roll = dice_system.roll_ab( # Use forced outcome for testing (no dice roll)
league_id=state.league_id, logger.info(f"Using forced outcome: {forced_outcome.value}")
game_id=state.game_id outcome = forced_outcome
) # Create a dummy AbRoll for the forced outcome
logger.info(f"AB Roll: {ab_roll}") ab_roll = AbRoll(
roll_id=f"forced_{state.game_id}_{state.play_count}",
roll_type=RollType.AB,
league_id=state.league_id,
timestamp=pendulum.now('UTC'),
game_id=state.game_id,
d6_one=1, # Dummy values - not used for forced outcomes
d6_two_a=3,
d6_two_b=4,
chaos_d20=10,
resolution_d20=10
)
else:
# Roll dice using our advanced AbRoll system
ab_roll = dice_system.roll_ab(
league_id=state.league_id,
game_id=state.game_id
)
logger.info(f"AB Roll: {ab_roll}")
# Get base outcome from chart # Get base outcome from chart
outcome = self.result_chart.get_outcome(ab_roll) outcome = self.result_chart.get_outcome(ab_roll)
logger.info(f"Base outcome: {outcome}") logger.info(f"Base outcome: {outcome}")
# Apply decisions (simplified for Phase 2) # Apply decisions (simplified for Phase 2)
# TODO: Implement full decision logic in Phase 3 # TODO: Implement full decision logic in Phase 3

View File

@ -211,11 +211,8 @@ class GameCommands:
try: try:
if forced_outcome: if forced_outcome:
display.print_info(f"🎯 Forcing outcome: {forced_outcome.value}") display.print_info(f"🎯 Forcing outcome: {forced_outcome.value}")
# TODO: Integrate with game_engine to properly apply forced outcomes
display.print_warning("⚠️ Manual outcome selection is experimental")
display.print_warning(" Using regular resolution for now (forced outcome noted)")
result = await game_engine.resolve_play(game_id) result = await game_engine.resolve_play(game_id, forced_outcome)
state = await game_engine.get_game_state(game_id) state = await game_engine.get_game_state(game_id)
if state: if state: