CLAUDE: Add interrupt plays, jump roll, and fielding roll testing commands to terminal client
Implemented features: - Interrupt play commands: * force_wild_pitch - Force wild pitch interrupt (advances runners 1 base) * force_passed_ball - Force passed ball interrupt (advances runners 1 base) - Jump roll testing commands: * roll_jump [league] - Roll jump dice for steal testing (1d20 + 2d6/1d20) * test_jump [count] [league] - Test jump roll distribution with statistics - Fielding roll testing commands: * roll_fielding <position> [league] - Roll fielding dice (1d20 + 3d6 + 1d100) * test_fielding <position> [count] [league] - Test fielding roll distribution All commands include: - Rich terminal formatting with colored output - Comprehensive help text and examples - TAB completion for all arguments - Input validation - Statistical analysis for test commands Jump rolls show: - Pickoff attempts (5% chance, check_roll=1) - Balk checks (5% chance, check_roll=2) - Normal jump (90%, 2d6 for steal success) Fielding rolls show: - Range check (1d20) - Error total (3d6, range 3-18) - Rare play detection (SBA: d100=1, PD: error_total=5) Testing commands provide: - Distribution tables with percentages - Visual bar charts - Expected vs observed statistics - Average calculations Files modified: - terminal_client/commands.py: Added 6 new command methods - terminal_client/repl.py: Added 6 new REPL commands with help - terminal_client/completions.py: Added TAB completion support
This commit is contained in:
parent
313c2c8b5f
commit
263e1536a9
@ -666,6 +666,315 @@ class GameCommands:
|
|||||||
logger.exception("Manual outcome error")
|
logger.exception("Manual outcome error")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def force_wild_pitch(self, game_id: UUID) -> bool:
|
||||||
|
"""
|
||||||
|
Force a wild pitch interrupt play.
|
||||||
|
|
||||||
|
Wild pitch advances all runners one base.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
game_id: Game to force wild pitch in
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
display.print_info("🎯 Forcing interrupt: WILD PITCH")
|
||||||
|
return await self.resolve_play(game_id, PlayOutcome.WILD_PITCH)
|
||||||
|
|
||||||
|
async def force_passed_ball(self, game_id: UUID) -> bool:
|
||||||
|
"""
|
||||||
|
Force a passed ball interrupt play.
|
||||||
|
|
||||||
|
Passed ball advances all runners one base.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
game_id: Game to force passed ball in
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
display.print_info("🎯 Forcing interrupt: PASSED BALL")
|
||||||
|
return await self.resolve_play(game_id, PlayOutcome.PASSED_BALL)
|
||||||
|
|
||||||
|
def roll_jump(self, league: str = 'sba', game_id: Optional[UUID] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Roll jump dice for stolen base testing.
|
||||||
|
|
||||||
|
Jump roll: 1d20 check + conditional 2d6 or 1d20
|
||||||
|
- check_roll == 1: Pickoff attempt (uses resolution_roll)
|
||||||
|
- check_roll == 2: Balk check (uses resolution_roll)
|
||||||
|
- check_roll >= 3: Normal jump (uses 2d6)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
league: League ID ('sba' or 'pd')
|
||||||
|
game_id: Optional game ID for context
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from app.core.roll_types import RollType
|
||||||
|
|
||||||
|
# Roll jump dice
|
||||||
|
roll = dice_system.roll_jump(league_id=league, game_id=game_id)
|
||||||
|
|
||||||
|
# Display results
|
||||||
|
display.print_success("✓ Jump roll completed!")
|
||||||
|
display.console.print(f"\n[bold cyan]Jump Roll Results:[/bold cyan]")
|
||||||
|
display.console.print(f" [cyan]Roll ID:[/cyan] {roll.roll_id}")
|
||||||
|
display.console.print(f" [cyan]League:[/cyan] {league.upper()}")
|
||||||
|
display.console.print(f" [cyan]Check Roll (1d20):[/cyan] {roll.check_roll}")
|
||||||
|
|
||||||
|
if roll.is_pickoff_check:
|
||||||
|
display.console.print(f"\n[bold red]🎯 PICKOFF ATTEMPT![/bold red]")
|
||||||
|
display.console.print(f" [cyan]Resolution (1d20):[/cyan] {roll.resolution_roll}")
|
||||||
|
display.console.print(f"\n[dim]Pitcher attempts to pick off runner[/dim]")
|
||||||
|
elif roll.is_balk_check:
|
||||||
|
display.console.print(f"\n[bold yellow]⚠️ BALK CHECK![/bold yellow]")
|
||||||
|
display.console.print(f" [cyan]Resolution (1d20):[/cyan] {roll.resolution_roll}")
|
||||||
|
display.console.print(f"\n[dim]Pitcher may have committed balk[/dim]")
|
||||||
|
else:
|
||||||
|
display.console.print(f" [cyan]Jump Dice (2d6):[/cyan] {roll.jump_total} ({roll.jump_dice_a}+{roll.jump_dice_b})")
|
||||||
|
display.console.print(f"\n[green]Normal steal attempt - use jump total for success check[/green]")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
display.print_error(f"Failed to roll jump: {e}")
|
||||||
|
logger.exception("Jump roll error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_jump(self, count: int = 10, league: str = 'sba') -> bool:
|
||||||
|
"""
|
||||||
|
Test jump roll distribution.
|
||||||
|
|
||||||
|
Rolls N jump rolls and displays distribution statistics.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: Number of rolls to test
|
||||||
|
league: League ID ('sba' or 'pd')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from collections import Counter
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
display.print_info(f"Rolling {count} jump rolls for {league.upper()} league...")
|
||||||
|
|
||||||
|
# Roll multiple times
|
||||||
|
pickoff_count = 0
|
||||||
|
balk_count = 0
|
||||||
|
normal_count = 0
|
||||||
|
jump_totals = []
|
||||||
|
|
||||||
|
for _ in range(count):
|
||||||
|
roll = dice_system.roll_jump(league_id=league)
|
||||||
|
if roll.is_pickoff_check:
|
||||||
|
pickoff_count += 1
|
||||||
|
elif roll.is_balk_check:
|
||||||
|
balk_count += 1
|
||||||
|
else:
|
||||||
|
normal_count += 1
|
||||||
|
jump_totals.append(roll.jump_total)
|
||||||
|
|
||||||
|
# Display summary
|
||||||
|
display.print_success(f"✓ Completed {count} jump rolls")
|
||||||
|
|
||||||
|
# Event distribution table
|
||||||
|
event_table = Table(title="Jump Roll Event Distribution", show_header=True, header_style="bold cyan")
|
||||||
|
event_table.add_column("Event Type", style="yellow", width=20)
|
||||||
|
event_table.add_column("Count", style="green", width=10, justify="right")
|
||||||
|
event_table.add_column("Percentage", style="cyan", width=12, justify="right")
|
||||||
|
event_table.add_column("Expected", style="dim", width=12, justify="right")
|
||||||
|
|
||||||
|
pickoff_pct = (pickoff_count / count) * 100
|
||||||
|
balk_pct = (balk_count / count) * 100
|
||||||
|
normal_pct = (normal_count / count) * 100
|
||||||
|
|
||||||
|
event_table.add_row("Pickoff Check", str(pickoff_count), f"{pickoff_pct:.1f}%", "5.0%")
|
||||||
|
event_table.add_row("Balk Check", str(balk_count), f"{balk_pct:.1f}%", "5.0%")
|
||||||
|
event_table.add_row("Normal Jump", str(normal_count), f"{normal_pct:.1f}%", "90.0%")
|
||||||
|
|
||||||
|
display.console.print(event_table)
|
||||||
|
|
||||||
|
# Jump total distribution (for normal rolls)
|
||||||
|
if jump_totals:
|
||||||
|
display.console.print(f"\n[bold cyan]Jump Total Distribution (2d6):[/bold cyan]")
|
||||||
|
counter = Counter(jump_totals)
|
||||||
|
|
||||||
|
jump_table = Table(show_header=True, header_style="bold cyan")
|
||||||
|
jump_table.add_column("Total", style="yellow", width=10, justify="right")
|
||||||
|
jump_table.add_column("Count", style="green", width=10, justify="right")
|
||||||
|
jump_table.add_column("Percentage", style="cyan", width=12, justify="right")
|
||||||
|
jump_table.add_column("Visual", style="white", width=30)
|
||||||
|
|
||||||
|
for total in range(2, 13): # 2-12 possible with 2d6
|
||||||
|
count_val = counter.get(total, 0)
|
||||||
|
pct = (count_val / len(jump_totals)) * 100 if jump_totals else 0
|
||||||
|
bar = "█" * int(pct / 2) # Scale bar
|
||||||
|
jump_table.add_row(str(total), str(count_val), f"{pct:.1f}%", bar)
|
||||||
|
|
||||||
|
display.console.print(jump_table)
|
||||||
|
|
||||||
|
# Statistics
|
||||||
|
if jump_totals:
|
||||||
|
avg = sum(jump_totals) / len(jump_totals)
|
||||||
|
display.console.print(f"\n[dim]Average jump total: {avg:.2f} (expected: 7.0)[/dim]")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
display.print_error(f"Failed to test jump rolls: {e}")
|
||||||
|
logger.exception("Test jump error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def roll_fielding(self, position: str, league: str = 'sba', game_id: Optional[UUID] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Roll fielding check dice for testing.
|
||||||
|
|
||||||
|
Fielding roll: 1d20 + 3d6 + 1d100
|
||||||
|
- d20: Range check
|
||||||
|
- 3d6: Error total (3-18)
|
||||||
|
- d100: Rare play check
|
||||||
|
|
||||||
|
Args:
|
||||||
|
position: Defensive position (P, C, 1B, 2B, 3B, SS, LF, CF, RF)
|
||||||
|
league: League ID ('sba' or 'pd')
|
||||||
|
game_id: Optional game ID for context
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Validate position
|
||||||
|
valid_positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']
|
||||||
|
position = position.upper()
|
||||||
|
if position not in valid_positions:
|
||||||
|
display.print_error(f"Invalid position: {position}")
|
||||||
|
display.console.print(f"[dim]Valid positions: {', '.join(valid_positions)}[/dim]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Roll fielding dice
|
||||||
|
roll = dice_system.roll_fielding(position=position, league_id=league, game_id=game_id)
|
||||||
|
|
||||||
|
# Display results
|
||||||
|
display.print_success(f"✓ Fielding roll completed for {position}!")
|
||||||
|
display.console.print(f"\n[bold cyan]Fielding Roll Results:[/bold cyan]")
|
||||||
|
display.console.print(f" [cyan]Roll ID:[/cyan] {roll.roll_id}")
|
||||||
|
display.console.print(f" [cyan]Position:[/cyan] {roll.position}")
|
||||||
|
display.console.print(f" [cyan]League:[/cyan] {league.upper()}")
|
||||||
|
display.console.print(f"\n[bold]Dice Components:[/bold]")
|
||||||
|
display.console.print(f" [cyan]Range (1d20):[/cyan] {roll.d20}")
|
||||||
|
display.console.print(f" [cyan]Error Dice (3d6):[/cyan] {roll.error_total} ({roll.d6_one}+{roll.d6_two}+{roll.d6_three})")
|
||||||
|
display.console.print(f" [cyan]Rare Play (1d100):[/cyan] {roll.d100}")
|
||||||
|
|
||||||
|
if roll.is_rare_play:
|
||||||
|
if league == 'sba':
|
||||||
|
display.console.print(f"\n[bold yellow]⚠️ RARE PLAY! (d100 = 1)[/bold yellow]")
|
||||||
|
else:
|
||||||
|
display.console.print(f"\n[bold yellow]⚠️ RARE PLAY! (error_total = 5)[/bold yellow]")
|
||||||
|
display.console.print(f"[dim]Unusual fielding event may occur[/dim]")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
display.print_error(f"Validation error: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
display.print_error(f"Failed to roll fielding: {e}")
|
||||||
|
logger.exception("Fielding roll error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_fielding(self, position: str, count: int = 10, league: str = 'sba') -> bool:
|
||||||
|
"""
|
||||||
|
Test fielding roll distribution for a position.
|
||||||
|
|
||||||
|
Rolls N fielding rolls and displays distribution statistics.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
position: Defensive position (P, C, 1B, 2B, 3B, SS, LF, CF, RF)
|
||||||
|
count: Number of rolls to test
|
||||||
|
league: League ID ('sba' or 'pd')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from collections import Counter
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
# Validate position
|
||||||
|
valid_positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']
|
||||||
|
position = position.upper()
|
||||||
|
if position not in valid_positions:
|
||||||
|
display.print_error(f"Invalid position: {position}")
|
||||||
|
display.console.print(f"[dim]Valid positions: {', '.join(valid_positions)}[/dim]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
display.print_info(f"Rolling {count} fielding checks for {position} in {league.upper()} league...")
|
||||||
|
|
||||||
|
# Roll multiple times
|
||||||
|
d20_values = []
|
||||||
|
error_totals = []
|
||||||
|
d100_values = []
|
||||||
|
rare_play_count = 0
|
||||||
|
|
||||||
|
for _ in range(count):
|
||||||
|
roll = dice_system.roll_fielding(position=position, league_id=league)
|
||||||
|
d20_values.append(roll.d20)
|
||||||
|
error_totals.append(roll.error_total)
|
||||||
|
d100_values.append(roll.d100)
|
||||||
|
if roll.is_rare_play:
|
||||||
|
rare_play_count += 1
|
||||||
|
|
||||||
|
# Display summary
|
||||||
|
display.print_success(f"✓ Completed {count} fielding rolls for {position}")
|
||||||
|
|
||||||
|
# Summary statistics
|
||||||
|
display.console.print(f"\n[bold cyan]Summary Statistics:[/bold cyan]")
|
||||||
|
display.console.print(f" [cyan]Rare Plays:[/cyan] {rare_play_count} ({(rare_play_count/count)*100:.1f}%)")
|
||||||
|
display.console.print(f" [cyan]Avg Range (d20):[/cyan] {sum(d20_values)/len(d20_values):.2f} (expected: 10.5)")
|
||||||
|
display.console.print(f" [cyan]Avg Error Total (3d6):[/cyan] {sum(error_totals)/len(error_totals):.2f} (expected: 10.5)")
|
||||||
|
|
||||||
|
# Error total distribution
|
||||||
|
display.console.print(f"\n[bold cyan]Error Total Distribution (3d6):[/bold cyan]")
|
||||||
|
counter = Counter(error_totals)
|
||||||
|
|
||||||
|
error_table = Table(show_header=True, header_style="bold cyan")
|
||||||
|
error_table.add_column("Total", style="yellow", width=10, justify="right")
|
||||||
|
error_table.add_column("Count", style="green", width=10, justify="right")
|
||||||
|
error_table.add_column("Percentage", style="cyan", width=12, justify="right")
|
||||||
|
error_table.add_column("Visual", style="white", width=30)
|
||||||
|
|
||||||
|
for total in range(3, 19): # 3-18 possible with 3d6
|
||||||
|
count_val = counter.get(total, 0)
|
||||||
|
pct = (count_val / count) * 100
|
||||||
|
bar = "█" * int(pct / 2) # Scale bar
|
||||||
|
error_table.add_row(str(total), str(count_val), f"{pct:.1f}%", bar)
|
||||||
|
|
||||||
|
display.console.print(error_table)
|
||||||
|
|
||||||
|
# Rare play info
|
||||||
|
if league == 'sba':
|
||||||
|
expected_rare = 1.0 # 1% (d100 = 1)
|
||||||
|
else:
|
||||||
|
expected_rare = 2.78 # ~2.78% (3d6 = 5)
|
||||||
|
|
||||||
|
display.console.print(f"\n[dim]Expected rare play rate: {expected_rare:.2f}%[/dim]")
|
||||||
|
display.console.print(f"[dim]Observed rare play rate: {(rare_play_count/count)*100:.2f}%[/dim]")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
display.print_error(f"Validation error: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
display.print_error(f"Failed to test fielding rolls: {e}")
|
||||||
|
logger.exception("Test fielding error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Singleton instance
|
# Singleton instance
|
||||||
game_commands = GameCommands()
|
game_commands = GameCommands()
|
||||||
|
|||||||
@ -41,6 +41,9 @@ class CompletionHelper:
|
|||||||
'bp_homerun', 'bp_single', 'bp_flyout', 'bp_lineout'
|
'bp_homerun', 'bp_single', 'bp_flyout', 'bp_lineout'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Valid positions for fielding rolls
|
||||||
|
VALID_POSITIONS = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_completions(text: str, options: List[str]) -> List[str]:
|
def filter_completions(text: str, options: List[str]) -> List[str]:
|
||||||
"""
|
"""
|
||||||
@ -259,6 +262,87 @@ class GameREPLCompletions:
|
|||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# ==================== New Command Completions ====================
|
||||||
|
|
||||||
|
def complete_roll_jump(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
|
||||||
|
"""
|
||||||
|
Complete roll_jump command.
|
||||||
|
|
||||||
|
Usage: roll_jump [league]
|
||||||
|
"""
|
||||||
|
return self.completion_helper.filter_completions(
|
||||||
|
text, self.completion_helper.VALID_LEAGUES
|
||||||
|
)
|
||||||
|
|
||||||
|
def complete_test_jump(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
|
||||||
|
"""
|
||||||
|
Complete test_jump command.
|
||||||
|
|
||||||
|
Usage: test_jump [count] [league]
|
||||||
|
"""
|
||||||
|
parts = line.split()
|
||||||
|
num_args = len(parts) - 1 # Exclude command name
|
||||||
|
|
||||||
|
if num_args == 0 or (num_args == 1 and not text):
|
||||||
|
# Suggest common counts
|
||||||
|
common_counts = ['10', '50', '100', '500', '1000']
|
||||||
|
return self.completion_helper.filter_completions(text, common_counts)
|
||||||
|
elif num_args == 1 or (num_args == 2 and not text):
|
||||||
|
# Suggest leagues
|
||||||
|
return self.completion_helper.filter_completions(
|
||||||
|
text, self.completion_helper.VALID_LEAGUES
|
||||||
|
)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def complete_roll_fielding(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
|
||||||
|
"""
|
||||||
|
Complete roll_fielding command.
|
||||||
|
|
||||||
|
Usage: roll_fielding <position> [league]
|
||||||
|
"""
|
||||||
|
parts = line.split()
|
||||||
|
num_args = len(parts) - 1 # Exclude command name
|
||||||
|
|
||||||
|
if num_args == 0 or (num_args == 1 and not text):
|
||||||
|
# Suggest positions
|
||||||
|
return self.completion_helper.filter_completions(
|
||||||
|
text, self.completion_helper.VALID_POSITIONS
|
||||||
|
)
|
||||||
|
elif num_args == 1 or (num_args == 2 and not text):
|
||||||
|
# Suggest leagues
|
||||||
|
return self.completion_helper.filter_completions(
|
||||||
|
text, self.completion_helper.VALID_LEAGUES
|
||||||
|
)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def complete_test_fielding(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
|
||||||
|
"""
|
||||||
|
Complete test_fielding command.
|
||||||
|
|
||||||
|
Usage: test_fielding <position> [count] [league]
|
||||||
|
"""
|
||||||
|
parts = line.split()
|
||||||
|
num_args = len(parts) - 1 # Exclude command name
|
||||||
|
|
||||||
|
if num_args == 0 or (num_args == 1 and not text):
|
||||||
|
# Suggest positions
|
||||||
|
return self.completion_helper.filter_completions(
|
||||||
|
text, self.completion_helper.VALID_POSITIONS
|
||||||
|
)
|
||||||
|
elif num_args == 1 or (num_args == 2 and not text):
|
||||||
|
# Suggest common counts
|
||||||
|
common_counts = ['10', '50', '100', '500', '1000']
|
||||||
|
return self.completion_helper.filter_completions(text, common_counts)
|
||||||
|
elif num_args == 2 or (num_args == 3 and not text):
|
||||||
|
# Suggest leagues
|
||||||
|
return self.completion_helper.filter_completions(
|
||||||
|
text, self.completion_helper.VALID_LEAGUES
|
||||||
|
)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
# ==================== Helper Methods ====================
|
# ==================== Helper Methods ====================
|
||||||
|
|
||||||
def completedefault(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
|
def completedefault(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
|
||||||
|
|||||||
@ -676,6 +676,196 @@ Press Ctrl+D or type 'quit' to exit.
|
|||||||
"""Show detailed help for clear command."""
|
"""Show detailed help for clear command."""
|
||||||
show_help('clear')
|
show_help('clear')
|
||||||
|
|
||||||
|
# ==================== Interrupt Play Commands ====================
|
||||||
|
|
||||||
|
def do_force_wild_pitch(self, arg):
|
||||||
|
"""
|
||||||
|
Force a wild pitch interrupt play.
|
||||||
|
|
||||||
|
Usage: force_wild_pitch
|
||||||
|
|
||||||
|
Wild pitch advances all runners one base. This is an interrupt play
|
||||||
|
(pa=0) and does not count as a plate appearance.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
force_wild_pitch # Runner on 2nd advances to 3rd
|
||||||
|
"""
|
||||||
|
async def _force_wild_pitch():
|
||||||
|
try:
|
||||||
|
gid = self._ensure_game()
|
||||||
|
await self._ensure_game_loaded(gid)
|
||||||
|
await game_commands.force_wild_pitch(gid)
|
||||||
|
except ValueError:
|
||||||
|
pass # Already printed error
|
||||||
|
|
||||||
|
self._run_async(_force_wild_pitch())
|
||||||
|
|
||||||
|
def do_force_passed_ball(self, arg):
|
||||||
|
"""
|
||||||
|
Force a passed ball interrupt play.
|
||||||
|
|
||||||
|
Usage: force_passed_ball
|
||||||
|
|
||||||
|
Passed ball advances all runners one base. This is an interrupt play
|
||||||
|
(pa=0) and does not count as a plate appearance.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
force_passed_ball # Runner on 1st advances to 2nd
|
||||||
|
"""
|
||||||
|
async def _force_passed_ball():
|
||||||
|
try:
|
||||||
|
gid = self._ensure_game()
|
||||||
|
await self._ensure_game_loaded(gid)
|
||||||
|
await game_commands.force_passed_ball(gid)
|
||||||
|
except ValueError:
|
||||||
|
pass # Already printed error
|
||||||
|
|
||||||
|
self._run_async(_force_passed_ball())
|
||||||
|
|
||||||
|
# ==================== Jump Roll Testing Commands ====================
|
||||||
|
|
||||||
|
def do_roll_jump(self, arg):
|
||||||
|
"""
|
||||||
|
Roll jump dice for stolen base testing.
|
||||||
|
|
||||||
|
Usage: roll_jump [league]
|
||||||
|
|
||||||
|
Jump roll components:
|
||||||
|
- 1d20 check roll (1=pickoff, 2=balk, 3+=normal)
|
||||||
|
- 2d6 for normal jump (if check >= 3)
|
||||||
|
- 1d20 resolution (if check == 1 or 2)
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
league 'sba' or 'pd' (default: sba)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
roll_jump # Roll for SBA league
|
||||||
|
roll_jump pd # Roll for PD league
|
||||||
|
|
||||||
|
The jump roll determines steal attempt outcomes:
|
||||||
|
- Pickoff check (5%): Pitcher attempts to pick off runner
|
||||||
|
- Balk check (5%): Pitcher may commit balk
|
||||||
|
- Normal jump (90%): Use 2d6 total for steal success check
|
||||||
|
"""
|
||||||
|
parts = arg.split()
|
||||||
|
league = parts[0] if parts else 'sba'
|
||||||
|
|
||||||
|
if league not in ['sba', 'pd']:
|
||||||
|
display.print_error("League must be 'sba' or 'pd'")
|
||||||
|
return
|
||||||
|
|
||||||
|
game_commands.roll_jump(league, self.current_game_id)
|
||||||
|
|
||||||
|
def do_test_jump(self, arg):
|
||||||
|
"""
|
||||||
|
Test jump roll distribution.
|
||||||
|
|
||||||
|
Usage: test_jump [count] [league]
|
||||||
|
|
||||||
|
Rolls N jump rolls and displays distribution statistics including:
|
||||||
|
- Pickoff check frequency (expected: 5%)
|
||||||
|
- Balk check frequency (expected: 5%)
|
||||||
|
- Normal jump frequency (expected: 90%)
|
||||||
|
- Jump total distribution (2d6, expected avg: 7.0)
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
count Number of rolls (default: 10)
|
||||||
|
league 'sba' or 'pd' (default: sba)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
test_jump # 10 rolls for SBA
|
||||||
|
test_jump 100 # 100 rolls for SBA
|
||||||
|
test_jump 50 pd # 50 rolls for PD
|
||||||
|
"""
|
||||||
|
parts = arg.split()
|
||||||
|
count = int(parts[0]) if parts else 10
|
||||||
|
league = parts[1] if len(parts) > 1 else 'sba'
|
||||||
|
|
||||||
|
if league not in ['sba', 'pd']:
|
||||||
|
display.print_error("League must be 'sba' or 'pd'")
|
||||||
|
return
|
||||||
|
|
||||||
|
game_commands.test_jump(count, league)
|
||||||
|
|
||||||
|
# ==================== Fielding Roll Testing Commands ====================
|
||||||
|
|
||||||
|
def do_roll_fielding(self, arg):
|
||||||
|
"""
|
||||||
|
Roll fielding check dice for testing.
|
||||||
|
|
||||||
|
Usage: roll_fielding <position> [league]
|
||||||
|
|
||||||
|
Fielding roll components:
|
||||||
|
- 1d20: Range check
|
||||||
|
- 3d6: Error total (3-18)
|
||||||
|
- 1d100: Rare play check
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
position P, C, 1B, 2B, 3B, SS, LF, CF, RF (required)
|
||||||
|
league 'sba' or 'pd' (default: sba)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
roll_fielding SS # Roll for shortstop (SBA)
|
||||||
|
roll_fielding P pd # Roll for pitcher (PD)
|
||||||
|
roll_fielding CF # Roll for center field
|
||||||
|
|
||||||
|
Rare plays:
|
||||||
|
- SBA: d100 = 1 (1% chance)
|
||||||
|
- PD: error_total = 5 (~2.78% chance)
|
||||||
|
"""
|
||||||
|
parts = arg.split()
|
||||||
|
if not parts:
|
||||||
|
display.print_error("Usage: roll_fielding <position> [league]")
|
||||||
|
display.console.print("[dim]Valid positions: P, C, 1B, 2B, 3B, SS, LF, CF, RF[/dim]")
|
||||||
|
return
|
||||||
|
|
||||||
|
position = parts[0]
|
||||||
|
league = parts[1] if len(parts) > 1 else 'sba'
|
||||||
|
|
||||||
|
if league not in ['sba', 'pd']:
|
||||||
|
display.print_error("League must be 'sba' or 'pd'")
|
||||||
|
return
|
||||||
|
|
||||||
|
game_commands.roll_fielding(position, league, self.current_game_id)
|
||||||
|
|
||||||
|
def do_test_fielding(self, arg):
|
||||||
|
"""
|
||||||
|
Test fielding roll distribution for a position.
|
||||||
|
|
||||||
|
Usage: test_fielding <position> [count] [league]
|
||||||
|
|
||||||
|
Rolls N fielding rolls and displays distribution statistics including:
|
||||||
|
- Rare play frequency
|
||||||
|
- Average range roll (d20, expected: 10.5)
|
||||||
|
- Average error total (3d6, expected: 10.5)
|
||||||
|
- Error total distribution (3-18)
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
position P, C, 1B, 2B, 3B, SS, LF, CF, RF (required)
|
||||||
|
count Number of rolls (default: 10)
|
||||||
|
league 'sba' or 'pd' (default: sba)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
test_fielding SS # 10 rolls for shortstop (SBA)
|
||||||
|
test_fielding 2B 100 # 100 rolls for second base
|
||||||
|
test_fielding CF 50 pd # 50 rolls for center field (PD)
|
||||||
|
"""
|
||||||
|
parts = arg.split()
|
||||||
|
if not parts:
|
||||||
|
display.print_error("Usage: test_fielding <position> [count] [league]")
|
||||||
|
display.console.print("[dim]Valid positions: P, C, 1B, 2B, 3B, SS, LF, CF, RF[/dim]")
|
||||||
|
return
|
||||||
|
|
||||||
|
position = parts[0]
|
||||||
|
count = int(parts[1]) if len(parts) > 1 else 10
|
||||||
|
league = parts[2] if len(parts) > 2 else 'sba'
|
||||||
|
|
||||||
|
if league not in ['sba', 'pd']:
|
||||||
|
display.print_error("League must be 'sba' or 'pd'")
|
||||||
|
return
|
||||||
|
|
||||||
|
game_commands.test_fielding(position, count, league)
|
||||||
|
|
||||||
# ==================== REPL Control Commands ====================
|
# ==================== REPL Control Commands ====================
|
||||||
|
|
||||||
def do_clear(self, arg):
|
def do_clear(self, arg):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user