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