366 lines
14 KiB
Python
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)
|