""" Custom character card management. Commands for creating and managing fictional player cards using YAML profiles. Supports both batters and pitchers with their respective schemas. """ import asyncio from datetime import datetime from pathlib import Path from typing import Optional, Literal import typer import yaml from rich.console import Console from rich.table import Table app = typer.Typer(no_args_is_help=True) console = Console() # Profile directory relative to this file PROFILES_DIR = Path(__file__).parent.parent / "custom" / "profiles" # Default x-check values for pitchers (from calcs_pitcher.py) DEFAULT_XCHECKS = { 'xcheck_p': 1.0, 'xcheck_c': 3.0, 'xcheck_1b': 2.0, 'xcheck_2b': 6.0, 'xcheck_3b': 3.0, 'xcheck_ss': 7.0, 'xcheck_lf': 2.0, 'xcheck_cf': 3.0, 'xcheck_rf': 2.0, } TOTAL_XCHECK = sum(DEFAULT_XCHECKS.values()) # 29.0 def get_profile_path(name: str) -> Path: """Get the path to a character profile YAML file.""" # Normalize name: lowercase, replace spaces with underscores normalized = name.lower().replace(" ", "_").replace("-", "_") return PROFILES_DIR / f"{normalized}.yaml" def load_profile(name: str) -> dict: """Load a character profile from YAML.""" profile_path = get_profile_path(name) if not profile_path.exists(): console.print(f"[red]Profile not found:[/red] {profile_path}") raise typer.Exit(1) with open(profile_path) as f: return yaml.safe_load(f) def detect_player_type(profile: dict) -> Literal['batter', 'pitcher']: """Detect player type from profile structure.""" # Explicit type field takes precedence if 'player_type' in profile: return profile['player_type'] # Check for pitcher-specific fields if 'pitching' in profile: return 'pitcher' if 'pitching_card_id' in profile: return 'pitcher' # Check ratings structure for pitcher-specific fields if 'ratings' in profile: sample_rating = profile['ratings'].get('vs_L') or profile['ratings'].get('vs_R') if sample_rating: # Pitchers have xcheck fields and double_cf instead of double_pull if 'xcheck_p' in sample_rating or 'double_cf' in sample_rating: return 'pitcher' return 'batter' def calc_batter_ops(ratings: dict) -> tuple[float, float, float]: """Calculate AVG, OBP, SLG from batter ratings dict.""" avg = ( ratings['homerun'] + ratings['bp_homerun'] / 2 + ratings['triple'] + ratings['double_three'] + ratings['double_two'] + ratings['double_pull'] + ratings['single_two'] + ratings['single_one'] + ratings['single_center'] + ratings['bp_single'] / 2 ) / 108 obp = avg + (ratings['hbp'] + ratings['walk']) / 108 slg = ( ratings['homerun'] * 4 + ratings['bp_homerun'] * 2 + ratings['triple'] * 3 + (ratings['double_three'] + ratings['double_two'] + ratings['double_pull']) * 2 + ratings['single_two'] + ratings['single_one'] + ratings['single_center'] + ratings['bp_single'] / 2 ) / 108 return avg, obp, slg def calc_pitcher_ops(ratings: dict) -> tuple[float, float, float]: """Calculate AVG, OBP, SLG allowed from pitcher ratings dict.""" # Pitchers use double_cf instead of double_pull avg = ( ratings['homerun'] + ratings['bp_homerun'] / 2 + ratings['triple'] + ratings['double_three'] + ratings['double_two'] + ratings.get('double_cf', 0) + ratings['single_two'] + ratings['single_one'] + ratings['single_center'] + ratings['bp_single'] / 2 ) / 108 obp = avg + (ratings['hbp'] + ratings['walk']) / 108 slg = ( ratings['homerun'] * 4 + ratings['bp_homerun'] * 2 + ratings['triple'] * 3 + (ratings['double_three'] + ratings['double_two'] + ratings.get('double_cf', 0)) * 2 + ratings['single_two'] + ratings['single_one'] + ratings['single_center'] + ratings['bp_single'] / 2 ) / 108 return avg, obp, slg def verify_batter_total(ratings: dict) -> float: """Verify batter ratings sum to 108.""" return sum([ ratings['homerun'], ratings['bp_homerun'], ratings['triple'], ratings['double_three'], ratings['double_two'], ratings['double_pull'], ratings['single_two'], ratings['single_one'], ratings['single_center'], ratings['bp_single'], ratings['walk'], ratings['hbp'], ratings['strikeout'], ratings['lineout'], ratings['popout'], ratings['flyout_a'], ratings['flyout_bq'], ratings['flyout_lf_b'], ratings['flyout_rf_b'], ratings['groundout_a'], ratings['groundout_b'], ratings['groundout_c'] ]) def verify_pitcher_total(ratings: dict) -> float: """Verify pitcher ratings sum to 108.""" return sum([ # Hits ratings['homerun'], ratings['bp_homerun'], ratings['triple'], ratings['double_three'], ratings['double_two'], ratings.get('double_cf', 0), ratings['single_two'], ratings['single_one'], ratings['single_center'], ratings['bp_single'], # On-base ratings['walk'], ratings['hbp'], # Outs ratings['strikeout'], ratings['flyout_lf_b'], ratings.get('flyout_cf_b', 0), ratings['flyout_rf_b'], ratings['groundout_a'], ratings['groundout_b'], # X-checks (29 total) ratings.get('xcheck_p', 1.0), ratings.get('xcheck_c', 3.0), ratings.get('xcheck_1b', 2.0), ratings.get('xcheck_2b', 6.0), ratings.get('xcheck_3b', 3.0), ratings.get('xcheck_ss', 7.0), ratings.get('xcheck_lf', 2.0), ratings.get('xcheck_cf', 3.0), ratings.get('xcheck_rf', 2.0), ]) @app.command("list") def list_profiles(): """List all available character profiles.""" if not PROFILES_DIR.exists(): console.print("[yellow]No profiles directory found.[/yellow]") console.print(f"Create profiles in: {PROFILES_DIR}") return profiles = list(PROFILES_DIR.glob("*.yaml")) if not profiles: console.print("[yellow]No character profiles found.[/yellow]") console.print(f"Create YAML profiles in: {PROFILES_DIR}") return table = Table(title="Custom Character Profiles") table.add_column("Name", style="cyan") table.add_column("Type", style="magenta") table.add_column("Target OPS", justify="right") table.add_column("Positions") table.add_column("Player ID", justify="right") for profile_path in sorted(profiles): try: with open(profile_path) as f: profile = yaml.safe_load(f) name = profile.get('name', profile_path.stem) player_type = detect_player_type(profile) target_ops = profile.get('target_ops', '-') positions = ', '.join(profile.get('positions', {}).keys()) if isinstance(profile.get('positions'), dict) else '-' player_id = str(profile.get('player_id', '-')) table.add_row(name, player_type, str(target_ops), positions, player_id) except Exception as e: table.add_row(profile_path.stem, "?", f"[red]Error: {e}[/red]", "", "") console.print(table) @app.command() def preview( character: str = typer.Argument(..., help="Character profile name (e.g., kalin_young)"), ): """Preview a custom character's calculated ratings.""" profile = load_profile(character) player_type = detect_player_type(profile) console.print() console.print("=" * 70) type_label = "PITCHER" if player_type == 'pitcher' else "BATTER" console.print(f"[bold]{profile['name']}[/bold] - CUSTOM {type_label} PREVIEW") console.print("=" * 70) console.print() console.print(f"Type: {player_type.capitalize()}") hand_label = "Throws" if player_type == 'pitcher' else "Bats" console.print(f"{hand_label}: {profile.get('hand', 'R')}") console.print(f"Target OPS: {profile.get('target_ops', 'N/A')}") console.print(f"Player ID: {profile.get('player_id', 'Not created')}") console.print(f"Cardset ID: {profile.get('cardset_id', 'N/A')}") # Display pitcher-specific info if player_type == 'pitcher' and 'pitching' in profile: pit = profile['pitching'] console.print() console.print("[bold]Pitching Roles:[/bold]") console.print(f" Starter Rating: {pit.get('starter_rating', '-')}") console.print(f" Relief Rating: {pit.get('relief_rating', '-')}") console.print(f" Closer Rating: {pit.get('closer_rating', '-')}") # Display positions if 'positions' in profile: console.print() console.print("[bold]Defensive Positions:[/bold]") for pos, stats in profile['positions'].items(): if isinstance(stats, dict): console.print(f" {pos}: Range {stats.get('range', '-')} / Error {stats.get('error', '-')} / Arm {stats.get('arm', '-')}") else: console.print(f" {pos}") # Display ratings if 'ratings' in profile: console.print() rating_label = "Pitching Ratings (OPS Allowed)" if player_type == 'pitcher' else "Batting Ratings" console.print(f"[bold]{rating_label}:[/bold]") calc_ops = calc_pitcher_ops if player_type == 'pitcher' else calc_batter_ops verify_total = verify_pitcher_total if player_type == 'pitcher' else verify_batter_total for vs_hand in ['vs_L', 'vs_R']: if vs_hand in profile['ratings']: ratings = profile['ratings'][vs_hand] avg, obp, slg = calc_ops(ratings) ops = obp + slg total = verify_total(ratings) # For pitchers, vs_L means "vs left-handed batters" hand_suffix = "HB" if player_type == 'pitcher' else "HP" console.print(f"\n VS {vs_hand[-1]}{hand_suffix}:") console.print(f" AVG: {avg:.3f} OBP: {obp:.3f} SLG: {slg:.3f} OPS: {ops:.3f}") console.print(f" Total Chances: {total:.2f} (must be 108.0)") if abs(total - 108.0) > 0.01: console.print(f" [red]WARNING: Total is not 108![/red]") # Calculate combined OPS if 'vs_L' in profile['ratings'] and 'vs_R' in profile['ratings']: _, obp_l, slg_l = calc_ops(profile['ratings']['vs_L']) _, obp_r, slg_r = calc_ops(profile['ratings']['vs_R']) ops_l = obp_l + slg_l ops_r = obp_r + slg_r # Pitchers use max(), batters use min() if player_type == 'pitcher': total_ops = (ops_l + ops_r + max(ops_l, ops_r)) / 3 else: total_ops = (ops_l + ops_r + min(ops_l, ops_r)) / 3 console.print(f"\n [bold]Combined OPS: {total_ops:.3f}[/bold]") # Display baserunning (batters only) if player_type == 'batter' and 'baserunning' in profile: br = profile['baserunning'] console.print("\n[bold]Baserunning:[/bold]") console.print(f" Steal Range: {br.get('steal_low', '-')}-{br.get('steal_high', '-')}") console.print(f" Steal Jump: {br.get('steal_jump', '-')}") console.print(f" Running: {br.get('running', '-')}") console.print() console.print("[yellow]Preview only - not submitted to database[/yellow]") @app.command() def submit( character: str = typer.Argument(..., help="Character profile name"), dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Preview changes without saving"), skip_s3: bool = typer.Option(False, "--skip-s3", help="Skip S3 upload after submit"), create: bool = typer.Option(False, "--create", "-c", help="Create new player in database (requires cardset_id in profile)"), ): """Submit a custom character to the database.""" profile = load_profile(character) player_type = detect_player_type(profile) console.print() console.print("=" * 70) action = "CREATING" if create else "SUBMITTING" console.print(f"[bold]{action} {profile['name']} ({player_type})[/bold]") console.print("=" * 70) if dry_run: console.print("[yellow]DRY RUN - no changes will be made[/yellow]") console.print() # Verify ratings verify_total = verify_pitcher_total if player_type == 'pitcher' else verify_batter_total if 'ratings' in profile: for vs_hand in ['vs_L', 'vs_R']: if vs_hand in profile['ratings']: total = verify_total(profile['ratings'][vs_hand]) if abs(total - 108.0) > 0.01: console.print(f"[red]ERROR: {vs_hand} ratings total is {total:.2f}, must be 108.0[/red]") raise typer.Exit(1) # Validate create requirements if create: if not profile.get('cardset_id'): console.print("[red]ERROR: cardset_id required in profile for --create[/red]") raise typer.Exit(1) if profile.get('player_id'): console.print("[red]ERROR: player_id already set - use submit without --create to update[/red]") raise typer.Exit(1) if dry_run: console.print("[green]Validation passed - ready to submit[/green]") return # Import database functions try: from pd_cards.core.db import db_get, db_put, db_patch, db_post except ImportError: # Fallback to old location during migration import sys sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from db_calls import db_get, db_put, db_patch, db_post async def do_create(): """Create a new player in the database.""" # Parse name name_parts = profile['name'].split() name_first = name_parts[0] name_last = ' '.join(name_parts[1:]) if len(name_parts) > 1 else name_parts[0] # Generate bbref_id for custom player (with timestamp to avoid conflicts) timestamp = int(datetime.now().timestamp()) bbref_id = f"custom_{name_last.lower().replace(' ', '')}{name_first[0].lower()}{timestamp}" cardset_id = profile['cardset_id'] hand = profile.get('hand', 'R') console.print(f"Name: {name_first} {name_last}") console.print(f"BBRef ID: {bbref_id}") console.print(f"Cardset: {cardset_id}") console.print() # Step 1: Create Player record (custom players don't need MLBPlayer) console.print("Creating Player record...") now = datetime.now() release_date = f"{now.year}-{now.month}-{now.day}" # Get first position for pos_1 positions = list(profile.get('positions', {}).keys()) pos_1 = positions[0] if positions else 'DH' player_payload = { 'p_name': f"{name_first} {name_last}", 'bbref_id': bbref_id, 'fangraphs_id': 0, 'mlbam_id': 0, 'retrosheet_id': '', 'hand': hand, 'mlb_team_id': 1, 'franchise_id': 1, # Default franchise 'cardset_id': cardset_id, 'description': 'Custom', 'is_custom': True, # Required fields 'cost': 100, # Default cost 'image': '', # Will be patched after 'mlbclub': 'Custom Ballplayers', 'franchise': 'Custom Ballplayers', 'set_num': 9999, 'rarity_id': 5, # Common 'pos_1': pos_1, } new_player = await db_post('players', payload=player_payload) player_id = new_player['player_id'] console.print(f"[green] Created Player (ID: {player_id})[/green]") # Step 3: Patch Player with image URL card_type = 'batting' if player_type == 'batter' else 'pitching' image_url = f"https://pd.manticorum.com/api/v2/players/{player_id}/{card_type}card?d={release_date}" await db_patch('players', object_id=player_id, params=[('image', image_url)]) console.print("[green] Updated Player with image URL[/green]") # Step 4: Create Card if player_type == 'batter': console.print("Creating BattingCard...") baserunning = profile.get('baserunning', {}) batting_card_payload = { 'cards': [{ 'player_id': player_id, 'key_bbref': bbref_id, 'key_fangraphs': 0, 'key_mlbam': 0, 'key_retro': '', 'name_first': name_first, 'name_last': name_last, 'steal_low': baserunning.get('steal_low', 5), 'steal_high': baserunning.get('steal_high', 12), 'steal_auto': 1 if baserunning.get('steal_auto') else 0, 'steal_jump': baserunning.get('steal_jump', 0.15), 'hit_and_run': baserunning.get('hit_and_run', 'C'), 'running': baserunning.get('running', 10), 'bunting': baserunning.get('bunting', 'C'), 'hand': hand, }] } await db_put('battingcards', payload=batting_card_payload, timeout=10) # Get the card ID bc_query = await db_get('battingcards', params=[('player_id', player_id)]) card_id = bc_query['cards'][0]['id'] card_id_field = 'battingcard_id' ratings_endpoint = 'battingcardratings' console.print(f"[green] Created BattingCard (ID: {card_id})[/green]") else: console.print("Creating PitchingCard...") pitching = profile.get('pitching', {}) pitching_card_payload = { 'cards': [{ 'player_id': player_id, 'key_bbref': bbref_id, 'key_fangraphs': 0, 'key_mlbam': 0, 'key_retro': '', 'name_first': name_first, 'name_last': name_last, 'hand': hand, 'starter_rating': pitching.get('starter_rating', 5), 'relief_rating': pitching.get('relief_rating', 5), 'closer_rating': pitching.get('closer_rating'), }] } await db_put('pitchingcards', payload=pitching_card_payload, timeout=10) # Get the card ID pc_query = await db_get('pitchingcards', params=[('player_id', player_id)]) card_id = pc_query['cards'][0]['id'] card_id_field = 'pitchingcard_id' ratings_endpoint = 'pitchingcardratings' console.print(f"[green] Created PitchingCard (ID: {card_id})[/green]") # Step 5: Create ratings console.print("Creating ratings...") ratings_list = [] for vs_hand, ratings in profile['ratings'].items(): hand_char = vs_hand[-1] # 'L' or 'R' rating_data = { card_id_field: card_id, 'vs_hand': hand_char, **ratings } ratings_list.append(rating_data) await db_put(ratings_endpoint, payload={'ratings': ratings_list}, timeout=10) console.print("[green] Created ratings[/green]") # Step 6: Create positions console.print("Creating positions...") positions_list = [] for pos, stats in profile.get('positions', {}).items(): if isinstance(stats, dict): positions_list.append({ 'player_id': player_id, 'position': pos, **stats }) else: positions_list.append({ 'player_id': player_id, 'position': pos, }) if positions_list: await db_put('cardpositions', payload={'positions': positions_list}, timeout=10) console.print(f"[green] Created {len(positions_list)} position(s)[/green]") # Step 7: Update YAML profile with IDs console.print("Updating profile with IDs...") profile['player_id'] = player_id if player_type == 'batter': profile['batting_card_id'] = card_id else: profile['pitching_card_id'] = card_id profile_path = get_profile_path(character) with open(profile_path, 'w') as f: yaml.dump(profile, f, default_flow_style=False, sort_keys=False) console.print(f"[green] Updated {profile_path.name}[/green]") console.print() console.print(f"[bold green]Successfully created {profile['name']}![/bold green]") console.print(f"Player ID: {player_id}") console.print(f"Card ID: {card_id}") console.print(f"URL: https://pd.manticorum.com/api/v2/players/{player_id}/{card_type}card?d={release_date}") async def do_submit(): """Update an existing player in the database.""" player_id = profile.get('player_id') if player_type == 'pitcher': card_id = profile.get('pitching_card_id') card_id_field = 'pitchingcard_id' ratings_endpoint = 'pitchingcardratings' else: card_id = profile.get('batting_card_id') card_id_field = 'battingcard_id' ratings_endpoint = 'battingcardratings' if not player_id or not card_id: id_field = 'pitching_card_id' if player_type == 'pitcher' else 'batting_card_id' console.print(f"[red]ERROR: Profile missing player_id or {id_field}[/red]") console.print("Use --create to create a new player, or add IDs to the profile.") raise typer.Exit(1) # Update ratings if 'ratings' in profile: console.print("Updating ratings...") ratings_list = [] for vs_hand, ratings in profile['ratings'].items(): hand = vs_hand[-1] # 'L' or 'R' rating_data = { card_id_field: card_id, 'vs_hand': hand, **ratings } ratings_list.append(rating_data) await db_put(ratings_endpoint, payload={'ratings': ratings_list}, timeout=10) console.print("[green] Ratings updated[/green]") # Update positions if 'positions' in profile: console.print("Updating positions...") positions_list = [] for pos, stats in profile['positions'].items(): if isinstance(stats, dict): positions_list.append({ 'player_id': player_id, 'position': pos, **stats }) else: positions_list.append({ 'player_id': player_id, 'position': pos, }) await db_put('cardpositions', payload={'positions': positions_list}, timeout=10) console.print("[green] Positions updated[/green]") console.print() console.print(f"[green]Successfully updated {profile['name']}![/green]") if not skip_s3: console.print() console.print("[yellow]S3 upload not yet implemented in CLI[/yellow]") console.print("Run manually: python check_cards_and_upload.py") if create: asyncio.run(do_create()) else: asyncio.run(do_submit()) @app.command() def new( name: str = typer.Option(..., "--name", "-n", help="Character name"), player_type: str = typer.Option("batter", "--type", "-t", help="Player type (batter/pitcher)"), hand: str = typer.Option("R", "--hand", "-h", help="Batting/throwing hand (L/R, or S for switch-hitter)"), target_ops: float = typer.Option(0.750, "--target-ops", help="Target OPS (or OPS allowed for pitchers)"), ): """Create a new character profile template.""" profile_path = get_profile_path(name) if profile_path.exists(): console.print(f"[red]Profile already exists:[/red] {profile_path}") raise typer.Exit(1) player_type = player_type.lower() if player_type not in ('batter', 'pitcher'): console.print(f"[red]Invalid player type:[/red] {player_type}") console.print("Must be 'batter' or 'pitcher'") raise typer.Exit(1) if player_type == 'pitcher': template = create_pitcher_template(name, hand, target_ops) else: template = create_batter_template(name, hand, target_ops) # Ensure directory exists PROFILES_DIR.mkdir(parents=True, exist_ok=True) with open(profile_path, 'w') as f: yaml.dump(template, f, default_flow_style=False, sort_keys=False) console.print(f"[green]Created {player_type} profile:[/green] {profile_path}") console.print() console.print("Next steps:") console.print(f" 1. Edit the profile: {profile_path}") console.print(f" 2. Preview: pd-cards custom preview {name.lower().replace(' ', '_')}") console.print(f" 3. Submit: pd-cards custom submit {name.lower().replace(' ', '_')}") def create_batter_template(name: str, hand: str, target_ops: float) -> dict: """Create a batter profile template.""" return { 'name': name, 'player_type': 'batter', 'hand': hand.upper(), 'target_ops': target_ops, 'cardset_id': None, 'player_id': None, 'batting_card_id': None, 'positions': { 'RF': {'range': 3, 'error': 7, 'arm': 0}, }, 'baserunning': { 'steal_jump': 0.15, 'steal_high': 12, 'steal_low': 5, 'steal_auto': 0, 'running': 10, 'hit_and_run': 'C', 'bunting': 'C', }, 'ratings': { 'vs_L': { 'homerun': 2.0, 'bp_homerun': 1.0, 'triple': 0.5, 'double_three': 0.0, 'double_two': 4.0, 'double_pull': 2.0, 'single_two': 5.0, 'single_one': 5.0, 'single_center': 5.0, 'bp_single': 3.0, 'walk': 10.0, 'hbp': 1.0, 'strikeout': 20.0, 'lineout': 12.0, 'popout': 2.0, 'flyout_a': 2.0, 'flyout_bq': 2.0, 'flyout_lf_b': 4.0, 'flyout_rf_b': 4.0, 'groundout_a': 5.0, 'groundout_b': 10.0, 'groundout_c': 8.5, 'pull_rate': 0.30, 'center_rate': 0.40, 'slap_rate': 0.30, }, 'vs_R': { 'homerun': 2.0, 'bp_homerun': 1.0, 'triple': 0.5, 'double_three': 0.0, 'double_two': 4.0, 'double_pull': 2.0, 'single_two': 5.0, 'single_one': 5.0, 'single_center': 5.0, 'bp_single': 3.0, 'walk': 10.0, 'hbp': 1.0, 'strikeout': 20.0, 'lineout': 12.0, 'popout': 2.0, 'flyout_a': 2.0, 'flyout_bq': 2.0, 'flyout_lf_b': 4.0, 'flyout_rf_b': 4.0, 'groundout_a': 5.0, 'groundout_b': 10.0, 'groundout_c': 8.5, 'pull_rate': 0.30, 'center_rate': 0.40, 'slap_rate': 0.30, }, }, } def create_pitcher_template(name: str, hand: str, target_ops: float) -> dict: """Create a pitcher profile template.""" return { 'name': name, 'player_type': 'pitcher', 'hand': hand.upper(), 'target_ops': target_ops, # OPS allowed 'cardset_id': None, 'player_id': None, 'pitching_card_id': None, 'positions': { 'P': None, # Pitchers just have P position, no defensive ratings }, 'pitching': { 'starter_rating': 5, 'relief_rating': 5, 'closer_rating': None, }, 'ratings': { # vs_L = performance vs left-handed batters (total = 108) 'vs_L': { # Hits allowed (23.5) 'homerun': 1.0, 'bp_homerun': 1.0, 'triple': 0.5, 'double_three': 0.0, 'double_two': 3.0, 'double_cf': 2.0, 'single_two': 4.0, 'single_one': 3.0, 'single_center': 4.0, 'bp_single': 5.0, # On-base allowed (9.0) 'walk': 8.0, 'hbp': 1.0, # Outs (46.5) 'strikeout': 20.0, 'flyout_lf_b': 4.0, 'flyout_cf_b': 5.0, 'flyout_rf_b': 4.0, 'groundout_a': 6.0, 'groundout_b': 7.5, # X-checks (fielder plays - 29 total) 'xcheck_p': 1.0, 'xcheck_c': 3.0, 'xcheck_1b': 2.0, 'xcheck_2b': 6.0, 'xcheck_3b': 3.0, 'xcheck_ss': 7.0, 'xcheck_lf': 2.0, 'xcheck_cf': 3.0, 'xcheck_rf': 2.0, }, # vs_R = performance vs right-handed batters (total = 108) 'vs_R': { # Hits allowed (27.0) 'homerun': 1.5, 'bp_homerun': 1.5, 'triple': 0.5, 'double_three': 0.0, 'double_two': 4.0, 'double_cf': 2.0, 'single_two': 4.5, 'single_one': 3.5, 'single_center': 4.5, 'bp_single': 5.0, # On-base allowed (10.0) 'walk': 9.0, 'hbp': 1.0, # Outs (42.0) 'strikeout': 15.0, 'flyout_lf_b': 4.0, 'flyout_cf_b': 5.0, 'flyout_rf_b': 4.0, 'groundout_a': 6.0, 'groundout_b': 8.0, # X-checks (29 total) 'xcheck_p': 1.0, 'xcheck_c': 3.0, 'xcheck_1b': 2.0, 'xcheck_2b': 6.0, 'xcheck_3b': 3.0, 'xcheck_ss': 7.0, 'xcheck_lf': 2.0, 'xcheck_cf': 3.0, 'xcheck_rf': 2.0, }, }, }