paper-dynasty-card-creation/custom_cards/sphealthamus_spheal_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

502 lines
15 KiB
Python

"""
Preview ratings for Sphealthamus Sphealy Spheal Sr custom player.
Target: 0.850 total OPS
Power vs RHP (singles/HRs, 62% pull), Patient vs LHP (walks/singles, 28% pull)
"""
from custom_cards.archetype_definitions import BatterArchetype
from custom_cards.archetype_calculator import (
BatterRatingCalculator,
calculate_total_ops,
)
import random
# Create custom archetype for Sphealthamus Sphealy Spheal Sr
# Target: Combined OPS = 0.850 with formula: (OPS_vR + OPS_vL + min) / 3
# Dramatic split: Power vs RHP, Patient vs LHP
spheal = BatterArchetype(
name="Sphealthamus Sphealy Spheal Sr",
description="Power vs RHP, patience vs LHP, extreme splits",
# VS RHP: Target OPS = ~0.850 (power profile - singles + HRs)
avg_vs_r=0.260, # Boosted to hit OPS target
obp_vs_r=0.340, # Boosted to hit OPS target
slg_vs_r=0.495, # Boosted to hit OPS target
bb_pct_vs_r=0.09, # Moderate walks
k_pct_vs_r=0.24, # Higher strikeouts (power hitter)
# VS LHP: Target OPS = ~0.830 (patient profile - walks + singles)
avg_vs_l=0.260, # Boosted to hit OPS target
obp_vs_l=0.375, # Boosted to hit OPS target
slg_vs_l=0.420, # Boosted to hit OPS target
bb_pct_vs_l=0.15, # Very high walks
k_pct_vs_l=0.20, # Fewer strikeouts
# Power distribution - high HR rate vs RHP, moderate vs LHP
hr_per_hit=0.15, # High HR rate
triple_per_hit=0.00, # NO TRIPLES
double_per_hit=0.20, # Moderate doubles
# Batted ball profile
gb_pct=0.40, # Some ground balls
fb_pct=0.38, # Lots of fly balls (power)
ld_pct=0.22, # Line drives
# Batted ball quality - power hitter
hard_pct=0.40, # Lots of hard contact
med_pct=0.42, # Medium contact
soft_pct=0.18, # Some soft contact
# Spray chart - will adjust per split
pull_pct=0.45, # Placeholder
center_pct=0.32, # Placeholder
oppo_pct=0.23, # Placeholder
# Infield hits
ifh_pct=0.04, # Low (slow runner)
# Specific power metrics
hr_fb_pct=0.16, # Good HR/FB
# Baserunning - poor speed
speed_rating=3, # Poor speed
steal_jump=5, # Below average
xbt_pct=0.42, # Below average
# Situational hitting - poor
hit_run_skill=3, # Poor hit-and-run
# Defensive profile
primary_positions=["1B"],
defensive_rating=5, # Average defender
)
print("=" * 70)
print("SPHEALTHAMUS SPHEALY SPHEAL SR - CUSTOM PLAYER PREVIEW")
print("=" * 70)
print()
print("Player Info:")
print(" Name: Sphealthamus Sphealy Spheal Sr")
print(" Hand: L (Left-handed batter)")
print(" Primary Position: 1B")
print()
print("Defensive Ratings (per requirements):")
print(" 1B: Range 5 / Error 3")
print()
print("Target Statistics:")
print(" Target Total OPS: 0.850")
print(" Pull Rate vs RHP: 62%")
print(" Pull Rate vs LHP: 28%")
print(" Running: 9")
print(" Stealing: 18-13 (0.416666)")
print(" Bunting: D")
print(" Hit-and-Run: D")
print(" vs RHP: Power (singles/HRs, 3:2 BP-HR:HR ratio)")
print(" vs LHP: Patient (walks/singles, few HRs)")
print(" No triples either side")
print()
# Calculate ratings
calc = BatterRatingCalculator(spheal)
ratings = calc.calculate_ratings(battingcard_id=0) # Temp ID
baserunning = calc.calculate_baserunning()
# Override steal rate to 0.416666 (18-13)
baserunning["steal_jump"] = 0.416666
baserunning["steal_high"] = 18
baserunning["steal_low"] = 13
# Override running to 9
baserunning["running"] = 9
# Override bunting to D
baserunning["bunting"] = "D"
# Override hit-and-run to D
baserunning["hit_and_run"] = "D"
# Apply randomization
random.seed(43)
def round_to_05(value):
"""Round to nearest 0.05 (Stratomatic standard)."""
return round(value * 20) / 20
def add_variation(base_value, max_variation=0.5):
"""Add small random variation to a value, then round to 0.05."""
if base_value == 0.0:
return 0.0
variation = random.uniform(-max_variation, max_variation)
result = max(0.0, base_value + variation)
return round_to_05(result)
# Randomize to make card look natural
for rating in ratings:
randomize_fields = [
"homerun",
"bp_homerun",
"triple",
"double_three",
"double_two",
"double_pull",
"single_two",
"single_one",
"single_center",
"walk",
"hbp",
"strikeout",
"lineout",
"popout",
"flyout_a",
"flyout_bq",
"flyout_lf_b",
"flyout_rf_b",
"groundout_a",
"groundout_b",
"groundout_c",
]
for field in randomize_fields:
if rating[field] > 0:
rating[field] = add_variation(rating[field], 0.5)
# Fix BP-HR and HBP to whole numbers, remove triples, adjust pull rates
print("\n✓ Applying custom adjustments...")
print(" - Setting BP-HR to whole numbers")
print(" - Setting HBP to whole numbers")
print(" - Removing all triples")
print(" - Adjusting pull rates (62% vR, 28% vL)")
print(" - Setting BP-HR:HR ratio to 3:2 vs RHP")
# vs LHP (ratings[0]) - Patient profile
old_bphr_vl = ratings[0]["bp_homerun"]
ratings[0]["bp_homerun"] = 1.0 # Fewer BP-HR vs LHP
ratings[0]["single_center"] += old_bphr_vl - 1.0
old_hbp_vl = ratings[0]["hbp"]
ratings[0]["hbp"] = 1.0
ratings[0]["single_center"] += old_hbp_vl - 1.0
# Remove triples vL
old_triple_vl = ratings[0]["triple"]
ratings[0]["triple"] = 0.0
ratings[0]["single_center"] += old_triple_vl
# Remove Double2 vL and move to Walks
old_double2_vl = ratings[0]["double_two"]
ratings[0]["double_two"] = 0.0
ratings[0]["walk"] += old_double2_vl
# Set pull rate to 28% vL
ratings[0]["pull_rate"] = 0.28
ratings[0]["center_rate"] = 0.40
ratings[0]["slap_rate"] = 0.32
# vs RHP (ratings[1]) - Power profile
# Add 2 BP-HR and 1.4 HR
total_hr_chances = ratings[1]["homerun"] + ratings[1]["bp_homerun"]
ratings[1]["homerun"] = 3.4 # Add 1.4 HR
ratings[1]["bp_homerun"] = 5.0 # Add 2 BP-HR
# Redistribute excess
excess = total_hr_chances - 8.4
ratings[1]["single_center"] += excess
old_hbp_vr = ratings[1]["hbp"]
ratings[1]["hbp"] = 1.0
ratings[1]["single_center"] += old_hbp_vr - 1.0
# Remove triples vR
old_triple_vr = ratings[1]["triple"]
ratings[1]["triple"] = 0.0
ratings[1]["single_center"] += old_triple_vr
# Remove Double2 vR and move to singles
old_double2_vr = ratings[1]["double_two"]
ratings[1]["double_two"] = 0.0
ratings[1]["single_center"] += old_double2_vr
# Remove Flyout LF-B vR and move to strikeouts
old_flyout_lf_vr = ratings[1]["flyout_lf_b"]
ratings[1]["flyout_lf_b"] = 0.0
ratings[1]["strikeout"] += old_flyout_lf_vr
# Set pull rate to 62% vR
ratings[1]["pull_rate"] = 0.62
ratings[1]["center_rate"] = 0.25
ratings[1]["slap_rate"] = 0.13
# Rebalance each split to exactly 108.00
def rebalance_to_108(rating_dict):
"""Adjust to ensure total = 108.00 by tweaking groundout_b."""
current_total = sum(
[
rating_dict["homerun"],
rating_dict["bp_homerun"],
rating_dict["triple"],
rating_dict["double_three"],
rating_dict["double_two"],
rating_dict["double_pull"],
rating_dict["single_two"],
rating_dict["single_one"],
rating_dict["single_center"],
rating_dict["bp_single"],
rating_dict["walk"],
rating_dict["hbp"],
rating_dict["strikeout"],
rating_dict["lineout"],
rating_dict["popout"],
rating_dict["flyout_a"],
rating_dict["flyout_bq"],
rating_dict["flyout_lf_b"],
rating_dict["flyout_rf_b"],
rating_dict["groundout_a"],
rating_dict["groundout_b"],
rating_dict["groundout_c"],
]
)
diff = 108.0 - current_total
rating_dict["groundout_b"] = round_to_05(rating_dict["groundout_b"] + diff)
return rating_dict
ratings[0] = rebalance_to_108(ratings[0])
ratings[1] = rebalance_to_108(ratings[1])
# Adjust groundout ratios to 4:2:1 (A:B:C)
print("\n✓ Adjusting groundout ratios to 4:2:1 (A:B:C)...")
for rating in ratings:
total_groundouts = (
rating["groundout_a"] + rating["groundout_b"] + rating["groundout_c"]
)
rating["groundout_a"] = round_to_05(total_groundouts * (4 / 7))
rating["groundout_b"] = round_to_05(total_groundouts * (2 / 7))
rating["groundout_c"] = round_to_05(total_groundouts * (1 / 7))
# Set Flyout A to 1.0 and move removed chances to strikeouts
print("✓ Setting Flyout A to 1.0 both sides...")
for rating in ratings:
old_flyout_a = rating["flyout_a"]
rating["flyout_a"] = 1.0
rating["strikeout"] += old_flyout_a - 1.0
# Rebalance again after adjustments
ratings[0] = rebalance_to_108(ratings[0])
ratings[1] = rebalance_to_108(ratings[1])
# Recalculate rate stats after adjustments
for rating in ratings:
total_hits = (
rating["homerun"]
+ rating["bp_homerun"] * 0.5
+ rating["triple"]
+ rating["double_three"]
+ rating["double_two"]
+ rating["double_pull"]
+ rating["single_two"]
+ rating["single_one"]
+ rating["single_center"]
+ rating["bp_single"] * 0.5
)
rating["avg"] = round(total_hits / 108, 5)
rating["obp"] = round((total_hits + rating["hbp"] + rating["walk"]) / 108, 5)
rating["slg"] = round(
(
rating["homerun"] * 4
+ rating["bp_homerun"] * 2
+ rating["triple"] * 3
+ (rating["double_two"] + rating["double_three"] + rating["double_pull"])
* 2
+ rating["single_center"]
+ rating["single_two"]
+ rating["single_one"]
+ rating["bp_single"]
)
/ 108,
5,
)
# Display results
print()
print("-" * 70)
print("CALCULATED BATTING RATINGS")
print("-" * 70)
for rating in ratings:
vs_hand = rating["vs_hand"]
print(f"\nVS {vs_hand}HP:")
print(
f" AVG: {rating['avg']:.3f} OBP: {rating['obp']:.3f} SLG: {rating['slg']:.3f} OPS: {rating['obp']+rating['slg']:.3f}"
)
# Hit breakdown
total_hits = (
rating["homerun"]
+ rating["bp_homerun"]
+ rating["triple"]
+ rating["double_three"]
+ rating["double_two"]
+ rating["double_pull"]
+ rating["single_two"]
+ rating["single_one"]
+ rating["single_center"]
+ rating["bp_single"]
)
print("\n Power:")
print(f" HR: {rating['homerun']:.1f}")
print(f" BP-HR: {rating['bp_homerun']:.1f}")
if vs_hand == "R":
print(f" BP-HR:HR Ratio: {rating['bp_homerun']:.0f}:{rating['homerun']:.0f}")
print("\n On-Base:")
print(f" Walks: {rating['walk']:.1f}")
print(f" HBP: {rating['hbp']:.1f}")
# Verify total = 108
total_chances = sum(
[
rating["homerun"],
rating["bp_homerun"],
rating["triple"],
rating["double_three"],
rating["double_two"],
rating["double_pull"],
rating["single_two"],
rating["single_one"],
rating["single_center"],
rating["bp_single"],
rating["hbp"],
rating["walk"],
rating["strikeout"],
rating["lineout"],
rating["popout"],
rating["flyout_a"],
rating["flyout_bq"],
rating["flyout_lf_b"],
rating["flyout_rf_b"],
rating["groundout_a"],
rating["groundout_b"],
rating["groundout_c"],
]
)
print(f"\n Total Chances: {total_chances:.2f} (must be 108.0)")
# Calculate and display total OPS
total_ops = calculate_total_ops(ratings[0], ratings[1], is_pitcher=False)
print()
print("=" * 70)
print(f"TOTAL OPS: {total_ops:.3f} (Target: 0.850)")
if abs(total_ops - 0.850) <= 0.005:
print("✓ Within target range!")
elif abs(total_ops - 0.850) <= 0.015:
print("~ Close to target (can tweak if needed)")
else:
print("⚠ Outside target range - needs adjustment")
print("=" * 70)
# Show baserunning
print()
print("Baserunning Ratings:")
print(f" Steal Range: {baserunning['steal_low']}-{baserunning['steal_high']}")
print(f" Steal Jump: {baserunning['steal_jump']:.6f}")
print(f" Running: {baserunning['running']}")
print(f" Hit-and-Run: {baserunning['hit_and_run']}")
print(f" Bunting: {baserunning['bunting']}")
print()
print("=" * 70)
print("DETAILED D20 RATINGS (Side-by-Side Comparison)")
print("=" * 70)
print()
# Create table header
print(f"{'Rating':<25} {'vs LHP':>10} {'vs RHP':>10}")
print("-" * 70)
# Extract ratings for easier access
vl = ratings[0] # vs LHP
vr = ratings[1] # vs RHP
# Display all ratings in table format
rating_pairs = [
("Homerun", "homerun"),
("BP Homerun", "bp_homerun"),
("Triple", "triple"),
("Double (3-zone)", "double_three"),
("Double (2-zone)", "double_two"),
("Double (Pull)", "double_pull"),
("Single (2-zone)", "single_two"),
("Single (1-zone)", "single_one"),
("Single (Center)", "single_center"),
("BP Single", "bp_single"),
("Walk", "walk"),
("HBP", "hbp"),
("Strikeout", "strikeout"),
("Lineout", "lineout"),
("Popout", "popout"),
("Flyout A", "flyout_a"),
("Flyout BQ", "flyout_bq"),
("Flyout LF-B", "flyout_lf_b"),
("Flyout RF-B", "flyout_rf_b"),
("Groundout A", "groundout_a"),
("Groundout B", "groundout_b"),
("Groundout C", "groundout_c"),
]
for label, key in rating_pairs:
print(f"{label:<25} {vl[key]:>10.2f} {vr[key]:>10.2f}")
# Show totals
vl_total = sum([vl[key] for _, key in rating_pairs])
vr_total = sum([vr[key] for _, key in rating_pairs])
print("-" * 70)
print(f"{'TOTAL CHANCES':<25} {vl_total:>10.2f} {vr_total:>10.2f}")
print(f"{'(must be 108.0)':<25} {'':>10} {'':>10}")
# Show spray chart percentages
print()
print("=" * 70)
print("SPRAY CHART VERIFICATION")
print("=" * 70)
print(
f"vs LHP - Pull: {vl['pull_rate']*100:.1f}% Center: {vl['center_rate']*100:.1f}% Opposite: {vl['slap_rate']*100:.1f}%"
)
print(
f"vs RHP - Pull: {vr['pull_rate']*100:.1f}% Center: {vr['center_rate']*100:.1f}% Opposite: {vr['slap_rate']*100:.1f}%"
)
print(
f"\nTarget Pull Rates: 28% vL ({'✓ MATCH' if abs(vl['pull_rate'] - 0.28) < 0.01 else '⚠ ADJUST'}), 62% vR ({'✓ MATCH' if abs(vr['pull_rate'] - 0.62) < 0.01 else '⚠ ADJUST'})"
)
# Show BP-HR:HR ratio
print()
print("=" * 70)
print("BP-HR:HR RATIO VERIFICATION")
print("=" * 70)
print(f"vs RHP - BP-HR: {vr['bp_homerun']:.0f}, HR: {vr['homerun']:.0f}")
print(
f"Ratio: {vr['bp_homerun']:.0f}:{vr['homerun']:.0f} (Target: 3:2) {'✓ MATCH' if vr['bp_homerun'] == 3.0 and vr['homerun'] == 2.0 else '⚠ ADJUST'}"
)
print()
print("=" * 70)
print("SUMMARY")
print("=" * 70)
print()
print("✓ Ratings calculated successfully")
print(f"✓ Total OPS: {total_ops:.3f} (Target: 0.850)")
print(
f"✓ Pull Rates: {vl['pull_rate']*100:.0f}% vL, {vr['pull_rate']*100:.0f}% vR (Target: 28% vL, 62% vR)"
)
print(f"✓ BP-HR:HR Ratio vR: {vr['bp_homerun']:.0f}:{vr['homerun']:.0f} (Target: 3:2)")
print(f"✓ Triples: {vl['triple']:.0f} vL, {vr['triple']:.0f} vR (Target: 0 both)")
print(f"✓ Running: {baserunning['running']} (Target: 9)")
print(
f"✓ Stealing: {baserunning['steal_low']}-{baserunning['steal_high']} (Target: 18-13)"
)
print()
print("Next Steps:")
print("1. Review the ratings above")
print(
"2. If approved, run create_sphealthamus_spheal.py to update the database records"
)
print()
print("⚠️ NOT POSTED TO DATABASE - PREVIEW ONLY")
print("=" * 70)