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

634 lines
22 KiB
Python

"""
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("\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("\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("\n✓ Applying power distribution adjustments...")
print(" Setting HR to 0.0 for both sides")
print(" 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("\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("\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(" Triples removed and redistributed to doubles")
# Adjust groundball distribution
print("\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("\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("\nPlayer: Valerie-Hecate Theolia (L)")
print("Position: C (Catcher)")
print("Cardset: 29 (Custom Characters)")
print("Description: 05 Custom")
print(f"Total OPS: {total_ops:.3f} / 0.855 target")
print("\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("\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("\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(" Proceeding without MLBPlayer linkage...")
mlbplayer_id = None
# Step 2: Create or update Player record
print("\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(" Updated image URL")
else:
# Create new player
print(" 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(" Updated with correct image URL")
# Step 3: Create BattingCard
print("\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(" 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("\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(" Ratings created (vL and vR)")
# Step 5: Create CardPositions with defensive ratings
print("\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(" Position created: C (Range 3, Error 5, PB 6, Overthrow 3, Arm 0)")
# Step 6: Update rarity and cost
print("\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(" Rarity set to: Starter (ID 3)")
print(" Cost set to: 91")
# Step 7: Generate card image and upload to S3
print("\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(" Updated player image URL to S3")
print("\n" + "=" * 70)
print("✅ SUCCESS!")
print("=" * 70)
print("\nValerie-Hecate Theolia created successfully!")
print(f" Player ID: {player_id}")
print(f" BattingCard ID: {battingcard_id}")
print(" Position: C")
print(" Bunting: A")
print(" Rarity: Starter")
print(" 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())