From c2ec561c822eadf321598e78ba5911e480bc8b10 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 2 Dec 2025 16:26:07 -0600 Subject: [PATCH] Add Luan Arroto custom card scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - luan_arroto_final.py: Card definition with 0.850 OPS target - submit_luan_arroto.py: Initial card creation script - update_luan_arroto.py: Update existing card ratings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- custom_cards/luan_arroto_final.py | 151 ++++++++++++++++ custom_cards/submit_luan_arroto.py | 278 +++++++++++++++++++++++++++++ custom_cards/update_luan_arroto.py | 180 +++++++++++++++++++ 3 files changed, 609 insertions(+) create mode 100644 custom_cards/luan_arroto_final.py create mode 100644 custom_cards/submit_luan_arroto.py create mode 100644 custom_cards/update_luan_arroto.py diff --git a/custom_cards/luan_arroto_final.py b/custom_cards/luan_arroto_final.py new file mode 100644 index 0000000..11ee76d --- /dev/null +++ b/custom_cards/luan_arroto_final.py @@ -0,0 +1,151 @@ +""" +Luan Arroto FINAL - With adjusted groundball ratios + +Groundball distribution: +- vs R: 3:3:1 ratio (A:B:C) - More double play balls vs RHP +- vs L: 3:2:1 ratio (A:B:C) - Balanced +""" + +from custom_cards.archetype_definitions import BatterArchetype +from custom_cards.archetype_calculator import BatterRatingCalculator, calculate_total_ops + + +def main(): + archetype = BatterArchetype( + name="Luan Arroto Final", + description="Contact hitter with platoon advantage and spray approach", + avg_vs_r=0.310, obp_vs_r=0.450, slg_vs_r=0.385, + bb_pct_vs_r=0.14, k_pct_vs_r=0.10, + avg_vs_l=0.285, obp_vs_l=0.455, slg_vs_l=0.355, + bb_pct_vs_l=0.17, k_pct_vs_l=0.09, + hr_per_hit=0.025, triple_per_hit=0.00, double_per_hit=0.22, + gb_pct=0.48, fb_pct=0.27, ld_pct=0.25, + hard_pct=0.28, med_pct=0.52, soft_pct=0.20, + pull_pct=0.215, center_pct=0.35, oppo_pct=0.435, + ifh_pct=0.09, hr_fb_pct=0.06, + speed_rating=8, steal_jump=3, xbt_pct=0.58, hit_run_skill=9, + primary_positions=["1B", "RF"], defensive_rating=6 + ) + + calc = BatterRatingCalculator(archetype) + ratings = calc.calculate_ratings(battingcard_id=0) + baserunning = calc.calculate_baserunning() + + # Adjust pull rates + ratings[1]['pull_rate'] = 0.33 # vs R + ratings[0]['pull_rate'] = 0.10 # vs L + for rating in ratings: + rating['slap_rate'] = 1.0 - rating['pull_rate'] - rating['center_rate'] + + # Adjust baserunning + baserunning.update({ + 'steal_low': 3, 'steal_high': 20, + 'steal_auto': 0, 'steal_jump': 0.0277777, 'running': 10 + }) + + # CUSTOM GROUNDBALL RATIOS + vl = ratings[0] + vr = ratings[1] + + # Calculate current totals + total_gb_vl = vl['groundout_a'] + vl['groundout_b'] + vl['groundout_c'] + total_gb_vr = vr['groundout_a'] + vr['groundout_b'] + vr['groundout_c'] + + print(f"\nOriginal groundball totals:") + print(f" vs L: {total_gb_vl:.2f} (A:{vl['groundout_a']:.2f} B:{vl['groundout_b']:.2f} C:{vl['groundout_c']:.2f})") + print(f" vs R: {total_gb_vr:.2f} (A:{vr['groundout_a']:.2f} B:{vr['groundout_b']:.2f} C:{vr['groundout_c']:.2f})") + + # vs L: 3:2:1 ratio (6 parts) + vl['groundout_a'] = (3/6) * total_gb_vl # 9.85 + vl['groundout_b'] = (2/6) * total_gb_vl # 6.57 + vl['groundout_c'] = (1/6) * total_gb_vl # 3.28 + + # vs R: 3:3:1 ratio (7 parts) + vr['groundout_a'] = (3/7) * total_gb_vr # 8.36 + vr['groundout_b'] = (3/7) * total_gb_vr # 8.36 + vr['groundout_c'] = (1/7) * total_gb_vr # 2.79 + + print(f"\nAdjusted groundball ratios:") + print(f" vs L (3:2:1): {total_gb_vl:.2f} (A:{vl['groundout_a']:.2f} B:{vl['groundout_b']:.2f} C:{vl['groundout_c']:.2f})") + print(f" vs R (3:3:1): {total_gb_vr:.2f} (A:{vr['groundout_a']:.2f} B:{vr['groundout_b']:.2f} C:{vr['groundout_c']:.2f})") + + total_ops = calculate_total_ops(vl, vr, is_pitcher=False) + + # Display complete comparison + print("\n" + "="*85) + print(" "*25 + "LUAN ARROTO - FINAL VERSION") + print("="*85) + + # SLASH LINES + print(f"\n{'SLASH LINE':<20} {'vs LHP':>20} {'vs RHP':>20} {'Difference':>20}") + print("-"*85) + print(f"{'AVG':<20} {vl['avg']:>20.3f} {vr['avg']:>20.3f} {vr['avg']-vl['avg']:>20.3f}") + print(f"{'OBP':<20} {vl['obp']:>20.3f} {vr['obp']:>20.3f} {vr['obp']-vl['obp']:>20.3f}") + print(f"{'SLG':<20} {vl['slg']:>20.3f} {vr['slg']:>20.3f} {vr['slg']-vl['slg']:>20.3f}") + print(f"{'OPS':<20} {vl['obp']+vl['slg']:>20.3f} {vr['obp']+vr['slg']:>20.3f} {(vr['obp']+vr['slg'])-(vl['obp']+vl['slg']):>20.3f}") + print(f"{'ISO':<20} {vl['slg']-vl['avg']:>20.3f} {vr['slg']-vr['avg']:>20.3f} {(vr['slg']-vr['avg'])-(vl['slg']-vl['avg']):>20.3f}") + print(f"\n{'COMBINED OPS':<20} {total_ops:>20.3f}") + + # HITS + print(f"\n{'HITS (out of 108)':<20} {'vs LHP':>20} {'vs RHP':>20} {'Difference':>20}") + print("-"*85) + print(f"{'Homerun':<20} {vl['homerun']:>20.2f} {vr['homerun']:>20.2f} {vr['homerun']-vl['homerun']:>20.2f}") + print(f"{'BP-Homerun':<20} {vl['bp_homerun']:>20.2f} {vr['bp_homerun']:>20.2f} {vr['bp_homerun']-vl['bp_homerun']:>20.2f}") + print(f"{'Triple':<20} {vl['triple']:>20.2f} {vr['triple']:>20.2f} {vr['triple']-vl['triple']:>20.2f}") + print(f"{'Double (2-zone)':<20} {vl['double_two']:>20.2f} {vr['double_two']:>20.2f} {vr['double_two']-vl['double_two']:>20.2f}") + print(f"{'Double (Pull)':<20} {vl['double_pull']:>20.2f} {vr['double_pull']:>20.2f} {vr['double_pull']-vl['double_pull']:>20.2f}") + print(f"{'Single (2-zone)':<20} {vl['single_two']:>20.2f} {vr['single_two']:>20.2f} {vr['single_two']-vl['single_two']:>20.2f}") + print(f"{'Single (1-zone)':<20} {vl['single_one']:>20.2f} {vr['single_one']:>20.2f} {vr['single_one']-vl['single_one']:>20.2f}") + print(f"{'Single (Center)':<20} {vl['single_center']:>20.2f} {vr['single_center']:>20.2f} {vr['single_center']-vl['single_center']:>20.2f}") + print(f"{'BP-Single':<20} {vl['bp_single']:>20.2f} {vr['bp_single']:>20.2f} {vr['bp_single']-vl['bp_single']:>20.2f}") + + # ON-BASE + print(f"\n{'ON-BASE EVENTS':<20} {'vs LHP':>20} {'vs RHP':>20} {'Difference':>20}") + print("-"*85) + print(f"{'HBP':<20} {vl['hbp']:>20.2f} {vr['hbp']:>20.2f} {vr['hbp']-vl['hbp']:>20.2f}") + print(f"{'Walk':<20} {vl['walk']:>20.2f} {vr['walk']:>20.2f} {vr['walk']-vl['walk']:>20.2f}") + + # OUTS + print(f"\n{'OUTS':<20} {'vs LHP':>20} {'vs RHP':>20} {'Difference':>20}") + print("-"*85) + print(f"{'Strikeout':<20} {vl['strikeout']:>20.2f} {vr['strikeout']:>20.2f} {vr['strikeout']-vl['strikeout']:>20.2f}") + print(f"{'Lineout':<20} {vl['lineout']:>20.2f} {vr['lineout']:>20.2f} {vr['lineout']-vl['lineout']:>20.2f}") + print(f"{'Flyout (total)':<20} {vl['flyout_bq']+vl['flyout_lf_b']+vl['flyout_rf_b']:>20.2f} {vr['flyout_bq']+vr['flyout_lf_b']+vr['flyout_rf_b']:>20.2f} {(vr['flyout_bq']+vr['flyout_lf_b']+vr['flyout_rf_b'])-(vl['flyout_bq']+vl['flyout_lf_b']+vl['flyout_rf_b']):>20.2f}") + + print(f"\n{'GROUNDOUTS (CUSTOM)':<20} {'vs LHP':>20} {'vs RHP':>20} {'Difference':>20}") + print("-"*85) + print(f"{' A (DP balls)':<20} {vl['groundout_a']:>20.2f} {vr['groundout_a']:>20.2f} {vr['groundout_a']-vl['groundout_a']:>20.2f}") + print(f"{' B':<20} {vl['groundout_b']:>20.2f} {vr['groundout_b']:>20.2f} {vr['groundout_b']-vl['groundout_b']:>20.2f}") + print(f"{' C':<20} {vl['groundout_c']:>20.2f} {vr['groundout_c']:>20.2f} {vr['groundout_c']-vl['groundout_c']:>20.2f}") + print(f"{' Total':<20} {vl['groundout_a']+vl['groundout_b']+vl['groundout_c']:>20.2f} {vr['groundout_a']+vr['groundout_b']+vr['groundout_c']:>20.2f} {(vr['groundout_a']+vr['groundout_b']+vr['groundout_c'])-(vl['groundout_a']+vl['groundout_b']+vl['groundout_c']):>20.2f}") + print(f"{' Ratio (A:B:C)':<20} {'3:2:1':>20} {'3:3:1':>20} {'-':>20}") + + # SPRAY + print(f"\n{'SPRAY CHART':<20} {'vs LHP':>20} {'vs RHP':>20} {'Difference':>20}") + print("-"*85) + print(f"{'Pull %':<20} {vl['pull_rate']*100:>19.1f}% {vr['pull_rate']*100:>19.1f}% {(vr['pull_rate']-vl['pull_rate'])*100:>19.1f}%") + print(f"{'Center %':<20} {vl['center_rate']*100:>19.1f}% {vr['center_rate']*100:>19.1f}% {(vr['center_rate']-vl['center_rate'])*100:>19.1f}%") + print(f"{'Opposite %':<20} {vl['slap_rate']*100:>19.1f}% {vr['slap_rate']*100:>19.1f}% {(vr['slap_rate']-vl['slap_rate'])*100:>19.1f}%") + + # BASERUNNING + print(f"\n{'BASERUNNING':<20} {'Value':>20}") + print("-"*85) + print(f"{'Running':<20} {baserunning['running']:>20}") + print(f"{'Steal Range':<20} {f'{baserunning['steal_low']}-{baserunning['steal_high']}':>20}") + print(f"{'Steal Jump':<20} {baserunning['steal_jump']:>20.7f}") + print(f"{'Hit-and-Run':<20} {baserunning['hit_and_run']:>20}") + + # DEFENSE + print(f"\n{'DEFENSE':<20} {'Range':>20} {'Error':>20} {'Arm':>20}") + print("-"*85) + print(f"{'1B':<20} {4:>20} {10:>20} {'-':>20}") + print(f"{'RF':<20} {4:>20} {6:>20} {'+2':>20}") + + print("\n" + "="*85) + print("✓ FINAL VERSION - All requirements met with custom groundball ratios") + print("="*85) + print("") + + +if __name__ == "__main__": + main() diff --git a/custom_cards/submit_luan_arroto.py b/custom_cards/submit_luan_arroto.py new file mode 100644 index 0000000..4bb293e --- /dev/null +++ b/custom_cards/submit_luan_arroto.py @@ -0,0 +1,278 @@ +""" +Submit Luan Arroto to Paper Dynasty Database + +FINAL VERSION with: +- Platoon splits (better vs RHP) +- No triples +- Custom groundball ratios (3:2:1 vs L, 3:3:1 vs R) +- Spray chart variation (10% pull vs L, 33% pull vs R) +""" + +import asyncio +from custom_cards.archetype_definitions import BatterArchetype +from custom_cards.archetype_calculator import BatterRatingCalculator, calculate_total_ops +from db_calls import db_get, db_post, db_patch, db_put +from creation_helpers import mlbteam_and_franchise +import boto3 +from datetime import datetime +import aiohttp +from exceptions import logger + +# 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' + +# Configuration +CARDSET_NAME = "2005 Custom" +CARDSET_ID = 29 +SEASON = 2005 +TEAM_ABBREV = "FA" # Free Agent +PLAYER_DESCRIPTION = "2005 Custom" + + +async def main(): + """Create Luan Arroto with final specifications.""" + + print("\n" + "="*70) + print("SUBMITTING LUAN ARROTO TO DATABASE") + print("="*70) + + # Step 1: Design archetype + print("\nCalculating ratings...") + archetype = BatterArchetype( + name="Luan Arroto Final", + description="Contact hitter with platoon advantage and spray approach", + avg_vs_r=0.310, obp_vs_r=0.450, slg_vs_r=0.385, + bb_pct_vs_r=0.14, k_pct_vs_r=0.10, + avg_vs_l=0.285, obp_vs_l=0.455, slg_vs_l=0.355, + bb_pct_vs_l=0.17, k_pct_vs_l=0.09, + hr_per_hit=0.025, triple_per_hit=0.00, double_per_hit=0.22, + gb_pct=0.48, fb_pct=0.27, ld_pct=0.25, + hard_pct=0.28, med_pct=0.52, soft_pct=0.20, + pull_pct=0.215, center_pct=0.35, oppo_pct=0.435, + ifh_pct=0.09, hr_fb_pct=0.06, + speed_rating=8, steal_jump=3, xbt_pct=0.58, hit_run_skill=9, + primary_positions=["1B", "RF"], defensive_rating=6 + ) + + # Step 2: Calculate ratings + calc = BatterRatingCalculator(archetype) + ratings = calc.calculate_ratings(battingcard_id=0) + baserunning = calc.calculate_baserunning() + + # Step 3: Apply customizations + ratings[1]['pull_rate'] = 0.33 # vs R + ratings[0]['pull_rate'] = 0.10 # vs L + for rating in ratings: + rating['slap_rate'] = 1.0 - rating['pull_rate'] - rating['center_rate'] + + baserunning.update({ + 'steal_low': 3, 'steal_high': 20, + 'steal_auto': 0, 'steal_jump': 0.0277777, 'running': 10 + }) + + # Step 4: Custom groundball ratios + vl = ratings[0] + vr = ratings[1] + + total_gb_vl = vl['groundout_a'] + vl['groundout_b'] + vl['groundout_c'] + total_gb_vr = vr['groundout_a'] + vr['groundout_b'] + vr['groundout_c'] + + # vs L: 3:2:1 ratio + vl['groundout_a'] = (3/6) * total_gb_vl + vl['groundout_b'] = (2/6) * total_gb_vl + vl['groundout_c'] = (1/6) * total_gb_vl + + # vs R: 3:3:1 ratio + vr['groundout_a'] = (3/7) * total_gb_vr + vr['groundout_b'] = (3/7) * total_gb_vr + vr['groundout_c'] = (1/7) * total_gb_vr + + # Verify + total_ops = calculate_total_ops(vl, vr, is_pitcher=False) + print(f" ✓ Combined OPS: {total_ops:.3f}") + print(f" ✓ ISO vs L: {vl['slg']-vl['avg']:.3f}") + print(f" ✓ ISO vs R: {vr['slg']-vr['avg']:.3f}") + print(f" ✓ Groundball ratios: vs L (3:2:1), vs R (3:3:1)") + + # Step 5: Use existing cardset + print(f"\nUsing cardset '{CARDSET_NAME}' (ID: {CARDSET_ID})...") + cardset = {'id': CARDSET_ID, 'name': CARDSET_NAME, 'season': SEASON} + print(f" ✓ Cardset ID: {cardset['id']}") + + # Step 6: Get team info + print(f"\nGetting team info for '{TEAM_ABBREV}'...") + mlb_team_id, franchise_id = mlbteam_and_franchise(TEAM_ABBREV) + print(f" ✓ MLB Team ID: {mlb_team_id}, Franchise ID: {franchise_id}") + + # Step 7: Skip MLBPlayer (API constraint issues) + print("\nSkipping MLBPlayer creation...") + bbref_id = "custom_arrotl01" + mlbplayer_id = None # Leave null per user request + print(f" ✓ Skipped MLBPlayer") + + # Step 8: Create Player + print("\nCreating Player record...") + now = datetime.now() + release_date = f"{now.year}-{now.month}-{now.day}" + + player_payload = { + 'p_name': 'Luan Arroto', + 'cost': '88', + 'image': 'change-me', + 'mlbclub': mlb_team_id, + 'franchise': franchise_id, + 'cardset_id': cardset['id'], + 'set_num': 99999, + 'rarity_id': 3, # Starter + 'pos_1': '1B', + 'description': PLAYER_DESCRIPTION, + 'bbref_id': bbref_id, + 'fangr_id': 0, + 'mlbplayer_id': mlbplayer_id, + } + + player = await db_post('players', payload=player_payload) + player_id = player['player_id'] + print(f" ✓ Created Player ID: {player_id}") + + # Step 9: Create BattingCard + print("\nCreating BattingCard...") + batting_card_payload = { + 'cards': [{ + 'player_id': player_id, + 'key_bbref': bbref_id, + 'key_fangraphs': 0, + 'key_mlbam': 0, + 'key_retro': '', + 'name_first': 'Luan', + 'name_last': 'Arroto', + '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': 'L', + }] + } + + await db_put('battingcards', payload=batting_card_payload, timeout=10) + + bc_query = await db_get('battingcards', params=[('player_id', player_id)]) + battingcard_id = bc_query['cards'][0]['id'] + print(f" ✓ Created BattingCard ID: {battingcard_id}") + + # Step 10: Create BattingCardRatings + print("\nCreating BattingCardRatings...") + for rating in ratings: + rating['battingcard_id'] = battingcard_id + rating['bat_hand'] = 'L' + + ratings_payload = {'ratings': ratings} + await db_put('battingcardratings', payload=ratings_payload, timeout=10) + print(f" ✓ Created ratings for vs L and vs R") + + # Step 11: Create CardPositions + print("\nCreating CardPositions...") + positions_payload = { + 'positions': [ + { + 'player_id': player_id, + 'position': '1B', + 'range': 4, + 'error': 10, + 'arm': None, + 'fielding_pct': None + }, + { + 'player_id': player_id, + 'position': 'RF', + 'range': 4, + 'error': 6, + 'arm': 2, + 'fielding_pct': None + } + ] + } + + await db_put('cardpositions', payload=positions_payload, timeout=10) + print(f" ✓ Created positions: 1B (range 4, error 10), RF (range 4, arm +2, error 6)") + + # Step 12: Generate and upload card image + print("\nGenerating card image...") + api_image_url = f"https://pd.manticorum.com/api/v2/players/{player_id}/battingcard?d={release_date}" + + try: + async with aiohttp.ClientSession() as session: + async with session.get(api_image_url, timeout=aiohttp.ClientTimeout(total=15)) as resp: + if resp.status == 200: + image_bytes = await resp.read() + image_size_kb = len(image_bytes) / 1024 + print(f" ✓ Fetched card image ({image_size_kb:.1f} KB)") + + # Upload to S3 + print("\nUploading to S3...") + s3_client = boto3.client('s3', region_name=AWS_REGION) + cardset_str = f'{cardset["id"]:03d}' + s3_key = f'cards/cardset-{cardset_str}/player-{player_id}/battingcard.png' + + s3_client.put_object( + Bucket=AWS_BUCKET_NAME, + Key=s3_key, + Body=image_bytes, + ContentType='image/png', + CacheControl='public, max-age=300', + Metadata={ + 'player-id': str(player_id), + 'card-type': 'batting', + 'upload-date': datetime.now().isoformat() + } + ) + + s3_url = f'{S3_BASE_URL}/{s3_key}?d={release_date}' + print(f" ✓ Uploaded to S3: {s3_key}") + + # Update player with S3 URL + await db_patch('players', object_id=player_id, params=[('image', s3_url)]) + print(f" ✓ Updated player record with S3 URL") + + else: + error_text = await resp.text() + print(f" ⚠ Card generation failed (HTTP {resp.status}): {error_text}") + + except Exception as e: + print(f" ⚠ Error during card generation/upload: {e}") + print(f" Player created but image not uploaded") + + # Summary + print("\n" + "="*70) + print("✓ LUAN ARROTO CREATED SUCCESSFULLY!") + print("="*70) + print(f"\nPlayer ID: {player_id}") + print(f"Cardset: {cardset['name']} (ID: {cardset['id']})") + print(f"Hand: L") + print(f"Positions: 1B (range 4, error 10), RF (range 4, arm +2, error 6)") + print(f"\nOffensive Profile:") + print(f" vs LHP: .285/.455/.355 (.810 OPS)") + print(f" vs RHP: .310/.450/.385 (.835 OPS)") + print(f" Combined OPS: {total_ops:.3f}") + print(f" Pull rates: 10% vs L, 33% vs R") + print(f"\nGroundball Distribution:") + print(f" vs LHP: 3:2:1 (A:9.85, B:6.57, C:3.28)") + print(f" vs RHP: 3:3:1 (A:8.36, B:8.36, C:2.79)") + print(f"\nBaserunning:") + print(f" Running: {baserunning['running']}/17") + print(f" Steal: {baserunning['steal_low']}-{baserunning['steal_high']}") + print(f" Jump: {baserunning['steal_jump']:.7f}") + print(f"\nView card:") + print(f" PNG: {api_image_url}") + print(f" HTML: {api_image_url}&html=true") + if 's3_url' in locals(): + print(f" S3: {s3_url}") + print("") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/custom_cards/update_luan_arroto.py b/custom_cards/update_luan_arroto.py new file mode 100644 index 0000000..e25756a --- /dev/null +++ b/custom_cards/update_luan_arroto.py @@ -0,0 +1,180 @@ +""" +Update Luan Arroto to 0.850 Combined OPS + +Changes: +- vs R: .310/.450/.385 → .322/.467/.400 +- vs L: .285/.455/.355 → .296/.473/.369 +- Combined OPS: 0.818 → 0.850 + +Scale factor: 1.0387 applied to all stats proportionally. +All ratios (groundball, spray, etc.) remain identical. +""" + +import asyncio +from custom_cards.archetype_definitions import BatterArchetype +from custom_cards.archetype_calculator import BatterRatingCalculator, calculate_total_ops +from db_calls import db_get, db_put, db_patch +import boto3 +from datetime import datetime +import aiohttp + +# 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' + +# Known IDs from database +PLAYER_ID = 13156 +BATTINGCARD_ID = 6163 +CARDSET_ID = 29 + + +async def main(): + """Update Luan Arroto to target 0.850 OPS.""" + + print("\n" + "="*70) + print("UPDATING LUAN ARROTO TO 0.850 OPS") + print("="*70) + + # Step 1: Calculate new ratings with scaled stats + print("\nCalculating new ratings (target OPS: 0.850)...") + + # Target stats (scaled by 1.0387 from original) + archetype = BatterArchetype( + name="Luan Arroto Updated", + description="Contact hitter with platoon advantage and spray approach", + # SCALED STATS for 0.850 OPS target + avg_vs_r=0.322, obp_vs_r=0.467, slg_vs_r=0.400, + bb_pct_vs_r=0.14, k_pct_vs_r=0.10, + avg_vs_l=0.296, obp_vs_l=0.473, slg_vs_l=0.369, + bb_pct_vs_l=0.17, k_pct_vs_l=0.09, + # All ratios unchanged + hr_per_hit=0.025, triple_per_hit=0.00, double_per_hit=0.22, + gb_pct=0.48, fb_pct=0.27, ld_pct=0.25, + hard_pct=0.28, med_pct=0.52, soft_pct=0.20, + pull_pct=0.215, center_pct=0.35, oppo_pct=0.435, + ifh_pct=0.09, hr_fb_pct=0.06, + speed_rating=8, steal_jump=3, xbt_pct=0.58, hit_run_skill=9, + primary_positions=["1B", "RF"], defensive_rating=6 + ) + + # Step 2: Calculate ratings + calc = BatterRatingCalculator(archetype) + ratings = calc.calculate_ratings(battingcard_id=BATTINGCARD_ID) + + # Step 3: Apply same customizations as original + ratings[1]['pull_rate'] = 0.33 # vs R + ratings[0]['pull_rate'] = 0.10 # vs L + for rating in ratings: + rating['slap_rate'] = 1.0 - rating['pull_rate'] - rating['center_rate'] + + # Step 4: Custom groundball ratios (same as original) + vl = ratings[0] + vr = ratings[1] + + total_gb_vl = vl['groundout_a'] + vl['groundout_b'] + vl['groundout_c'] + total_gb_vr = vr['groundout_a'] + vr['groundout_b'] + vr['groundout_c'] + + # vs L: 3:2:1 ratio + vl['groundout_a'] = (3/6) * total_gb_vl + vl['groundout_b'] = (2/6) * total_gb_vl + vl['groundout_c'] = (1/6) * total_gb_vl + + # vs R: 3:3:1 ratio + vr['groundout_a'] = (3/7) * total_gb_vr + vr['groundout_b'] = (3/7) * total_gb_vr + vr['groundout_c'] = (1/7) * total_gb_vr + + # Verify OPS + total_ops = calculate_total_ops(vl, vr, is_pitcher=False) + print(f" ✓ Combined OPS: {total_ops:.3f}") + print(f" ✓ vs LHP: .{int(vl['avg']*1000):03d}/.{int(vl['obp']*1000):03d}/.{int(vl['slg']*1000):03d} ({vl['obp']+vl['slg']:.3f} OPS)") + print(f" ✓ vs RHP: .{int(vr['avg']*1000):03d}/.{int(vr['obp']*1000):03d}/.{int(vr['slg']*1000):03d} ({vr['obp']+vr['slg']:.3f} OPS)") + + # Step 5: Update batting card ratings + print("\nUpdating BattingCardRatings in database...") + for rating in ratings: + rating['battingcard_id'] = BATTINGCARD_ID + rating['bat_hand'] = 'L' + + ratings_payload = {'ratings': ratings} + await db_put('battingcardratings', payload=ratings_payload, timeout=10) + print(f" ✓ Updated ratings for vs L and vs R") + + # Step 6: Generate new card image + print("\nGenerating new card image...") + now = datetime.now() + release_date = f"{now.year}-{now.month}-{now.day}" + timestamp = int(now.timestamp()) + cache_bust_date = f"{now.year}-{now.month}-{now.day}-{timestamp}" + + api_image_url = f"https://pd.manticorum.com/api/v2/players/{PLAYER_ID}/battingcard?d={cache_bust_date}" + + try: + async with aiohttp.ClientSession() as session: + async with session.get(api_image_url, timeout=aiohttp.ClientTimeout(total=15)) as resp: + if resp.status == 200: + image_bytes = await resp.read() + image_size_kb = len(image_bytes) / 1024 + print(f" ✓ Fetched card image ({image_size_kb:.1f} KB)") + + # Step 7: Upload to S3 + print("\nUploading to S3...") + s3_client = boto3.client('s3', region_name=AWS_REGION) + cardset_str = f'{CARDSET_ID:03d}' + s3_key = f'cards/cardset-{cardset_str}/player-{PLAYER_ID}/battingcard.png' + + s3_client.put_object( + Bucket=AWS_BUCKET_NAME, + Key=s3_key, + Body=image_bytes, + ContentType='image/png', + CacheControl='public, max-age=300', + Metadata={ + 'player-id': str(PLAYER_ID), + 'card-type': 'batting', + 'upload-date': datetime.now().isoformat(), + 'combined-ops': f'{total_ops:.3f}' + } + ) + + s3_url = f'{S3_BASE_URL}/{s3_key}?d={release_date}' + print(f" ✓ Uploaded to S3: {s3_key}") + + # Step 8: Update player with new S3 URL + await db_patch('players', object_id=PLAYER_ID, params=[('image', s3_url)]) + print(f" ✓ Updated player record with new S3 URL") + + else: + error_text = await resp.text() + print(f" ⚠ Card generation failed (HTTP {resp.status}): {error_text}") + + except Exception as e: + print(f" ⚠ Error during card generation/upload: {e}") + import traceback + traceback.print_exc() + + # Summary + print("\n" + "="*70) + print("✓ LUAN ARROTO UPDATED SUCCESSFULLY!") + print("="*70) + print(f"\nPlayer ID: {PLAYER_ID}") + print(f"BattingCard ID: {BATTINGCARD_ID}") + print(f"\nOLD Offensive Profile:") + print(f" vs LHP: .285/.455/.355 (.810 OPS)") + print(f" vs RHP: .310/.450/.385 (.835 OPS)") + print(f" Combined OPS: 0.818") + print(f"\nNEW Offensive Profile:") + print(f" vs LHP: .296/.473/.369 (.842 OPS)") + print(f" vs RHP: .322/.467/.400 (.867 OPS)") + print(f" Combined OPS: {total_ops:.3f}") + print(f"\nView updated card:") + print(f" PNG: https://pd.manticorum.com/api/v2/players/{PLAYER_ID}/battingcard?d={cache_bust_date}") + print(f" HTML: https://pd.manticorum.com/api/v2/players/{PLAYER_ID}/battingcard?d={cache_bust_date}&html=true") + if 's3_url' in locals(): + print(f" S3: {s3_url}") + print("") + + +if __name__ == "__main__": + asyncio.run(main())