From 8ecce0f5adde32f4780401e9399a0b0d4210e494 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Thu, 30 Oct 2025 15:39:35 -0500 Subject: [PATCH] CLAUDE: Implement forced outcome feature for terminal client testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/app/core/game_engine.py | 11 ++++++-- backend/app/core/play_resolver.py | 43 +++++++++++++++++++++-------- backend/terminal_client/commands.py | 5 +--- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/backend/app/core/game_engine.py b/backend/app/core/game_engine.py index b46804e..a4a6376 100644 --- a/backend/app/core/game_engine.py +++ b/backend/app/core/game_engine.py @@ -324,7 +324,7 @@ class GameEngine: logger.warning(f"Offensive decision timeout for game {state.game_id}, using default") 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 @@ -335,6 +335,13 @@ class GameEngine: 4. Update game state in DB 5. Check for inning change (outs >= 3) 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) if not state: @@ -347,7 +354,7 @@ class GameEngine: offensive_decision = OffensiveDecision(**state.decisions_this_play.get('offensive', {})) # 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 if game_id not in self._rolls_this_inning: diff --git a/backend/app/core/play_resolver.py b/backend/app/core/play_resolver.py index 2de5cef..e188c90 100644 --- a/backend/app/core/play_resolver.py +++ b/backend/app/core/play_resolver.py @@ -11,9 +11,10 @@ Updated: 2025-10-29 - Integrated universal PlayOutcome enum import logging from dataclasses import dataclass from typing import Optional, List +import pendulum 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.config import PlayOutcome @@ -135,7 +136,8 @@ class PlayResolver: self, state: GameState, defensive_decision: DefensiveDecision, - offensive_decision: OffensiveDecision + offensive_decision: OffensiveDecision, + forced_outcome: Optional[PlayOutcome] = None ) -> PlayResult: """ Resolve a complete play @@ -144,22 +146,41 @@ class PlayResolver: state: Current game state defensive_decision: Defensive team's choices offensive_decision: Offensive team's choices + forced_outcome: If provided, use this outcome instead of rolling dice (for testing) Returns: PlayResult with complete outcome """ logger.info(f"Resolving play - Inning {state.inning} {state.half}, {state.outs} outs") - # 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}") + if forced_outcome: + # Use forced outcome for testing (no dice roll) + logger.info(f"Using forced outcome: {forced_outcome.value}") + outcome = forced_outcome + # Create a dummy AbRoll for the forced outcome + 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 - outcome = self.result_chart.get_outcome(ab_roll) - logger.info(f"Base outcome: {outcome}") + # Get base outcome from chart + outcome = self.result_chart.get_outcome(ab_roll) + logger.info(f"Base outcome: {outcome}") # Apply decisions (simplified for Phase 2) # TODO: Implement full decision logic in Phase 3 diff --git a/backend/terminal_client/commands.py b/backend/terminal_client/commands.py index 57c5e8f..fce5367 100644 --- a/backend/terminal_client/commands.py +++ b/backend/terminal_client/commands.py @@ -211,11 +211,8 @@ class GameCommands: try: if forced_outcome: 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) if state: