paper-dynasty-card-creation/create_kalin_young.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

539 lines
20 KiB
Python

"""
Create custom card for Kalin Young
All-fields outfielder with low pull rate and 0.855 OPS target
UPGRADED: OPS 0.820 → 0.855 via OBP (HBP +1.0, walks increased)
Pull rate vL 25% → 33%, vR stays 25%
Steal rate 0.0833 → 0.22222
"""
import asyncio
from custom_cards.archetype_definitions import BatterArchetype
from custom_cards.archetype_calculator import (
BatterRatingCalculator,
calculate_total_ops,
)
from creation_helpers import mlbteam_and_franchise
from db_calls import db_get, db_put
from datetime import datetime
import random
# AWS Configuration
AWS_BUCKET_NAME = "paper-dynasty"
AWS_REGION = "us-east-1"
S3_BASE_URL = f"https://{AWS_BUCKET_NAME}.s3.{AWS_REGION}.amazonaws.com"
async def create_kalin_young():
"""Create Kalin Young custom card."""
print("=" * 70)
print("CREATING KALIN YOUNG")
print("=" * 70)
# Player details
name_first = "Kalin"
name_last = "Young"
hand = "R" # Right-handed batter (assuming, can adjust if needed)
team_abbrev = "SEA" # Placeholder team
positions = ["RF", "LF"] # Outfielder
# Get team info
mlb_team_id, franchise_id = mlbteam_and_franchise(team_abbrev)
# Cardset setup - Always use cardset 29 for custom characters
cardset_id = 29
cardset = {"id": cardset_id, "name": "Custom Characters"}
season = 2005
player_description = "05 Custom"
print(
f"✓ Using cardset ID: {cardset['id']} - Player description: '{player_description}'"
)
# 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
# Calibrated to hit exact target after archetype calculator processing
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(f"\n✓ Created custom archetype: {kalin_young.name}")
print(
f" Base stats vR: {kalin_young.avg_vs_r:.3f}/{kalin_young.obp_vs_r:.3f}/{kalin_young.slg_vs_r:.3f} (OPS: {kalin_young.obp_vs_r + kalin_young.slg_vs_r:.3f})"
)
print(
f" Base stats vL: {kalin_young.avg_vs_l:.3f}/{kalin_young.obp_vs_l:.3f}/{kalin_young.slg_vs_l:.3f} (OPS: {kalin_young.obp_vs_l + kalin_young.slg_vs_l:.3f})"
)
# 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
print("\n✓ Setting steal rate to 0.22222 (15-7)...")
baserunning["steal_jump"] = 0.22222
baserunning["steal_high"] = 15
baserunning["steal_low"] = 7
# Override running to 13
print("✓ Setting running to 13...")
baserunning["running"] = 13
# Apply randomization to make results look more natural
print("\n✓ Applying randomization (±0.5) and rounding to 0.05...")
random.seed(42) # For reproducibility
for rating in ratings:
# Fields to randomize (exclude exact targets)
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: # Only randomize non-zero values
randomization = random.uniform(-0.5, 0.5)
new_value = rating[field] + randomization
# Round to nearest 0.05
rating[field] = round(new_value * 20) / 20
# Ensure non-negative
rating[field] = max(0.05, rating[field])
# 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
# Fix total chances to exactly 108.0
for rating in ratings:
total = 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["walk"],
rating["hbp"],
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"],
]
)
diff = 108.0 - total
if abs(diff) > 0.01:
# Add/subtract the difference to groundout_b (most common out type)
rating["groundout_b"] += diff
rating["groundout_b"] = round(rating["groundout_b"] * 20) / 20
# Recalculate rate stats (BP results multiply by 0.5 for AVG/OBP only)
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)
# SLG: BP-HR gets 2 bases, BP-1B gets 1 base
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 adjusted ratings
print("\n" + "=" * 70)
print("FINAL RATINGS (TWO-COLUMN TABLE)")
print("=" * 70)
# Two-column table display
vl = ratings[0] # vs LHP
vr = ratings[1] # vs RHP
print(f"\n{'RATING':<25} {'VS LHP':>12} {'VS RHP':>12}")
print("-" * 50)
print(f"{'AVG':<25} {vl['avg']:>12.3f} {vr['avg']:>12.3f}")
print(f"{'OBP':<25} {vl['obp']:>12.3f} {vr['obp']:>12.3f}")
print(f"{'SLG':<25} {vl['slg']:>12.3f} {vr['slg']:>12.3f}")
print(f"{'OPS':<25} {vl['obp']+vl['slg']:>12.3f} {vr['obp']+vr['slg']:>12.3f}")
print()
print(f"{'HITS':<25}")
print(f"{' Homerun':<25} {vl['homerun']:>12.1f} {vr['homerun']:>12.1f}")
print(f"{' BP Homerun':<25} {vl['bp_homerun']:>12.1f} {vr['bp_homerun']:>12.1f}")
print(f"{' Triple':<25} {vl['triple']:>12.1f} {vr['triple']:>12.1f}")
print(
f"{' Double (3B)':<25} {vl['double_three']:>12.1f} {vr['double_three']:>12.1f}"
)
print(f"{' Double (2B)':<25} {vl['double_two']:>12.1f} {vr['double_two']:>12.1f}")
print(
f"{' Double (Pull)':<25} {vl['double_pull']:>12.1f} {vr['double_pull']:>12.1f}"
)
print(f"{' Single (2B)':<25} {vl['single_two']:>12.1f} {vr['single_two']:>12.1f}")
print(f"{' Single (1B)':<25} {vl['single_one']:>12.1f} {vr['single_one']:>12.1f}")
print(
f"{' Single (Center)':<25} {vl['single_center']:>12.1f} {vr['single_center']:>12.1f}"
)
print(f"{' BP Single':<25} {vl['bp_single']:>12.1f} {vr['bp_single']:>12.1f}")
print()
print(f"{'ON-BASE':<25}")
print(f"{' Walk':<25} {vl['walk']:>12.1f} {vr['walk']:>12.1f}")
print(f"{' HBP':<25} {vl['hbp']:>12.1f} {vr['hbp']:>12.1f}")
print()
print(f"{'OUTS':<25}")
print(f"{' Strikeout':<25} {vl['strikeout']:>12.1f} {vr['strikeout']:>12.1f}")
print(f"{' Lineout':<25} {vl['lineout']:>12.1f} {vr['lineout']:>12.1f}")
print(f"{' Popout':<25} {vl['popout']:>12.1f} {vr['popout']:>12.1f}")
print(f"{' Flyout A':<25} {vl['flyout_a']:>12.1f} {vr['flyout_a']:>12.1f}")
print(f"{' Flyout BQ':<25} {vl['flyout_bq']:>12.1f} {vr['flyout_bq']:>12.1f}")
print(
f"{' Flyout LF B':<25} {vl['flyout_lf_b']:>12.1f} {vr['flyout_lf_b']:>12.1f}"
)
print(
f"{' Flyout RF B':<25} {vl['flyout_rf_b']:>12.1f} {vr['flyout_rf_b']:>12.1f}"
)
print(
f"{' Groundout A':<25} {vl['groundout_a']:>12.1f} {vr['groundout_a']:>12.1f}"
)
print(
f"{' Groundout B':<25} {vl['groundout_b']:>12.1f} {vr['groundout_b']:>12.1f}"
)
print(
f"{' Groundout C':<25} {vl['groundout_c']:>12.1f} {vr['groundout_c']:>12.1f}"
)
print()
print(f"{'SPRAY CHART':<25}")
print(f"{' Pull %':<25} {vl['pull_rate']:>11.1%} {vr['pull_rate']:>11.1%}")
print(f"{' Center %':<25} {vl['center_rate']:>11.1%} {vr['center_rate']:>11.1%}")
print(f"{' Opposite %':<25} {vl['slap_rate']:>11.1%} {vr['slap_rate']:>11.1%}")
print()
# Calculate totals
total_vl = sum(
[
vl["homerun"],
vl["bp_homerun"],
vl["triple"],
vl["double_three"],
vl["double_two"],
vl["double_pull"],
vl["single_two"],
vl["single_one"],
vl["single_center"],
vl["bp_single"],
vl["walk"],
vl["hbp"],
vl["strikeout"],
vl["lineout"],
vl["popout"],
vl["flyout_a"],
vl["flyout_bq"],
vl["flyout_lf_b"],
vl["flyout_rf_b"],
vl["groundout_a"],
vl["groundout_b"],
vl["groundout_c"],
]
)
total_vr = sum(
[
vr["homerun"],
vr["bp_homerun"],
vr["triple"],
vr["double_three"],
vr["double_two"],
vr["double_pull"],
vr["single_two"],
vr["single_one"],
vr["single_center"],
vr["bp_single"],
vr["walk"],
vr["hbp"],
vr["strikeout"],
vr["lineout"],
vr["popout"],
vr["flyout_a"],
vr["flyout_bq"],
vr["flyout_lf_b"],
vr["flyout_rf_b"],
vr["groundout_a"],
vr["groundout_b"],
vr["groundout_c"],
]
)
print(f"{'TOTAL CHANCES':<25} {total_vl:>12.1f} {total_vr:>12.1f}")
print("-" * 50)
# Calculate and display total OPS
total_ops = calculate_total_ops(ratings[0], ratings[1], is_pitcher=False)
print(f"\nTotal OPS: {total_ops:.3f} (Target: 0.855)")
print("\nBaserunning:")
print(
f" Steal: {baserunning['steal_low']}-{baserunning['steal_high']} (Auto: {baserunning['steal_auto']}, Jump: {baserunning['steal_jump']})"
)
print(
f" Running: {baserunning['running']} Hit-and-Run: {baserunning['hit_and_run']}"
)
# Summary
print("\n" + "=" * 70)
print("SUMMARY")
print("=" * 70)
print(f"\nPlayer: Kalin Young ({hand})")
print("Positions: RF (primary), LF (secondary)")
print("Cardset: 29 (Custom Characters)")
print("Description: 05 Custom")
print(f"Total OPS: {total_ops:.3f} / 0.855 target")
print(
f"Pull Rate: vL={vl['pull_rate']*100:.0f}% (target 33%) / vR={vr['pull_rate']*100:.0f}% (target 25%)"
)
print("\n" + "=" * 70)
print("DATABASE UPDATE (BattingCard + Ratings ONLY)")
print("=" * 70)
# Set bunting to C (average)
baserunning["bunting"] = "C"
print("\n✓ Bunting set to: C")
# Create database records
bbref_id = f"custom_{name_last.lower()}{name_first[0].lower()}01"
now = datetime.now()
release_date = f"{now.year}-{now.month}-{now.day}"
# Step 1: Look up existing Player record (NO POST/PATCH)
print("\n✓ Looking up existing Player record (READ ONLY)...")
p_query = await db_get(
"players", params=[("bbref_id", bbref_id), ("cardset_id", cardset["id"])]
)
if p_query and p_query.get("count", 0) > 0:
player_id = p_query["players"][0]["player_id"]
print(f" Found existing Player ID: {player_id}")
print(" (Skipping Player POST/PATCH per user request)")
else:
print(
" ERROR: Player not found! Cannot update BattingCard without player_id."
)
print(f" Searched for bbref_id={bbref_id}, cardset_id={cardset['id']}")
return
# Step 2: Update BattingCard
print("\n✓ Updating BattingCard...")
batting_card_payload = {
"cards": [
{
"player_id": player_id,
"key_bbref": bbref_id,
"key_fangraphs": 0,
"key_mlbam": 0,
"key_retro": "",
"name_first": name_first,
"name_last": name_last,
"steal_low": baserunning["steal_low"],
"steal_high": baserunning["steal_high"],
"steal_auto": baserunning["steal_auto"],
"steal_jump": baserunning["steal_jump"],
"hit_and_run": baserunning["hit_and_run"],
"running": baserunning["running"],
"hand": hand,
"bunting": baserunning["bunting"],
}
]
}
await db_put("battingcards", payload=batting_card_payload, timeout=10)
print(" BattingCard updated")
# Get the card ID
bc_query = await db_get("battingcards", params=[("player_id", player_id)])
battingcard_id = bc_query["cards"][0]["id"]
print(f" BattingCard ID: {battingcard_id}")
# Step 3: Update BattingCardRatings
print("\n✓ Updating BattingCardRatings...")
for rating in ratings:
rating["battingcard_id"] = battingcard_id
ratings_payload = {"ratings": ratings}
await db_put("battingcardratings", payload=ratings_payload, timeout=10)
print(" Ratings updated (vL and vR)")
# Skipped: CardPositions (per user request - only BattingCard + Ratings)
print("\n✓ Skipping CardPositions update (per user request)")
# Skipped: Rarity/Cost update (per user request - no Player PATCH)
print("✓ Skipping rarity/cost update (per user request)")
# Card preview URL
print("\n✓ Skipping card image generation and S3 upload (will do after review)...")
api_image_url = f"https://pd.manticorum.com/api/v2/players/{player_id}/battingcard?d={release_date}"
s3_url = f"https://{AWS_BUCKET_NAME}.s3.{AWS_REGION}.amazonaws.com/cards/cardset-{cardset['id']:03d}/player-{player_id}/battingcard.png?d={release_date}"
print(f" Card preview URL: {api_image_url}")
print(f" Future S3 URL: {s3_url}")
print("\n" + "=" * 70)
print("✅ SUCCESS!")
print("=" * 70)
print("\nKalin Young BattingCard + Ratings UPGRADED successfully!")
print(f" Player ID: {player_id} (unchanged)")
print(f" BattingCard ID: {battingcard_id}")
print(" Bunting: C")
print(f" Running: {baserunning['running']}")
print(
f" Stealing: {baserunning['steal_low']}-{baserunning['steal_high']} ({baserunning['steal_jump']:.5f})"
)
print(" Pull Rate: vL=33% / vR=25%")
print(" HBP: vL=1.0 / vR=2.0")
print(f" Total OPS: {total_ops:.3f} (target: 0.855)")
print("\n Updated: BattingCard, BattingCardRatings")
print(" Skipped: Player, CardPositions (per user request)")
print(f"\n Card Preview URL: {api_image_url}")
print(" (Image not yet uploaded to S3 - awaiting review)")
print("\n" + "=" * 70)
if __name__ == "__main__":
asyncio.run(create_kalin_young())