paper-dynasty-card-creation/custom_cards/will_the_thrill_preview.py
2025-11-14 09:51:04 -06:00

366 lines
14 KiB
Python

"""
Preview ratings for "Will the Thrill" custom player.
Target: 0.825 total OPS, lots of singles, few XBH, low HR, slightly more power vs L
"""
from custom_cards.archetype_definitions import BatterArchetype
from custom_cards.archetype_calculator import BatterRatingCalculator, calculate_total_ops
# Create custom archetype for Will the Thrill
# Target total OPS = 0.825 with formula: (OPS_vR + OPS_vL + min) / 3
# Working backwards: 0.825 * 3 = 2.475
# If OPS_vL = 0.865 and OPS_vR = 0.805, then min = 0.805
# 0.865 + 0.805 + 0.805 = 2.475 ✓
will_the_thrill = BatterArchetype(
name="Will the Thrill",
description="High contact singles hitter with gap power, slightly more pop vs LHP",
# VS RHP: Target OPS = 0.805
# High contact, lots of singles, some doubles, few HR
avg_vs_r=0.285, # Good average
obp_vs_r=0.345, # Decent OBP (AVG + some walks)
slg_vs_r=0.460, # Modest slugging (singles-heavy)
bb_pct_vs_r=0.07, # Low-moderate walks
k_pct_vs_r=0.15, # Good contact (low K)
# VS LHP: Target OPS = 0.865 (slightly more power)
# Better power vs lefties, more doubles/HR
avg_vs_l=0.300, # Better average vs L
obp_vs_l=0.360, # Better OBP
slg_vs_l=0.505, # More power vs L
bb_pct_vs_l=0.07, # Similar walks
k_pct_vs_l=0.14, # Slightly less K vs L
# Power distribution - LOW HR, few XBH, LOTS of singles
hr_per_hit=0.04, # Very low HR rate (4% of hits)
triple_per_hit=0.02, # Few triples
double_per_hit=0.22, # Moderate doubles (gap power)
# Singles = 72% of hits
# Batted ball profile - contact-oriented
gb_pct=0.45, # Moderate ground balls
fb_pct=0.30, # Lower fly balls (less power)
ld_pct=0.25, # Good line drive rate
# Batted ball quality - contact over power
hard_pct=0.33, # Moderate hard contact
med_pct=0.50, # Lots of medium contact
soft_pct=0.17, # Some soft contact
# Spray chart - all fields
pull_pct=0.38, # Some pull
center_pct=0.36, # Good center %
oppo_pct=0.26, # Uses whole field
# Infield hits
ifh_pct=0.08, # Decent speed for infield hits
# Specific power metrics
hr_fb_pct=0.08, # Low HR/FB (not a power hitter)
# Baserunning - decent speed
speed_rating=6, # Above average speed
steal_jump=6, # Good reads
xbt_pct=0.52, # Takes extra bases
# Situational hitting
hit_run_skill=8, # Good contact = good hit-and-run
# Defensive profile
primary_positions=["LF", "2B"],
defensive_rating=6, # Above average defender
)
# Calculate ratings
calc = BatterRatingCalculator(will_the_thrill)
ratings = calc.calculate_ratings(battingcard_id=0) # Temp ID
baserunning = calc.calculate_baserunning()
# Manual correction: Hit-and-Run should be 'A' based on actual BABIP of .428
# (High contact, low K, low HR = very high BABIP)
baserunning['hit_and_run'] = 'A'
# Manual correction: Remove triples from vs RHP (only keep vs LHP)
ratings[1]['triple'] = 0.0 # ratings[1] is vs RHP
# Redistribute the triple chances to singles
ratings[1]['single_center'] += 0.75
# Adjust flyball distribution
# FlyA is rare - defaults to 0.0, only 1.0 for power hitters
# Will is NOT a power hitter, so flyA = 0.0
# VS RHP (ratings[1]): More flyballs to RF, flyA = 0
ratings[1]['flyout_a'] = 0.0 # Only for power hitters
ratings[1]['flyout_bq'] = 5.75 # Increase to absorb flyA
ratings[1]['flyout_lf_b'] = 4.00 # Lower - NOT the emphasis
ratings[1]['flyout_rf_b'] = 4.55 # HIGHER - more to RF as requested
# VS LHP (ratings[0]): More flyballs to LF, flyA = 0
ratings[0]['flyout_a'] = 0.0 # Only for power hitters
ratings[0]['flyout_bq'] = 5.55 # Increase to absorb flyA
ratings[0]['flyout_lf_b'] = 4.55 # HIGHER - more to LF as requested
ratings[0]['flyout_rf_b'] = 4.00 # Lower - NOT the emphasis
# VS RHP: Slightly more strikeouts (take from lineouts)
ratings[1]['strikeout'] = 15.50 # Increase from 14.40
ratings[1]['lineout'] = 10.80 # Decrease from 11.90 to balance
# Adjust groundball ratio to 3:2:1.5 (gbA:gbB:gbC)
# VS RHP: Total groundouts = 21.40
ratings[1]['groundout_a'] = 9.85 # 3 parts
ratings[1]['groundout_b'] = 6.60 # 2 parts
ratings[1]['groundout_c'] = 4.95 # 1.5 parts
# VS LHP: Total groundouts = 21.15
ratings[0]['groundout_a'] = 9.75 # 3 parts
ratings[0]['groundout_b'] = 6.50 # 2 parts
ratings[0]['groundout_c'] = 4.90 # 1.5 parts
# Add light randomization to make the card less homogenous
# Small variations (+/- 0.25 to 0.75) to make it feel more natural
import random
random.seed(42) # Consistent randomization for Will the Thrill
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 # Don't randomize zeros
variation = random.uniform(-max_variation, max_variation)
result = max(0.0, base_value + variation)
return round_to_05(result)
# Randomize singles distribution (keeping BP-SI fixed)
ratings[0]['single_two'] = add_variation(ratings[0]['single_two'], 0.75)
ratings[0]['single_one'] = add_variation(ratings[0]['single_one'], 0.50)
ratings[0]['single_center'] = add_variation(ratings[0]['single_center'], 0.60)
ratings[1]['single_two'] = add_variation(ratings[1]['single_two'], 0.60)
ratings[1]['single_one'] = add_variation(ratings[1]['single_one'], 0.75)
ratings[1]['single_center'] = add_variation(ratings[1]['single_center'], 0.50)
# Randomize doubles (keeping total roughly similar)
ratings[0]['double_two'] = add_variation(ratings[0]['double_two'], 0.40)
ratings[0]['double_pull'] = add_variation(ratings[0]['double_pull'], 0.35)
ratings[1]['double_two'] = add_variation(ratings[1]['double_two'], 0.35)
ratings[1]['double_pull'] = add_variation(ratings[1]['double_pull'], 0.40)
# Randomize flyouts slightly
ratings[0]['flyout_bq'] = add_variation(ratings[0]['flyout_bq'], 0.45)
ratings[0]['flyout_lf_b'] = add_variation(ratings[0]['flyout_lf_b'], 0.30)
ratings[0]['flyout_rf_b'] = add_variation(ratings[0]['flyout_rf_b'], 0.30)
ratings[1]['flyout_bq'] = add_variation(ratings[1]['flyout_bq'], 0.40)
ratings[1]['flyout_lf_b'] = add_variation(ratings[1]['flyout_lf_b'], 0.35)
ratings[1]['flyout_rf_b'] = add_variation(ratings[1]['flyout_rf_b'], 0.35)
# Randomize groundouts slightly
ratings[0]['groundout_a'] = add_variation(ratings[0]['groundout_a'], 0.50)
ratings[0]['groundout_b'] = add_variation(ratings[0]['groundout_b'], 0.40)
ratings[0]['groundout_c'] = add_variation(ratings[0]['groundout_c'], 0.35)
ratings[1]['groundout_a'] = add_variation(ratings[1]['groundout_a'], 0.45)
ratings[1]['groundout_b'] = add_variation(ratings[1]['groundout_b'], 0.50)
ratings[1]['groundout_c'] = add_variation(ratings[1]['groundout_c'], 0.40)
# Small variation on lineouts
ratings[0]['lineout'] = add_variation(ratings[0]['lineout'], 0.40)
ratings[1]['lineout'] = add_variation(ratings[1]['lineout'], 0.45)
# Rebalance each split to exactly 108.00
def rebalance_to_108(rating_dict):
"""Adjust to ensure total = 108.00 by tweaking single_center (most common result)."""
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['single_center'] = round_to_05(rating_dict['single_center'] + diff)
return rating_dict
ratings[0] = rebalance_to_108(ratings[0])
ratings[1] = rebalance_to_108(ratings[1])
# Display results
print("="*70)
print("WILL THE THRILL - CUSTOM PLAYER PREVIEW")
print("="*70)
print()
print("Player Info:")
print(" Name: Will the Thrill")
print(" Hand: R (Right-handed batter)")
print(" Primary Position: LF")
print(" Secondary Position: 2B")
print()
# Show defensive ratings (will need to be manually created)
print("Defensive Ratings (to be set manually):")
print(" LF: Range 3 / Error 7 / Arm +2")
print(" 2B: Range 4 / Error 12 / Arm (default)")
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'])
total_doubles = rating['double_pull'] + rating['double_two'] + rating['double_three']
total_singles = (rating['single_two'] + rating['single_one'] +
rating['single_center'] + rating['bp_single'])
print(f"\n Hit Distribution (out of {total_hits:.1f} total hits):")
print(f" Singles: {total_singles:.1f} ({100*total_singles/total_hits:.1f}%)")
print(f" Doubles: {total_doubles:.1f} ({100*total_doubles/total_hits:.1f}%)")
print(f" Triples: {rating['triple']:.1f} ({100*rating['triple']/total_hits:.1f}%)")
print(f" HR: {rating['homerun']+rating['bp_homerun']:.1f} ({100*(rating['homerun']+rating['bp_homerun'])/total_hits:.1f}%)")
# On-base
print(f"\n On-Base:")
print(f" Walks: {rating['walk']:.1f}")
print(f" HBP: {rating['hbp']:.1f}")
print(f" Strikeouts: {rating['strikeout']:.1f}")
# Outs distribution
total_outs = (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'])
fly_outs = (rating['flyout_a'] + rating['flyout_bq'] +
rating['flyout_lf_b'] + rating['flyout_rf_b'])
ground_outs = rating['groundout_a'] + rating['groundout_b'] + rating['groundout_c']
print(f"\n Outs Distribution:")
print(f" Strikeouts: {rating['strikeout']:.1f}")
print(f" Line outs: {rating['lineout']:.1f}")
print(f" Fly outs: {fly_outs:.1f}")
print(f" Ground outs: {ground_outs:.1f}")
print(f" Pop outs: {rating['popout']:.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.825)")
if abs(total_ops - 0.825) <= 0.005:
print("✓ Within target range!")
elif abs(total_ops - 0.825) <= 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 (Stratomatic Format):")
print(f" Steal Range: {baserunning['steal_low']}-{baserunning['steal_high']} (on 2d10)")
print(f" Steal Auto: {baserunning['steal_auto']} (0=No, 1=Yes)")
print(f" Steal Jump: {baserunning['steal_jump']} (out of 1.0, = {int(baserunning['steal_jump']*36)}/36 chances)")
print(f" Running: {baserunning['running']} (scale 8-17)")
print(f" Hit-and-Run: {baserunning['hit_and_run']} (letter grade A/B/C/D)")
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}")
print()
print("="*70)
print("NEXT STEPS")
print("="*70)
print()
print("1. Review the ratings above")
print("2. If you want adjustments, I can:")
print(" - Tweak the power distribution (more/less HR)")
print(" - Adjust contact rate (K rate)")
print(" - Fine-tune OPS to hit exact target")
print(" - Modify hit distribution (singles/doubles ratio)")
print()
print("3. When you approve, I will create:")
print(" - MLBPlayer record")
print(" - Player record")
print(" - BattingCard record")
print(" - BattingCardRatings (vs L and vs R)")
print(" - CardPosition records (LF, 2B)")
print()
print("4. Defensive ratings (Range/Error/Arm) will need to be")
print(" set via defenders module or manual database update")
print()
print("⚠️ NOT POSTED TO DATABASE - AWAITING YOUR APPROVAL")
print("="*70)