paper-dynasty-card-creation/create_sphealthamus_spheal.py
Cal Corum 9d9c507e84 Update Sphealy custom card to 0.850 OPS and cost 188
Increased target OPS from 0.820 to 0.850 with adjusted stat splits:
- vs RHP: .260/.340/.495 (power profile)
- vs LHP: .260/.375/.420 (patient/OBP profile)
- Cost updated from 85 to 188

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 10:53:27 -06:00

506 lines
20 KiB
Python

"""
Create custom card for Sphealthamus Sphealy Spheal Sr
Power hitter vs RHP (singles/HRs), patient hitter vs LHP (walks/singles)
Extreme pull tendency vs RHP (62%), all-fields vs LHP (28%)
"""
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_post, db_put, db_patch
from datetime import datetime
import boto3
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_sphealthamus_spheal():
"""Create Sphealthamus Sphealy Spheal Sr custom card."""
print("="*70)
print("CREATING SPHEALTHAMUS SPHEALY SPHEAL SR")
print("="*70)
# Player details
name_first = "Sphealthamus Sphealy"
name_last = "Spheal Sr"
hand = "L" # Left-handed batter
team_abbrev = "SEA" # Placeholder team
positions = ["1B"] # First baseman
# 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 Sphealthamus Sphealy Spheal Sr
# Target: Combined OPS = 0.850
# vs RHP: Power (singles + HRs with 3:2 BP-HR:HR ratio), high pull (62%)
# vs LHP: Patient (walks + singles), low pull (28%)
spheal = BatterArchetype(
name="Sphealthamus Sphealy Spheal Sr",
description="Power vs RHP, patience vs LHP, extreme splits",
# VS RHP: Target OPS = ~0.850 (power profile - singles + HRs)
avg_vs_r=0.260, # Boosted to hit OPS target
obp_vs_r=0.340, # Boosted to hit OPS target
slg_vs_r=0.495, # Boosted to hit OPS target
bb_pct_vs_r=0.09, # Moderate walks
k_pct_vs_r=0.24, # Higher strikeouts (power hitter)
# VS LHP: Target OPS = ~0.830 (patient profile - walks + singles)
avg_vs_l=0.260, # Boosted to hit OPS target
obp_vs_l=0.375, # Boosted to hit OPS target
slg_vs_l=0.420, # Boosted to hit OPS target
bb_pct_vs_l=0.15, # Very high walks
k_pct_vs_l=0.20, # Fewer strikeouts
# Power distribution - high HR rate vs RHP, moderate vs LHP
hr_per_hit=0.15, # High HR rate
triple_per_hit=0.00, # NO TRIPLES
double_per_hit=0.20, # Moderate doubles
# Batted ball profile
gb_pct=0.40, # Some ground balls
fb_pct=0.38, # Lots of fly balls (power)
ld_pct=0.22, # Line drives
# Batted ball quality - power hitter
hard_pct=0.40, # Lots of hard contact
med_pct=0.42, # Medium contact
soft_pct=0.18, # Some soft contact
# Spray chart - EXTREME PULL vs RHP, all-fields vs LHP
pull_pct=0.45, # Will adjust per split
center_pct=0.32, # Will adjust per split
oppo_pct=0.23, # Will adjust per split
# Infield hits
ifh_pct=0.04, # Low (slow runner)
# Specific power metrics
hr_fb_pct=0.16, # Good HR/FB
# Baserunning - poor speed
speed_rating=3, # Poor speed
steal_jump=5, # Below average
xbt_pct=0.42, # Below average
# Situational hitting - poor
hit_run_skill=3, # Poor hit-and-run
# Defensive profile
primary_positions=["1B"],
defensive_rating=5, # Average defender
)
print(f"\n✓ Created custom archetype: {spheal.name}")
print(f" Base stats vR: {spheal.avg_vs_r:.3f}/{spheal.obp_vs_r:.3f}/{spheal.slg_vs_r:.3f} (OPS: {spheal.obp_vs_r + spheal.slg_vs_r:.3f})")
print(f" Base stats vL: {spheal.avg_vs_l:.3f}/{spheal.obp_vs_l:.3f}/{spheal.slg_vs_l:.3f} (OPS: {spheal.obp_vs_l + spheal.slg_vs_l:.3f})")
# Calculate ratings
calc = BatterRatingCalculator(spheal)
ratings = calc.calculate_ratings(battingcard_id=0) # Temp ID
baserunning = calc.calculate_baserunning()
# Override steal rate to 0.416666 (18-13)
print(f"\n✓ Setting steal rate to 0.416666 (18-13)...")
baserunning['steal_jump'] = 0.416666
baserunning['steal_high'] = 18
baserunning['steal_low'] = 13
# Override running to 9
print(f"✓ Setting running to 9...")
baserunning['running'] = 9
# Override bunting to D
print(f"✓ Setting bunting to D...")
baserunning['bunting'] = 'D'
# Override hit-and-run to D
print(f"✓ Setting hit-and-run to D...")
baserunning['hit_and_run'] = 'D'
# Apply randomization to make results look more natural
print(f"\n✓ Applying randomization (±0.5) and rounding to 0.05...")
random.seed(43) # Different seed for different character
for rating in ratings:
# Fields to randomize
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:
randomization = random.uniform(-0.5, 0.5)
new_value = rating[field] + randomization
rating[field] = round(new_value * 20) / 20
rating[field] = max(0.05, rating[field])
# Fix BP-HR and HBP to whole numbers, remove triples, adjust pull rates
print(f"\n✓ Applying custom adjustments...")
print(f" - Setting BP-HR to whole numbers")
print(f" - Setting HBP to whole numbers")
print(f" - Removing all triples")
print(f" - Adjusting pull rates (62% vR, 28% vL)")
print(f" - Setting BP-HR:HR ratio to 3:2 vs RHP")
# vs LHP (ratings[0]) - Patient profile
old_bphr_vl = ratings[0]['bp_homerun']
ratings[0]['bp_homerun'] = 1.0 # Fewer BP-HR vs LHP
ratings[0]['single_center'] += (old_bphr_vl - 1.0)
old_hbp_vl = ratings[0]['hbp']
ratings[0]['hbp'] = 1.0
ratings[0]['single_center'] += (old_hbp_vl - 1.0)
# Remove triples vL
old_triple_vl = ratings[0]['triple']
ratings[0]['triple'] = 0.0
ratings[0]['single_center'] += old_triple_vl
# Remove Double2 vL and move to Walks
old_double2_vl = ratings[0]['double_two']
ratings[0]['double_two'] = 0.0
ratings[0]['walk'] += old_double2_vl
# Set pull rate to 28% vL
ratings[0]['pull_rate'] = 0.28
ratings[0]['center_rate'] = 0.40
ratings[0]['slap_rate'] = 0.32
# vs RHP (ratings[1]) - Power profile
# Add 2 BP-HR and 1.4 HR
total_hr_chances = ratings[1]['homerun'] + ratings[1]['bp_homerun']
ratings[1]['homerun'] = 3.4 # Add 1.4 HR
ratings[1]['bp_homerun'] = 5.0 # Add 2 BP-HR
# Redistribute excess
excess = total_hr_chances - 8.4
ratings[1]['single_center'] += excess
old_hbp_vr = ratings[1]['hbp']
ratings[1]['hbp'] = 1.0
ratings[1]['single_center'] += (old_hbp_vr - 1.0)
# Remove triples vR
old_triple_vr = ratings[1]['triple']
ratings[1]['triple'] = 0.0
ratings[1]['single_center'] += old_triple_vr
# Remove Double2 vR and move to singles
old_double2_vr = ratings[1]['double_two']
ratings[1]['double_two'] = 0.0
ratings[1]['single_center'] += old_double2_vr
# Remove Flyout LF-B vR and move to strikeouts
old_flyout_lf_vr = ratings[1]['flyout_lf_b']
ratings[1]['flyout_lf_b'] = 0.0
ratings[1]['strikeout'] += old_flyout_lf_vr
# Set pull rate to 62% vR
ratings[1]['pull_rate'] = 0.62
ratings[1]['center_rate'] = 0.25
ratings[1]['slap_rate'] = 0.13
# 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:
rating['groundout_b'] += diff
rating['groundout_b'] = round(rating['groundout_b'] * 20) / 20
# Adjust groundout ratios to 4:2:1 (A:B:C)
print(f"\n✓ Adjusting groundout ratios to 4:2:1 (A:B:C)...")
for rating in ratings:
total_groundouts = rating['groundout_a'] + rating['groundout_b'] + rating['groundout_c']
rating['groundout_a'] = round(total_groundouts * (4/7) * 20) / 20
rating['groundout_b'] = round(total_groundouts * (2/7) * 20) / 20
rating['groundout_c'] = round(total_groundouts * (1/7) * 20) / 20
# Set Flyout A to 1.0 and move removed chances to strikeouts
print(f"✓ Setting Flyout A to 1.0 both sides...")
for rating in ratings:
old_flyout_a = rating['flyout_a']
rating['flyout_a'] = 1.0
rating['strikeout'] += (old_flyout_a - 1.0)
# Rebalance again after adjustments
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:
rating['groundout_b'] += diff
rating['groundout_b'] = round(rating['groundout_b'] * 20) / 20
# Recalculate rate stats
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 adjusted ratings
print("\n" + "="*70)
print("FINAL RATINGS (TWO-COLUMN TABLE)")
print("="*70)
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"{'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 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.850)")
print(f"\nBaserunning:")
print(f" Steal: {baserunning['steal_low']}-{baserunning['steal_high']} (Jump: {baserunning['steal_jump']})")
print(f" Running: {baserunning['running']} Hit-and-Run: {baserunning['hit_and_run']} Bunting: {baserunning['bunting']}")
# Summary
print("\n" + "="*70)
print("SUMMARY")
print("="*70)
print(f"\nPlayer: Sphealthamus Sphealy Spheal Sr ({hand})")
print(f"Position: 1B (Range 5, Error 3)")
print(f"Cardset: 29 (Custom Characters)")
print(f"Description: 05 Custom")
print(f"Total OPS: {total_ops:.3f} / 0.850 target")
print(f"Pull Rate: {vl['pull_rate']*100:.0f}% vL / {vr['pull_rate']*100:.0f}% vR (Target: 28% vL, 62% vR)")
print(f"BP-HR:HR ratio vR: {vr['bp_homerun']:.0f}:{vr['homerun']:.0f} (Target: 3:2)")
print("\n" + "="*70)
print("DATABASE CREATION")
print("="*70)
# Create database records
bbref_id = f"custom_{name_last.lower().replace(' ', '')}{name_first[0].lower()}01"
# Step 1: Create/verify MLBPlayer record
print(f"\n✓ Checking for existing MLBPlayer record...")
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:
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}"
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}")
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:
print(f" Creating new Player record...")
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,
'rarity_id': 5,
'image': temp_image_url,
'set_num': 9999,
'pos_1': '1B',
}
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}")
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': baserunning['bunting'],
}]
}
await db_put('battingcards', payload=batting_card_payload, timeout=10)
print(f" BattingCard created")
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
print(f"\n✓ Creating CardPosition (1B)...")
positions_payload = {
'positions': [{
'player_id': player_id,
'variant': 0,
'position': '1B',
'innings': 1,
'range': 5,
'error': 3,
}]
}
await db_put('cardpositions', payload=positions_payload, timeout=10)
print(f" Position created: 1B (Range 5, Error 3)")
# Step 6: Update rarity and cost
print(f"\n✓ Updating rarity and cost...")
target_rarity_id = 3 # Starter
target_cost = 188
await db_patch('players', object_id=player_id, params=[('rarity_id', target_rarity_id)])
await db_patch('players', object_id=player_id, params=[('cost', target_cost)])
print(f" Rarity set to: Starter (ID 3)")
print(f" Cost set to: {target_cost}")
# Step 7: Skip image generation for now
print(f"\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(f"\nSphealthamus Sphealy Spheal Sr created successfully!")
print(f" Player ID: {player_id}")
print(f" BattingCard ID: {battingcard_id}")
print(f" Position: 1B (Range 5, Error 3)")
print(f" Bunting: D Hit-and-Run: D")
print(f" Running: {baserunning['running']}")
print(f" Stealing: {baserunning['steal_low']}-{baserunning['steal_high']} ({baserunning['steal_jump']})")
print(f" Pull Rates: {vl['pull_rate']*100:.0f}% vL, {vr['pull_rate']*100:.0f}% vR")
print(f" Rarity: Starter")
print(f" Cost: {target_cost}")
print(f" Total OPS: {total_ops:.3f}")
print(f"\n Card Preview URL: {api_image_url}")
print(f" (Image not yet uploaded to S3 - awaiting review)")
print("\n" + "="*70)
if __name__ == "__main__":
asyncio.run(create_sphealthamus_spheal())