CLAUDE: Add X-Check testing to resolve_with command

Added ability to test X-Check defensive plays directly in the terminal
client using the resolve_with command. This allows testing the complete
X-Check resolution system (defense tables, error charts, runner advancement)
with actual player ratings.

Changes:
- repl.py: Updated do_resolve_with() to parse "x-check <position>" syntax
  - Accepts "x-check", "xcheck", or "x_check" followed by position
  - Validates position (P, C, 1B, 2B, 3B, SS, LF, CF, RF)
  - Passes xcheck_position parameter through to commands

- commands.py: Updated resolve_play() to accept xcheck_position parameter
  - Passes xcheck_position to game_engine.resolve_play()
  - Shows "🎯 Forcing X-Check to: <position>" message

- game_engine.py: Updated resolve_play() to accept xcheck_position parameter
  - For X_CHECK outcomes, uses xcheck_position as hit_location
  - Enables full X-Check resolution with defense tables and error charts

- help_text.py: Updated resolve_with help documentation
  - Added x-check usage syntax and examples
  - Documented position parameter requirement
  - Added note about using actual player ratings

Usage:
 > defensive
 > offensive
 > resolve_with x-check SS    # Test X-Check to shortstop
 > resolve_with x-check LF    # Test X-Check to left field

This integrates the existing play_resolver._resolve_x_check() logic,
providing a way to test the complete X-Check system including defense
range adjustments, table lookups, SPD tests, G2#/G3# conversion, error
charts, and runner advancement.

🤖 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-04 15:59:16 -06:00
parent 8fb740fe3e
commit bb78de2b84
4 changed files with 53 additions and 13 deletions

View File

@ -400,7 +400,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, forced_outcome: Optional[PlayOutcome] = None) -> PlayResult:
async def resolve_play(self, game_id: UUID, forced_outcome: Optional[PlayOutcome] = None, xcheck_position: Optional[str] = None) -> PlayResult:
"""
Resolve the current play with dice roll
@ -415,6 +415,7 @@ class GameEngine:
Args:
game_id: Game to resolve
forced_outcome: If provided, use this outcome instead of rolling dice (for testing)
xcheck_position: For X_CHECK outcomes, the position to check (SS, LF, etc.)
Returns:
PlayResult with complete outcome
@ -443,9 +444,12 @@ class GameEngine:
"Use resolve_manual_play() for manual mode or resolve_auto_play() for auto mode."
)
# For X_CHECK, use xcheck_position as the hit_location parameter
hit_location = xcheck_position if forced_outcome == PlayOutcome.X_CHECK else None
result = resolver.resolve_outcome(
outcome=forced_outcome,
hit_location=None, # Testing doesn't specify location
hit_location=hit_location, # For X_CHECK, this is the position being checked
state=state,
defensive_decision=defensive_decision,
offensive_decision=offensive_decision,

View File

@ -199,22 +199,26 @@ class GameCommands:
logger.exception("Offensive decision error")
return False
async def resolve_play(self, game_id: UUID, forced_outcome: Optional[PlayOutcome] = None) -> bool:
async def resolve_play(self, game_id: UUID, forced_outcome: Optional[PlayOutcome] = None, xcheck_position: Optional[str] = None) -> bool:
"""
Resolve the current play.
Args:
game_id: Game to resolve
forced_outcome: If provided, use this outcome instead of rolling dice
xcheck_position: For X_CHECK outcomes, the position to check (SS, LF, etc.)
Returns:
True if successful, False otherwise
"""
try:
if forced_outcome:
if xcheck_position:
display.print_info(f"🎯 Forcing X-Check to: {xcheck_position}")
else:
display.print_info(f"🎯 Forcing outcome: {forced_outcome.value}")
result = await game_engine.resolve_play(game_id, forced_outcome)
result = await game_engine.resolve_play(game_id, forced_outcome, xcheck_position)
state = await game_engine.get_game_state(game_id)
if state:

View File

@ -268,12 +268,17 @@ HELP_DATA = {
'resolve_with': {
'summary': 'Resolve current play with a specific outcome (bypassing dice rolls)',
'usage': 'resolve_with <OUTCOME>',
'usage': 'resolve_with <OUTCOME>\n resolve_with x-check <POSITION>',
'options': [
{
'name': 'OUTCOME',
'type': 'STRING',
'desc': 'PlayOutcome enum value. Use list_outcomes to see all available values.'
},
{
'name': 'POSITION',
'type': 'P|C|1B|2B|3B|SS|LF|CF|RF',
'desc': 'For x-check: defensive position to test (required)'
}
],
'examples': [
@ -281,9 +286,11 @@ HELP_DATA = {
'resolve_with homerun',
'resolve_with groundball_a',
'resolve_with double_uncapped',
'resolve_with strikeout'
'resolve_with strikeout',
'resolve_with x-check SS # Test X-Check to shortstop',
'resolve_with x-check LF # Test X-Check to left field'
],
'notes': 'Experimental feature for testing specific scenarios without random dice rolls. Useful for testing runner advancement, scoring, and game state changes with known outcomes.'
'notes': 'Experimental feature for testing specific scenarios without random dice rolls. X-Check mode uses full defense tables and error charts with actual player ratings.'
},
'quick_play': {

View File

@ -289,9 +289,11 @@ Press Ctrl+D or type 'quit' to exit.
Resolve the current play with a specific outcome (for testing).
Usage: resolve_with <outcome>
resolve_with x-check <position>
Arguments:
outcome PlayOutcome value (e.g., single_1, homerun, strikeout)
position For x-check: P, C, 1B, 2B, 3B, SS, LF, CF, RF
This command allows you to force a specific outcome instead of
rolling dice, useful for testing runner advancement, specific
@ -304,31 +306,54 @@ Press Ctrl+D or type 'quit' to exit.
resolve_with homerun
resolve_with groundball_a
resolve_with double_uncapped
resolve_with x-check SS # Test X-Check to shortstop
resolve_with x-check LF # Test X-Check to left field
"""
async def _resolve_with():
try:
gid = self._ensure_game()
await self._ensure_game_loaded(gid)
# Parse outcome argument
outcome_str = arg.strip().lower()
if not outcome_str:
# Parse arguments
args = arg.strip().lower().split()
if not args:
display.print_error("Missing outcome argument")
display.print_info("Usage: resolve_with <outcome>")
display.print_info(" resolve_with x-check <position>")
display.print_info("Use 'list_outcomes' to see available values")
return
# Check for x-check with position
outcome_str = args[0]
xcheck_position = None
if outcome_str in ['x-check', 'xcheck', 'x_check']:
if len(args) < 2:
display.print_error("Missing position for x-check")
display.print_info("Usage: resolve_with x-check <position>")
display.print_info("Valid positions: P, C, 1B, 2B, 3B, SS, LF, CF, RF")
return
outcome_str = 'x_check'
xcheck_position = args[1].upper()
# Validate position
valid_positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']
if xcheck_position not in valid_positions:
display.print_error(f"Invalid position: {xcheck_position}")
display.print_info(f"Valid positions: {', '.join(valid_positions)}")
return
# Try to convert string to PlayOutcome enum
from app.config import PlayOutcome
try:
outcome = PlayOutcome(outcome_str)
except ValueError:
display.print_error(f"Invalid outcome: {outcome_str}")
display.print_info("Use 'list_outcomes' to see valid values")
display.print_info("Use 'list_outcomes' to see available values")
return
# Use shared command with forced outcome
await game_commands.resolve_play(gid, forced_outcome=outcome)
await game_commands.resolve_play(gid, forced_outcome=outcome, xcheck_position=xcheck_position)
except ValueError:
pass