""" Create custom card for Valerie-Hecate Theolia Left-handed contact-hitting catcher with 0.855 OPS target """ import asyncio from custom_cards.archetype_definitions import get_archetype from custom_cards.archetype_calculator import BatterRatingCalculator, calculate_total_ops from creation_helpers import mlbteam_and_franchise from db_calls import db_get, db_post, db_put, db_patch from datetime import datetime import boto3 # 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_valerie_theolia(): """Create Valerie-Hecate Theolia custom card.""" print("="*70) print("CREATING VALERIE-HECATE THEOLIA") print("="*70) # Player details name_first = "Valerie-Hecate" name_last = "Theolia" hand = "L" # Left-handed batter team_abbrev = "NYY" # New York Yankees (placeholder - user can specify) positions = ["C"] # Catcher # 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}'") # Start with Contact Hitter archetype and boost to 0.855 OPS archetype = get_archetype('batter', 'contact_hitter') print(f"\n✓ Using archetype: {archetype.name}") print(f" Base stats: {archetype.avg_vs_r:.3f}/{archetype.obp_vs_r:.3f}/{archetype.slg_vs_r:.3f}") # Boost stats to reach 0.855 OPS target # Need to increase from 0.787 to 0.855 (+0.068) # Increase AVG, OBP, and SLG for both splits print(f"\n✓ Boosting stats to reach 0.855 OPS target...") # Adjust archetype stats to reach 0.855 target (accounting for BP * 0.5 in AVG, full in SLG) # Tuning with corrected SLG formula - need to reduce SLG more archetype.avg_vs_r = 0.289 archetype.obp_vs_r = 0.350 archetype.slg_vs_r = 0.435 archetype.avg_vs_l = 0.279 archetype.obp_vs_l = 0.340 archetype.slg_vs_l = 0.420 print(f" Adjusted vR: {archetype.avg_vs_r:.3f}/{archetype.obp_vs_r:.3f}/{archetype.slg_vs_r:.3f} (OPS: {archetype.obp_vs_r + archetype.slg_vs_r:.3f})") print(f" Adjusted vL: {archetype.avg_vs_l:.3f}/{archetype.obp_vs_l:.3f}/{archetype.slg_vs_l:.3f} (OPS: {archetype.obp_vs_l + archetype.slg_vs_l:.3f})") # Calculate adjusted ratings calc = BatterRatingCalculator(archetype) ratings = calc.calculate_ratings(battingcard_id=0) # Temp ID baserunning = calc.calculate_baserunning() # Override steal rate to 0.05555555 print(f"\n✓ Setting steal rate to 0.05555555...") # Very low steal rate baserunning['steal_jump'] = 0.05555555 # With 5.555% success rate, approximate values: baserunning['steal_high'] = 11 baserunning['steal_low'] = 7 # Manual adjustments for power distribution print(f"\n✓ Applying power distribution adjustments...") print(f" Setting HR to 0.0 for both sides") print(f" Setting BP-HR to 0.0 vL, 1.0 vR") # vL (ratings[0]) old_hr_vl = ratings[0]['homerun'] old_bphr_vl = ratings[0]['bp_homerun'] ratings[0]['homerun'] = 0.0 ratings[0]['bp_homerun'] = 0.0 # Redistribute to singles (not doubles) to reduce SLG redistrib_vl = old_hr_vl + old_bphr_vl ratings[0]['single_center'] += redistrib_vl # vR (ratings[1]) old_hr_vr = ratings[1]['homerun'] old_bphr_vr = ratings[1]['bp_homerun'] ratings[1]['homerun'] = 0.0 ratings[1]['bp_homerun'] = 1.0 # Redistribute excess to singles (not doubles) to maintain total chances and reduce SLG redistrib_vr = old_hr_vr + (old_bphr_vr - 1.0) ratings[1]['single_center'] += redistrib_vr # Apply randomization to make results look more natural # Randomize all non-HR and non-BP-single results by ±0.5 import random print(f"\n✓ Applying randomization (±0.5) and rounding to 0.05...") for rating in ratings: # Fields to randomize (exclude homerun, bp_homerun for vR if it's exactly 1.0, and bp_single) randomize_fields = [ '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' ] # Only randomize bp_homerun if it's not exactly 1.0 (our target value) if rating['bp_homerun'] != 1.0: randomize_fields.insert(0, 'bp_homerun') 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]) # Remove all triples print(f"\n✓ Removing all triples...") for rating in ratings: triple_value = rating['triple'] rating['triple'] = 0.0 # Redistribute to doubles (maintain gap power) rating['double_two'] += triple_value rating['double_two'] = round(rating['double_two'] * 20) / 20 print(f" Triples removed and redistributed to doubles") # Adjust groundball distribution print(f"\n✓ Adjusting groundball distribution...") # vL (ratings[0]): Distribute half of gbC to gbA and gbB vl_gbc_half = ratings[0]['groundout_c'] / 2 vl_redistrib = vl_gbc_half / 2 ratings[0]['groundout_a'] += vl_redistrib ratings[0]['groundout_b'] += vl_redistrib ratings[0]['groundout_c'] -= vl_gbc_half # vR (ratings[1]): Transfer 1.0 from gbC to both gbA and gbB ratings[1]['groundout_a'] += 1.0 ratings[1]['groundout_b'] += 1.0 ratings[1]['groundout_c'] -= 2.0 # Round to 0.05 for rating in ratings: rating['groundout_a'] = round(rating['groundout_a'] * 20) / 20 rating['groundout_b'] = round(rating['groundout_b'] * 20) / 20 rating['groundout_c'] = round(rating['groundout_c'] * 20) / 20 print(f" vL: gbA={ratings[0]['groundout_a']:.2f}, gbB={ratings[0]['groundout_b']:.2f}, gbC={ratings[0]['groundout_c']:.2f}") print(f" vR: gbA={ratings[1]['groundout_a']:.2f}, gbB={ratings[1]['groundout_b']:.2f}, gbC={ratings[1]['groundout_c']:.2f}") # 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 (not 0.5*2), BP-1B gets 1 base (not 0.5*1) 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] vr = ratings[1] 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(f"\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: Valerie-Hecate Theolia (L)") print(f"Position: C (Catcher)") print(f"Cardset: 29 (Custom Characters)") print(f"Description: 05 Custom") print(f"Total OPS: {total_ops:.3f} / 0.855 target") print(f"\nGB Distribution Check:") print(f" vL: gbA={ratings[0]['groundout_a']:.1f} < gbB={ratings[0]['groundout_b']:.1f} ✓") print(f" vR: gbA={ratings[1]['groundout_a']:.1f} < gbB={ratings[1]['groundout_b']:.1f} ✓") print("\n" + "="*70) print("DATABASE CREATION") print("="*70) # Set bunting to A baserunning['bunting'] = 'A' print(f"\n✓ Bunting set to: A") # Create database records bbref_id = f"custom_{name_last.lower()}{name_first[0].lower()}01" # Step 1: Create/verify MLBPlayer record print(f"\n✓ Checking for existing MLBPlayer record...") # Try searching by first_name and last_name mlb_query = await db_get('mlbplayers', params=[('first_name', name_first), ('last_name', name_last)]) if mlb_query and mlb_query.get('count', 0) > 0: mlbplayer_id = mlb_query['players'][0]['id'] print(f" Using existing MLBPlayer ID: {mlbplayer_id}") else: try: mlbplayer_payload = { 'key_bbref': bbref_id, 'key_fangraphs': 0, 'key_mlbam': 0, 'key_retro': '', 'first_name': name_first, 'last_name': name_last, } new_mlbplayer = await db_post('mlbplayers/one', payload=mlbplayer_payload) mlbplayer_id = new_mlbplayer['id'] print(f" Created MLBPlayer ID: {mlbplayer_id}") except ValueError as e: # Player exists but couldn't be found - skip MLBPlayer creation print(f" MLBPlayer creation failed: {e}") print(f" Proceeding without MLBPlayer linkage...") mlbplayer_id = None # Step 2: Create or update Player record print(f"\n✓ Checking for existing Player record...") now = datetime.now() release_date = f"{now.year}-{now.month}-{now.day}" # Check if player already exists 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" Using existing Player ID: {player_id}") # Update the image URL image_url = f"https://pd.manticorum.com/api/v2/players/{player_id}/battingcard?d={release_date}" await db_patch('players', object_id=player_id, params=[('image', image_url)]) print(f" Updated image URL") else: # Create new player print(f" Creating new Player record...") # Temporary placeholder image URL (will update after getting player_id) temp_image_url = f"https://pd.manticorum.com/api/v2/players/0/battingcard?d={release_date}" player_payload = { 'p_name': f"{name_first} {name_last}", 'bbref_id': bbref_id, 'fangr_id': 0, 'strat_code': 0, 'hand': hand, 'mlbclub': 'Custom Ballplayers', 'franchise': 'Custom Ballplayers', 'cardset_id': cardset['id'], 'description': player_description, 'is_custom': True, 'cost': 100, # Placeholder 'rarity_id': 5, # Common placeholder 'image': temp_image_url, 'set_num': 9999, # Custom player set number 'pos_1': 'C', # Primary position } # Only add mlbplayer_id if we have one if mlbplayer_id: player_payload['mlbplayer_id'] = mlbplayer_id new_player = await db_post('players', payload=player_payload) player_id = new_player['player_id'] print(f" Created Player ID: {player_id}") # Update with correct image URL image_url = f"https://pd.manticorum.com/api/v2/players/{player_id}/battingcard?d={release_date}" await db_patch('players', object_id=player_id, params=[('image', image_url)]) print(f" Updated with correct image URL") # Step 3: Create BattingCard print(f"\n✓ Creating 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': 'A', }] } await db_put('battingcards', payload=batting_card_payload, timeout=10) print(f" BattingCard created") # Get the created 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 4: Create BattingCardRatings print(f"\n✓ Creating BattingCardRatings...") for rating in ratings: rating['battingcard_id'] = battingcard_id ratings_payload = {'ratings': ratings} await db_put('battingcardratings', payload=ratings_payload, timeout=10) print(f" Ratings created (vL and vR)") # Step 5: Create CardPositions with defensive ratings print(f"\n✓ Creating CardPosition (Catcher)...") positions_payload = { 'positions': [{ 'player_id': player_id, 'variant': 0, 'position': 'C', 'innings': 1, 'range': 3, # Range 3 'error': 5, # Error 5 'arm': 0, # Arm 0 'pb': 6, # PB 6 'overthrow': 3, # Overthrow 3 }] } await db_put('cardpositions', payload=positions_payload, timeout=10) print(f" Position created: C (Range 3, Error 5, PB 6, Overthrow 3, Arm 0)") # Step 6: Update rarity and cost print(f"\n✓ Updating rarity to Starter and cost to 91...") await db_patch('players', object_id=player_id, params=[('rarity_id', 3)]) # Starter = 3 await db_patch('players', object_id=player_id, params=[('cost', 91)]) print(f" Rarity set to: Starter (ID 3)") print(f" Cost set to: 91") # Step 7: Generate card image and upload to S3 print(f"\n✓ Generating and uploading card image to AWS S3...") # Fetch the card image from API import aiohttp api_image_url = f"https://pd.manticorum.com/api/v2/players/{player_id}/battingcard?d={release_date}" async with aiohttp.ClientSession() as session: async with session.get(api_image_url) as response: if response.status == 200: image_bytes = await response.read() print(f" Fetched card image ({len(image_bytes)} bytes)") else: raise ValueError(f"Failed to fetch card image: {response.status}") # Upload to S3 s3_client = boto3.client('s3', region_name=AWS_REGION) s3_key = f"cards/cardset-{cardset['id']:03d}/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' ) print(f" Uploaded to S3: {s3_key}") # Update player with S3 URL s3_url = f"https://{AWS_BUCKET_NAME}.s3.{AWS_REGION}.amazonaws.com/{s3_key}?d={release_date}" await db_patch('players', object_id=player_id, params=[('image', s3_url)]) print(f" Updated player image URL to S3") print("\n" + "="*70) print("✅ SUCCESS!") print("="*70) print(f"\nValerie-Hecate Theolia created successfully!") print(f" Player ID: {player_id}") print(f" BattingCard ID: {battingcard_id}") print(f" Position: C") print(f" Bunting: A") print(f" Rarity: Starter") print(f" Cost: 91") print(f" Total OPS: {total_ops:.3f}") print(f" S3 Image URL: {s3_url}") print("\n" + "="*70) if __name__ == "__main__": asyncio.run(create_valerie_theolia())