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

434 lines
15 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("\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("\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)