""" 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)