Add pitcher support to custom cards CLI
- Add pitcher detection via player_type field or ratings schema - Separate calc_ops and verify_total for batters vs pitchers - Pitcher template with correct schema (double_cf, xcheck fields, etc.) - Combined OPS formula: max() for pitchers, min() for batters - Add --type option to 'pd-cards custom new' command - Migrate Tony Smehrik to YAML pitcher profile Pitcher schema differences from batters: - double_cf instead of double_pull - flyout_cf_b instead of flyout_a/flyout_bq - No groundout_c - xcheck_* fields (29 chances for fielder plays) - pitching block for starter/relief/closer ratings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2e28d29ced
commit
8ac89cfcd8
@ -2,11 +2,12 @@
|
|||||||
Custom character card management.
|
Custom character card management.
|
||||||
|
|
||||||
Commands for creating and managing fictional player cards using YAML profiles.
|
Commands for creating and managing fictional player cards using YAML profiles.
|
||||||
|
Supports both batters and pitchers with their respective schemas.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional, Literal
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
import yaml
|
import yaml
|
||||||
@ -19,6 +20,20 @@ console = Console()
|
|||||||
# Profile directory relative to this file
|
# Profile directory relative to this file
|
||||||
PROFILES_DIR = Path(__file__).parent.parent / "custom" / "profiles"
|
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:
|
def get_profile_path(name: str) -> Path:
|
||||||
"""Get the path to a character profile YAML file."""
|
"""Get the path to a character profile YAML file."""
|
||||||
@ -38,8 +53,31 @@ def load_profile(name: str) -> dict:
|
|||||||
return yaml.safe_load(f)
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
|
||||||
def calc_ops(ratings: dict) -> tuple[float, float, float]:
|
def detect_player_type(profile: dict) -> Literal['batter', 'pitcher']:
|
||||||
"""Calculate AVG, OBP, SLG, OPS from ratings dict."""
|
"""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 = (
|
avg = (
|
||||||
ratings['homerun'] + ratings['bp_homerun'] / 2 + ratings['triple'] +
|
ratings['homerun'] + ratings['bp_homerun'] / 2 + ratings['triple'] +
|
||||||
ratings['double_three'] + ratings['double_two'] + ratings['double_pull'] +
|
ratings['double_three'] + ratings['double_two'] + ratings['double_pull'] +
|
||||||
@ -56,8 +94,27 @@ def calc_ops(ratings: dict) -> tuple[float, float, float]:
|
|||||||
return avg, obp, slg
|
return avg, obp, slg
|
||||||
|
|
||||||
|
|
||||||
def verify_total(ratings: dict) -> float:
|
def calc_pitcher_ops(ratings: dict) -> tuple[float, float, float]:
|
||||||
"""Verify ratings sum to 108."""
|
"""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([
|
return sum([
|
||||||
ratings['homerun'], ratings['bp_homerun'], ratings['triple'],
|
ratings['homerun'], ratings['bp_homerun'], ratings['triple'],
|
||||||
ratings['double_three'], ratings['double_two'], ratings['double_pull'],
|
ratings['double_three'], ratings['double_two'], ratings['double_pull'],
|
||||||
@ -69,6 +126,27 @@ def verify_total(ratings: dict) -> float:
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
@app.command("list")
|
||||||
def list_profiles():
|
def list_profiles():
|
||||||
"""List all available character profiles."""
|
"""List all available character profiles."""
|
||||||
@ -85,6 +163,7 @@ def list_profiles():
|
|||||||
|
|
||||||
table = Table(title="Custom Character Profiles")
|
table = Table(title="Custom Character Profiles")
|
||||||
table.add_column("Name", style="cyan")
|
table.add_column("Name", style="cyan")
|
||||||
|
table.add_column("Type", style="magenta")
|
||||||
table.add_column("Target OPS", justify="right")
|
table.add_column("Target OPS", justify="right")
|
||||||
table.add_column("Positions")
|
table.add_column("Positions")
|
||||||
table.add_column("Player ID", justify="right")
|
table.add_column("Player ID", justify="right")
|
||||||
@ -94,12 +173,13 @@ def list_profiles():
|
|||||||
with open(profile_path) as f:
|
with open(profile_path) as f:
|
||||||
profile = yaml.safe_load(f)
|
profile = yaml.safe_load(f)
|
||||||
name = profile.get('name', profile_path.stem)
|
name = profile.get('name', profile_path.stem)
|
||||||
|
player_type = detect_player_type(profile)
|
||||||
target_ops = profile.get('target_ops', '-')
|
target_ops = profile.get('target_ops', '-')
|
||||||
positions = ', '.join(profile.get('positions', {}).keys()) if isinstance(profile.get('positions'), dict) else '-'
|
positions = ', '.join(profile.get('positions', {}).keys()) if isinstance(profile.get('positions'), dict) else '-'
|
||||||
player_id = str(profile.get('player_id', '-'))
|
player_id = str(profile.get('player_id', '-'))
|
||||||
table.add_row(name, str(target_ops), positions, player_id)
|
table.add_row(name, player_type, str(target_ops), positions, player_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
table.add_row(profile_path.stem, f"[red]Error: {e}[/red]", "", "")
|
table.add_row(profile_path.stem, "?", f"[red]Error: {e}[/red]", "", "")
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
@ -110,29 +190,50 @@ def preview(
|
|||||||
):
|
):
|
||||||
"""Preview a custom character's calculated ratings."""
|
"""Preview a custom character's calculated ratings."""
|
||||||
profile = load_profile(character)
|
profile = load_profile(character)
|
||||||
|
player_type = detect_player_type(profile)
|
||||||
|
|
||||||
console.print()
|
console.print()
|
||||||
console.print("=" * 70)
|
console.print("=" * 70)
|
||||||
console.print(f"[bold]{profile['name']}[/bold] - CUSTOM PLAYER PREVIEW")
|
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("=" * 70)
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
console.print(f"Hand: {profile.get('hand', 'R')}")
|
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"Target OPS: {profile.get('target_ops', 'N/A')}")
|
||||||
console.print(f"Player ID: {profile.get('player_id', 'Not created')}")
|
console.print(f"Player ID: {profile.get('player_id', 'Not created')}")
|
||||||
console.print(f"Cardset ID: {profile.get('cardset_id', 'N/A')}")
|
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()
|
||||||
|
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
|
# Display positions
|
||||||
if 'positions' in profile:
|
if 'positions' in profile:
|
||||||
|
console.print()
|
||||||
console.print("[bold]Defensive Positions:[/bold]")
|
console.print("[bold]Defensive Positions:[/bold]")
|
||||||
for pos, stats in profile['positions'].items():
|
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', '-')}")
|
console.print(f" {pos}: Range {stats.get('range', '-')} / Error {stats.get('error', '-')} / Arm {stats.get('arm', '-')}")
|
||||||
console.print()
|
else:
|
||||||
|
console.print(f" {pos}")
|
||||||
|
|
||||||
# Display ratings
|
# Display ratings
|
||||||
if 'ratings' in profile:
|
if 'ratings' in profile:
|
||||||
console.print("[bold]Batting Ratings:[/bold]")
|
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']:
|
for vs_hand in ['vs_L', 'vs_R']:
|
||||||
if vs_hand in profile['ratings']:
|
if vs_hand in profile['ratings']:
|
||||||
ratings = profile['ratings'][vs_hand]
|
ratings = profile['ratings'][vs_hand]
|
||||||
@ -140,7 +241,9 @@ def preview(
|
|||||||
ops = obp + slg
|
ops = obp + slg
|
||||||
total = verify_total(ratings)
|
total = verify_total(ratings)
|
||||||
|
|
||||||
console.print(f"\n VS {vs_hand[-1]}HP:")
|
# 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" AVG: {avg:.3f} OBP: {obp:.3f} SLG: {slg:.3f} OPS: {ops:.3f}")
|
||||||
console.print(f" Total Chances: {total:.2f} (must be 108.0)")
|
console.print(f" Total Chances: {total:.2f} (must be 108.0)")
|
||||||
|
|
||||||
@ -149,17 +252,21 @@ def preview(
|
|||||||
|
|
||||||
# Calculate combined OPS
|
# Calculate combined OPS
|
||||||
if 'vs_L' in profile['ratings'] and 'vs_R' in profile['ratings']:
|
if 'vs_L' in profile['ratings'] and 'vs_R' in profile['ratings']:
|
||||||
_, _, slg_l = calc_ops(profile['ratings']['vs_L'])
|
_, obp_l, slg_l = calc_ops(profile['ratings']['vs_L'])
|
||||||
_, obp_l, _ = calc_ops(profile['ratings']['vs_L'])
|
_, obp_r, slg_r = calc_ops(profile['ratings']['vs_R'])
|
||||||
_, _, slg_r = calc_ops(profile['ratings']['vs_R'])
|
|
||||||
_, obp_r, _ = calc_ops(profile['ratings']['vs_R'])
|
|
||||||
ops_l = obp_l + slg_l
|
ops_l = obp_l + slg_l
|
||||||
ops_r = obp_r + slg_r
|
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
|
total_ops = (ops_l + ops_r + min(ops_l, ops_r)) / 3
|
||||||
|
|
||||||
console.print(f"\n [bold]Combined OPS: {total_ops:.3f}[/bold]")
|
console.print(f"\n [bold]Combined OPS: {total_ops:.3f}[/bold]")
|
||||||
|
|
||||||
# Display baserunning
|
# Display baserunning (batters only)
|
||||||
if 'baserunning' in profile:
|
if player_type == 'batter' and 'baserunning' in profile:
|
||||||
br = profile['baserunning']
|
br = profile['baserunning']
|
||||||
console.print("\n[bold]Baserunning:[/bold]")
|
console.print("\n[bold]Baserunning:[/bold]")
|
||||||
console.print(f" Steal Range: {br.get('steal_low', '-')}-{br.get('steal_high', '-')}")
|
console.print(f" Steal Range: {br.get('steal_low', '-')}-{br.get('steal_high', '-')}")
|
||||||
@ -178,10 +285,11 @@ def submit(
|
|||||||
):
|
):
|
||||||
"""Submit a custom character to the database."""
|
"""Submit a custom character to the database."""
|
||||||
profile = load_profile(character)
|
profile = load_profile(character)
|
||||||
|
player_type = detect_player_type(profile)
|
||||||
|
|
||||||
console.print()
|
console.print()
|
||||||
console.print("=" * 70)
|
console.print("=" * 70)
|
||||||
console.print(f"[bold]SUBMITTING {profile['name']}[/bold]")
|
console.print(f"[bold]SUBMITTING {profile['name']} ({player_type})[/bold]")
|
||||||
console.print("=" * 70)
|
console.print("=" * 70)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
@ -189,6 +297,8 @@ def submit(
|
|||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
# Verify ratings
|
# Verify ratings
|
||||||
|
verify_total = verify_pitcher_total if player_type == 'pitcher' else verify_batter_total
|
||||||
|
|
||||||
if 'ratings' in profile:
|
if 'ratings' in profile:
|
||||||
for vs_hand in ['vs_L', 'vs_R']:
|
for vs_hand in ['vs_L', 'vs_R']:
|
||||||
if vs_hand in profile['ratings']:
|
if vs_hand in profile['ratings']:
|
||||||
@ -212,10 +322,19 @@ def submit(
|
|||||||
|
|
||||||
async def do_submit():
|
async def do_submit():
|
||||||
player_id = profile.get('player_id')
|
player_id = profile.get('player_id')
|
||||||
batting_card_id = profile.get('batting_card_id')
|
|
||||||
|
|
||||||
if not player_id or not batting_card_id:
|
if player_type == 'pitcher':
|
||||||
console.print("[red]ERROR: Profile missing player_id or batting_card_id[/red]")
|
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("Create the player first, then update the profile with IDs.")
|
console.print("Create the player first, then update the profile with IDs.")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
@ -226,13 +345,13 @@ def submit(
|
|||||||
for vs_hand, ratings in profile['ratings'].items():
|
for vs_hand, ratings in profile['ratings'].items():
|
||||||
hand = vs_hand[-1] # 'L' or 'R'
|
hand = vs_hand[-1] # 'L' or 'R'
|
||||||
rating_data = {
|
rating_data = {
|
||||||
'battingcard_id': batting_card_id,
|
card_id_field: card_id,
|
||||||
'vs_hand': hand,
|
'vs_hand': hand,
|
||||||
**ratings
|
**ratings
|
||||||
}
|
}
|
||||||
ratings_list.append(rating_data)
|
ratings_list.append(rating_data)
|
||||||
|
|
||||||
await db_put('battingcardratings', payload={'ratings': ratings_list}, timeout=10)
|
await db_put(ratings_endpoint, payload={'ratings': ratings_list}, timeout=10)
|
||||||
console.print("[green] Ratings updated[/green]")
|
console.print("[green] Ratings updated[/green]")
|
||||||
|
|
||||||
# Update positions
|
# Update positions
|
||||||
@ -240,11 +359,17 @@ def submit(
|
|||||||
console.print("Updating positions...")
|
console.print("Updating positions...")
|
||||||
positions_list = []
|
positions_list = []
|
||||||
for pos, stats in profile['positions'].items():
|
for pos, stats in profile['positions'].items():
|
||||||
|
if isinstance(stats, dict):
|
||||||
positions_list.append({
|
positions_list.append({
|
||||||
'player_id': player_id,
|
'player_id': player_id,
|
||||||
'position': pos,
|
'position': pos,
|
||||||
**stats
|
**stats
|
||||||
})
|
})
|
||||||
|
else:
|
||||||
|
positions_list.append({
|
||||||
|
'player_id': player_id,
|
||||||
|
'position': pos,
|
||||||
|
})
|
||||||
|
|
||||||
await db_put('cardpositions', payload={'positions': positions_list}, timeout=10)
|
await db_put('cardpositions', payload={'positions': positions_list}, timeout=10)
|
||||||
console.print("[green] Positions updated[/green]")
|
console.print("[green] Positions updated[/green]")
|
||||||
@ -263,8 +388,9 @@ def submit(
|
|||||||
@app.command()
|
@app.command()
|
||||||
def new(
|
def new(
|
||||||
name: str = typer.Option(..., "--name", "-n", help="Character name"),
|
name: str = typer.Option(..., "--name", "-n", help="Character name"),
|
||||||
hand: str = typer.Option("R", "--hand", "-h", help="Batting hand (L/R/S)"),
|
player_type: str = typer.Option("batter", "--type", "-t", help="Player type (batter/pitcher)"),
|
||||||
target_ops: float = typer.Option(0.750, "--target-ops", help="Target OPS"),
|
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."""
|
"""Create a new character profile template."""
|
||||||
profile_path = get_profile_path(name)
|
profile_path = get_profile_path(name)
|
||||||
@ -273,14 +399,41 @@ def new(
|
|||||||
console.print(f"[red]Profile already exists:[/red] {profile_path}")
|
console.print(f"[red]Profile already exists:[/red] {profile_path}")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
# Create template
|
player_type = player_type.lower()
|
||||||
template = {
|
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,
|
'name': name,
|
||||||
|
'player_type': 'batter',
|
||||||
'hand': hand.upper(),
|
'hand': hand.upper(),
|
||||||
'target_ops': target_ops,
|
'target_ops': target_ops,
|
||||||
'cardset_id': None, # Set after creation
|
'cardset_id': None,
|
||||||
'player_id': None, # Set after creation
|
'player_id': None,
|
||||||
'batting_card_id': None, # Set after creation
|
'batting_card_id': None,
|
||||||
|
|
||||||
'positions': {
|
'positions': {
|
||||||
'RF': {'range': 3, 'error': 7, 'arm': 0},
|
'RF': {'range': 3, 'error': 7, 'arm': 0},
|
||||||
@ -320,15 +473,62 @@ def new(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ensure directory exists
|
|
||||||
PROFILES_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
with open(profile_path, 'w') as f:
|
def create_pitcher_template(name: str, hand: str, target_ops: float) -> dict:
|
||||||
yaml.dump(template, f, default_flow_style=False, sort_keys=False)
|
"""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,
|
||||||
|
|
||||||
console.print(f"[green]Created profile:[/green] {profile_path}")
|
'positions': {
|
||||||
console.print()
|
'P': None, # Pitchers just have P position, no defensive ratings
|
||||||
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(' ', '_')}")
|
'pitching': {
|
||||||
console.print(f" 3. Submit: pd-cards custom submit {name.lower().replace(' ', '_')}")
|
'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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
99
pd_cards/custom/profiles/tony_smehrik.yaml
Normal file
99
pd_cards/custom/profiles/tony_smehrik.yaml
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# Tony Smehrik - Custom Pitcher
|
||||||
|
# Left-handed SP/RP with same-side dominance
|
||||||
|
#
|
||||||
|
# Target Combined OPS: 0.585
|
||||||
|
# - OPS vs L: ~0.465 (dominant same-side)
|
||||||
|
# - OPS vs R: ~0.645 (weaker opposite-side)
|
||||||
|
# K-rate vs L: ~20% (average)
|
||||||
|
# K-rate vs R: ~8% (very low)
|
||||||
|
# FB% vs L: ~35% (average)
|
||||||
|
# FB% vs R: ~45% (high)
|
||||||
|
|
||||||
|
name: Tony Smehrik
|
||||||
|
player_type: pitcher
|
||||||
|
hand: L
|
||||||
|
target_ops: 0.585 # OPS allowed (combined)
|
||||||
|
cardset_id: 29
|
||||||
|
player_id: null # Set after creation
|
||||||
|
pitching_card_id: null # Set after creation
|
||||||
|
|
||||||
|
positions:
|
||||||
|
P: null
|
||||||
|
|
||||||
|
pitching:
|
||||||
|
starter_rating: 5
|
||||||
|
relief_rating: 5
|
||||||
|
closer_rating: null
|
||||||
|
|
||||||
|
ratings:
|
||||||
|
# vs_L = performance vs left-handed batters (same-side dominant)
|
||||||
|
# Target: .185/.240/.225 OPS ~0.465
|
||||||
|
# Total must = 108
|
||||||
|
vs_L:
|
||||||
|
# Hits allowed (~15 total hits for .185 AVG area)
|
||||||
|
homerun: 0.5
|
||||||
|
bp_homerun: 0.5
|
||||||
|
triple: 0.25
|
||||||
|
double_three: 0.0
|
||||||
|
double_two: 3.0
|
||||||
|
double_cf: 1.0
|
||||||
|
single_two: 3.0
|
||||||
|
single_one: 2.5
|
||||||
|
single_center: 3.0
|
||||||
|
bp_single: 5.0
|
||||||
|
# On-base allowed (~6 for OBP gap)
|
||||||
|
walk: 5.0
|
||||||
|
hbp: 1.0
|
||||||
|
# Outs - high K rate vs same side (~22 Ks)
|
||||||
|
strikeout: 22.0
|
||||||
|
flyout_lf_b: 4.5
|
||||||
|
flyout_cf_b: 6.0
|
||||||
|
flyout_rf_b: 4.0
|
||||||
|
groundout_a: 7.75
|
||||||
|
groundout_b: 10.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
|
||||||
|
|
||||||
|
# vs_R = performance vs right-handed batters (opposite-side weaker)
|
||||||
|
# Target: .245/.305/.340 OPS ~0.645
|
||||||
|
# Total must = 108
|
||||||
|
vs_R:
|
||||||
|
# Hits allowed (~22 total hits for .245 AVG area)
|
||||||
|
homerun: 1.5
|
||||||
|
bp_homerun: 1.5
|
||||||
|
triple: 0.5
|
||||||
|
double_three: 0.0
|
||||||
|
double_two: 4.5
|
||||||
|
double_cf: 1.5
|
||||||
|
single_two: 4.0
|
||||||
|
single_one: 3.5
|
||||||
|
single_center: 4.0
|
||||||
|
bp_single: 5.0
|
||||||
|
# On-base allowed (~7 for OBP gap)
|
||||||
|
walk: 6.0
|
||||||
|
hbp: 1.0
|
||||||
|
# Outs - very low K rate vs opposite side (~9 Ks)
|
||||||
|
strikeout: 9.0
|
||||||
|
flyout_lf_b: 5.0
|
||||||
|
flyout_cf_b: 8.0
|
||||||
|
flyout_rf_b: 5.0
|
||||||
|
groundout_a: 7.0
|
||||||
|
groundout_b: 12.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
|
||||||
Loading…
Reference in New Issue
Block a user