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")
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:

View File

@ -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

View File

@ -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: