paper-dynasty-card-creation/custom_cards/tony_smehrik_preview.py
Cal Corum 1de8b1db2f Add custom card profiles, S3 upload with timestamp cache-busting, and CLI enhancements
- Add Sippie Swartzel custom batter profile (0.820 OPS, SS/RF, no HR power)
- Update Kalin Young profile (0.891 OPS, All-Star rarity)
- Update Admiral Ball Traits profile with innings field
- Fix S3 cache-busting to include Unix timestamp for same-day updates
- Add pd_cards/core/upload.py and scouting.py modules
- Add custom card submission scripts and documentation
- Add uv.lock for dependency tracking
2026-01-25 21:57:35 -06:00

489 lines
19 KiB
Python

"""
Tony Smehrik - Custom Pitcher Card Preview
FIRST PASS - Target specs:
- Name: Tony Smehrik
- Hand: Left
- Position: RP, SP (starter_rating: 5, relief_rating: 5, closer_rating: None)
- Target Combined OPS: 0.585
- Target OPS vs L: 0.495 (dominant vs lefties - same-side advantage)
- K-rate vs L: average (~20%)
- K-rate vs R: very low (~12%)
- Flyball rate vs L: average (~35%)
- Flyball rate vs R: high (~45%)
- Team: Custom Ballplayers
- Cardset ID: 29
Uses CORRECT pitcher schema with:
- double_cf (not double_pull)
- flyout_lf_b, flyout_cf_b, flyout_rf_b (no flyout_a, flyout_bq)
- groundout_a, groundout_b only (no groundout_c)
- xcheck fields (P, C, 1B, 2B, 3B, SS, LF, CF, RF) = 29 chances total
"""
import asyncio
from dataclasses import dataclass
from creation_helpers import mround, sanitize_chance_output
# Default x-check values (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 calculate_pitcher_rating(
vs_hand: str,
pit_hand: str,
avg: float,
obp: float,
slg: float,
bb_pct: float,
k_pct: float,
hr_per_hit: float,
triple_per_hit: float,
double_per_hit: float,
fb_pct: float,
gb_pct: float,
hard_pct: float,
med_pct: float,
soft_pct: float,
oppo_pct: float,
hr_fb_pct: float = 0.10,
) -> dict:
"""
Calculate pitcher card ratings using the correct schema.
Total chances = 108:
- Hits (HR, BP-HR, 3B, 2B, 1B variants)
- On-base (BB, HBP)
- Outs (K, flyouts, groundouts)
- X-checks (29 chances that redirect to pitcher's card)
"""
# Calculate base chances (108 total, minus 29 for x-checks = 79 "real" chances)
# But wait - the x-checks ARE part of the 108. They're outs that redirect.
# So: hits + ob + outs = 108, where outs includes K, flyouts, groundouts, AND xchecks
# From calcs_pitcher.py, total_chances includes xchecks in the out count
# Let's calculate hits and OB first, then outs fill the rest
# Hits allowed (adjusted for the 1.2x offense modifier used in game)
# Note: calcs_pitcher subtracts 0.05 from AVG for BP results
all_hits = sanitize_chance_output((avg - 0.05) * 108)
# OB (walks + HBP)
all_other_ob = sanitize_chance_output(min(bb_pct * 108, 0.8 * 108)) # Cap at ~86 chances
# Outs (everything else)
all_outs = mround(108 - all_hits - all_other_ob, base=0.5)
# ===== HITS DISTRIBUTION =====
# Singles
single_pct = 1.0 - hr_per_hit - triple_per_hit - double_per_hit
total_singles = sanitize_chance_output(all_hits * single_pct)
bp_single = 5.0 if total_singles >= 5 else 0.0
rem_singles = total_singles - bp_single
# Distribute singles based on contact quality
single_two = sanitize_chance_output(rem_singles / 2) if hard_pct >= 0.2 else 0.0
rem_singles -= single_two
single_one = sanitize_chance_output(rem_singles) if soft_pct >= 0.2 else 0.0
rem_singles -= single_one
single_center = sanitize_chance_output(rem_singles)
# XBH
rem_xbh = all_hits - bp_single - single_two - single_one - single_center
# Doubles
xbh_total = hr_per_hit + triple_per_hit + double_per_hit
if xbh_total > 0:
do_rate = double_per_hit / xbh_total
tr_rate = triple_per_hit / xbh_total
hr_rate = hr_per_hit / xbh_total
else:
do_rate = tr_rate = hr_rate = 0.0
raw_doubles = sanitize_chance_output(rem_xbh * do_rate)
double_two = raw_doubles if soft_pct > 0.2 else 0.0
double_cf = mround(raw_doubles - double_two)
double_three = 0.0 # Reserved for special cases
rem_xbh -= (double_two + double_cf + double_three)
# Triples
triple = sanitize_chance_output(rem_xbh * tr_rate)
rem_xbh = mround(rem_xbh - triple)
# Home runs
raw_hr = rem_xbh
if hr_fb_pct < 0.08:
bp_homerun = sanitize_chance_output(raw_hr, min_chances=1.0, rounding=1.0)
homerun = 0.0
elif hr_fb_pct > 0.28:
homerun = raw_hr
bp_homerun = 0.0
elif hr_fb_pct > 0.18:
bp_homerun = sanitize_chance_output(raw_hr * 0.4, min_chances=1.0, rounding=1.0)
homerun = mround(raw_hr - bp_homerun)
else:
bp_homerun = sanitize_chance_output(raw_hr * 0.75, min_chances=1.0, rounding=1.0)
homerun = mround(raw_hr - bp_homerun)
# ===== ON-BASE DISTRIBUTION =====
# Assume 90% walks, 10% HBP
hbp = mround(all_other_ob * 0.10)
walk = mround(all_other_ob - hbp)
# ===== OUTS DISTRIBUTION =====
# Strikeouts (adjusted for K%)
# K rate is K/PA, but we need K/(AB-H) for out distribution
# Simplified: use K% directly scaled to outs
raw_so = sanitize_chance_output(all_outs * k_pct * 1.2) # 1.2x modifier from calcs_pitcher
# Cap strikeouts to leave room for other outs
current_total = (homerun + bp_homerun + triple + double_three + double_two + double_cf +
single_two + single_one + single_center + bp_single + hbp + walk)
max_so = 108 - current_total - TOTAL_XCHECK - 5 # Leave at least 5 for flyouts/groundouts
strikeout = min(raw_so, max_so)
# Remaining outs (after K and x-checks)
rem_outs = 108 - current_total - strikeout - TOTAL_XCHECK
# Flyouts vs groundouts based on FB%/GB%
total_batted = fb_pct + gb_pct
if total_batted > 0:
fb_share = fb_pct / total_batted
gb_share = gb_pct / total_batted
else:
fb_share = gb_share = 0.5
all_flyouts = sanitize_chance_output(rem_outs * fb_share)
all_groundouts = rem_outs - all_flyouts
# Distribute flyouts by field (pitcher hand affects distribution)
if pit_hand == 'L':
# Lefty: more fly balls to LF (opposite field for RHB)
flyout_lf_b = sanitize_chance_output(all_flyouts * oppo_pct)
flyout_rf_b = sanitize_chance_output(all_flyouts * 0.25)
else:
# Righty: more fly balls to RF (opposite field for LHB)
flyout_rf_b = sanitize_chance_output(all_flyouts * oppo_pct)
flyout_lf_b = sanitize_chance_output(all_flyouts * 0.25)
flyout_cf_b = all_flyouts - flyout_lf_b - flyout_rf_b
# Distribute groundouts (A = DP potential, B = routine)
groundout_a = sanitize_chance_output(all_groundouts * soft_pct)
groundout_b = sanitize_chance_output(all_groundouts - groundout_a)
# Build the rating dict
rating = {
'pitchingcard_id': 0, # Will be set on submission
'vs_hand': vs_hand,
'homerun': mround(homerun),
'bp_homerun': mround(bp_homerun),
'triple': mround(triple),
'double_three': mround(double_three),
'double_two': mround(double_two),
'double_cf': mround(double_cf),
'single_two': mround(single_two),
'single_one': mround(single_one),
'single_center': mround(single_center),
'bp_single': mround(bp_single),
'hbp': mround(hbp),
'walk': mround(walk),
'strikeout': mround(strikeout),
'flyout_lf_b': mround(flyout_lf_b),
'flyout_cf_b': mround(flyout_cf_b),
'flyout_rf_b': mround(flyout_rf_b),
'groundout_a': mround(groundout_a),
'groundout_b': mround(groundout_b),
**DEFAULT_XCHECKS,
# Calculated stats for display
'avg': avg,
'obp': obp,
'slg': slg,
}
# Verify total and adjust if needed
total = sum([
rating['homerun'], rating['bp_homerun'], rating['triple'],
rating['double_three'], rating['double_two'], rating['double_cf'],
rating['single_two'], rating['single_one'], rating['single_center'], rating['bp_single'],
rating['hbp'], rating['walk'], rating['strikeout'],
rating['flyout_lf_b'], rating['flyout_cf_b'], rating['flyout_rf_b'],
rating['groundout_a'], rating['groundout_b'],
rating['xcheck_p'], rating['xcheck_c'], rating['xcheck_1b'], rating['xcheck_2b'],
rating['xcheck_3b'], rating['xcheck_ss'], rating['xcheck_lf'], rating['xcheck_cf'], rating['xcheck_rf']
])
# Adjust strikeouts to hit exactly 108
diff = 108 - total
if abs(diff) > 0.01:
rating['strikeout'] = mround(rating['strikeout'] + diff)
return rating
def preview_pitcher():
"""Preview Tony Smehrik's ratings without submitting to database."""
print("\n" + "="*70)
print("TONY SMEHRIK - CUSTOM PITCHER PREVIEW")
print("="*70)
# Target: Combined OPS 0.585, OPS vs L 0.495
# Combined OPS formula for pitchers: (OPS_vR + OPS_vL + max(OPS_vL, OPS_vR)) / 3
# If OPS_vL = 0.495 and OPS_vR > OPS_vL:
# 0.585 = (OPS_vR + 0.495 + OPS_vR) / 3
# 1.755 = 2*OPS_vR + 0.495
# OPS_vR = (1.755 - 0.495) / 2 = 0.630
pit_hand = 'L'
# vs LHB (Same-side - Dominant)
# Target OPS: 0.465 (lowered from 0.495)
# OBP ~0.240, SLG ~0.225
print("\nCalculating vs LHB (same-side advantage)...")
vl = calculate_pitcher_rating(
vs_hand='L',
pit_hand=pit_hand,
avg=0.185, # Lowered from 0.195
obp=0.240, # Lowered from 0.252
slg=0.225, # Lowered from 0.243
bb_pct=0.055, # Slightly lower walks
k_pct=0.20, # Average K-rate
hr_per_hit=0.04,
triple_per_hit=0.02,
double_per_hit=0.22,
fb_pct=0.35, # Average flyball
gb_pct=0.45,
hard_pct=0.30,
med_pct=0.50,
soft_pct=0.20,
oppo_pct=0.26,
hr_fb_pct=0.06,
)
# vs RHB (Opposite-side - Weaker)
# Target OPS: 0.645 (raised from 0.630)
# OBP ~0.305, SLG ~0.340
print("Calculating vs RHB (opposite-side)...")
vr = calculate_pitcher_rating(
vs_hand='R',
pit_hand=pit_hand,
avg=0.245, # Raised from 0.238
obp=0.305, # Raised from 0.298
slg=0.340, # Raised from 0.332
bb_pct=0.075, # Slightly more walks
k_pct=0.08, # Very low K-rate (~8%)
hr_per_hit=0.07,
triple_per_hit=0.02,
double_per_hit=0.24,
fb_pct=0.45, # High flyball
gb_pct=0.35,
hard_pct=0.34,
med_pct=0.46,
soft_pct=0.20,
oppo_pct=0.26,
hr_fb_pct=0.09,
)
# ===== MANUAL ADJUSTMENTS =====
# Set half of ALL on-base chances (hits + BB + HBP) to HR
# Split 1:1 between HR and BP-HR
# BP-HR and HBP must be whole numbers
# vs LHB: Total OB = Hits + BB + HBP
vl_total_hits = (vl['homerun'] + vl['bp_homerun'] + vl['triple'] +
vl['double_three'] + vl['double_two'] + vl['double_cf'] +
vl['single_two'] + vl['single_one'] + vl['single_center'] + vl['bp_single'])
vl_total_ob = vl_total_hits + vl['walk'] + vl['hbp']
vl_hr_total = vl_total_ob / 2 # Half of all OB → HR
# Split 1:1, BP-HR must be whole number
vl['bp_homerun'] = round(vl_hr_total / 2) # Whole number
vl['homerun'] = round((vl_hr_total - vl['bp_homerun']) * 20) / 20
# Ensure HBP is whole number
vl['hbp'] = 1.0
# vs RHB: Total OB = Hits + BB + HBP
vr_total_hits = (vr['homerun'] + vr['bp_homerun'] + vr['triple'] +
vr['double_three'] + vr['double_two'] + vr['double_cf'] +
vr['single_two'] + vr['single_one'] + vr['single_center'] + vr['bp_single'])
vr_total_ob = vr_total_hits + vr['walk'] + vr['hbp']
vr_hr_total = vr_total_ob / 2 # Half of all OB → HR
# Split 1:1, BP-HR must be whole number
vr['bp_homerun'] = round(vr_hr_total / 2) # Whole number
vr['homerun'] = round((vr_hr_total - vr['bp_homerun']) * 20) / 20
# Ensure HBP is whole number
vr['hbp'] = 1.0
# The HR chances come FROM the existing hit pool, so we need to reduce other hits
# to keep total hits the same. Reduce singles proportionally.
vl_new_hr_total = vl['homerun'] + vl['bp_homerun']
vl_old_hr_total = 0 # Started with 0 HR
vl_hr_increase = vl_new_hr_total - vl_old_hr_total
# Reduce singles to compensate - take from single_one, single_two, bp_single
vl_singles_to_reduce = vl_hr_increase
# Take from single_one first, then single_two, then bp_single
reduce_from_one = min(vl['single_one'], vl_singles_to_reduce)
vl['single_one'] -= reduce_from_one
vl_singles_to_reduce -= reduce_from_one
reduce_from_two = min(vl['single_two'], vl_singles_to_reduce)
vl['single_two'] -= reduce_from_two
vl_singles_to_reduce -= reduce_from_two
reduce_from_bp = min(vl['bp_single'], vl_singles_to_reduce)
vl['bp_single'] -= reduce_from_bp
vl_singles_to_reduce -= reduce_from_bp
# If still need to reduce, take from doubles
if vl_singles_to_reduce > 0:
vl['double_cf'] = max(0, vl['double_cf'] - vl_singles_to_reduce)
vr_new_hr_total = vr['homerun'] + vr['bp_homerun']
vr_old_hr_total = 0.80 + 1.00 # Had some HR already
vr_hr_increase = vr_new_hr_total - vr_old_hr_total
# Reduce singles to compensate
vr_singles_to_reduce = vr_hr_increase
reduce_from_one = min(vr['single_one'], vr_singles_to_reduce)
vr['single_one'] -= reduce_from_one
vr_singles_to_reduce -= reduce_from_one
reduce_from_two = min(vr['single_two'], vr_singles_to_reduce)
vr['single_two'] -= reduce_from_two
vr_singles_to_reduce -= reduce_from_two
reduce_from_bp = min(vr['bp_single'], vr_singles_to_reduce)
vr['bp_single'] -= reduce_from_bp
vr_singles_to_reduce -= reduce_from_bp
if vr_singles_to_reduce > 0:
vr['double_cf'] = max(0, vr['double_cf'] - vr_singles_to_reduce)
# Force BP-SI to specific values
vl['bp_single'] = 5.0
vr['bp_single'] = 3.0
# Recalculate totals and adjust strikeouts to hit 108
for rating in [vl, vr]:
total = sum([
rating['homerun'], rating['bp_homerun'], rating['triple'],
rating['double_three'], rating['double_two'], rating['double_cf'],
rating['single_two'], rating['single_one'], rating['single_center'], rating['bp_single'],
rating['hbp'], rating['walk'], rating['strikeout'],
rating['flyout_lf_b'], rating['flyout_cf_b'], rating['flyout_rf_b'],
rating['groundout_a'], rating['groundout_b'],
rating['xcheck_p'], rating['xcheck_c'], rating['xcheck_1b'], rating['xcheck_2b'],
rating['xcheck_3b'], rating['xcheck_ss'], rating['xcheck_lf'], rating['xcheck_cf'], rating['xcheck_rf']
])
diff = 108 - total
rating['strikeout'] = round((rating['strikeout'] + diff) * 20) / 20
print(f"\nApplied HR adjustments + BP-SI override (vL: 5.0, vR: 3.0):")
print(f" vs LHB: Total OB was {vl_total_ob:.2f} → Half ({vl_hr_total:.2f}) to HR (HR:{vl['homerun']:.2f} + BP-HR:{vl['bp_homerun']:.0f})")
print(f" vs RHB: Total OB was {vr_total_ob:.2f} → Half ({vr_hr_total:.2f}) to HR (HR:{vr['homerun']:.2f} + BP-HR:{vr['bp_homerun']:.0f})")
# Calculate combined OPS
ops_vl = vl['obp'] + vl['slg']
ops_vr = vr['obp'] + vr['slg']
combined_ops = (ops_vr + ops_vl + max(ops_vl, ops_vr)) / 3
# Display results
print("\n" + "-"*70)
print("TONY SMEHRIK (L) - SP/RP")
print("-"*70)
print(f"\nTarget OPS: 0.585 combined, 0.495 vs L")
print(f"Actual OPS: {combined_ops:.3f} combined, {ops_vl:.3f} vs L, {ops_vr:.3f} vs R")
for rating in [vl, vr]:
vs_hand = rating['vs_hand']
print(f"\n{'='*35}")
print(f"VS {vs_hand}HB:")
print(f"{'='*35}")
ops = rating['obp'] + rating['slg']
print(f" AVG: {rating['avg']:.3f} OBP: {rating['obp']:.3f} SLG: {rating['slg']:.3f} OPS: {ops:.3f}")
# Show hit distribution
total_hits = (rating['homerun'] + rating['bp_homerun'] + rating['triple'] +
rating['double_three'] + rating['double_two'] + rating['double_cf'] +
rating['single_two'] + rating['single_one'] + rating['single_center'] + rating['bp_single'])
doubles = rating['double_cf'] + rating['double_two'] + rating['double_three']
singles = total_hits - rating['homerun'] - rating['bp_homerun'] - rating['triple'] - doubles
print(f"\n HITS ALLOWED: {total_hits:.2f}")
print(f" HR: {rating['homerun']:.2f} + BP-HR: {rating['bp_homerun']:.2f}")
print(f" 3B: {rating['triple']:.2f}")
print(f" 2B: {doubles:.2f} (double**: {rating['double_two']:.2f}, double(cf): {rating['double_cf']:.2f})")
print(f" 1B: {singles:.2f} (1B**: {rating['single_two']:.2f}, 1B*: {rating['single_one']:.2f}, 1B(cf): {rating['single_center']:.2f}, BP-1B: {rating['bp_single']:.2f})")
# Show walks/strikeouts
print(f"\n PLATE DISCIPLINE:")
print(f" BB: {rating['walk']:.2f} HBP: {rating['hbp']:.2f}")
print(f" K: {rating['strikeout']:.2f}")
# Show batted ball outs
flyouts = rating['flyout_lf_b'] + rating['flyout_cf_b'] + rating['flyout_rf_b']
groundouts = rating['groundout_a'] + rating['groundout_b']
xchecks = sum([rating[f'xcheck_{pos}'] for pos in ['p', 'c', '1b', '2b', '3b', 'ss', 'lf', 'cf', 'rf']])
total_outs = rating['strikeout'] + flyouts + groundouts + xchecks
print(f"\n OUTS: {total_outs:.2f}")
print(f" Strikeouts: {rating['strikeout']:.2f}")
print(f" Flyouts: {flyouts:.2f} (LF-B: {rating['flyout_lf_b']:.2f}, CF-B: {rating['flyout_cf_b']:.2f}, RF-B: {rating['flyout_rf_b']:.2f})")
print(f" Groundouts: {groundouts:.2f} (A: {rating['groundout_a']:.2f}, B: {rating['groundout_b']:.2f})")
print(f" X-Checks: {xchecks:.2f} (P:{rating['xcheck_p']:.0f} C:{rating['xcheck_c']:.0f} 1B:{rating['xcheck_1b']:.0f} 2B:{rating['xcheck_2b']:.0f} 3B:{rating['xcheck_3b']:.0f} SS:{rating['xcheck_ss']:.0f} LF:{rating['xcheck_lf']:.0f} CF:{rating['xcheck_cf']:.0f} RF:{rating['xcheck_rf']:.0f})")
batted_outs = flyouts + groundouts
if batted_outs > 0:
fb_rate = flyouts / batted_outs
gb_rate = groundouts / batted_outs
print(f" FB%: {fb_rate*100:.1f}% GB%: {gb_rate*100:.1f}%")
# Verify total = 108
total = sum([
rating['homerun'], rating['bp_homerun'], rating['triple'],
rating['double_three'], rating['double_two'], rating['double_cf'],
rating['single_two'], rating['single_one'], rating['single_center'], rating['bp_single'],
rating['hbp'], rating['walk'], rating['strikeout'],
rating['flyout_lf_b'], rating['flyout_cf_b'], rating['flyout_rf_b'],
rating['groundout_a'], rating['groundout_b'],
rating['xcheck_p'], rating['xcheck_c'], rating['xcheck_1b'], rating['xcheck_2b'],
rating['xcheck_3b'], rating['xcheck_ss'], rating['xcheck_lf'], rating['xcheck_cf'], rating['xcheck_rf']
])
print(f"\n Total chances: {total:.2f} (should be 108.0)")
# Summary
print("\n" + "="*70)
print("SUMMARY")
print("="*70)
print(f"\nName: Tony Smehrik")
print(f"Hand: L")
print(f"Positions: SP, RP")
print(f"Starter Rating: 5 | Relief Rating: 5 | Closer Rating: None")
print(f"\nCombined OPS Against: {combined_ops:.3f} (target: 0.585)")
print(f"OPS vs LHB: {ops_vl:.3f} (target: 0.495)")
print(f"OPS vs RHB: {ops_vr:.3f} (target: ~0.630)")
print(f"\nK-rate vs L: ~20% (average)")
print(f"K-rate vs R: ~12% (very low)")
print(f"\nFB% vs L: ~35% (average)")
print(f"FB% vs R: ~45% (high)")
print("\n" + "-"*70)
print("Ready to submit? Run: python -m custom_cards.submit_tony_smehrik")
print("-"*70 + "\n")
return vl, vr
if __name__ == "__main__":
preview_pitcher()