paper-dynasty-card-creation/custom_cards/tony_smehrik_preview.py
Cal Corum 0a17745389 Run black and ruff across entire codebase
Standardize formatting with black and apply ruff auto-fixes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 14:24:33 -05:00

625 lines
21 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
"""
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("\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("\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("\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("\nName: Tony Smehrik")
print("Hand: L")
print("Positions: SP, RP")
print("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("\nK-rate vs L: ~20% (average)")
print("K-rate vs R: ~12% (very low)")
print("\nFB% vs L: ~35% (average)")
print("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()