paper-dynasty-card-creation/pd_cards/commands/custom.py
Cal Corum fe0ab5e1bd Add --create flag to custom submit command
- Add --create/-c flag to create new players directly from YAML profiles
- Skip MLBPlayer creation (not needed for custom players)
- Auto-populate required API fields (cost, rarity, mlbclub, etc.)
- Update YAML profile with player_id and card_id after creation
- Add Adm Ball Traits custom player profile

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 14:23:20 -06:00

723 lines
29 KiB
Python

"""
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,
},
},
}