Standardize formatting with black and apply ruff auto-fixes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
469 lines
15 KiB
Python
469 lines
15 KiB
Python
"""
|
|
Preview ratings for Kalin Young custom player.
|
|
Target: 0.855 total OPS (UPGRADED from 0.820)
|
|
Pull rate: vL=33%, vR=25%
|
|
Steal rate: 0.22222
|
|
"""
|
|
|
|
from custom_cards.archetype_definitions import BatterArchetype
|
|
from custom_cards.archetype_calculator import (
|
|
BatterRatingCalculator,
|
|
calculate_total_ops,
|
|
)
|
|
import random
|
|
|
|
# Create custom archetype for Kalin Young
|
|
# Target: Combined OPS = 0.855 with formula: (OPS_vR + OPS_vL + min) / 3
|
|
# UPGRADE: Increased OBP via HBP (+1.0) and walks
|
|
|
|
kalin_young = BatterArchetype(
|
|
name="Kalin Young",
|
|
description="All-fields outfielder with low pull rate, balanced contact and power",
|
|
# VS RHP: Target OPS ~0.815 (OBP ~0.350 + SLG ~0.465) - aiming for 0.855 total
|
|
avg_vs_r=0.240, # Good average
|
|
obp_vs_r=0.350, # UPGRADED OBP (+0.035)
|
|
slg_vs_r=0.435, # Decent power (unchanged)
|
|
bb_pct_vs_r=0.115, # INCREASED walks (0.09 → 0.115)
|
|
k_pct_vs_r=0.21, # Decent contact
|
|
# VS LHP: Target OPS ~0.830 (OBP ~0.365 + SLG ~0.465) - aiming for 0.855 total
|
|
avg_vs_l=0.250, # Better average vs L
|
|
obp_vs_l=0.360, # UPGRADED OBP (+0.035)
|
|
slg_vs_l=0.445, # More power vs L (unchanged)
|
|
bb_pct_vs_l=0.115, # INCREASED walks (0.09 → 0.115)
|
|
k_pct_vs_l=0.20, # Slightly better contact vs L
|
|
# Power distribution - moderate power, balanced
|
|
hr_per_hit=0.09, # Moderate HR rate
|
|
triple_per_hit=0.03, # Some triples (outfield speed)
|
|
double_per_hit=0.26, # Good gap power
|
|
# Singles = 62% of hits
|
|
# Batted ball profile - balanced
|
|
gb_pct=0.43, # Moderate ground balls
|
|
fb_pct=0.34, # Moderate fly balls
|
|
ld_pct=0.23, # Line drives
|
|
# Batted ball quality
|
|
hard_pct=0.36, # Good hard contact
|
|
med_pct=0.46, # Lots of medium contact
|
|
soft_pct=0.18, # Some soft contact
|
|
# Spray chart - LOW PULL RATE (25%)
|
|
pull_pct=0.25, # Low pull (per requirements)
|
|
center_pct=0.40, # High center usage
|
|
oppo_pct=0.35, # Good opposite field usage
|
|
# Infield hits
|
|
ifh_pct=0.07, # Moderate speed for infield hits
|
|
# Specific power metrics
|
|
hr_fb_pct=0.11, # Moderate HR/FB
|
|
# Baserunning - good speed
|
|
speed_rating=6, # Above average speed
|
|
steal_jump=6, # Good reads
|
|
xbt_pct=0.53, # Takes extra bases
|
|
# Situational hitting
|
|
hit_run_skill=7, # Good contact = good hit-and-run
|
|
# Defensive profile
|
|
primary_positions=["RF", "LF"],
|
|
defensive_rating=6, # Above average defender
|
|
)
|
|
|
|
print("=" * 70)
|
|
print("KALIN YOUNG - CUSTOM PLAYER PREVIEW")
|
|
print("=" * 70)
|
|
print()
|
|
print("Player Info:")
|
|
print(" Name: Kalin Young")
|
|
print(" Hand: R (Right-handed batter)")
|
|
print(" Primary Position: RF")
|
|
print(" Secondary Position: LF")
|
|
print()
|
|
|
|
# Show defensive ratings
|
|
print("Defensive Ratings (per requirements):")
|
|
print(" RF: Range 3 / Error 7 / Arm 0")
|
|
print(" LF: Range 4 / Error 7 / Arm 0")
|
|
print()
|
|
|
|
print("Target Statistics (UPGRADED):")
|
|
print(" Target Total OPS: 0.855 (was 0.820)")
|
|
print(" Target Pull Rate: vL=33%, vR=25%")
|
|
print(" Running: 13")
|
|
print(" Stealing: 15-7 (0.22222)")
|
|
print(" HBP: 2.0 both splits (+1.0)")
|
|
print()
|
|
|
|
# Calculate ratings
|
|
calc = BatterRatingCalculator(kalin_young)
|
|
ratings = calc.calculate_ratings(battingcard_id=0) # Temp ID
|
|
baserunning = calc.calculate_baserunning()
|
|
|
|
# Override steal rate to 0.22222 - UPGRADED steal success
|
|
baserunning["steal_jump"] = 0.22222
|
|
baserunning["steal_high"] = 15
|
|
baserunning["steal_low"] = 7
|
|
|
|
# Override running to 13
|
|
baserunning["running"] = 13
|
|
|
|
# Set bunting to C (average)
|
|
baserunning["bunting"] = "C"
|
|
|
|
# Apply randomization to make results look more natural
|
|
random.seed(42) # For reproducibility
|
|
|
|
|
|
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 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 (CRITICAL RULE)
|
|
# UPGRADE: HBP increased from 1 to 2 both splits
|
|
print("\n✓ Setting BP-HR to whole numbers (2 vL, 1 vR)...")
|
|
print("✓ Setting HBP to whole numbers (2 both splits) - UPGRADED +1.0...")
|
|
print("✓ Removing Triple vL...")
|
|
|
|
# vs LHP (ratings[0])
|
|
old_bphr_vl = ratings[0]["bp_homerun"]
|
|
ratings[0]["bp_homerun"] = 2.0
|
|
# Redistribute difference to single_center
|
|
ratings[0]["single_center"] += old_bphr_vl - 2.0
|
|
|
|
old_hbp_vl = ratings[0]["hbp"]
|
|
ratings[0]["hbp"] = 2.0 # UPGRADED from 1.0
|
|
# Redistribute difference to outs (since we're adding OBP)
|
|
ratings[0]["groundout_b"] += old_hbp_vl - 2.0
|
|
|
|
# Remove triple vL
|
|
old_triple_vl = ratings[0]["triple"]
|
|
ratings[0]["triple"] = 0.0
|
|
# Redistribute to singles
|
|
ratings[0]["single_center"] += old_triple_vl
|
|
|
|
# vs RHP (ratings[1])
|
|
old_bphr_vr = ratings[1]["bp_homerun"]
|
|
ratings[1]["bp_homerun"] = 1.0
|
|
# Redistribute difference to single_center
|
|
ratings[1]["single_center"] += old_bphr_vr - 1.0
|
|
|
|
old_hbp_vr = ratings[1]["hbp"]
|
|
ratings[1]["hbp"] = 2.0 # UPGRADED from 1.0
|
|
# Redistribute difference to outs (since we're adding OBP)
|
|
ratings[1]["groundout_b"] += old_hbp_vr - 2.0
|
|
|
|
# Adjust pull rate: vL to 33%, vR stays at 25%
|
|
print("✓ Adjusting pull rate vL to 33% (vR stays 25%)...")
|
|
ratings[0]["pull_rate"] = 0.33
|
|
ratings[0]["center_rate"] = 0.37 # Reduce center to compensate
|
|
ratings[0]["slap_rate"] = 0.30 # Reduce oppo slightly
|
|
# vR stays at archetype defaults (~25%)
|
|
|
|
# Manual adjustments for vL
|
|
print("✓ Manual vL adjustments: HBP -1.0 → BB, K -5.0 → GB-B +3.0, GB-C +2.0...")
|
|
ratings[0]["hbp"] -= 1.0 # 2.0 → 1.0
|
|
ratings[0]["walk"] += 1.0 # Add to BB
|
|
ratings[0]["strikeout"] -= 5.0 # Remove 5.0 K
|
|
ratings[0]["groundout_b"] += 3.0 # Add 3.0 to GB-B
|
|
ratings[0]["groundout_c"] += 2.0 # Add 2.0 to GB-C
|
|
|
|
|
|
# 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])
|
|
|
|
# Recalculate rate stats after randomization
|
|
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("-" * 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}%)"
|
|
)
|
|
|
|
# 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.855)")
|
|
if abs(total_ops - 0.855) <= 0.005:
|
|
print("✓ Within target range!")
|
|
elif abs(total_ops - 0.855) <= 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']:.5f} ({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(f" Bunting: {baserunning['bunting']} (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}")
|
|
|
|
# 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 Rate: 25% ({'✓ MATCH' if abs(vl['pull_rate'] - 0.25) < 0.03 else '⚠ ADJUST'})"
|
|
)
|
|
|
|
print()
|
|
print("=" * 70)
|
|
print("SUMMARY (UPGRADED)")
|
|
print("=" * 70)
|
|
print()
|
|
print("✓ Ratings calculated successfully")
|
|
print(f"✓ Total OPS: {total_ops:.3f} (Target: 0.855)")
|
|
print(
|
|
f"✓ Pull Rate: vL={vl['pull_rate']*100:.0f}% (target 33%) / vR={vr['pull_rate']*100:.0f}% (target 25%)"
|
|
)
|
|
print(f"✓ Running: {baserunning['running']} (Target: 13)")
|
|
print(
|
|
f"✓ Stealing: {baserunning['steal_low']}-{baserunning['steal_high']} (Target: 15-7, rate: 0.22222)"
|
|
)
|
|
print("✓ HBP: 2.0 both splits (upgraded +1.0)")
|
|
print()
|
|
print("Next Steps:")
|
|
print("1. Review the ratings above")
|
|
print("2. If approved, run create_kalin_young.py to UPDATE the database records")
|
|
print("3. Card will be UPDATED with:")
|
|
print(" - Cardset: 29 (Custom Characters)")
|
|
print(
|
|
" - Positions: RF (Range 3, Error 7, Arm 0), LF (Range 4, Error 7, Arm 0) - UNCHANGED"
|
|
)
|
|
print(" - Rarity: Starter - UNCHANGED")
|
|
print(" - Cost: ~85 - UNCHANGED")
|
|
print()
|
|
print("⚠️ NOT POSTED TO DATABASE - PREVIEW ONLY")
|
|
print("=" * 70)
|