Add custom card profiles, S3 upload with timestamp cache-busting, and CLI enhancements
- Add Sippie Swartzel custom batter profile (0.820 OPS, SS/RF, no HR power) - Update Kalin Young profile (0.891 OPS, All-Star rarity) - Update Admiral Ball Traits profile with innings field - Fix S3 cache-busting to include Unix timestamp for same-day updates - Add pd_cards/core/upload.py and scouting.py modules - Add custom card submission scripts and documentation - Add uv.lock for dependency tracking
This commit is contained in:
parent
9a121d370f
commit
1de8b1db2f
@ -10,22 +10,24 @@ from exceptions import logger
|
||||
|
||||
|
||||
# Configuration
|
||||
CARDSET_NAME = '2005 Live'
|
||||
START_ID = None # Integer to only start pulling cards at player_id START_ID
|
||||
TEST_COUNT = 9999 # integer to stop after TEST_COUNT calls
|
||||
CARDSET_NAME = "2005 Live"
|
||||
START_ID = None # Integer to only start pulling cards at player_id START_ID
|
||||
TEST_COUNT = 9999 # integer to stop after TEST_COUNT calls
|
||||
HTML_CARDS = False # boolean to only check and not generate cards
|
||||
SKIP_ARMS = False
|
||||
SKIP_BATS = False
|
||||
|
||||
# AWS Configuration
|
||||
AWS_BUCKET_NAME = 'paper-dynasty' # Change to your bucket name
|
||||
AWS_REGION = 'us-east-1' # Change to your region
|
||||
S3_BASE_URL = f'https://{AWS_BUCKET_NAME}.s3.{AWS_REGION}.amazonaws.com'
|
||||
UPLOAD_TO_S3 = True # Set to False to skip S3 upload (testing) - STEP 6: Upload validated cards
|
||||
AWS_BUCKET_NAME = "paper-dynasty" # Change to your bucket name
|
||||
AWS_REGION = "us-east-1" # Change to your region
|
||||
S3_BASE_URL = f"https://{AWS_BUCKET_NAME}.s3.{AWS_REGION}.amazonaws.com"
|
||||
UPLOAD_TO_S3 = (
|
||||
True # Set to False to skip S3 upload (testing) - STEP 6: Upload validated cards
|
||||
)
|
||||
UPDATE_PLAYER_URLS = True # Set to False to skip player URL updates (testing) - STEP 6: Update player URLs
|
||||
|
||||
# Initialize S3 client
|
||||
s3_client = boto3.client('s3', region_name=AWS_REGION) if UPLOAD_TO_S3 else None
|
||||
s3_client = boto3.client("s3", region_name=AWS_REGION) if UPLOAD_TO_S3 else None
|
||||
|
||||
|
||||
async def fetch_card_image(session, card_url: str, timeout: int = 6) -> bytes:
|
||||
@ -42,17 +44,25 @@ async def fetch_card_image(session, card_url: str, timeout: int = 6) -> bytes:
|
||||
"""
|
||||
import aiohttp
|
||||
|
||||
async with session.get(card_url, timeout=aiohttp.ClientTimeout(total=timeout)) as resp:
|
||||
async with session.get(
|
||||
card_url, timeout=aiohttp.ClientTimeout(total=timeout)
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
logger.info(f'Fetched card image from {card_url}')
|
||||
logger.info(f"Fetched card image from {card_url}")
|
||||
return await resp.read()
|
||||
else:
|
||||
error_text = await resp.text()
|
||||
logger.error(f'Failed to fetch card: {error_text}')
|
||||
raise ValueError(f'Card fetch error: {error_text}')
|
||||
logger.error(f"Failed to fetch card: {error_text}")
|
||||
raise ValueError(f"Card fetch error: {error_text}")
|
||||
|
||||
|
||||
def upload_card_to_s3(image_data: bytes, player_id: int, card_type: str, release_date: str, cardset_id: int) -> str:
|
||||
def upload_card_to_s3(
|
||||
image_data: bytes,
|
||||
player_id: int,
|
||||
card_type: str,
|
||||
release_date: str,
|
||||
cardset_id: int,
|
||||
) -> str:
|
||||
"""
|
||||
Upload card image to S3 and return the S3 URL with cache-busting param.
|
||||
|
||||
@ -67,60 +77,65 @@ def upload_card_to_s3(image_data: bytes, player_id: int, card_type: str, release
|
||||
Full S3 URL with ?d= parameter
|
||||
"""
|
||||
# Format cardset_id with 3 digits and leading zeros
|
||||
cardset_str = f'{cardset_id:03d}'
|
||||
s3_key = f'cards/cardset-{cardset_str}/player-{player_id}/{card_type}card.png'
|
||||
cardset_str = f"{cardset_id:03d}"
|
||||
s3_key = f"cards/cardset-{cardset_str}/player-{player_id}/{card_type}card.png"
|
||||
|
||||
try:
|
||||
s3_client.put_object(
|
||||
Bucket=AWS_BUCKET_NAME,
|
||||
Key=s3_key,
|
||||
Body=image_data,
|
||||
ContentType='image/png',
|
||||
CacheControl='public, max-age=300', # 5 minute cache
|
||||
ContentType="image/png",
|
||||
CacheControl="public, max-age=300", # 5 minute cache
|
||||
Metadata={
|
||||
'player-id': str(player_id),
|
||||
'card-type': card_type,
|
||||
'upload-date': datetime.datetime.now().isoformat()
|
||||
}
|
||||
"player-id": str(player_id),
|
||||
"card-type": card_type,
|
||||
"upload-date": datetime.datetime.now().isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
# Return URL with cache-busting parameter
|
||||
s3_url = f'{S3_BASE_URL}/{s3_key}?d={release_date}'
|
||||
logger.info(f'Uploaded {card_type} card for player {player_id} to S3: {s3_url}')
|
||||
s3_url = f"{S3_BASE_URL}/{s3_key}?d={release_date}"
|
||||
logger.info(f"Uploaded {card_type} card for player {player_id} to S3: {s3_url}")
|
||||
return s3_url
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to upload {card_type} card for player {player_id}: {e}')
|
||||
logger.error(f"Failed to upload {card_type} card for player {player_id}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def main(args):
|
||||
import aiohttp
|
||||
|
||||
print(f'Searching for cardset: {CARDSET_NAME}')
|
||||
c_query = await db_get('cardsets', params=[('name', CARDSET_NAME)])
|
||||
print(f"Searching for cardset: {CARDSET_NAME}")
|
||||
c_query = await db_get("cardsets", params=[("name", CARDSET_NAME)])
|
||||
|
||||
if not c_query or c_query['count'] == 0:
|
||||
print(f'I do not see a cardset named {CARDSET_NAME}')
|
||||
if not c_query or c_query["count"] == 0:
|
||||
print(f"I do not see a cardset named {CARDSET_NAME}")
|
||||
return
|
||||
cardset = c_query['cardsets'][0]
|
||||
cardset = c_query["cardsets"][0]
|
||||
del c_query
|
||||
|
||||
p_query = await db_get(
|
||||
'players',
|
||||
params=[('inc_dex', False), ('cardset_id', cardset['id']), ('short_output', True)]
|
||||
"players",
|
||||
params=[
|
||||
("inc_dex", False),
|
||||
("cardset_id", cardset["id"]),
|
||||
("short_output", True),
|
||||
],
|
||||
)
|
||||
if not p_query or p_query['count'] == 0:
|
||||
raise ValueError('No players returned from Paper Dynasty API')
|
||||
all_players = p_query['players']
|
||||
if not p_query or p_query["count"] == 0:
|
||||
raise ValueError("No players returned from Paper Dynasty API")
|
||||
all_players = p_query["players"]
|
||||
del p_query
|
||||
|
||||
# Generate release date for cache busting
|
||||
# Generate release date for cache busting (include timestamp for same-day updates)
|
||||
now = datetime.datetime.now()
|
||||
release_date = f'{now.year}-{now.month}-{now.day}'
|
||||
timestamp = int(now.timestamp())
|
||||
release_date = f"{now.year}-{now.month}-{now.day}-{timestamp}"
|
||||
|
||||
# PD API base URL for card generation
|
||||
PD_API_URL = 'https://pd.manticorum.com/api'
|
||||
PD_API_URL = "https://pd.manticorum.com/api"
|
||||
|
||||
errors = []
|
||||
successes = []
|
||||
@ -130,61 +145,72 @@ async def main(args):
|
||||
count = -1
|
||||
start_time = datetime.datetime.now()
|
||||
|
||||
print(f'\nRelease date for cards: {release_date}')
|
||||
print(f'S3 Upload: {"ENABLED" if UPLOAD_TO_S3 else "DISABLED"}')
|
||||
print(f'URL Update: {"ENABLED" if UPDATE_PLAYER_URLS else "DISABLED"}\n')
|
||||
print(f"\nRelease date for cards: {release_date}")
|
||||
print(f"S3 Upload: {'ENABLED' if UPLOAD_TO_S3 else 'DISABLED'}")
|
||||
print(f"URL Update: {'ENABLED' if UPDATE_PLAYER_URLS else 'DISABLED'}\n")
|
||||
|
||||
# Create persistent aiohttp session for all card fetches
|
||||
async with aiohttp.ClientSession() as session:
|
||||
for x in all_players:
|
||||
if 'pitching' in x['image'] and SKIP_ARMS:
|
||||
if "pitching" in x["image"] and SKIP_ARMS:
|
||||
pass
|
||||
elif 'batting' in x['image'] and SKIP_BATS:
|
||||
elif "batting" in x["image"] and SKIP_BATS:
|
||||
pass
|
||||
elif START_ID is not None and START_ID > x['player_id']:
|
||||
elif START_ID is not None and START_ID > x["player_id"]:
|
||||
pass
|
||||
elif 'sombaseball' in x['image']:
|
||||
errors.append((x, f'Bad card url: {x["image"]}'))
|
||||
elif "sombaseball" in x["image"]:
|
||||
errors.append((x, f"Bad card url: {x['image']}"))
|
||||
else:
|
||||
count += 1
|
||||
if count % 20 == 0:
|
||||
print(f'Card #{count + 1} being pulled is {x["p_name"]}...')
|
||||
print(f"Card #{count + 1} being pulled is {x['p_name']}...")
|
||||
elif TEST_COUNT is not None and TEST_COUNT < count:
|
||||
print(f'Done test run')
|
||||
print(f"Done test run")
|
||||
break
|
||||
|
||||
# Determine card type from existing image URL
|
||||
card_type = 'pitching' if 'pitching' in x['image'] else 'batting'
|
||||
card_type = "pitching" if "pitching" in x["image"] else "batting"
|
||||
|
||||
# Generate card URL from PD API (forces fresh generation from database)
|
||||
pd_card_url = f'{PD_API_URL}/v2/players/{x["player_id"]}/{card_type}card?d={release_date}'
|
||||
pd_card_url = f"{PD_API_URL}/v2/players/{x['player_id']}/{card_type}card?d={release_date}"
|
||||
|
||||
if HTML_CARDS:
|
||||
card_url = f'{pd_card_url}&html=true'
|
||||
card_url = f"{pd_card_url}&html=true"
|
||||
timeout = 2
|
||||
else:
|
||||
card_url = pd_card_url
|
||||
timeout = 6
|
||||
|
||||
try:
|
||||
|
||||
# Upload to S3 if enabled
|
||||
if UPLOAD_TO_S3 and not HTML_CARDS:
|
||||
# Fetch card image bytes directly
|
||||
image_bytes = await fetch_card_image(session, card_url, timeout=timeout)
|
||||
s3_url = upload_card_to_s3(image_bytes, x['player_id'], card_type, release_date, cardset['id'])
|
||||
uploads.append((x['player_id'], card_type, s3_url))
|
||||
image_bytes = await fetch_card_image(
|
||||
session, card_url, timeout=timeout
|
||||
)
|
||||
s3_url = upload_card_to_s3(
|
||||
image_bytes,
|
||||
x["player_id"],
|
||||
card_type,
|
||||
release_date,
|
||||
cardset["id"],
|
||||
)
|
||||
uploads.append((x["player_id"], card_type, s3_url))
|
||||
|
||||
# Update player record with new S3 URL
|
||||
if UPDATE_PLAYER_URLS:
|
||||
await db_patch('players', object_id=x['player_id'], params=[
|
||||
('image', s3_url)
|
||||
])
|
||||
url_updates.append((x['player_id'], card_type, s3_url))
|
||||
logger.info(f'Updated player {x["player_id"]} image URL to S3')
|
||||
await db_patch(
|
||||
"players",
|
||||
object_id=x["player_id"],
|
||||
params=[("image", s3_url)],
|
||||
)
|
||||
url_updates.append((x["player_id"], card_type, s3_url))
|
||||
logger.info(
|
||||
f"Updated player {x['player_id']} image URL to S3"
|
||||
)
|
||||
else:
|
||||
# Just validate card exists (old behavior)
|
||||
logger.info(f'calling the card url')
|
||||
logger.info(f"calling the card url")
|
||||
resp = await url_get(card_url, timeout=timeout)
|
||||
|
||||
except ConnectionError as e:
|
||||
@ -197,40 +223,56 @@ async def main(args):
|
||||
errors.append((x, e))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'S3 upload/update failed for player {x["player_id"]}: {e}')
|
||||
errors.append((x, f'S3 error: {e}'))
|
||||
logger.error(
|
||||
f"S3 upload/update failed for player {x['player_id']}: {e}"
|
||||
)
|
||||
errors.append((x, f"S3 error: {e}"))
|
||||
continue
|
||||
|
||||
# Handle image2 (dual-position players)
|
||||
if x['image2'] is not None:
|
||||
if x["image2"] is not None:
|
||||
# Determine second card type
|
||||
card_type2 = 'pitching' if 'pitching' in x['image2'] else 'batting'
|
||||
card_type2 = "pitching" if "pitching" in x["image2"] else "batting"
|
||||
|
||||
# Generate card URL from PD API (forces fresh generation from database)
|
||||
pd_card_url2 = f'{PD_API_URL}/v2/players/{x["player_id"]}/{card_type2}card?d={release_date}'
|
||||
pd_card_url2 = f"{PD_API_URL}/v2/players/{x['player_id']}/{card_type2}card?d={release_date}"
|
||||
|
||||
if HTML_CARDS:
|
||||
card_url2 = f'{pd_card_url2}&html=true'
|
||||
card_url2 = f"{pd_card_url2}&html=true"
|
||||
else:
|
||||
card_url2 = pd_card_url2
|
||||
|
||||
if 'sombaseball' in x['image2']:
|
||||
errors.append((x, f'Bad card url: {x["image2"]}'))
|
||||
if "sombaseball" in x["image2"]:
|
||||
errors.append((x, f"Bad card url: {x['image2']}"))
|
||||
else:
|
||||
try:
|
||||
if UPLOAD_TO_S3 and not HTML_CARDS:
|
||||
# Fetch second card image bytes directly from PD API
|
||||
image_bytes2 = await fetch_card_image(session, card_url2, timeout=6)
|
||||
s3_url2 = upload_card_to_s3(image_bytes2, x['player_id'], card_type2, release_date, cardset['id'])
|
||||
uploads.append((x['player_id'], card_type2, s3_url2))
|
||||
image_bytes2 = await fetch_card_image(
|
||||
session, card_url2, timeout=6
|
||||
)
|
||||
s3_url2 = upload_card_to_s3(
|
||||
image_bytes2,
|
||||
x["player_id"],
|
||||
card_type2,
|
||||
release_date,
|
||||
cardset["id"],
|
||||
)
|
||||
uploads.append((x["player_id"], card_type2, s3_url2))
|
||||
|
||||
# Update player record with new S3 URL for image2
|
||||
if UPDATE_PLAYER_URLS:
|
||||
await db_patch('players', object_id=x['player_id'], params=[
|
||||
('image2', s3_url2)
|
||||
])
|
||||
url_updates.append((x['player_id'], card_type2, s3_url2))
|
||||
logger.info(f'Updated player {x["player_id"]} image2 URL to S3')
|
||||
await db_patch(
|
||||
"players",
|
||||
object_id=x["player_id"],
|
||||
params=[("image2", s3_url2)],
|
||||
)
|
||||
url_updates.append(
|
||||
(x["player_id"], card_type2, s3_url2)
|
||||
)
|
||||
logger.info(
|
||||
f"Updated player {x['player_id']} image2 URL to S3"
|
||||
)
|
||||
else:
|
||||
# Just validate card exists (old behavior)
|
||||
resp = await url_get(card_url2, timeout=6)
|
||||
@ -247,42 +289,44 @@ async def main(args):
|
||||
errors.append((x, e))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'S3 upload/update failed for player {x["player_id"]} image2: {e}')
|
||||
errors.append((x, f'S3 error (image2): {e}'))
|
||||
logger.error(
|
||||
f"S3 upload/update failed for player {x['player_id']} image2: {e}"
|
||||
)
|
||||
errors.append((x, f"S3 error (image2): {e}"))
|
||||
else:
|
||||
successes.append(x)
|
||||
|
||||
# Print summary
|
||||
print(f'\n{"="*60}')
|
||||
print(f'SUMMARY')
|
||||
print(f'{"="*60}')
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"SUMMARY")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
if len(errors) > 0:
|
||||
logger.error(f'All Errors:')
|
||||
logger.error(f"All Errors:")
|
||||
for x in errors:
|
||||
logger.error(f'ID {x[0]["player_id"]} {x[0]["p_name"]} - Error: {x[1]}')
|
||||
logger.error(f"ID {x[0]['player_id']} {x[0]['p_name']} - Error: {x[1]}")
|
||||
|
||||
if len(successes) > 0:
|
||||
logger.debug(f'All Successes:')
|
||||
logger.debug(f"All Successes:")
|
||||
for x in successes:
|
||||
logger.info(f'ID {x["player_id"]} {x["p_name"]}')
|
||||
logger.info(f"ID {x['player_id']} {x['p_name']}")
|
||||
|
||||
p_run_time = datetime.datetime.now() - start_time
|
||||
|
||||
print(f'\nErrors: {len(errors)}')
|
||||
print(f'Successes: {len(successes)}')
|
||||
print(f"\nErrors: {len(errors)}")
|
||||
print(f"Successes: {len(successes)}")
|
||||
|
||||
if UPLOAD_TO_S3:
|
||||
print(f'S3 Uploads: {len(uploads)}')
|
||||
print(f"S3 Uploads: {len(uploads)}")
|
||||
if len(uploads) > 0:
|
||||
print(f' First upload: {uploads[0][2]}')
|
||||
print(f" First upload: {uploads[0][2]}")
|
||||
|
||||
if UPDATE_PLAYER_URLS:
|
||||
print(f'URL Updates: {len(url_updates)}')
|
||||
print(f"URL Updates: {len(url_updates)}")
|
||||
|
||||
print(f'\nTotal runtime: {p_run_time.total_seconds():.2f} seconds')
|
||||
print(f'{"="*60}')
|
||||
print(f"\nTotal runtime: {p_run_time.total_seconds():.2f} seconds")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main(sys.argv[1:]))
|
||||
|
||||
203
custom_cards/submit_kalin_young.py
Normal file
203
custom_cards/submit_kalin_young.py
Normal file
@ -0,0 +1,203 @@
|
||||
"""
|
||||
Submit Kalin Young updates to Paper Dynasty Database
|
||||
|
||||
Updates:
|
||||
- BattingCardRatings: +1.55 walks/singles, -3.10 strikeouts (both splits)
|
||||
- CardPositions: RF Range 2/Error 6, LF Range 4/Error 6
|
||||
- Target OPS: 0.880 (from 0.837)
|
||||
|
||||
Does NOT upload to AWS - user will review first.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from db_calls import db_get, db_put, db_patch
|
||||
from datetime import datetime
|
||||
|
||||
# Player info
|
||||
PLAYER_ID = 13009
|
||||
BATTINGCARD_ID = 6072
|
||||
CARDSET_ID = 29
|
||||
|
||||
# New ratings (from kalin_young_preview.py)
|
||||
NEW_RATINGS = {
|
||||
'L': { # vs LHP
|
||||
'battingcard_id': BATTINGCARD_ID,
|
||||
'vs_hand': 'L',
|
||||
'homerun': 2.05,
|
||||
'bp_homerun': 2.00,
|
||||
'triple': 0.00,
|
||||
'double_three': 0.00,
|
||||
'double_two': 6.00,
|
||||
'double_pull': 2.35,
|
||||
'single_two': 5.05,
|
||||
'single_one': 5.10,
|
||||
'single_center': 5.80, # +1.55
|
||||
'bp_single': 5.00,
|
||||
'walk': 16.05, # +1.55
|
||||
'hbp': 1.00,
|
||||
'strikeout': 15.40, # -3.10
|
||||
'lineout': 15.00,
|
||||
'popout': 0.00,
|
||||
'flyout_a': 0.00,
|
||||
'flyout_bq': 0.75,
|
||||
'flyout_lf_b': 3.65,
|
||||
'flyout_rf_b': 3.95,
|
||||
'groundout_a': 3.95,
|
||||
'groundout_b': 10.00,
|
||||
'groundout_c': 4.90,
|
||||
'pull_rate': 0.33,
|
||||
'center_rate': 0.37,
|
||||
'slap_rate': 0.30,
|
||||
},
|
||||
'R': { # vs RHP
|
||||
'battingcard_id': BATTINGCARD_ID,
|
||||
'vs_hand': 'R',
|
||||
'homerun': 2.10,
|
||||
'bp_homerun': 1.00,
|
||||
'triple': 1.25,
|
||||
'double_three': 0.00,
|
||||
'double_two': 6.35,
|
||||
'double_pull': 1.80,
|
||||
'single_two': 5.40,
|
||||
'single_one': 4.95,
|
||||
'single_center': 6.05, # +1.55
|
||||
'bp_single': 5.00,
|
||||
'walk': 14.55, # +1.55
|
||||
'hbp': 2.00,
|
||||
'strikeout': 17.90, # -3.10
|
||||
'lineout': 12.00,
|
||||
'popout': 1.00,
|
||||
'flyout_a': 0.00,
|
||||
'flyout_bq': 0.50,
|
||||
'flyout_lf_b': 4.10,
|
||||
'flyout_rf_b': 4.00,
|
||||
'groundout_a': 4.00,
|
||||
'groundout_b': 5.00,
|
||||
'groundout_c': 9.05,
|
||||
'pull_rate': 0.25,
|
||||
'center_rate': 0.40,
|
||||
'slap_rate': 0.35,
|
||||
}
|
||||
}
|
||||
|
||||
# New defensive positions
|
||||
NEW_POSITIONS = [
|
||||
{
|
||||
'player_id': PLAYER_ID,
|
||||
'position': 'RF',
|
||||
'range': 2, # was 3
|
||||
'error': 6, # was 7
|
||||
'arm': 0,
|
||||
'fielding_pct': None
|
||||
},
|
||||
{
|
||||
'player_id': PLAYER_ID,
|
||||
'position': 'LF',
|
||||
'range': 4,
|
||||
'error': 6, # was 7
|
||||
'arm': 0,
|
||||
'fielding_pct': None
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def calc_ops(r):
|
||||
"""Calculate OPS from ratings dict."""
|
||||
avg = (r['homerun'] + r['bp_homerun']/2 + r['triple'] + r['double_three'] +
|
||||
r['double_two'] + r['double_pull'] + r['single_two'] + r['single_one'] +
|
||||
r['single_center'] + r['bp_single']/2) / 108
|
||||
obp = avg + (r['hbp'] + r['walk']) / 108
|
||||
slg = (r['homerun']*4 + r['bp_homerun']*2 + r['triple']*3 +
|
||||
(r['double_three'] + r['double_two'] + r['double_pull'])*2 +
|
||||
r['single_two'] + r['single_one'] + r['single_center'] + r['bp_single']/2) / 108
|
||||
return obp + slg
|
||||
|
||||
|
||||
def verify_total(r):
|
||||
"""Verify ratings sum to 108."""
|
||||
return sum([
|
||||
r['homerun'], r['bp_homerun'], r['triple'],
|
||||
r['double_three'], r['double_two'], r['double_pull'],
|
||||
r['single_two'], r['single_one'], r['single_center'], r['bp_single'],
|
||||
r['walk'], r['hbp'], r['strikeout'],
|
||||
r['lineout'], r['popout'],
|
||||
r['flyout_a'], r['flyout_bq'], r['flyout_lf_b'], r['flyout_rf_b'],
|
||||
r['groundout_a'], r['groundout_b'], r['groundout_c']
|
||||
])
|
||||
|
||||
|
||||
async def main():
|
||||
"""Update Kalin Young's batting card ratings and defensive positions."""
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("UPDATING KALIN YOUNG IN DATABASE")
|
||||
print("="*70)
|
||||
print(f"\nPlayer ID: {PLAYER_ID}")
|
||||
print(f"BattingCard ID: {BATTINGCARD_ID}")
|
||||
|
||||
# Verify totals
|
||||
print("\nVerifying ratings totals...")
|
||||
for hand, ratings in NEW_RATINGS.items():
|
||||
total = verify_total(ratings)
|
||||
ops = calc_ops(ratings)
|
||||
print(f" vs {hand}HP: Total={total:.2f}, OPS={ops:.3f}")
|
||||
if abs(total - 108.0) > 0.01:
|
||||
print(f" ⚠ WARNING: Total is not 108!")
|
||||
return
|
||||
|
||||
# Calculate combined OPS
|
||||
ops_vl = calc_ops(NEW_RATINGS['L'])
|
||||
ops_vr = calc_ops(NEW_RATINGS['R'])
|
||||
total_ops = (ops_vl + ops_vr + min(ops_vl, ops_vr)) / 3
|
||||
print(f" Combined OPS: {total_ops:.3f}")
|
||||
|
||||
# Step 1: Update BattingCardRatings
|
||||
print("\nUpdating BattingCardRatings...")
|
||||
ratings_list = [NEW_RATINGS['L'], NEW_RATINGS['R']]
|
||||
ratings_payload = {'ratings': ratings_list}
|
||||
|
||||
try:
|
||||
result = await db_put('battingcardratings', payload=ratings_payload, timeout=10)
|
||||
print(f" ✓ Updated ratings: {result}")
|
||||
except Exception as e:
|
||||
print(f" ✗ Error updating ratings: {e}")
|
||||
return
|
||||
|
||||
# Step 2: Update CardPositions
|
||||
print("\nUpdating CardPositions...")
|
||||
positions_payload = {'positions': NEW_POSITIONS}
|
||||
|
||||
try:
|
||||
result = await db_put('cardpositions', payload=positions_payload, timeout=10)
|
||||
print(f" ✓ Updated positions: {result}")
|
||||
except Exception as e:
|
||||
print(f" ✗ Error updating positions: {e}")
|
||||
return
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*70)
|
||||
print("✓ KALIN YOUNG UPDATED SUCCESSFULLY!")
|
||||
print("="*70)
|
||||
print(f"\nPlayer ID: {PLAYER_ID}")
|
||||
print(f"Cardset: 2005 Custom (ID: {CARDSET_ID})")
|
||||
print(f"\nRating Changes:")
|
||||
print(f" Walk vL: 14.50 → 16.05 (+1.55)")
|
||||
print(f" Walk vR: 13.00 → 14.55 (+1.55)")
|
||||
print(f" Single (Center) vL: 4.25 → 5.80 (+1.55)")
|
||||
print(f" Single (Center) vR: 4.50 → 6.05 (+1.55)")
|
||||
print(f" Strikeout vL: 18.50 → 15.40 (-3.10)")
|
||||
print(f" Strikeout vR: 21.00 → 17.90 (-3.10)")
|
||||
print(f"\nOPS: 0.837 → {total_ops:.3f}")
|
||||
print(f"\nDefensive Updates:")
|
||||
print(f" RF: Range 3→2, Error 7→6, Arm 0")
|
||||
print(f" LF: Range 4, Error 7→6, Arm 0")
|
||||
print(f"\n⚠️ AWS upload skipped - review card first:")
|
||||
now = datetime.now()
|
||||
release_date = f"{now.year}-{now.month}-{now.day}"
|
||||
print(f" PNG: https://pd.manticorum.com/api/v2/players/{PLAYER_ID}/battingcard?d={release_date}")
|
||||
print(f" HTML: https://pd.manticorum.com/api/v2/players/{PLAYER_ID}/battingcard?d={release_date}&html=true")
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
318
custom_cards/submit_tony_smehrik.py
Normal file
318
custom_cards/submit_tony_smehrik.py
Normal file
@ -0,0 +1,318 @@
|
||||
"""
|
||||
Submit Tony Smehrik to Paper Dynasty Database
|
||||
|
||||
Custom pitcher with:
|
||||
- Hand: Left
|
||||
- Position: SP/RP (starter_rating: 5, relief_rating: 5)
|
||||
- Combined OPS: 0.585
|
||||
- OPS vs L: 0.465 (dominant same-side)
|
||||
- OPS vs R: 0.645 (weaker opposite-side)
|
||||
- K-rate vs L: ~20% (average)
|
||||
- K-rate vs R: ~8% (very low)
|
||||
- FB% vs L: ~44% (average)
|
||||
- FB% vs R: ~56% (high)
|
||||
- Team: Custom Ballplayers
|
||||
- Cardset ID: 29
|
||||
|
||||
NO AWS UPLOAD - preview via PD API first
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from db_calls import db_get, db_post, db_patch, db_put
|
||||
from custom_cards.tony_smehrik_preview import calculate_pitcher_rating, DEFAULT_XCHECKS
|
||||
|
||||
# Configuration
|
||||
CARDSET_ID = 29
|
||||
CARDSET_NAME = "2005 Custom"
|
||||
SEASON = 2005
|
||||
PLAYER_DESCRIPTION = "2005 Custom"
|
||||
|
||||
# Player info
|
||||
PLAYER_NAME_FIRST = "Tony"
|
||||
PLAYER_NAME_LAST = "Smehrik"
|
||||
HAND = "L"
|
||||
STARTER_RATING = 5
|
||||
RELIEF_RATING = 5
|
||||
CLOSER_RATING = None
|
||||
|
||||
|
||||
async def main():
|
||||
"""Create Tony Smehrik pitcher card in database."""
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("SUBMITTING TONY SMEHRIK TO DATABASE")
|
||||
print("="*70)
|
||||
|
||||
# Step 1: Calculate ratings (same as preview)
|
||||
print("\nCalculating ratings...")
|
||||
pit_hand = 'L'
|
||||
|
||||
vl = calculate_pitcher_rating(
|
||||
vs_hand='L',
|
||||
pit_hand=pit_hand,
|
||||
avg=0.185,
|
||||
obp=0.240,
|
||||
slg=0.225,
|
||||
bb_pct=0.055,
|
||||
k_pct=0.20,
|
||||
hr_per_hit=0.04,
|
||||
triple_per_hit=0.02,
|
||||
double_per_hit=0.22,
|
||||
fb_pct=0.35,
|
||||
gb_pct=0.45,
|
||||
hard_pct=0.30,
|
||||
med_pct=0.50,
|
||||
soft_pct=0.20,
|
||||
oppo_pct=0.26,
|
||||
hr_fb_pct=0.06,
|
||||
)
|
||||
|
||||
vr = calculate_pitcher_rating(
|
||||
vs_hand='R',
|
||||
pit_hand=pit_hand,
|
||||
avg=0.245,
|
||||
obp=0.305,
|
||||
slg=0.340,
|
||||
bb_pct=0.075,
|
||||
k_pct=0.08,
|
||||
hr_per_hit=0.07,
|
||||
triple_per_hit=0.02,
|
||||
double_per_hit=0.24,
|
||||
fb_pct=0.45,
|
||||
gb_pct=0.35,
|
||||
hard_pct=0.34,
|
||||
med_pct=0.46,
|
||||
soft_pct=0.20,
|
||||
oppo_pct=0.26,
|
||||
hr_fb_pct=0.09,
|
||||
)
|
||||
|
||||
# Apply same manual adjustments as preview
|
||||
# HR adjustments: half of all OB chances go to HR
|
||||
vl_total_hits = (vl['homerun'] + vl['bp_homerun'] + vl['triple'] +
|
||||
vl['double_three'] + vl['double_two'] + vl['double_cf'] +
|
||||
vl['single_two'] + vl['single_one'] + vl['single_center'] + vl['bp_single'])
|
||||
vl_total_ob = vl_total_hits + vl['walk'] + vl['hbp']
|
||||
vl_hr_total = vl_total_ob / 2
|
||||
vl['bp_homerun'] = round(vl_hr_total / 2)
|
||||
vl['homerun'] = round((vl_hr_total - vl['bp_homerun']) * 20) / 20
|
||||
vl['hbp'] = 1.0
|
||||
|
||||
vr_total_hits = (vr['homerun'] + vr['bp_homerun'] + vr['triple'] +
|
||||
vr['double_three'] + vr['double_two'] + vr['double_cf'] +
|
||||
vr['single_two'] + vr['single_one'] + vr['single_center'] + vr['bp_single'])
|
||||
vr_total_ob = vr_total_hits + vr['walk'] + vr['hbp']
|
||||
vr_hr_total = vr_total_ob / 2
|
||||
vr['bp_homerun'] = round(vr_hr_total / 2)
|
||||
vr['homerun'] = round((vr_hr_total - vr['bp_homerun']) * 20) / 20
|
||||
vr['hbp'] = 1.0
|
||||
|
||||
# Reduce singles to compensate for HR increase
|
||||
vl_new_hr_total = vl['homerun'] + vl['bp_homerun']
|
||||
vl_hr_increase = vl_new_hr_total
|
||||
vl_singles_to_reduce = vl_hr_increase
|
||||
reduce_from_one = min(vl['single_one'], vl_singles_to_reduce)
|
||||
vl['single_one'] -= reduce_from_one
|
||||
vl_singles_to_reduce -= reduce_from_one
|
||||
reduce_from_two = min(vl['single_two'], vl_singles_to_reduce)
|
||||
vl['single_two'] -= reduce_from_two
|
||||
vl_singles_to_reduce -= reduce_from_two
|
||||
reduce_from_bp = min(vl['bp_single'], vl_singles_to_reduce)
|
||||
vl['bp_single'] -= reduce_from_bp
|
||||
vl_singles_to_reduce -= reduce_from_bp
|
||||
if vl_singles_to_reduce > 0:
|
||||
vl['double_cf'] = max(0, vl['double_cf'] - vl_singles_to_reduce)
|
||||
|
||||
vr_new_hr_total = vr['homerun'] + vr['bp_homerun']
|
||||
vr_old_hr_total = 0.80 + 1.00
|
||||
vr_hr_increase = vr_new_hr_total - vr_old_hr_total
|
||||
vr_singles_to_reduce = vr_hr_increase
|
||||
reduce_from_one = min(vr['single_one'], vr_singles_to_reduce)
|
||||
vr['single_one'] -= reduce_from_one
|
||||
vr_singles_to_reduce -= reduce_from_one
|
||||
reduce_from_two = min(vr['single_two'], vr_singles_to_reduce)
|
||||
vr['single_two'] -= reduce_from_two
|
||||
vr_singles_to_reduce -= reduce_from_two
|
||||
reduce_from_bp = min(vr['bp_single'], vr_singles_to_reduce)
|
||||
vr['bp_single'] -= reduce_from_bp
|
||||
vr_singles_to_reduce -= reduce_from_bp
|
||||
if vr_singles_to_reduce > 0:
|
||||
vr['double_cf'] = max(0, vr['double_cf'] - vr_singles_to_reduce)
|
||||
|
||||
# Force BP-SI values
|
||||
vl['bp_single'] = 5.0
|
||||
vr['bp_single'] = 3.0
|
||||
|
||||
# Adjust strikeouts to hit 108
|
||||
for rating in [vl, vr]:
|
||||
total = sum([
|
||||
rating['homerun'], rating['bp_homerun'], rating['triple'],
|
||||
rating['double_three'], rating['double_two'], rating['double_cf'],
|
||||
rating['single_two'], rating['single_one'], rating['single_center'], rating['bp_single'],
|
||||
rating['hbp'], rating['walk'], rating['strikeout'],
|
||||
rating['flyout_lf_b'], rating['flyout_cf_b'], rating['flyout_rf_b'],
|
||||
rating['groundout_a'], rating['groundout_b'],
|
||||
rating['xcheck_p'], rating['xcheck_c'], rating['xcheck_1b'], rating['xcheck_2b'],
|
||||
rating['xcheck_3b'], rating['xcheck_ss'], rating['xcheck_lf'], rating['xcheck_cf'], rating['xcheck_rf']
|
||||
])
|
||||
diff = 108 - total
|
||||
rating['strikeout'] = round((rating['strikeout'] + diff) * 20) / 20
|
||||
|
||||
# Calculate OPS for verification
|
||||
ops_vl = vl['obp'] + vl['slg']
|
||||
ops_vr = vr['obp'] + vr['slg']
|
||||
combined_ops = (ops_vr + ops_vl + max(ops_vl, ops_vr)) / 3
|
||||
|
||||
print(f" OPS vs L: {ops_vl:.3f}")
|
||||
print(f" OPS vs R: {ops_vr:.3f}")
|
||||
print(f" Combined OPS: {combined_ops:.3f}")
|
||||
|
||||
# Step 2: Verify cardset exists
|
||||
print(f"\nVerifying cardset '{CARDSET_NAME}' (ID: {CARDSET_ID})...")
|
||||
c_query = await db_get('cardsets', params=[('id', CARDSET_ID)])
|
||||
if c_query['count'] == 0:
|
||||
print(f" ERROR: Cardset ID {CARDSET_ID} not found!")
|
||||
return
|
||||
print(f" ✓ Cardset verified")
|
||||
|
||||
# Step 3: Create Player record
|
||||
print("\nCreating Player record...")
|
||||
now = datetime.now()
|
||||
release_date = f"{now.year}-{now.month}-{now.day}"
|
||||
bbref_id = f"custom_{PLAYER_NAME_LAST.lower()}{PLAYER_NAME_FIRST[0].lower()}01"
|
||||
|
||||
player_payload = {
|
||||
'p_name': f'{PLAYER_NAME_FIRST} {PLAYER_NAME_LAST}',
|
||||
'cost': '100', # Default cost
|
||||
'image': 'change-me',
|
||||
'mlbclub': 'Custom Ballplayers',
|
||||
'franchise': 'Custom Ballplayers',
|
||||
'cardset_id': CARDSET_ID,
|
||||
'set_num': 99999,
|
||||
'rarity_id': 3, # Starter rarity
|
||||
'pos_1': 'SP',
|
||||
'pos_2': 'RP',
|
||||
'description': PLAYER_DESCRIPTION,
|
||||
'bbref_id': bbref_id,
|
||||
'fangr_id': 0,
|
||||
'mlbplayer_id': None,
|
||||
'is_custom': True,
|
||||
}
|
||||
|
||||
player = await db_post('players', payload=player_payload)
|
||||
player_id = player['player_id']
|
||||
print(f" ✓ Created Player ID: {player_id}")
|
||||
|
||||
# Update player with API image URL for preview
|
||||
api_image_url = f"https://pd.manticorum.com/api/v2/players/{player_id}/pitchingcard?d={release_date}"
|
||||
await db_patch('players', object_id=player_id, params=[('image', api_image_url)])
|
||||
print(f" ✓ Updated Player with API image URL")
|
||||
|
||||
# Step 4: Create PitchingCard
|
||||
print("\nCreating PitchingCard...")
|
||||
pitching_card_payload = {
|
||||
'cards': [{
|
||||
'player_id': player_id,
|
||||
'key_bbref': bbref_id,
|
||||
'key_fangraphs': 0,
|
||||
'key_mlbam': 0,
|
||||
'key_retro': '',
|
||||
'name_first': PLAYER_NAME_FIRST,
|
||||
'name_last': PLAYER_NAME_LAST,
|
||||
'hand': HAND,
|
||||
'starter_rating': STARTER_RATING,
|
||||
'relief_rating': RELIEF_RATING,
|
||||
'closer_rating': CLOSER_RATING,
|
||||
}]
|
||||
}
|
||||
|
||||
await db_put('pitchingcards', payload=pitching_card_payload, timeout=10)
|
||||
|
||||
# Get the created card ID
|
||||
pc_query = await db_get('pitchingcards', params=[('player_id', player_id)])
|
||||
pitchingcard_id = pc_query['cards'][0]['id']
|
||||
print(f" ✓ Created PitchingCard ID: {pitchingcard_id}")
|
||||
|
||||
# Step 5: Create PitchingCardRatings
|
||||
print("\nCreating PitchingCardRatings...")
|
||||
|
||||
# Build ratings payload (remove display-only fields)
|
||||
def build_rating_payload(rating, pitchingcard_id):
|
||||
return {
|
||||
'pitchingcard_id': pitchingcard_id,
|
||||
'vs_hand': rating['vs_hand'],
|
||||
'homerun': rating['homerun'],
|
||||
'bp_homerun': rating['bp_homerun'],
|
||||
'triple': rating['triple'],
|
||||
'double_three': rating['double_three'],
|
||||
'double_two': rating['double_two'],
|
||||
'double_cf': rating['double_cf'],
|
||||
'single_two': rating['single_two'],
|
||||
'single_one': rating['single_one'],
|
||||
'single_center': rating['single_center'],
|
||||
'bp_single': rating['bp_single'],
|
||||
'hbp': rating['hbp'],
|
||||
'walk': rating['walk'],
|
||||
'strikeout': rating['strikeout'],
|
||||
'flyout_lf_b': rating['flyout_lf_b'],
|
||||
'flyout_cf_b': rating['flyout_cf_b'],
|
||||
'flyout_rf_b': rating['flyout_rf_b'],
|
||||
'groundout_a': rating['groundout_a'],
|
||||
'groundout_b': rating['groundout_b'],
|
||||
'xcheck_p': rating['xcheck_p'],
|
||||
'xcheck_c': rating['xcheck_c'],
|
||||
'xcheck_1b': rating['xcheck_1b'],
|
||||
'xcheck_2b': rating['xcheck_2b'],
|
||||
'xcheck_3b': rating['xcheck_3b'],
|
||||
'xcheck_ss': rating['xcheck_ss'],
|
||||
'xcheck_lf': rating['xcheck_lf'],
|
||||
'xcheck_cf': rating['xcheck_cf'],
|
||||
'xcheck_rf': rating['xcheck_rf'],
|
||||
}
|
||||
|
||||
ratings_payload = {
|
||||
'ratings': [
|
||||
build_rating_payload(vl, pitchingcard_id),
|
||||
build_rating_payload(vr, pitchingcard_id),
|
||||
]
|
||||
}
|
||||
|
||||
await db_put('pitchingcardratings', payload=ratings_payload, timeout=10)
|
||||
print(f" ✓ Created ratings for vs L and vs R")
|
||||
|
||||
# Step 6: Create CardPositions
|
||||
print("\nCreating CardPositions...")
|
||||
positions_payload = {
|
||||
'positions': [
|
||||
{
|
||||
'player_id': player_id,
|
||||
'position': 'P',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
await db_put('cardpositions', payload=positions_payload, timeout=10)
|
||||
print(f" ✓ Created position record: P")
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*70)
|
||||
print("✓ TONY SMEHRIK CREATED SUCCESSFULLY!")
|
||||
print("="*70)
|
||||
print(f"\nPlayer ID: {player_id}")
|
||||
print(f"PitchingCard ID: {pitchingcard_id}")
|
||||
print(f"Cardset: {CARDSET_NAME} (ID: {CARDSET_ID})")
|
||||
print(f"Hand: {HAND}")
|
||||
print(f"Starter Rating: {STARTER_RATING} | Relief Rating: {RELIEF_RATING}")
|
||||
print(f"\nOffensive Profile (OPS Against):")
|
||||
print(f" vs LHB: .185/.240/.225 ({ops_vl:.3f} OPS)")
|
||||
print(f" vs RHB: .245/.305/.340 ({ops_vr:.3f} OPS)")
|
||||
print(f" Combined: {combined_ops:.3f}")
|
||||
print(f"\nPreview card (NO S3 upload yet):")
|
||||
print(f" PNG: {api_image_url}")
|
||||
print(f" HTML: {api_image_url}&html=true")
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
488
custom_cards/tony_smehrik_preview.py
Normal file
488
custom_cards/tony_smehrik_preview.py
Normal file
@ -0,0 +1,488 @@
|
||||
"""
|
||||
Tony Smehrik - Custom Pitcher Card Preview
|
||||
|
||||
FIRST PASS - Target specs:
|
||||
- Name: Tony Smehrik
|
||||
- Hand: Left
|
||||
- Position: RP, SP (starter_rating: 5, relief_rating: 5, closer_rating: None)
|
||||
- Target Combined OPS: 0.585
|
||||
- Target OPS vs L: 0.495 (dominant vs lefties - same-side advantage)
|
||||
- K-rate vs L: average (~20%)
|
||||
- K-rate vs R: very low (~12%)
|
||||
- Flyball rate vs L: average (~35%)
|
||||
- Flyball rate vs R: high (~45%)
|
||||
- Team: Custom Ballplayers
|
||||
- Cardset ID: 29
|
||||
|
||||
Uses CORRECT pitcher schema with:
|
||||
- double_cf (not double_pull)
|
||||
- flyout_lf_b, flyout_cf_b, flyout_rf_b (no flyout_a, flyout_bq)
|
||||
- groundout_a, groundout_b only (no groundout_c)
|
||||
- xcheck fields (P, C, 1B, 2B, 3B, SS, LF, CF, RF) = 29 chances total
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from creation_helpers import mround, sanitize_chance_output
|
||||
|
||||
|
||||
# Default x-check values (from calcs_pitcher.py)
|
||||
DEFAULT_XCHECKS = {
|
||||
'xcheck_p': 1.0,
|
||||
'xcheck_c': 3.0,
|
||||
'xcheck_1b': 2.0,
|
||||
'xcheck_2b': 6.0,
|
||||
'xcheck_3b': 3.0,
|
||||
'xcheck_ss': 7.0,
|
||||
'xcheck_lf': 2.0,
|
||||
'xcheck_cf': 3.0,
|
||||
'xcheck_rf': 2.0,
|
||||
}
|
||||
TOTAL_XCHECK = sum(DEFAULT_XCHECKS.values()) # 29.0
|
||||
|
||||
|
||||
def calculate_pitcher_rating(
|
||||
vs_hand: str,
|
||||
pit_hand: str,
|
||||
avg: float,
|
||||
obp: float,
|
||||
slg: float,
|
||||
bb_pct: float,
|
||||
k_pct: float,
|
||||
hr_per_hit: float,
|
||||
triple_per_hit: float,
|
||||
double_per_hit: float,
|
||||
fb_pct: float,
|
||||
gb_pct: float,
|
||||
hard_pct: float,
|
||||
med_pct: float,
|
||||
soft_pct: float,
|
||||
oppo_pct: float,
|
||||
hr_fb_pct: float = 0.10,
|
||||
) -> dict:
|
||||
"""
|
||||
Calculate pitcher card ratings using the correct schema.
|
||||
|
||||
Total chances = 108:
|
||||
- Hits (HR, BP-HR, 3B, 2B, 1B variants)
|
||||
- On-base (BB, HBP)
|
||||
- Outs (K, flyouts, groundouts)
|
||||
- X-checks (29 chances that redirect to pitcher's card)
|
||||
"""
|
||||
# Calculate base chances (108 total, minus 29 for x-checks = 79 "real" chances)
|
||||
# But wait - the x-checks ARE part of the 108. They're outs that redirect.
|
||||
# So: hits + ob + outs = 108, where outs includes K, flyouts, groundouts, AND xchecks
|
||||
|
||||
# From calcs_pitcher.py, total_chances includes xchecks in the out count
|
||||
# Let's calculate hits and OB first, then outs fill the rest
|
||||
|
||||
# Hits allowed (adjusted for the 1.2x offense modifier used in game)
|
||||
# Note: calcs_pitcher subtracts 0.05 from AVG for BP results
|
||||
all_hits = sanitize_chance_output((avg - 0.05) * 108)
|
||||
|
||||
# OB (walks + HBP)
|
||||
all_other_ob = sanitize_chance_output(min(bb_pct * 108, 0.8 * 108)) # Cap at ~86 chances
|
||||
|
||||
# Outs (everything else)
|
||||
all_outs = mround(108 - all_hits - all_other_ob, base=0.5)
|
||||
|
||||
# ===== HITS DISTRIBUTION =====
|
||||
# Singles
|
||||
single_pct = 1.0 - hr_per_hit - triple_per_hit - double_per_hit
|
||||
total_singles = sanitize_chance_output(all_hits * single_pct)
|
||||
|
||||
bp_single = 5.0 if total_singles >= 5 else 0.0
|
||||
rem_singles = total_singles - bp_single
|
||||
|
||||
# Distribute singles based on contact quality
|
||||
single_two = sanitize_chance_output(rem_singles / 2) if hard_pct >= 0.2 else 0.0
|
||||
rem_singles -= single_two
|
||||
|
||||
single_one = sanitize_chance_output(rem_singles) if soft_pct >= 0.2 else 0.0
|
||||
rem_singles -= single_one
|
||||
|
||||
single_center = sanitize_chance_output(rem_singles)
|
||||
|
||||
# XBH
|
||||
rem_xbh = all_hits - bp_single - single_two - single_one - single_center
|
||||
|
||||
# Doubles
|
||||
xbh_total = hr_per_hit + triple_per_hit + double_per_hit
|
||||
if xbh_total > 0:
|
||||
do_rate = double_per_hit / xbh_total
|
||||
tr_rate = triple_per_hit / xbh_total
|
||||
hr_rate = hr_per_hit / xbh_total
|
||||
else:
|
||||
do_rate = tr_rate = hr_rate = 0.0
|
||||
|
||||
raw_doubles = sanitize_chance_output(rem_xbh * do_rate)
|
||||
double_two = raw_doubles if soft_pct > 0.2 else 0.0
|
||||
double_cf = mround(raw_doubles - double_two)
|
||||
double_three = 0.0 # Reserved for special cases
|
||||
|
||||
rem_xbh -= (double_two + double_cf + double_three)
|
||||
|
||||
# Triples
|
||||
triple = sanitize_chance_output(rem_xbh * tr_rate)
|
||||
rem_xbh = mround(rem_xbh - triple)
|
||||
|
||||
# Home runs
|
||||
raw_hr = rem_xbh
|
||||
if hr_fb_pct < 0.08:
|
||||
bp_homerun = sanitize_chance_output(raw_hr, min_chances=1.0, rounding=1.0)
|
||||
homerun = 0.0
|
||||
elif hr_fb_pct > 0.28:
|
||||
homerun = raw_hr
|
||||
bp_homerun = 0.0
|
||||
elif hr_fb_pct > 0.18:
|
||||
bp_homerun = sanitize_chance_output(raw_hr * 0.4, min_chances=1.0, rounding=1.0)
|
||||
homerun = mround(raw_hr - bp_homerun)
|
||||
else:
|
||||
bp_homerun = sanitize_chance_output(raw_hr * 0.75, min_chances=1.0, rounding=1.0)
|
||||
homerun = mround(raw_hr - bp_homerun)
|
||||
|
||||
# ===== ON-BASE DISTRIBUTION =====
|
||||
# Assume 90% walks, 10% HBP
|
||||
hbp = mround(all_other_ob * 0.10)
|
||||
walk = mround(all_other_ob - hbp)
|
||||
|
||||
# ===== OUTS DISTRIBUTION =====
|
||||
# Strikeouts (adjusted for K%)
|
||||
# K rate is K/PA, but we need K/(AB-H) for out distribution
|
||||
# Simplified: use K% directly scaled to outs
|
||||
raw_so = sanitize_chance_output(all_outs * k_pct * 1.2) # 1.2x modifier from calcs_pitcher
|
||||
|
||||
# Cap strikeouts to leave room for other outs
|
||||
current_total = (homerun + bp_homerun + triple + double_three + double_two + double_cf +
|
||||
single_two + single_one + single_center + bp_single + hbp + walk)
|
||||
max_so = 108 - current_total - TOTAL_XCHECK - 5 # Leave at least 5 for flyouts/groundouts
|
||||
strikeout = min(raw_so, max_so)
|
||||
|
||||
# Remaining outs (after K and x-checks)
|
||||
rem_outs = 108 - current_total - strikeout - TOTAL_XCHECK
|
||||
|
||||
# Flyouts vs groundouts based on FB%/GB%
|
||||
total_batted = fb_pct + gb_pct
|
||||
if total_batted > 0:
|
||||
fb_share = fb_pct / total_batted
|
||||
gb_share = gb_pct / total_batted
|
||||
else:
|
||||
fb_share = gb_share = 0.5
|
||||
|
||||
all_flyouts = sanitize_chance_output(rem_outs * fb_share)
|
||||
all_groundouts = rem_outs - all_flyouts
|
||||
|
||||
# Distribute flyouts by field (pitcher hand affects distribution)
|
||||
if pit_hand == 'L':
|
||||
# Lefty: more fly balls to LF (opposite field for RHB)
|
||||
flyout_lf_b = sanitize_chance_output(all_flyouts * oppo_pct)
|
||||
flyout_rf_b = sanitize_chance_output(all_flyouts * 0.25)
|
||||
else:
|
||||
# Righty: more fly balls to RF (opposite field for LHB)
|
||||
flyout_rf_b = sanitize_chance_output(all_flyouts * oppo_pct)
|
||||
flyout_lf_b = sanitize_chance_output(all_flyouts * 0.25)
|
||||
flyout_cf_b = all_flyouts - flyout_lf_b - flyout_rf_b
|
||||
|
||||
# Distribute groundouts (A = DP potential, B = routine)
|
||||
groundout_a = sanitize_chance_output(all_groundouts * soft_pct)
|
||||
groundout_b = sanitize_chance_output(all_groundouts - groundout_a)
|
||||
|
||||
# Build the rating dict
|
||||
rating = {
|
||||
'pitchingcard_id': 0, # Will be set on submission
|
||||
'vs_hand': vs_hand,
|
||||
'homerun': mround(homerun),
|
||||
'bp_homerun': mround(bp_homerun),
|
||||
'triple': mround(triple),
|
||||
'double_three': mround(double_three),
|
||||
'double_two': mround(double_two),
|
||||
'double_cf': mround(double_cf),
|
||||
'single_two': mround(single_two),
|
||||
'single_one': mround(single_one),
|
||||
'single_center': mround(single_center),
|
||||
'bp_single': mround(bp_single),
|
||||
'hbp': mround(hbp),
|
||||
'walk': mround(walk),
|
||||
'strikeout': mround(strikeout),
|
||||
'flyout_lf_b': mround(flyout_lf_b),
|
||||
'flyout_cf_b': mround(flyout_cf_b),
|
||||
'flyout_rf_b': mround(flyout_rf_b),
|
||||
'groundout_a': mround(groundout_a),
|
||||
'groundout_b': mround(groundout_b),
|
||||
**DEFAULT_XCHECKS,
|
||||
# Calculated stats for display
|
||||
'avg': avg,
|
||||
'obp': obp,
|
||||
'slg': slg,
|
||||
}
|
||||
|
||||
# Verify total and adjust if needed
|
||||
total = sum([
|
||||
rating['homerun'], rating['bp_homerun'], rating['triple'],
|
||||
rating['double_three'], rating['double_two'], rating['double_cf'],
|
||||
rating['single_two'], rating['single_one'], rating['single_center'], rating['bp_single'],
|
||||
rating['hbp'], rating['walk'], rating['strikeout'],
|
||||
rating['flyout_lf_b'], rating['flyout_cf_b'], rating['flyout_rf_b'],
|
||||
rating['groundout_a'], rating['groundout_b'],
|
||||
rating['xcheck_p'], rating['xcheck_c'], rating['xcheck_1b'], rating['xcheck_2b'],
|
||||
rating['xcheck_3b'], rating['xcheck_ss'], rating['xcheck_lf'], rating['xcheck_cf'], rating['xcheck_rf']
|
||||
])
|
||||
|
||||
# Adjust strikeouts to hit exactly 108
|
||||
diff = 108 - total
|
||||
if abs(diff) > 0.01:
|
||||
rating['strikeout'] = mround(rating['strikeout'] + diff)
|
||||
|
||||
return rating
|
||||
|
||||
|
||||
def preview_pitcher():
|
||||
"""Preview Tony Smehrik's ratings without submitting to database."""
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("TONY SMEHRIK - CUSTOM PITCHER PREVIEW")
|
||||
print("="*70)
|
||||
|
||||
# Target: Combined OPS 0.585, OPS vs L 0.495
|
||||
# Combined OPS formula for pitchers: (OPS_vR + OPS_vL + max(OPS_vL, OPS_vR)) / 3
|
||||
# If OPS_vL = 0.495 and OPS_vR > OPS_vL:
|
||||
# 0.585 = (OPS_vR + 0.495 + OPS_vR) / 3
|
||||
# 1.755 = 2*OPS_vR + 0.495
|
||||
# OPS_vR = (1.755 - 0.495) / 2 = 0.630
|
||||
|
||||
pit_hand = 'L'
|
||||
|
||||
# vs LHB (Same-side - Dominant)
|
||||
# Target OPS: 0.465 (lowered from 0.495)
|
||||
# OBP ~0.240, SLG ~0.225
|
||||
print("\nCalculating vs LHB (same-side advantage)...")
|
||||
vl = calculate_pitcher_rating(
|
||||
vs_hand='L',
|
||||
pit_hand=pit_hand,
|
||||
avg=0.185, # Lowered from 0.195
|
||||
obp=0.240, # Lowered from 0.252
|
||||
slg=0.225, # Lowered from 0.243
|
||||
bb_pct=0.055, # Slightly lower walks
|
||||
k_pct=0.20, # Average K-rate
|
||||
hr_per_hit=0.04,
|
||||
triple_per_hit=0.02,
|
||||
double_per_hit=0.22,
|
||||
fb_pct=0.35, # Average flyball
|
||||
gb_pct=0.45,
|
||||
hard_pct=0.30,
|
||||
med_pct=0.50,
|
||||
soft_pct=0.20,
|
||||
oppo_pct=0.26,
|
||||
hr_fb_pct=0.06,
|
||||
)
|
||||
|
||||
# vs RHB (Opposite-side - Weaker)
|
||||
# Target OPS: 0.645 (raised from 0.630)
|
||||
# OBP ~0.305, SLG ~0.340
|
||||
print("Calculating vs RHB (opposite-side)...")
|
||||
vr = calculate_pitcher_rating(
|
||||
vs_hand='R',
|
||||
pit_hand=pit_hand,
|
||||
avg=0.245, # Raised from 0.238
|
||||
obp=0.305, # Raised from 0.298
|
||||
slg=0.340, # Raised from 0.332
|
||||
bb_pct=0.075, # Slightly more walks
|
||||
k_pct=0.08, # Very low K-rate (~8%)
|
||||
hr_per_hit=0.07,
|
||||
triple_per_hit=0.02,
|
||||
double_per_hit=0.24,
|
||||
fb_pct=0.45, # High flyball
|
||||
gb_pct=0.35,
|
||||
hard_pct=0.34,
|
||||
med_pct=0.46,
|
||||
soft_pct=0.20,
|
||||
oppo_pct=0.26,
|
||||
hr_fb_pct=0.09,
|
||||
)
|
||||
|
||||
# ===== MANUAL ADJUSTMENTS =====
|
||||
# Set half of ALL on-base chances (hits + BB + HBP) to HR
|
||||
# Split 1:1 between HR and BP-HR
|
||||
# BP-HR and HBP must be whole numbers
|
||||
|
||||
# vs LHB: Total OB = Hits + BB + HBP
|
||||
vl_total_hits = (vl['homerun'] + vl['bp_homerun'] + vl['triple'] +
|
||||
vl['double_three'] + vl['double_two'] + vl['double_cf'] +
|
||||
vl['single_two'] + vl['single_one'] + vl['single_center'] + vl['bp_single'])
|
||||
vl_total_ob = vl_total_hits + vl['walk'] + vl['hbp']
|
||||
vl_hr_total = vl_total_ob / 2 # Half of all OB → HR
|
||||
# Split 1:1, BP-HR must be whole number
|
||||
vl['bp_homerun'] = round(vl_hr_total / 2) # Whole number
|
||||
vl['homerun'] = round((vl_hr_total - vl['bp_homerun']) * 20) / 20
|
||||
# Ensure HBP is whole number
|
||||
vl['hbp'] = 1.0
|
||||
|
||||
# vs RHB: Total OB = Hits + BB + HBP
|
||||
vr_total_hits = (vr['homerun'] + vr['bp_homerun'] + vr['triple'] +
|
||||
vr['double_three'] + vr['double_two'] + vr['double_cf'] +
|
||||
vr['single_two'] + vr['single_one'] + vr['single_center'] + vr['bp_single'])
|
||||
vr_total_ob = vr_total_hits + vr['walk'] + vr['hbp']
|
||||
vr_hr_total = vr_total_ob / 2 # Half of all OB → HR
|
||||
# Split 1:1, BP-HR must be whole number
|
||||
vr['bp_homerun'] = round(vr_hr_total / 2) # Whole number
|
||||
vr['homerun'] = round((vr_hr_total - vr['bp_homerun']) * 20) / 20
|
||||
# Ensure HBP is whole number
|
||||
vr['hbp'] = 1.0
|
||||
|
||||
# The HR chances come FROM the existing hit pool, so we need to reduce other hits
|
||||
# to keep total hits the same. Reduce singles proportionally.
|
||||
vl_new_hr_total = vl['homerun'] + vl['bp_homerun']
|
||||
vl_old_hr_total = 0 # Started with 0 HR
|
||||
vl_hr_increase = vl_new_hr_total - vl_old_hr_total
|
||||
# Reduce singles to compensate - take from single_one, single_two, bp_single
|
||||
vl_singles_to_reduce = vl_hr_increase
|
||||
# Take from single_one first, then single_two, then bp_single
|
||||
reduce_from_one = min(vl['single_one'], vl_singles_to_reduce)
|
||||
vl['single_one'] -= reduce_from_one
|
||||
vl_singles_to_reduce -= reduce_from_one
|
||||
reduce_from_two = min(vl['single_two'], vl_singles_to_reduce)
|
||||
vl['single_two'] -= reduce_from_two
|
||||
vl_singles_to_reduce -= reduce_from_two
|
||||
reduce_from_bp = min(vl['bp_single'], vl_singles_to_reduce)
|
||||
vl['bp_single'] -= reduce_from_bp
|
||||
vl_singles_to_reduce -= reduce_from_bp
|
||||
# If still need to reduce, take from doubles
|
||||
if vl_singles_to_reduce > 0:
|
||||
vl['double_cf'] = max(0, vl['double_cf'] - vl_singles_to_reduce)
|
||||
|
||||
vr_new_hr_total = vr['homerun'] + vr['bp_homerun']
|
||||
vr_old_hr_total = 0.80 + 1.00 # Had some HR already
|
||||
vr_hr_increase = vr_new_hr_total - vr_old_hr_total
|
||||
# Reduce singles to compensate
|
||||
vr_singles_to_reduce = vr_hr_increase
|
||||
reduce_from_one = min(vr['single_one'], vr_singles_to_reduce)
|
||||
vr['single_one'] -= reduce_from_one
|
||||
vr_singles_to_reduce -= reduce_from_one
|
||||
reduce_from_two = min(vr['single_two'], vr_singles_to_reduce)
|
||||
vr['single_two'] -= reduce_from_two
|
||||
vr_singles_to_reduce -= reduce_from_two
|
||||
reduce_from_bp = min(vr['bp_single'], vr_singles_to_reduce)
|
||||
vr['bp_single'] -= reduce_from_bp
|
||||
vr_singles_to_reduce -= reduce_from_bp
|
||||
if vr_singles_to_reduce > 0:
|
||||
vr['double_cf'] = max(0, vr['double_cf'] - vr_singles_to_reduce)
|
||||
|
||||
# Force BP-SI to specific values
|
||||
vl['bp_single'] = 5.0
|
||||
vr['bp_single'] = 3.0
|
||||
|
||||
# Recalculate totals and adjust strikeouts to hit 108
|
||||
for rating in [vl, vr]:
|
||||
total = sum([
|
||||
rating['homerun'], rating['bp_homerun'], rating['triple'],
|
||||
rating['double_three'], rating['double_two'], rating['double_cf'],
|
||||
rating['single_two'], rating['single_one'], rating['single_center'], rating['bp_single'],
|
||||
rating['hbp'], rating['walk'], rating['strikeout'],
|
||||
rating['flyout_lf_b'], rating['flyout_cf_b'], rating['flyout_rf_b'],
|
||||
rating['groundout_a'], rating['groundout_b'],
|
||||
rating['xcheck_p'], rating['xcheck_c'], rating['xcheck_1b'], rating['xcheck_2b'],
|
||||
rating['xcheck_3b'], rating['xcheck_ss'], rating['xcheck_lf'], rating['xcheck_cf'], rating['xcheck_rf']
|
||||
])
|
||||
diff = 108 - total
|
||||
rating['strikeout'] = round((rating['strikeout'] + diff) * 20) / 20
|
||||
|
||||
print(f"\nApplied HR adjustments + BP-SI override (vL: 5.0, vR: 3.0):")
|
||||
print(f" vs LHB: Total OB was {vl_total_ob:.2f} → Half ({vl_hr_total:.2f}) to HR (HR:{vl['homerun']:.2f} + BP-HR:{vl['bp_homerun']:.0f})")
|
||||
print(f" vs RHB: Total OB was {vr_total_ob:.2f} → Half ({vr_hr_total:.2f}) to HR (HR:{vr['homerun']:.2f} + BP-HR:{vr['bp_homerun']:.0f})")
|
||||
|
||||
# Calculate combined OPS
|
||||
ops_vl = vl['obp'] + vl['slg']
|
||||
ops_vr = vr['obp'] + vr['slg']
|
||||
combined_ops = (ops_vr + ops_vl + max(ops_vl, ops_vr)) / 3
|
||||
|
||||
# Display results
|
||||
print("\n" + "-"*70)
|
||||
print("TONY SMEHRIK (L) - SP/RP")
|
||||
print("-"*70)
|
||||
|
||||
print(f"\nTarget OPS: 0.585 combined, 0.495 vs L")
|
||||
print(f"Actual OPS: {combined_ops:.3f} combined, {ops_vl:.3f} vs L, {ops_vr:.3f} vs R")
|
||||
|
||||
for rating in [vl, vr]:
|
||||
vs_hand = rating['vs_hand']
|
||||
print(f"\n{'='*35}")
|
||||
print(f"VS {vs_hand}HB:")
|
||||
print(f"{'='*35}")
|
||||
|
||||
ops = rating['obp'] + rating['slg']
|
||||
print(f" AVG: {rating['avg']:.3f} OBP: {rating['obp']:.3f} SLG: {rating['slg']:.3f} OPS: {ops:.3f}")
|
||||
|
||||
# Show hit distribution
|
||||
total_hits = (rating['homerun'] + rating['bp_homerun'] + rating['triple'] +
|
||||
rating['double_three'] + rating['double_two'] + rating['double_cf'] +
|
||||
rating['single_two'] + rating['single_one'] + rating['single_center'] + rating['bp_single'])
|
||||
doubles = rating['double_cf'] + rating['double_two'] + rating['double_three']
|
||||
singles = total_hits - rating['homerun'] - rating['bp_homerun'] - rating['triple'] - doubles
|
||||
|
||||
print(f"\n HITS ALLOWED: {total_hits:.2f}")
|
||||
print(f" HR: {rating['homerun']:.2f} + BP-HR: {rating['bp_homerun']:.2f}")
|
||||
print(f" 3B: {rating['triple']:.2f}")
|
||||
print(f" 2B: {doubles:.2f} (double**: {rating['double_two']:.2f}, double(cf): {rating['double_cf']:.2f})")
|
||||
print(f" 1B: {singles:.2f} (1B**: {rating['single_two']:.2f}, 1B*: {rating['single_one']:.2f}, 1B(cf): {rating['single_center']:.2f}, BP-1B: {rating['bp_single']:.2f})")
|
||||
|
||||
# Show walks/strikeouts
|
||||
print(f"\n PLATE DISCIPLINE:")
|
||||
print(f" BB: {rating['walk']:.2f} HBP: {rating['hbp']:.2f}")
|
||||
print(f" K: {rating['strikeout']:.2f}")
|
||||
|
||||
# Show batted ball outs
|
||||
flyouts = rating['flyout_lf_b'] + rating['flyout_cf_b'] + rating['flyout_rf_b']
|
||||
groundouts = rating['groundout_a'] + rating['groundout_b']
|
||||
xchecks = sum([rating[f'xcheck_{pos}'] for pos in ['p', 'c', '1b', '2b', '3b', 'ss', 'lf', 'cf', 'rf']])
|
||||
total_outs = rating['strikeout'] + flyouts + groundouts + xchecks
|
||||
|
||||
print(f"\n OUTS: {total_outs:.2f}")
|
||||
print(f" Strikeouts: {rating['strikeout']:.2f}")
|
||||
print(f" Flyouts: {flyouts:.2f} (LF-B: {rating['flyout_lf_b']:.2f}, CF-B: {rating['flyout_cf_b']:.2f}, RF-B: {rating['flyout_rf_b']:.2f})")
|
||||
print(f" Groundouts: {groundouts:.2f} (A: {rating['groundout_a']:.2f}, B: {rating['groundout_b']:.2f})")
|
||||
print(f" X-Checks: {xchecks:.2f} (P:{rating['xcheck_p']:.0f} C:{rating['xcheck_c']:.0f} 1B:{rating['xcheck_1b']:.0f} 2B:{rating['xcheck_2b']:.0f} 3B:{rating['xcheck_3b']:.0f} SS:{rating['xcheck_ss']:.0f} LF:{rating['xcheck_lf']:.0f} CF:{rating['xcheck_cf']:.0f} RF:{rating['xcheck_rf']:.0f})")
|
||||
|
||||
batted_outs = flyouts + groundouts
|
||||
if batted_outs > 0:
|
||||
fb_rate = flyouts / batted_outs
|
||||
gb_rate = groundouts / batted_outs
|
||||
print(f" FB%: {fb_rate*100:.1f}% GB%: {gb_rate*100:.1f}%")
|
||||
|
||||
# Verify total = 108
|
||||
total = sum([
|
||||
rating['homerun'], rating['bp_homerun'], rating['triple'],
|
||||
rating['double_three'], rating['double_two'], rating['double_cf'],
|
||||
rating['single_two'], rating['single_one'], rating['single_center'], rating['bp_single'],
|
||||
rating['hbp'], rating['walk'], rating['strikeout'],
|
||||
rating['flyout_lf_b'], rating['flyout_cf_b'], rating['flyout_rf_b'],
|
||||
rating['groundout_a'], rating['groundout_b'],
|
||||
rating['xcheck_p'], rating['xcheck_c'], rating['xcheck_1b'], rating['xcheck_2b'],
|
||||
rating['xcheck_3b'], rating['xcheck_ss'], rating['xcheck_lf'], rating['xcheck_cf'], rating['xcheck_rf']
|
||||
])
|
||||
print(f"\n Total chances: {total:.2f} (should be 108.0)")
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*70)
|
||||
print("SUMMARY")
|
||||
print("="*70)
|
||||
print(f"\nName: Tony Smehrik")
|
||||
print(f"Hand: L")
|
||||
print(f"Positions: SP, RP")
|
||||
print(f"Starter Rating: 5 | Relief Rating: 5 | Closer Rating: None")
|
||||
print(f"\nCombined OPS Against: {combined_ops:.3f} (target: 0.585)")
|
||||
print(f"OPS vs LHB: {ops_vl:.3f} (target: 0.495)")
|
||||
print(f"OPS vs RHB: {ops_vr:.3f} (target: ~0.630)")
|
||||
print(f"\nK-rate vs L: ~20% (average)")
|
||||
print(f"K-rate vs R: ~12% (very low)")
|
||||
print(f"\nFB% vs L: ~35% (average)")
|
||||
print(f"FB% vs R: ~45% (high)")
|
||||
|
||||
print("\n" + "-"*70)
|
||||
print("Ready to submit? Run: python -m custom_cards.submit_tony_smehrik")
|
||||
print("-"*70 + "\n")
|
||||
|
||||
return vl, vr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
preview_pitcher()
|
||||
172
docs/CUSTOM_CARD_TIERS.md
Normal file
172
docs/CUSTOM_CARD_TIERS.md
Normal file
@ -0,0 +1,172 @@
|
||||
# Custom Card Tier System
|
||||
|
||||
Custom cards are rewards earned by Paper Dynasty players. Players earn their initial card and subsequent upgrades through gameplay achievements.
|
||||
|
||||
## Progression Path
|
||||
|
||||
Cards progress through tiers in this order:
|
||||
|
||||
```
|
||||
Low Starter → Mid Starter → High Starter →
|
||||
Low All-Star → Mid All-Star → High All-Star →
|
||||
Low MVP → Mid MVP → High MVP →
|
||||
Low HoF → Mid HoF → High HoF →
|
||||
(Legendary tiers follow .x20/.x50/.x80 pattern)
|
||||
```
|
||||
|
||||
## Target OPS by Tier
|
||||
|
||||
### Batters
|
||||
|
||||
| Tier | Code | Target OPS | Notes |
|
||||
|------|------|------------|-------|
|
||||
| Low Starter | L-STR | 0.820 | Entry-level custom card |
|
||||
| Mid Starter | M-STR | 0.850 | First upgrade |
|
||||
| High Starter | H-STR | 0.880 | |
|
||||
| Low All-Star | L-AS | 0.920 | |
|
||||
| Mid All-Star | M-AS | 0.950 | |
|
||||
| High All-Star | H-AS | 0.980 | |
|
||||
| Low MVP | L-MVP | 1.025 | |
|
||||
| Mid MVP | M-MVP | 1.075 | |
|
||||
| High MVP | H-MVP | 1.150 | |
|
||||
| Low Hall of Fame | L-HOF | 1.220 | |
|
||||
| Mid Hall of Fame | M-HOF | 1.250 | |
|
||||
| High Hall of Fame | H-HOF | 1.280 | |
|
||||
| Legendary+ | LEG-1 | 1.320 | Further upgrades follow pattern |
|
||||
| Legendary++ | LEG-2 | 1.350 | |
|
||||
| Legendary+++ | LEG-3 | 1.380 | |
|
||||
|
||||
**Pattern for Legendary+:** Each subsequent tier adds +0.030 OPS following the .x20/.x50/.x80 pattern.
|
||||
|
||||
### Pitchers (OPS Allowed)
|
||||
|
||||
Pitchers use inverted logic - lower OPS allowed = better pitcher. **Starting pitchers and relievers have separate progressions** - relievers are held to a higher standard (lower OPS) at each tier.
|
||||
|
||||
#### Starting Pitchers
|
||||
|
||||
| Tier | Code | Target OPS | Notes |
|
||||
|------|------|------------|-------|
|
||||
| Low Starter | L-STR | 0.580 | Entry-level custom card |
|
||||
| Mid Starter | M-STR | 0.560 | |
|
||||
| High Starter | H-STR | 0.540 | |
|
||||
| Low All-Star | L-AS | 0.520 | |
|
||||
| Mid All-Star | M-AS | 0.500 | |
|
||||
| High All-Star | H-AS | 0.485 | |
|
||||
| Low MVP | L-MVP | 0.460 | |
|
||||
| Mid MVP | M-MVP | 0.435 | |
|
||||
| High MVP | H-MVP | 0.410 | |
|
||||
| Low Hall of Fame | L-HOF | 0.390 | |
|
||||
| Mid Hall of Fame | M-HOF | 0.370 | |
|
||||
| High Hall of Fame | H-HOF | 0.350 | |
|
||||
|
||||
**Beyond H-HOF:** Decrease OPS by 0.015 per upgrade (0.335 → 0.320 → 0.305...)
|
||||
|
||||
#### Relief Pitchers
|
||||
|
||||
| Tier | Code | Target OPS | Notes |
|
||||
|------|------|------------|-------|
|
||||
| Low Starter | L-STR | 0.540 | Entry-level custom card |
|
||||
| Mid Starter | M-STR | 0.515 | |
|
||||
| High Starter | H-STR | 0.490 | |
|
||||
| Low All-Star | L-AS | 0.465 | |
|
||||
| Mid All-Star | M-AS | 0.440 | |
|
||||
| High All-Star | H-AS | 0.415 | |
|
||||
| Low MVP | L-MVP | 0.380 | |
|
||||
| Mid MVP | M-MVP | 0.360 | |
|
||||
| High MVP | H-MVP | 0.340 | |
|
||||
| Low Hall of Fame | L-HOF | 0.320 | |
|
||||
| Mid Hall of Fame | M-HOF | 0.300 | |
|
||||
| High Hall of Fame | H-HOF | 0.280 | |
|
||||
|
||||
**Beyond H-HOF:** Decrease OPS by 0.020 per upgrade (0.260 → 0.240 → 0.220...)
|
||||
|
||||
## YAML Profile Schema
|
||||
|
||||
Each custom card profile should include tier tracking:
|
||||
|
||||
```yaml
|
||||
name: Player Name
|
||||
player_type: batter # or pitcher
|
||||
pitcher_role: starter # starter or reliever (pitchers only)
|
||||
hand: R
|
||||
|
||||
# Tier tracking
|
||||
tier: H-STR # Current tier code
|
||||
tier_history: # Upgrade history
|
||||
- tier: L-STR
|
||||
date: 2025-01-15
|
||||
reason: Initial card creation
|
||||
- tier: M-STR
|
||||
date: 2025-03-20
|
||||
reason: Season 5 championship reward
|
||||
- tier: H-STR
|
||||
date: 2025-06-10
|
||||
reason: MVP award
|
||||
|
||||
target_ops: 0.880 # Should match tier target (use SP or RP table for pitchers)
|
||||
cardset_id: 29
|
||||
player_id: 12345
|
||||
batting_card_id: 6789 # or pitching_card_id for pitchers
|
||||
|
||||
# ... rest of profile
|
||||
```
|
||||
|
||||
## Upgrade Process
|
||||
|
||||
When upgrading a card:
|
||||
|
||||
1. **Identify current tier** from profile
|
||||
2. **Determine next tier** from progression path
|
||||
3. **Calculate OPS delta** needed (next_target - current_target)
|
||||
4. **Distribute delta** across ratings:
|
||||
- Increase positive outcomes (hits, walks)
|
||||
- Decrease negative outcomes (strikeouts, outs)
|
||||
- Maintain player's characteristic style (spray charts, power/contact balance)
|
||||
5. **Update profile**:
|
||||
- Set new `tier` code
|
||||
- Update `target_ops` to new tier target
|
||||
- Add entry to `tier_history`
|
||||
- Update `ratings` with new values
|
||||
6. **Submit to database** via `pd-cards custom submit`
|
||||
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
# Preview current tier status
|
||||
pd-cards custom preview kalin_young
|
||||
|
||||
# Upgrade a player to next tier
|
||||
pd-cards custom upgrade kalin_young --reason "Season 6 reward"
|
||||
|
||||
# Upgrade to specific tier
|
||||
pd-cards custom upgrade kalin_young --to-tier M-AS --reason "Special event"
|
||||
|
||||
# List all players by tier
|
||||
pd-cards custom list --by-tier
|
||||
```
|
||||
|
||||
## OPS Calculation Reference
|
||||
|
||||
For batters, Combined OPS uses the formula:
|
||||
```
|
||||
combined_ops = (ops_vL + ops_vR + min(ops_vL, ops_vR)) / 3
|
||||
```
|
||||
|
||||
For pitchers (OPS allowed):
|
||||
```
|
||||
combined_ops = (ops_vL + ops_vR + max(ops_vL, ops_vR)) / 3
|
||||
```
|
||||
|
||||
This weights the weaker split more heavily for batters (penalizes platoon weaknesses) and the stronger split for pitchers (rewards same-side dominance).
|
||||
|
||||
## Maintaining Player Identity
|
||||
|
||||
When upgrading, preserve the player's characteristic profile:
|
||||
|
||||
- **Power hitters**: Increase HR/XBH proportionally
|
||||
- **Contact hitters**: Increase singles, reduce strikeouts
|
||||
- **Patient hitters**: Increase walks
|
||||
- **Speedsters**: Improve baserunning stats alongside OPS
|
||||
- **Defensive specialists**: May upgrade defense alongside OPS
|
||||
|
||||
Avoid creating "generic good players" - each upgrade should feel like a natural evolution of the player's established style.
|
||||
657
docs/PD_CARDS_CLI_REFERENCE.md
Normal file
657
docs/PD_CARDS_CLI_REFERENCE.md
Normal file
@ -0,0 +1,657 @@
|
||||
# pd-cards CLI Reference
|
||||
|
||||
Complete command reference for the Paper Dynasty card creation CLI.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Install in development mode
|
||||
uv pip install -e .
|
||||
|
||||
# Verify installation
|
||||
pd-cards version
|
||||
```
|
||||
|
||||
## Command Structure
|
||||
|
||||
```
|
||||
pd-cards <subcommand> <command> [OPTIONS]
|
||||
```
|
||||
|
||||
**Subcommands:**
|
||||
- `custom` - Custom character card management
|
||||
- `live-series` - Live season card updates
|
||||
- `retrosheet` - Historical Retrosheet data processing
|
||||
- `scouting` - Scouting report generation
|
||||
- `upload` - Card image upload to S3
|
||||
|
||||
---
|
||||
|
||||
## custom - Custom Character Cards
|
||||
|
||||
Manage fictional player cards using YAML profiles stored in `pd_cards/custom/profiles/`.
|
||||
|
||||
### custom list
|
||||
|
||||
List all available character profiles.
|
||||
|
||||
```bash
|
||||
pd-cards custom list
|
||||
```
|
||||
|
||||
**Output:** Table showing name, type (batter/pitcher), target OPS, positions, and player ID.
|
||||
|
||||
---
|
||||
|
||||
### custom preview
|
||||
|
||||
Preview a character's calculated ratings without submitting.
|
||||
|
||||
```bash
|
||||
pd-cards custom preview <character>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `character` - Profile name (e.g., `kalin_young`, `tony_smehrik`)
|
||||
|
||||
**Output:** Displays:
|
||||
- Player info (name, hand, target OPS, IDs)
|
||||
- Defensive positions with ratings
|
||||
- Batting/pitching ratings vs L and R
|
||||
- Calculated AVG/OBP/SLG/OPS for each platoon split
|
||||
- Verification that ratings total 108
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
pd-cards custom preview kalin_young
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### custom submit
|
||||
|
||||
Submit a custom character to the database.
|
||||
|
||||
```bash
|
||||
pd-cards custom submit <character> [OPTIONS]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `character` - Profile name
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Description |
|
||||
|--------|-------|-------------|
|
||||
| `--dry-run` | `-n` | Preview changes without saving |
|
||||
| `--skip-s3` | | Skip S3 upload after submit |
|
||||
|
||||
**Requirements:** Profile must have valid `player_id` and `batting_card_id`/`pitching_card_id` set.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
pd-cards custom submit kalin_young --dry-run # Validate first
|
||||
pd-cards custom submit kalin_young # Actually submit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### custom new
|
||||
|
||||
Create a new character profile template.
|
||||
|
||||
```bash
|
||||
pd-cards custom new --name <name> [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--name` | `-n` | (required) | Character name |
|
||||
| `--type` | `-t` | `batter` | Player type (`batter` or `pitcher`) |
|
||||
| `--hand` | `-h` | `R` | Batting/throwing hand (`L`, `R`, or `S` for switch) |
|
||||
| `--target-ops` | | `0.750` | Target OPS (or OPS allowed for pitchers) |
|
||||
|
||||
**Output:** Creates YAML file at `pd_cards/custom/profiles/<name>.yaml`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
pd-cards custom new --name "Mike Power" --type batter --hand R --target-ops 0.850
|
||||
pd-cards custom new --name "Sandy Lefty" --type pitcher --hand L --target-ops 0.620
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## live-series - Live Season Updates
|
||||
|
||||
Generate cards from current season FanGraphs/Baseball Reference data.
|
||||
|
||||
### live-series update
|
||||
|
||||
Update live series cards from statistical data.
|
||||
|
||||
```bash
|
||||
pd-cards live-series update --cardset <name> [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--cardset` | `-c` | (required) | Target cardset name |
|
||||
| `--season` | `-s` | (from cardset) | Season year |
|
||||
| `--games` | `-g` | `162` | Number of games played (1-162) |
|
||||
| `--description` | `-d` | (year) | Player description |
|
||||
| `--pull-fielding/--no-pull-fielding` | | `True` | Pull fielding stats from Baseball Reference |
|
||||
| `--post-batters/--skip-batters` | | `True` | Post batting cards and ratings |
|
||||
| `--post-pitchers/--skip-pitchers` | | `True` | Post pitching cards and ratings |
|
||||
| `--post-fielders/--skip-fielders` | | `True` | Post card positions |
|
||||
| `--post-players/--skip-players` | | `True` | Post player updates |
|
||||
| `--live/--not-live` | | `True` | Look up current MLB clubs from statsapi |
|
||||
| `--ignore-limits` | | `False` | Ignore minimum PA/TBF requirements |
|
||||
| `--dry-run` | `-n` | `False` | Preview without saving to database |
|
||||
|
||||
**Data Input:** Reads from `data-input/{cardset} Cardset/`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
pd-cards live-series update --cardset "2025 Season" --games 81 --dry-run
|
||||
pd-cards live-series update --cardset "2025 Season" --games 162
|
||||
pd-cards live-series update --cardset "2025 Season" --games 40 --ignore-limits
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### live-series status
|
||||
|
||||
Show status of live series cardsets.
|
||||
|
||||
```bash
|
||||
pd-cards live-series status [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Description |
|
||||
|--------|-------|-------------|
|
||||
| `--cardset` | `-c` | Filter by cardset name |
|
||||
|
||||
**Output:** Table showing cardset ID, name, season, and player count.
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
pd-cards live-series status
|
||||
pd-cards live-series status --cardset "2025"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## retrosheet - Historical Data Processing
|
||||
|
||||
Generate cards from Retrosheet play-by-play historical data.
|
||||
|
||||
### retrosheet process
|
||||
|
||||
Process Retrosheet data and create player cards.
|
||||
|
||||
```bash
|
||||
pd-cards retrosheet process <year> --cardset-id <id> [OPTIONS]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `year` - Season year to process (e.g., 2005)
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--cardset-id` | `-c` | (required) | Target cardset ID |
|
||||
| `--description` | `-d` | `Live` | Player description (`Live` or `<Month> PotM`) |
|
||||
| `--start` | | `{year}0301` | Start date YYYYMMDD |
|
||||
| `--end` | | `{year}1002` | End date YYYYMMDD |
|
||||
| `--events` | `-e` | `retrosheets_events_{year}.csv` | Retrosheet events CSV filename |
|
||||
| `--input` | `-i` | `data-input/{year} Live Cardset/` | Data input directory |
|
||||
| `--season-pct` | | `1.0` | Season percentage (0.0-1.0) |
|
||||
| `--min-pa-vl` | | `20` (Live) / `1` (PotM) | Minimum PA vs LHP |
|
||||
| `--min-pa-vr` | | `40` (Live) / `1` (PotM) | Minimum PA vs RHP |
|
||||
| `--post/--no-post` | | `True` | Post data to database |
|
||||
| `--dry-run` | `-n` | `False` | Preview without saving |
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
pd-cards retrosheet process 2005 --cardset-id 27 --description Live --dry-run
|
||||
pd-cards retrosheet process 2005 --cardset-id 27 --description Live
|
||||
pd-cards retrosheet process 2005 --cardset-id 28 --description "April PotM" --start 20050401 --end 20050430
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### retrosheet arms
|
||||
|
||||
Generate outfield arm ratings from Retrosheet play-by-play data.
|
||||
|
||||
```bash
|
||||
pd-cards retrosheet arms <year> --events <file> [OPTIONS]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `year` - Season year
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--events` | `-e` | (required) | Retrosheet events CSV file |
|
||||
| `--output` | `-o` | `data-output/retrosheet_arm_ratings_{year}.csv` | Output CSV file |
|
||||
| `--season-pct` | | `1.0` | Season percentage for min sample |
|
||||
|
||||
**Output:** CSV with player_id, position, arm_rating, balls_fielded, assist_rate, z_score
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
pd-cards retrosheet arms 2005 --events data-input/retrosheet/retrosheets_events_2005.csv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### retrosheet validate
|
||||
|
||||
Validate positions for a cardset.
|
||||
|
||||
```bash
|
||||
pd-cards retrosheet validate <cardset_id> [OPTIONS]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `cardset_id` - Cardset ID to validate
|
||||
|
||||
**Options:**
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `--api` | `https://pd.manticorum.com/api` | API URL |
|
||||
|
||||
**Checks:**
|
||||
- Anomalous DH counts (should be <5 for full-season cards)
|
||||
- Missing outfield positions (LF, CF, RF)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
pd-cards retrosheet validate 27
|
||||
pd-cards retrosheet validate 27 --api https://pddev.manticorum.com/api
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### retrosheet defense
|
||||
|
||||
Fetch defensive statistics from Baseball Reference.
|
||||
|
||||
```bash
|
||||
pd-cards retrosheet defense <year> [OPTIONS]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `year` - Season year to fetch
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--output` | `-o` | `data-input/{year} Live Cardset/` | Output directory |
|
||||
|
||||
**Output:** Creates `defense_{position}.csv` files for: c, 1b, 2b, 3b, ss, lf, cf, rf, of, p
|
||||
|
||||
**Note:** Includes 8-second delay between requests for rate limiting.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
pd-cards retrosheet defense 2005 --output "data-input/2005 Live Cardset/"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## scouting - Scouting Reports
|
||||
|
||||
Generate scouting reports and ratings comparisons.
|
||||
|
||||
### scouting batters
|
||||
|
||||
Generate batting scouting reports.
|
||||
|
||||
```bash
|
||||
pd-cards scouting batters [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--cardset-id` | `-c` | (all) | Cardset ID(s) to include (repeatable) |
|
||||
| `--output` | `-o` | `scouting` | Output directory |
|
||||
|
||||
**Output:** Creates `batting-basic.csv` and `batting-ratings.csv`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
pd-cards scouting batters --cardset-id 27
|
||||
pd-cards scouting batters --cardset-id 27 --cardset-id 29
|
||||
pd-cards scouting batters --output reports/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### scouting pitchers
|
||||
|
||||
Generate pitching scouting reports.
|
||||
|
||||
```bash
|
||||
pd-cards scouting pitchers [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--cardset-id` | `-c` | (all) | Cardset ID(s) to include (repeatable) |
|
||||
| `--output` | `-o` | `scouting` | Output directory |
|
||||
|
||||
**Output:** Creates `pitching-basic.csv` and `pitching-ratings.csv`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
pd-cards scouting pitchers --cardset-id 27
|
||||
pd-cards scouting pitchers --cardset-id 27 --cardset-id 29
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### scouting all
|
||||
|
||||
Generate all scouting reports (batters and pitchers).
|
||||
|
||||
```bash
|
||||
pd-cards scouting all [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--cardset-id` | `-c` | (all) | Cardset ID(s) to include (repeatable) |
|
||||
| `--output` | `-o` | `scouting` | Output directory |
|
||||
|
||||
**Output:** Creates all four scouting CSV files.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
pd-cards scouting all --cardset-id 27
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## upload - S3 Card Upload
|
||||
|
||||
Upload card images to AWS S3.
|
||||
|
||||
### upload s3
|
||||
|
||||
Upload card images to AWS S3 bucket.
|
||||
|
||||
```bash
|
||||
pd-cards upload s3 --cardset <name> [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--cardset` | `-c` | (required) | Cardset name to upload |
|
||||
| `--start-id` | | | Player ID to start from (for resuming) |
|
||||
| `--limit` | `-l` | | Limit number of cards to process |
|
||||
| `--html` | | `False` | Upload HTML preview cards instead of PNG |
|
||||
| `--skip-batters` | | `False` | Skip batting cards |
|
||||
| `--skip-pitchers` | | `False` | Skip pitching cards |
|
||||
| `--upload/--no-upload` | | `True` | Upload to S3 |
|
||||
| `--update-urls/--no-update-urls` | | `True` | Update player URLs in database |
|
||||
| `--dry-run` | `-n` | `False` | Preview without uploading |
|
||||
|
||||
**Prerequisites:** AWS CLI configured with credentials (`~/.aws/credentials`)
|
||||
|
||||
**S3 URL Structure:** `cards/cardset-{id:03d}/player-{player_id}/{batting|pitching}card.png?d={date}`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
pd-cards upload s3 --cardset "2005 Live" --dry-run
|
||||
pd-cards upload s3 --cardset "2005 Live" --limit 10
|
||||
pd-cards upload s3 --cardset "2005 Live" --start-id 5000
|
||||
pd-cards upload s3 --cardset "2005 Live" --skip-pitchers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### upload refresh
|
||||
|
||||
Refresh (re-generate and re-upload) card images.
|
||||
|
||||
```bash
|
||||
pd-cards upload refresh --cardset <name> [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--cardset` | `-c` | (required) | Cardset name |
|
||||
| `--limit` | `-l` | | Limit number of cards |
|
||||
| `--dry-run` | `-n` | `False` | Preview without refreshing |
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
pd-cards upload refresh --cardset "2005 Live" --limit 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### upload check
|
||||
|
||||
Check and validate card images without uploading.
|
||||
|
||||
```bash
|
||||
pd-cards upload check --cardset <name> [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--cardset` | `-c` | (required) | Cardset name |
|
||||
| `--limit` | `-l` | | Limit number of cards to check |
|
||||
| `--output` | `-o` | `data-output` | Output directory |
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
pd-cards upload check --cardset "2005 Live" --limit 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## YAML Profile Schema
|
||||
|
||||
**See also:** [`docs/CUSTOM_CARD_TIERS.md`](CUSTOM_CARD_TIERS.md) for tier progression system.
|
||||
|
||||
### Batter Profile
|
||||
|
||||
```yaml
|
||||
name: Player Name
|
||||
player_type: batter
|
||||
hand: R # L, R, or S (switch)
|
||||
|
||||
# Tier tracking (see CUSTOM_CARD_TIERS.md for values)
|
||||
tier: M-STR # Current tier code
|
||||
target_ops: 0.850 # Must match tier target
|
||||
tier_history:
|
||||
- tier: L-STR
|
||||
date: 2025-01-15
|
||||
reason: Initial card creation
|
||||
- tier: M-STR
|
||||
date: 2025-03-20
|
||||
reason: Season 5 championship reward
|
||||
|
||||
cardset_id: 29
|
||||
player_id: null # Set after creation
|
||||
batting_card_id: null
|
||||
|
||||
positions:
|
||||
RF:
|
||||
range: 3 # 1-5 (lower = better)
|
||||
error: 7 # 1-20 (higher = better)
|
||||
arm: 0 # -6 to +5 (negative = strong)
|
||||
LF:
|
||||
range: 4
|
||||
error: 6
|
||||
arm: 2
|
||||
|
||||
baserunning:
|
||||
steal_jump: 0.166667 # Fraction for steal success
|
||||
steal_high: 15 # High end of steal range
|
||||
steal_low: 8 # Low end of steal range
|
||||
steal_auto: 0 # Auto-steal threshold
|
||||
running: 12 # Base running rating
|
||||
hit_and_run: A # A/B/C/D
|
||||
bunting: B # A/B/C/D
|
||||
|
||||
ratings:
|
||||
vs_L: # All values must sum to 108
|
||||
homerun: 2.0
|
||||
bp_homerun: 1.0 # Ballpark home run
|
||||
triple: 0.5
|
||||
double_three: 0.0 # Double to 3-zone
|
||||
double_two: 4.0 # Double to 2-zone
|
||||
double_pull: 2.0 # Pulled double
|
||||
single_two: 5.0
|
||||
single_one: 5.0
|
||||
single_center: 5.0
|
||||
bp_single: 3.0 # Ballpark single
|
||||
walk: 10.0
|
||||
hbp: 1.0
|
||||
strikeout: 20.0
|
||||
lineout: 12.0
|
||||
popout: 2.0
|
||||
flyout_a: 2.0
|
||||
flyout_bq: 2.0
|
||||
flyout_lf_b: 4.0
|
||||
flyout_rf_b: 4.0
|
||||
groundout_a: 5.0
|
||||
groundout_b: 10.0
|
||||
groundout_c: 8.5
|
||||
pull_rate: 0.30
|
||||
center_rate: 0.40
|
||||
slap_rate: 0.30
|
||||
vs_R:
|
||||
# Same structure as vs_L
|
||||
```
|
||||
|
||||
### Pitcher Profile
|
||||
|
||||
```yaml
|
||||
name: Pitcher Name
|
||||
player_type: pitcher
|
||||
hand: L
|
||||
target_ops: 0.620 # OPS allowed
|
||||
cardset_id: 29
|
||||
player_id: null
|
||||
pitching_card_id: null
|
||||
|
||||
positions:
|
||||
P: null
|
||||
|
||||
pitching:
|
||||
starter_rating: 5 # 1-5 (higher = better starter)
|
||||
relief_rating: 5 # 1-5 (higher = better reliever)
|
||||
closer_rating: null # null or 1-5 for closers
|
||||
|
||||
ratings:
|
||||
vs_L: # All values must sum to 108 (includes 29 for x-checks)
|
||||
homerun: 1.0
|
||||
bp_homerun: 1.0
|
||||
triple: 0.5
|
||||
double_three: 0.0
|
||||
double_two: 3.0
|
||||
double_cf: 2.0 # Pitchers use double_cf, not double_pull
|
||||
single_two: 4.0
|
||||
single_one: 3.0
|
||||
single_center: 4.0
|
||||
bp_single: 5.0
|
||||
walk: 8.0
|
||||
hbp: 1.0
|
||||
strikeout: 20.0
|
||||
flyout_lf_b: 4.0
|
||||
flyout_cf_b: 5.0 # Pitchers have flyout_cf_b
|
||||
flyout_rf_b: 4.0
|
||||
groundout_a: 6.0
|
||||
groundout_b: 7.5
|
||||
# X-checks (fielder plays) - must sum to 29
|
||||
xcheck_p: 1.0
|
||||
xcheck_c: 3.0
|
||||
xcheck_1b: 2.0
|
||||
xcheck_2b: 6.0
|
||||
xcheck_3b: 3.0
|
||||
xcheck_ss: 7.0
|
||||
xcheck_lf: 2.0
|
||||
xcheck_cf: 3.0
|
||||
xcheck_rf: 2.0
|
||||
vs_R:
|
||||
# Same structure as vs_L
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Creating a New Custom Character
|
||||
|
||||
```bash
|
||||
# 1. Create template
|
||||
pd-cards custom new --name "John Slugger" --type batter --hand R --target-ops 0.850
|
||||
|
||||
# 2. Edit the YAML file
|
||||
# pd_cards/custom/profiles/john_slugger.yaml
|
||||
|
||||
# 3. Preview ratings
|
||||
pd-cards custom preview john_slugger
|
||||
|
||||
# 4. Create player in database (get IDs)
|
||||
# Use Paper Dynasty admin interface
|
||||
|
||||
# 5. Update YAML with player_id and batting_card_id
|
||||
|
||||
# 6. Submit
|
||||
pd-cards custom submit john_slugger --dry-run
|
||||
pd-cards custom submit john_slugger
|
||||
```
|
||||
|
||||
### Processing a Historical Season
|
||||
|
||||
```bash
|
||||
# 1. Fetch defensive stats
|
||||
pd-cards retrosheet defense 2005 --output "data-input/2005 Live Cardset/"
|
||||
|
||||
# 2. Generate arm ratings
|
||||
pd-cards retrosheet arms 2005 --events data-input/retrosheet/retrosheets_events_2005.csv
|
||||
|
||||
# 3. Process cards
|
||||
pd-cards retrosheet process 2005 --cardset-id 27 --description Live --dry-run
|
||||
pd-cards retrosheet process 2005 --cardset-id 27 --description Live
|
||||
|
||||
# 4. Validate positions
|
||||
pd-cards retrosheet validate 27
|
||||
|
||||
# 5. Upload to S3
|
||||
pd-cards upload s3 --cardset "2005 Live" --dry-run
|
||||
pd-cards upload s3 --cardset "2005 Live"
|
||||
|
||||
# 6. Generate scouting reports
|
||||
pd-cards scouting all --cardset-id 27
|
||||
```
|
||||
|
||||
### Updating Live Season Cards
|
||||
|
||||
```bash
|
||||
# Mid-season update (81 games)
|
||||
pd-cards live-series update --cardset "2025 Season" --games 81 --dry-run
|
||||
pd-cards live-series update --cardset "2025 Season" --games 81
|
||||
|
||||
# Full season update
|
||||
pd-cards live-series update --cardset "2025 Season" --games 162
|
||||
|
||||
# Upload updated cards
|
||||
pd-cards upload s3 --cardset "2025 Season"
|
||||
```
|
||||
670
pd_cards/core/scouting.py
Normal file
670
pd_cards/core/scouting.py
Normal file
@ -0,0 +1,670 @@
|
||||
"""
|
||||
Scouting report generation core logic.
|
||||
|
||||
Business logic for generating batting and pitching scouting reports.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
from functools import partial
|
||||
import multiprocessing
|
||||
from pathlib import Path
|
||||
from typing import Literal, Optional, List
|
||||
|
||||
import pandas as pd
|
||||
|
||||
# These imports are resolved at runtime when called from CLI
|
||||
# since the CLI adds the parent directory to sys.path
|
||||
from db_calls import db_get
|
||||
from exceptions import logger, log_exception
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Shared Utilities
|
||||
# =============================================================================
|
||||
|
||||
def log_time(
|
||||
which: Literal['start', 'end'],
|
||||
message: str = '',
|
||||
print_to_console: bool = True,
|
||||
start_time: datetime.datetime = None
|
||||
) -> Optional[datetime.datetime]:
|
||||
"""Log timing information for operations."""
|
||||
if print_to_console and len(message) == 0:
|
||||
log_exception(KeyError, 'A message must be included when print_to_console equals True')
|
||||
|
||||
if which == 'start':
|
||||
logger.info(f'starting timer - {message}')
|
||||
if print_to_console:
|
||||
print(message)
|
||||
return datetime.datetime.now()
|
||||
|
||||
elif start_time is not None:
|
||||
logger.info(f'ending timer - {message}: {(datetime.datetime.now() - start_time).total_seconds():.2f}s\n')
|
||||
if print_to_console:
|
||||
print(f'{message}\n')
|
||||
return None
|
||||
|
||||
else:
|
||||
log_exception(KeyError, 'start_time must be passed to log_time() when which equals \'end\'')
|
||||
|
||||
|
||||
async def fetch_data(data: tuple) -> dict:
|
||||
"""Fetch data from API endpoint."""
|
||||
start_time = log_time('start', print_to_console=False)
|
||||
this_query = await db_get(endpoint=data[0], params=data[1])
|
||||
log_time('end', print_to_console=False, start_time=start_time)
|
||||
return this_query
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Batting Scouting
|
||||
# =============================================================================
|
||||
|
||||
def build_series(label: str, code: str, pos_code: str, all_positions: list) -> pd.Series:
|
||||
"""Build a pandas Series from position data."""
|
||||
logger.info(f'Building {label} series for {pos_code}')
|
||||
return pd.Series(
|
||||
dict([(x['player']['player_id'], x[code]) for x in all_positions if x['position'] == pos_code]),
|
||||
name=f'{label} {pos_code}'
|
||||
)
|
||||
|
||||
|
||||
def build_ranges(all_positions: list, pos_code: str) -> pd.Series:
|
||||
"""Build range rating series for a position."""
|
||||
return build_series('Range', 'range', pos_code, all_positions)
|
||||
|
||||
|
||||
def build_errors(all_positions: list, pos_code: str) -> pd.Series:
|
||||
"""Build error rating series for a position."""
|
||||
x = build_series('Error', 'error', pos_code, all_positions)
|
||||
logger.info(f'error ratings:\n{x}')
|
||||
return x
|
||||
|
||||
|
||||
def build_of_arms(all_positions: list, pos_code: str) -> pd.Series:
|
||||
"""Build outfield arm rating series."""
|
||||
logger.info(f'Building OF series for {pos_code}')
|
||||
return pd.Series(
|
||||
dict([(x['player']['player_id'], x['arm']) for x in all_positions if x['position'] == pos_code]),
|
||||
name='Arm OF'
|
||||
)
|
||||
|
||||
|
||||
def build_c_arms(all_positions: list, pos_code: str) -> pd.Series:
|
||||
"""Build catcher arm rating series."""
|
||||
x = build_series('Arm', 'arm', pos_code, all_positions)
|
||||
logger.info(f'arm ratings:\n{x}')
|
||||
return x
|
||||
|
||||
|
||||
def build_c_pb(all_positions: list, pos_code: str) -> pd.Series:
|
||||
"""Build catcher passed ball rating series."""
|
||||
return build_series('PB', 'pb', pos_code, all_positions)
|
||||
|
||||
|
||||
def build_c_throw(all_positions: list, pos_code: str) -> pd.Series:
|
||||
"""Build catcher overthrow rating series."""
|
||||
return build_series('Throw', 'overthrow', pos_code, all_positions)
|
||||
|
||||
|
||||
async def get_batting_scouting_dfs(cardset_ids: List[int] = None) -> pd.DataFrame:
|
||||
"""
|
||||
Fetch and build batting scouting dataframes from API.
|
||||
|
||||
Args:
|
||||
cardset_ids: List of cardset IDs to filter by (empty = all)
|
||||
|
||||
Returns:
|
||||
DataFrame with batting ratings and defensive positions joined
|
||||
"""
|
||||
cardset_ids = cardset_ids or []
|
||||
cardset_params = [('cardset_id', x) for x in cardset_ids]
|
||||
ratings_params = [('team_id', 31), ('ts', 's37136685556r6135248705'), *cardset_params]
|
||||
|
||||
API_CALLS = [
|
||||
('battingcardratings', [('vs_hand', 'vL'), *ratings_params]),
|
||||
('battingcardratings', [('vs_hand', 'vR'), *ratings_params]),
|
||||
('cardpositions', cardset_params)
|
||||
]
|
||||
|
||||
start_time = log_time('start', message='Pulling all batting card ratings and positions')
|
||||
tasks = [fetch_data(params) for params in API_CALLS]
|
||||
api_data = await asyncio.gather(*tasks)
|
||||
log_time('end', f'Pulled {api_data[0]["count"] + api_data[1]["count"]} batting card ratings and {api_data[2]["count"]} positions', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', message='Building base dataframes')
|
||||
|
||||
vl_vals = api_data[0]['ratings']
|
||||
for x in vl_vals:
|
||||
x.update(x['battingcard'])
|
||||
x['player_id'] = x['battingcard']['player']['player_id']
|
||||
x['player_name'] = x['battingcard']['player']['p_name']
|
||||
x['rarity'] = x['battingcard']['player']['rarity']['name']
|
||||
x['cardset_id'] = x['battingcard']['player']['cardset']['id']
|
||||
x['cardset_name'] = x['battingcard']['player']['cardset']['name']
|
||||
del x['battingcard']
|
||||
del x['player']
|
||||
|
||||
vr_vals = api_data[1]['ratings']
|
||||
for x in vr_vals:
|
||||
x['player_id'] = x['battingcard']['player']['player_id']
|
||||
del x['battingcard']
|
||||
|
||||
vl = pd.DataFrame(vl_vals)
|
||||
vr = pd.DataFrame(vr_vals)
|
||||
|
||||
log_time('end', 'Base dataframes are complete', start_time=start_time)
|
||||
start_time = log_time('start', message='Building combined dataframe')
|
||||
|
||||
bat_df = pd.merge(vl, vr, on='player_id', suffixes=('_vl', '_vr')).set_index('player_id', drop=False)
|
||||
|
||||
log_time('end', 'Combined dataframe is complete', start_time=start_time)
|
||||
|
||||
POSITION_DATA = api_data[2]['positions']
|
||||
series_list = []
|
||||
POSITIONS = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']
|
||||
|
||||
start_time = log_time('start', message='Building range series')
|
||||
with multiprocessing.Pool(processes=min(8, multiprocessing.cpu_count())) as pool:
|
||||
get_ranges = partial(build_ranges, POSITION_DATA)
|
||||
ranges = pool.map(get_ranges, POSITIONS)
|
||||
series_list.extend(ranges)
|
||||
log_time('end', f'Processed {len(ranges)} position ranges', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', message='Building error series')
|
||||
with multiprocessing.Pool(processes=min(8, multiprocessing.cpu_count())) as pool:
|
||||
get_errors = partial(build_errors, POSITION_DATA)
|
||||
errors = pool.map(get_errors, POSITIONS)
|
||||
series_list.extend(errors)
|
||||
log_time('end', f'Processed {len(errors)} position errors', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', message='Building OF arm series')
|
||||
lf_arms = build_of_arms(POSITION_DATA, 'LF')
|
||||
cf_arms = build_of_arms(POSITION_DATA, 'CF')
|
||||
rf_arms = build_of_arms(POSITION_DATA, 'RF')
|
||||
|
||||
combined_series = lf_arms.combine(cf_arms, max, fill_value=0)
|
||||
combined_series = combined_series.combine(rf_arms, max, fill_value=0)
|
||||
series_list.extend([combined_series])
|
||||
log_time('end', f'Processed {len(combined_series)} OF arms', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', message='Building C arm series')
|
||||
c_arms = build_c_arms(POSITION_DATA, 'C')
|
||||
series_list.extend([c_arms])
|
||||
log_time('end', f'Processed {len(c_arms)} catcher arms', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', message='Building C PB series')
|
||||
with multiprocessing.Pool(processes=min(8, multiprocessing.cpu_count())) as pool:
|
||||
get_pb = partial(build_c_pb, POSITION_DATA)
|
||||
passed_ball = pool.map(get_pb, ['C'])
|
||||
series_list.extend(passed_ball)
|
||||
log_time('end', f'Processed {len(passed_ball)} C PB series', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', message='Building C OT series')
|
||||
with multiprocessing.Pool(processes=min(8, multiprocessing.cpu_count())) as pool:
|
||||
get_throw = partial(build_c_throw, POSITION_DATA)
|
||||
overthrows = pool.map(get_throw, ['C'])
|
||||
series_list.extend(overthrows)
|
||||
log_time('end', f'Processed {len(overthrows)} C OT series', start_time=start_time)
|
||||
|
||||
logger.info(f'series_list: {series_list}')
|
||||
return bat_df.join(series_list)
|
||||
|
||||
|
||||
async def calc_batting_basic(batting_dfs: pd.DataFrame, output_dir: Path) -> None:
|
||||
"""
|
||||
Calculate basic batting scouting metrics and save to CSV.
|
||||
|
||||
Args:
|
||||
batting_dfs: DataFrame with batting ratings
|
||||
output_dir: Directory to save output CSV
|
||||
"""
|
||||
def get_raw_speed(df_data):
|
||||
speed_raw = df_data['running'] / 20 + df_data['steal_jump']
|
||||
if df_data['steal_auto']:
|
||||
speed_raw += 0.5
|
||||
return speed_raw
|
||||
|
||||
start_time = log_time('start', 'Beginning Speed calcs')
|
||||
raw_series = batting_dfs.apply(get_raw_speed, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
batting_dfs['Speed'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Speed calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning Stealing calcs')
|
||||
def get_raw_steal(df_data):
|
||||
return (
|
||||
((df_data['steal_high'] / 20) + (df_data['steal_low'] / 20)) * df_data['steal_jump']
|
||||
)
|
||||
raw_series = batting_dfs.apply(get_raw_steal, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
batting_dfs['Steal'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Stealing calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning Reaction calcs')
|
||||
def get_raw_reaction(df_data):
|
||||
raw_total = 0
|
||||
for pos_range in [df_data['Range C'], df_data['Range 1B'], df_data['Range 2B'], df_data['Range 3B'],
|
||||
df_data['Range SS'], df_data['Range LF'], df_data['Range CF'], df_data['Range RF']]:
|
||||
if pd.notna(pos_range):
|
||||
raw_total += 10 ** (5 - pos_range)
|
||||
return raw_total
|
||||
raw_series = batting_dfs.apply(get_raw_reaction, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
batting_dfs['Reaction'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Reaction calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning Arm calcs')
|
||||
def get_raw_arm(df_data):
|
||||
of_arm = None
|
||||
of_pos = None
|
||||
if pd.notna(df_data['Range RF']):
|
||||
of_pos = 'RF'
|
||||
elif pd.notna(df_data['Range CF']):
|
||||
of_pos = 'CF'
|
||||
elif pd.notna(df_data['Range LF']):
|
||||
of_pos = 'LF'
|
||||
|
||||
if of_pos is not None:
|
||||
if df_data['Arm OF'] < 0:
|
||||
of_raw = df_data['Arm OF'] * -10
|
||||
else:
|
||||
of_raw = (5 - df_data['Arm OF'])
|
||||
|
||||
if of_pos == 'RF':
|
||||
of_raw = of_raw * 1.5
|
||||
of_raw += ((6 - df_data['Range RF']) * 4)
|
||||
elif of_pos == 'CF':
|
||||
of_raw += ((6 - df_data['Range CF']) * 3)
|
||||
elif of_pos == 'LF':
|
||||
of_raw = of_raw / 2
|
||||
of_raw += ((6 - df_data['Range LF']) * 2)
|
||||
of_arm = of_raw
|
||||
|
||||
if_arm = None
|
||||
if pd.notna(df_data['Range 3B']) or pd.notna(df_data['Range 2B']) or pd.notna(df_data['Range 1B']) or \
|
||||
pd.notna(df_data['Range SS']):
|
||||
range_totals = 0
|
||||
if pd.notna(df_data['Range 3B']):
|
||||
range_totals += ((6 - df_data['Range 3B']) * 5)
|
||||
if pd.notna(df_data['Range SS']):
|
||||
range_totals += ((6 - df_data['Range SS']) * 4)
|
||||
if pd.notna(df_data['Range 2B']):
|
||||
range_totals += ((6 - df_data['Range 2B']) * 3)
|
||||
if pd.notna(df_data['Range 1B']):
|
||||
range_totals += (6 - df_data['Range 1B'])
|
||||
if_arm = 100 - (50 - range_totals)
|
||||
|
||||
c_arm = None
|
||||
if pd.notna(df_data['Arm C']):
|
||||
if df_data['Arm C'] == -5:
|
||||
c_arm = 100
|
||||
else:
|
||||
temp_arm = 20 + ((10 - df_data['Arm C']) * 3) + (20 - df_data['PB C']) + (20 - df_data['Throw C']) - \
|
||||
df_data['Error C']
|
||||
c_arm = min(100, temp_arm)
|
||||
|
||||
if c_arm is not None:
|
||||
return c_arm
|
||||
elif of_arm is not None:
|
||||
return of_arm
|
||||
elif if_arm is not None:
|
||||
return if_arm
|
||||
else:
|
||||
return 1
|
||||
raw_series = batting_dfs.apply(get_raw_arm, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
batting_dfs['Arm'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Arm calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning Fielding calcs')
|
||||
def get_raw_fielding(df_data):
|
||||
if_error, of_error, c_error = 0, 0, 0
|
||||
denom = 0
|
||||
if pd.notna(df_data['Error 3B']) or pd.notna(df_data['Error 2B']) or pd.notna(df_data['Error 1B']) or \
|
||||
pd.notna(df_data['Error SS']):
|
||||
raw_if = 100
|
||||
if pd.notna(df_data['Error 3B']):
|
||||
raw_if -= (df_data['Error 3B'] * 2)
|
||||
if pd.notna(df_data['Error SS']):
|
||||
raw_if -= (df_data['Error SS'] * .75)
|
||||
if pd.notna(df_data['Error 2B']):
|
||||
raw_if -= (df_data['Error 2B'] * 1.25)
|
||||
if pd.notna(df_data['Error 1B']):
|
||||
raw_if -= (df_data['Error 1B'] * 2)
|
||||
if_error = max(1, raw_if)
|
||||
denom += 1
|
||||
|
||||
if pd.notna(df_data['Error LF']) or pd.notna(df_data['Error CF']) or pd.notna(df_data['Error RF']):
|
||||
raw_of = 100
|
||||
if pd.notna(df_data['Error LF']):
|
||||
raw_of -= (df_data['Error LF'] * 2)
|
||||
if pd.notna(df_data['Error CF']):
|
||||
raw_of -= (df_data['Error CF'] * .75)
|
||||
if pd.notna(df_data['Error RF']):
|
||||
raw_of -= (df_data['Error RF'] * 1.25)
|
||||
of_error = max(1, raw_of)
|
||||
denom += 1
|
||||
|
||||
if pd.notna(df_data['Error C']):
|
||||
c_error = max(100 - (df_data['Error C'] * 5) - df_data['Throw C'] - df_data['PB C'], 1)
|
||||
denom += 1
|
||||
|
||||
return sum([if_error, of_error, c_error]) / max(denom, 1)
|
||||
raw_series = batting_dfs.apply(get_raw_fielding, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
batting_dfs['Fielding'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Fielding calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning AVG vL calcs')
|
||||
rank_series = batting_dfs['avg_vl'].rank(pct=True)
|
||||
batting_dfs['Contact L'] = round(rank_series * 100)
|
||||
log_time('end', 'Done AVG vL calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning AVG vR calcs')
|
||||
rank_series = batting_dfs['avg_vr'].rank(pct=True)
|
||||
batting_dfs['Contact R'] = round(rank_series * 100)
|
||||
log_time('end', 'Done AVG vR calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning PWR vL calcs')
|
||||
rank_series = batting_dfs['slg_vl'].rank(pct=True)
|
||||
batting_dfs['Power L'] = round(rank_series * 100)
|
||||
log_time('end', 'Done PWR vL calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning PWR vR calcs')
|
||||
rank_series = batting_dfs['slg_vr'].rank(pct=True)
|
||||
batting_dfs['Power R'] = round(rank_series * 100)
|
||||
log_time('end', 'Done PWR vR calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning Vision calcs')
|
||||
def get_raw_vision(df_data):
|
||||
return (
|
||||
((((df_data['obp_vr'] * 0.67) + (df_data['obp_vl'] * 0.33)) -
|
||||
((df_data['avg_vr'] * 0.67) + (df_data['avg_vl'] * 0.33))) * 5) -
|
||||
(((df_data['strikeout_vl'] * 0.33) + (df_data['strikeout_vr'] * 0.67)) / 208)
|
||||
)
|
||||
raw_series = batting_dfs.apply(get_raw_vision, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
batting_dfs['Vision'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Vision calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning Rating calcs')
|
||||
def get_raw_rating(df_data):
|
||||
return (
|
||||
((df_data['Reaction'] + df_data['Arm'] + df_data['Fielding']) * 2) +
|
||||
(df_data['Speed'] + df_data['Steal']) +
|
||||
((((df_data['Contact R'] + df_data['Power R']) * 0.67) +
|
||||
((df_data['Contact L'] + df_data['Power L']) * 0.33) + df_data['Vision']) * 6)
|
||||
)
|
||||
raw_series = batting_dfs.apply(get_raw_rating, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
batting_dfs['Rating'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Rating calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning write to file')
|
||||
output = batting_dfs[[
|
||||
'player_id', 'player_name', 'Rating', 'Contact R', 'Contact L', 'Power R', 'Power L', 'Vision', 'Speed',
|
||||
'Steal', 'Reaction', 'Arm', 'Fielding', 'hand', 'cardset_name'
|
||||
]]
|
||||
csv_file = pd.DataFrame(output).to_csv(index=False)
|
||||
output_file = output_dir / 'batting-basic.csv'
|
||||
with open(output_file, 'w') as file:
|
||||
file.write(csv_file)
|
||||
log_time('end', 'Done writing to file', start_time=start_time)
|
||||
|
||||
|
||||
async def calc_batting_ratings(batting_dfs: pd.DataFrame, output_dir: Path) -> None:
|
||||
"""
|
||||
Filter batting ratings and save to CSV.
|
||||
|
||||
Args:
|
||||
batting_dfs: DataFrame with batting ratings
|
||||
output_dir: Directory to save output CSV
|
||||
"""
|
||||
start_time = log_time('start', 'Beginning Ratings filtering')
|
||||
output = batting_dfs
|
||||
first = ['player_id', 'player_name', 'cardset_name', 'rarity', 'hand', 'variant']
|
||||
exclude = first + ['id_vl', 'id_vr', 'vs_hand_vl', 'vs_hand_vr']
|
||||
output = output[first + [col for col in output.columns if col not in exclude]]
|
||||
log_time('end', 'Done filtering ratings', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning write to file')
|
||||
csv_file = pd.DataFrame(output).to_csv(index=False)
|
||||
output_file = output_dir / 'batting-ratings.csv'
|
||||
with open(output_file, 'w') as file:
|
||||
file.write(csv_file)
|
||||
log_time('end', 'Done writing to file', start_time=start_time)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Pitching Scouting
|
||||
# =============================================================================
|
||||
|
||||
async def get_pitching_scouting_dfs(cardset_ids: List[int] = None) -> pd.DataFrame:
|
||||
"""
|
||||
Fetch and build pitching scouting dataframes from API.
|
||||
|
||||
Args:
|
||||
cardset_ids: List of cardset IDs to filter by (empty = all)
|
||||
|
||||
Returns:
|
||||
DataFrame with pitching ratings and defensive positions joined
|
||||
"""
|
||||
cardset_ids = cardset_ids or []
|
||||
cardset_params = [('cardset_id', x) for x in cardset_ids]
|
||||
ratings_params = [('team_id', 31), ('ts', 's37136685556r6135248705'), *cardset_params]
|
||||
|
||||
API_CALLS = [
|
||||
('pitchingcardratings', [('vs_hand', 'vL'), *ratings_params]),
|
||||
('pitchingcardratings', [('vs_hand', 'vR'), *ratings_params]),
|
||||
('cardpositions', [('position', 'P'), *cardset_params])
|
||||
]
|
||||
|
||||
start_time = log_time('start', message='Pulling all pitching card ratings and positions')
|
||||
tasks = [fetch_data(params) for params in API_CALLS]
|
||||
api_data = await asyncio.gather(*tasks)
|
||||
log_time('end', f'Pulled {api_data[0]["count"] + api_data[1]["count"]} pitching card ratings and {api_data[2]["count"]} positions', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', message='Building base dataframes')
|
||||
|
||||
vl_vals = api_data[0]['ratings']
|
||||
for x in vl_vals:
|
||||
x.update(x['pitchingcard'])
|
||||
x['player_id'] = x['pitchingcard']['player']['player_id']
|
||||
x['player_name'] = x['pitchingcard']['player']['p_name']
|
||||
x['rarity'] = x['pitchingcard']['player']['rarity']['name']
|
||||
x['cardset_id'] = x['pitchingcard']['player']['cardset']['id']
|
||||
x['cardset_name'] = x['pitchingcard']['player']['cardset']['name']
|
||||
x['starter_rating'] = x['pitchingcard']['starter_rating']
|
||||
x['relief_rating'] = x['pitchingcard']['relief_rating']
|
||||
x['closer_rating'] = x['pitchingcard']['closer_rating']
|
||||
del x['pitchingcard'], x['player']
|
||||
|
||||
vr_vals = api_data[1]['ratings']
|
||||
for x in vr_vals:
|
||||
x['player_id'] = x['pitchingcard']['player']['player_id']
|
||||
del x['pitchingcard']
|
||||
|
||||
vl = pd.DataFrame(vl_vals)
|
||||
vr = pd.DataFrame(vr_vals)
|
||||
|
||||
pit_df = pd.merge(vl, vr, on='player_id', suffixes=('_vl', '_vr')).set_index('player_id', drop=False)
|
||||
|
||||
log_time('end', 'Base dataframes are complete', start_time=start_time)
|
||||
start_time = log_time('start', message='Building defense series')
|
||||
|
||||
positions = api_data[2]['positions']
|
||||
series_list = [
|
||||
pd.Series(
|
||||
dict([(x['player']['player_id'], x['range']) for x in positions]),
|
||||
name='Range P'
|
||||
),
|
||||
pd.Series(
|
||||
dict([(x['player']['player_id'], x['error']) for x in positions]),
|
||||
name='Error P'
|
||||
)
|
||||
]
|
||||
log_time('end', f'Processed {len(positions)} defense series', start_time=start_time)
|
||||
logger.info(f'series_list: {series_list}')
|
||||
|
||||
return pit_df.join(series_list)
|
||||
|
||||
|
||||
async def calc_pitching_basic(pitching_dfs: pd.DataFrame, output_dir: Path) -> None:
|
||||
"""
|
||||
Calculate basic pitching scouting metrics and save to CSV.
|
||||
|
||||
Args:
|
||||
pitching_dfs: DataFrame with pitching ratings
|
||||
output_dir: Directory to save output CSV
|
||||
"""
|
||||
raw_data = pitching_dfs
|
||||
|
||||
def get_raw_leftcontrol(df_data):
|
||||
return ((1 - (df_data['obp_vl'] - df_data['avg_vl'])) * 100) + (1 - (df_data['wild_pitch'] / 20))
|
||||
|
||||
start_time = log_time('start', 'Beginning Control L calcs')
|
||||
raw_series = raw_data.apply(get_raw_leftcontrol, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
raw_data['Control L'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Control L calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning Control R calcs')
|
||||
def get_raw_rightcontrol(df_data):
|
||||
return ((1 - (df_data['obp_vr'] - df_data['avg_vr'])) * 100) + (1 - (df_data['wild_pitch'] / 20))
|
||||
raw_series = raw_data.apply(get_raw_rightcontrol, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
raw_data['Control R'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Control R calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning Stuff L calcs')
|
||||
def get_raw_leftstuff(df_data):
|
||||
return 10 - (df_data['slg_vl'] + df_data['slg_vl'] + ((df_data['homerun_vl'] + df_data['bp_homerun_vl']) / 108))
|
||||
raw_series = raw_data.apply(get_raw_leftstuff, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
raw_data['Stuff L'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Stuff L calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning Stuff R calcs')
|
||||
def get_raw_rightstuff(df_data):
|
||||
return 10 - (df_data['slg_vr'] + df_data['slg_vr'] + ((df_data['homerun_vr'] + df_data['bp_homerun_vr']) / 108))
|
||||
raw_series = raw_data.apply(get_raw_rightstuff, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
raw_data['Stuff R'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Stuff R calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning Fielding calcs')
|
||||
def get_raw_fielding(df_data):
|
||||
return ((6 - df_data['Range P']) * 10) + (50 - df_data['Error P'])
|
||||
raw_series = raw_data.apply(get_raw_fielding, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
logger.info(f'max fld: {raw_series.max()} / min fld: {raw_series.min()}')
|
||||
raw_data['Fielding'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Fielding calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning Stamina calcs')
|
||||
def get_raw_stamina(df_data):
|
||||
spow = df_data['starter_rating'] if pd.isna(df_data['starter_rating']) else -1
|
||||
rpow = df_data['relief_rating'] if pd.isna(df_data['relief_rating']) else -1
|
||||
this_pow = spow if spow > rpow else rpow
|
||||
return (((this_pow * (df_data['obp_vr'] * (2 / 3))) + (this_pow * (df_data['obp_vl'] / 3))) * 4.5) + this_pow
|
||||
raw_series = raw_data.apply(get_raw_stamina, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
raw_data['Stamina'] = round(rank_series * 100)
|
||||
log_time('end', 'Done Stamina calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning H/9 calcs')
|
||||
def get_raw_hit(df_data):
|
||||
return 1 - (df_data['avg_vr'] * (2 / 3)) + (df_data['avg_vl'] / 3)
|
||||
raw_series = raw_data.apply(get_raw_hit, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
raw_data['H/9'] = round(rank_series * 100)
|
||||
log_time('end', 'Done H/9 calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning K/9 calcs')
|
||||
def get_raw_k(df_data):
|
||||
return ((df_data['strikeout_vr'] / 108) * (2 / 3)) + ((df_data['strikeout_vl'] / 108) / 3)
|
||||
raw_series = raw_data.apply(get_raw_k, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
raw_data['K/9'] = round(rank_series * 100)
|
||||
log_time('end', 'Done K/9 calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning BB/9 calcs')
|
||||
def get_raw_bb(df_data):
|
||||
return ((df_data['walk_vr'] / 108) * (2 / 3)) + ((df_data['walk_vl'] / 108) / 3)
|
||||
raw_series = raw_data.apply(get_raw_bb, axis=1)
|
||||
rank_series = raw_series.rank(pct=True, ascending=False)
|
||||
raw_data['BB/9'] = round(rank_series * 100)
|
||||
log_time('end', 'Done BB/9 calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning HR/9 calcs')
|
||||
def get_raw_hr(df_data):
|
||||
return 1 - (
|
||||
(((df_data['homerun_vr'] + df_data['bp_homerun_vr']) / 108) * (2 / 3)) +
|
||||
(((df_data['homerun_vl'] + df_data['bp_homerun_vl']) / 108) / 3)
|
||||
)
|
||||
raw_series = raw_data.apply(get_raw_hr, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
raw_data['HR/9'] = round(rank_series * 100)
|
||||
log_time('end', 'Done HR/9 calcs', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning Rating calcs')
|
||||
def get_raw_rating(df_data):
|
||||
spow = df_data['starter_rating'] if pd.isna(df_data['starter_rating']) else -1
|
||||
rpow = df_data['relief_rating'] if pd.isna(df_data['relief_rating']) else -1
|
||||
|
||||
if spow > rpow and spow >= 4:
|
||||
return (
|
||||
((df_data['H/9'] + df_data['K/9'] + df_data['BB/9'] + df_data['HR/9']) * 5) +
|
||||
(df_data['Fielding']) + (df_data['Stamina'] * 5) +
|
||||
(((df_data['Stuff L'] / 3) + (df_data['Stuff R'] * (2 / 3))) * 4) +
|
||||
(((df_data['Control L'] / 3) + (df_data['Control R'] * (2 / 3))) * 2)
|
||||
)
|
||||
else:
|
||||
return (
|
||||
((df_data['H/9'] + df_data['K/9'] + df_data['BB/9'] + df_data['HR/9']) * 5) +
|
||||
(df_data['Fielding']) + (df_data['Stamina'] * 5) +
|
||||
(((df_data['Stuff L'] / 3) + (df_data['Stuff R'] * (2 / 3))) * 4) +
|
||||
(((df_data['Control L'] / 3) + (df_data['Control R'] * (2 / 3))) * 2)
|
||||
)
|
||||
raw_series = raw_data.apply(get_raw_rating, axis=1)
|
||||
rank_series = raw_series.rank(pct=True)
|
||||
raw_data['Rating'] = round(rank_series * 100)
|
||||
|
||||
output = raw_data[[
|
||||
'player_id', 'player_name', 'Rating', 'Control R', 'Control L', 'Stuff R', 'Stuff L', 'Stamina', 'Fielding',
|
||||
'H/9', 'K/9', 'BB/9', 'HR/9', 'hand', 'cardset_name'
|
||||
]]
|
||||
|
||||
log_time('end', 'Done Rating calcs', start_time=start_time)
|
||||
start_time = log_time('start', 'Beginning write csv')
|
||||
|
||||
csv_file = pd.DataFrame(output).to_csv(index=False)
|
||||
output_file = output_dir / 'pitching-basic.csv'
|
||||
with open(output_file, 'w') as file:
|
||||
file.write(csv_file)
|
||||
log_time('end', 'Done writing to file', start_time=start_time)
|
||||
|
||||
|
||||
async def calc_pitching_ratings(pitching_dfs: pd.DataFrame, output_dir: Path) -> None:
|
||||
"""
|
||||
Filter pitching ratings and save to CSV.
|
||||
|
||||
Args:
|
||||
pitching_dfs: DataFrame with pitching ratings
|
||||
output_dir: Directory to save output CSV
|
||||
"""
|
||||
start_time = log_time('start', 'Beginning Ratings filtering')
|
||||
output = pitching_dfs
|
||||
first = ['player_id', 'player_name', 'cardset_name', 'rarity', 'hand', 'variant']
|
||||
exclude = first + ['id_vl', 'id_vr', 'vs_hand_vl', 'vs_hand_vr']
|
||||
output = output[first + [col for col in output.columns if col not in exclude]]
|
||||
log_time('end', 'Done filtering ratings', start_time=start_time)
|
||||
|
||||
start_time = log_time('start', 'Beginning write to file')
|
||||
csv_file = pd.DataFrame(output).to_csv(index=False)
|
||||
output_file = output_dir / 'pitching-ratings.csv'
|
||||
with open(output_file, 'w') as file:
|
||||
file.write(csv_file)
|
||||
log_time('end', 'Done writing to file', start_time=start_time)
|
||||
553
pd_cards/core/upload.py
Normal file
553
pd_cards/core/upload.py
Normal file
@ -0,0 +1,553 @@
|
||||
"""
|
||||
Card image upload and management core logic.
|
||||
|
||||
Business logic for uploading card images to AWS S3 and managing card URLs.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
from typing import Optional, List, Tuple
|
||||
import urllib.parse
|
||||
|
||||
# These imports are resolved at runtime when called from CLI
|
||||
# since the CLI adds the parent directory to sys.path
|
||||
from db_calls import db_get, db_patch, db_post, url_get
|
||||
from exceptions import logger
|
||||
|
||||
|
||||
# AWS Configuration
|
||||
DEFAULT_AWS_BUCKET = "paper-dynasty"
|
||||
DEFAULT_AWS_REGION = "us-east-1"
|
||||
|
||||
|
||||
def get_s3_base_url(
|
||||
bucket: str = DEFAULT_AWS_BUCKET, region: str = DEFAULT_AWS_REGION
|
||||
) -> str:
|
||||
"""Get the S3 base URL for a bucket."""
|
||||
return f"https://{bucket}.s3.{region}.amazonaws.com"
|
||||
|
||||
|
||||
async def fetch_card_image(session, card_url: str, timeout: int = 6) -> bytes:
|
||||
"""
|
||||
Fetch card image from URL and return raw bytes.
|
||||
|
||||
Args:
|
||||
session: aiohttp ClientSession to use for the request
|
||||
card_url: URL to fetch the card from
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
Raw PNG image bytes
|
||||
"""
|
||||
import aiohttp
|
||||
|
||||
async with session.get(
|
||||
card_url, timeout=aiohttp.ClientTimeout(total=timeout)
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
logger.info(f"Fetched card image from {card_url}")
|
||||
return await resp.read()
|
||||
else:
|
||||
error_text = await resp.text()
|
||||
logger.error(f"Failed to fetch card: {error_text}")
|
||||
raise ValueError(f"Card fetch error: {error_text}")
|
||||
|
||||
|
||||
def upload_card_to_s3(
|
||||
s3_client,
|
||||
image_data: bytes,
|
||||
player_id: int,
|
||||
card_type: str,
|
||||
release_date: str,
|
||||
cardset_id: int,
|
||||
bucket: str = DEFAULT_AWS_BUCKET,
|
||||
region: str = DEFAULT_AWS_REGION,
|
||||
) -> str:
|
||||
"""
|
||||
Upload card image to S3 and return the S3 URL with cache-busting param.
|
||||
|
||||
Args:
|
||||
s3_client: Boto3 S3 client
|
||||
image_data: Raw PNG image bytes
|
||||
player_id: Player ID
|
||||
card_type: 'batting' or 'pitching'
|
||||
release_date: Date string for cache busting (e.g., '2025-11-8')
|
||||
cardset_id: Cardset ID (will be zero-padded to 3 digits)
|
||||
bucket: S3 bucket name
|
||||
region: AWS region
|
||||
|
||||
Returns:
|
||||
Full S3 URL with ?d= parameter
|
||||
"""
|
||||
# Format cardset_id with 3 digits and leading zeros
|
||||
cardset_str = f"{cardset_id:03d}"
|
||||
s3_key = f"cards/cardset-{cardset_str}/player-{player_id}/{card_type}card.png"
|
||||
s3_base_url = get_s3_base_url(bucket, region)
|
||||
|
||||
try:
|
||||
s3_client.put_object(
|
||||
Bucket=bucket,
|
||||
Key=s3_key,
|
||||
Body=image_data,
|
||||
ContentType="image/png",
|
||||
CacheControl="public, max-age=300", # 5 minute cache
|
||||
Metadata={
|
||||
"player-id": str(player_id),
|
||||
"card-type": card_type,
|
||||
"upload-date": datetime.datetime.now().isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
# Return URL with cache-busting parameter
|
||||
s3_url = f"{s3_base_url}/{s3_key}?d={release_date}"
|
||||
logger.info(f"Uploaded {card_type} card for player {player_id} to S3: {s3_url}")
|
||||
return s3_url
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to upload {card_type} card for player {player_id}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def upload_cards_to_s3(
|
||||
cardset_name: str,
|
||||
start_id: Optional[int] = None,
|
||||
limit: Optional[int] = None,
|
||||
html_cards: bool = False,
|
||||
skip_batters: bool = False,
|
||||
skip_pitchers: bool = False,
|
||||
upload: bool = True,
|
||||
update_urls: bool = True,
|
||||
bucket: str = DEFAULT_AWS_BUCKET,
|
||||
region: str = DEFAULT_AWS_REGION,
|
||||
on_progress: callable = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Upload card images to S3 for a cardset.
|
||||
|
||||
Args:
|
||||
cardset_name: Name of the cardset to process
|
||||
start_id: Player ID to start from (for resuming)
|
||||
limit: Maximum number of cards to process
|
||||
html_cards: Fetch HTML preview cards instead of PNG
|
||||
skip_batters: Skip batting cards
|
||||
skip_pitchers: Skip pitching cards
|
||||
upload: Actually upload to S3
|
||||
update_urls: Update player URLs in database
|
||||
bucket: S3 bucket name
|
||||
region: AWS region
|
||||
on_progress: Callback function for progress updates
|
||||
|
||||
Returns:
|
||||
Dict with counts of errors, successes, uploads, url_updates
|
||||
"""
|
||||
import aiohttp
|
||||
import boto3
|
||||
|
||||
# Look up cardset
|
||||
c_query = await db_get("cardsets", params=[("name", cardset_name)])
|
||||
if not c_query or c_query["count"] == 0:
|
||||
raise ValueError(f'Cardset "{cardset_name}" not found')
|
||||
cardset = c_query["cardsets"][0]
|
||||
|
||||
# Get all players
|
||||
p_query = await db_get(
|
||||
"players",
|
||||
params=[
|
||||
("inc_dex", False),
|
||||
("cardset_id", cardset["id"]),
|
||||
("short_output", True),
|
||||
],
|
||||
)
|
||||
if not p_query or p_query["count"] == 0:
|
||||
raise ValueError("No players returned from Paper Dynasty API")
|
||||
all_players = p_query["players"]
|
||||
|
||||
# Generate release date for cache busting (include timestamp for same-day updates)
|
||||
now = datetime.datetime.now()
|
||||
timestamp = int(now.timestamp())
|
||||
release_date = f"{now.year}-{now.month}-{now.day}-{timestamp}"
|
||||
|
||||
# PD API base URL for card generation
|
||||
PD_API_URL = "https://pd.manticorum.com/api"
|
||||
|
||||
# Initialize S3 client if uploading
|
||||
s3_client = boto3.client("s3", region_name=region) if upload else None
|
||||
|
||||
errors = []
|
||||
successes = []
|
||||
uploads = []
|
||||
url_updates = []
|
||||
cxn_error = False
|
||||
count = 0
|
||||
max_count = limit or 9999
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
for x in all_players:
|
||||
# Apply filters
|
||||
if "pitching" in x["image"] and skip_pitchers:
|
||||
continue
|
||||
if "batting" in x["image"] and skip_batters:
|
||||
continue
|
||||
if start_id is not None and start_id > x["player_id"]:
|
||||
continue
|
||||
if "sombaseball" in x["image"]:
|
||||
errors.append((x, f"Bad card url: {x['image']}"))
|
||||
continue
|
||||
if count >= max_count:
|
||||
break
|
||||
|
||||
count += 1
|
||||
if on_progress and count % 20 == 0:
|
||||
on_progress(count, x["p_name"])
|
||||
|
||||
# Determine card type from existing image URL
|
||||
card_type = "pitching" if "pitching" in x["image"] else "batting"
|
||||
|
||||
# Generate card URL from PD API (forces fresh generation from database)
|
||||
pd_card_url = f"{PD_API_URL}/v2/players/{x['player_id']}/{card_type}card?d={release_date}"
|
||||
|
||||
if html_cards:
|
||||
card_url = f"{pd_card_url}&html=true"
|
||||
timeout = 2
|
||||
else:
|
||||
card_url = pd_card_url
|
||||
timeout = 6
|
||||
|
||||
try:
|
||||
if upload and not html_cards:
|
||||
# Fetch card image bytes directly
|
||||
image_bytes = await fetch_card_image(
|
||||
session, card_url, timeout=timeout
|
||||
)
|
||||
s3_url = upload_card_to_s3(
|
||||
s3_client,
|
||||
image_bytes,
|
||||
x["player_id"],
|
||||
card_type,
|
||||
release_date,
|
||||
cardset["id"],
|
||||
bucket,
|
||||
region,
|
||||
)
|
||||
uploads.append((x["player_id"], card_type, s3_url))
|
||||
|
||||
# Update player record with new S3 URL
|
||||
if update_urls:
|
||||
await db_patch(
|
||||
"players",
|
||||
object_id=x["player_id"],
|
||||
params=[("image", s3_url)],
|
||||
)
|
||||
url_updates.append((x["player_id"], card_type, s3_url))
|
||||
logger.info(f"Updated player {x['player_id']} image URL to S3")
|
||||
else:
|
||||
# Just validate card exists
|
||||
logger.info(f"Validating card URL: {card_url}")
|
||||
await url_get(card_url, timeout=timeout)
|
||||
|
||||
except ConnectionError as e:
|
||||
if cxn_error:
|
||||
raise e
|
||||
cxn_error = True
|
||||
errors.append((x, e))
|
||||
|
||||
except ValueError as e:
|
||||
errors.append((x, e))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"S3 upload/update failed for player {x['player_id']}: {e}"
|
||||
)
|
||||
errors.append((x, f"S3 error: {e}"))
|
||||
continue
|
||||
|
||||
# Handle image2 (dual-position players)
|
||||
if x["image2"] is not None:
|
||||
card_type2 = "pitching" if "pitching" in x["image2"] else "batting"
|
||||
pd_card_url2 = f"{PD_API_URL}/v2/players/{x['player_id']}/{card_type2}card?d={release_date}"
|
||||
|
||||
if html_cards:
|
||||
card_url2 = f"{pd_card_url2}&html=true"
|
||||
else:
|
||||
card_url2 = pd_card_url2
|
||||
|
||||
if "sombaseball" in x["image2"]:
|
||||
errors.append((x, f"Bad card url: {x['image2']}"))
|
||||
else:
|
||||
try:
|
||||
if upload and not html_cards:
|
||||
image_bytes2 = await fetch_card_image(
|
||||
session, card_url2, timeout=6
|
||||
)
|
||||
s3_url2 = upload_card_to_s3(
|
||||
s3_client,
|
||||
image_bytes2,
|
||||
x["player_id"],
|
||||
card_type2,
|
||||
release_date,
|
||||
cardset["id"],
|
||||
bucket,
|
||||
region,
|
||||
)
|
||||
uploads.append((x["player_id"], card_type2, s3_url2))
|
||||
|
||||
if update_urls:
|
||||
await db_patch(
|
||||
"players",
|
||||
object_id=x["player_id"],
|
||||
params=[("image2", s3_url2)],
|
||||
)
|
||||
url_updates.append(
|
||||
(x["player_id"], card_type2, s3_url2)
|
||||
)
|
||||
logger.info(
|
||||
f"Updated player {x['player_id']} image2 URL to S3"
|
||||
)
|
||||
else:
|
||||
await url_get(card_url2, timeout=6)
|
||||
|
||||
successes.append(x)
|
||||
|
||||
except ConnectionError as e:
|
||||
if cxn_error:
|
||||
raise e
|
||||
cxn_error = True
|
||||
errors.append((x, e))
|
||||
|
||||
except ValueError as e:
|
||||
errors.append((x, e))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"S3 upload/update failed for player {x['player_id']} image2: {e}"
|
||||
)
|
||||
errors.append((x, f"S3 error (image2): {e}"))
|
||||
else:
|
||||
successes.append(x)
|
||||
|
||||
return {
|
||||
"errors": errors,
|
||||
"successes": successes,
|
||||
"uploads": uploads,
|
||||
"url_updates": url_updates,
|
||||
"release_date": release_date,
|
||||
"cardset": cardset,
|
||||
}
|
||||
|
||||
|
||||
async def refresh_card_images(
|
||||
cardset_name: str,
|
||||
limit: Optional[int] = None,
|
||||
html_cards: bool = False,
|
||||
on_progress: callable = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Refresh card images for a cardset by triggering regeneration.
|
||||
|
||||
Args:
|
||||
cardset_name: Name of the cardset to process
|
||||
limit: Maximum number of cards to process
|
||||
html_cards: Fetch HTML preview cards instead of PNG
|
||||
on_progress: Callback function for progress updates
|
||||
|
||||
Returns:
|
||||
Dict with counts of errors, successes
|
||||
"""
|
||||
# Look up cardset
|
||||
c_query = await db_get("cardsets", params=[("name", cardset_name)])
|
||||
if not c_query or c_query["count"] == 0:
|
||||
raise ValueError(f'Cardset "{cardset_name}" not found')
|
||||
cardset = c_query["cardsets"][0]
|
||||
|
||||
CARD_BASE_URL = "https://pd.manticorum.com/api/v2/players"
|
||||
|
||||
# Get all players
|
||||
p_query = await db_get(
|
||||
"players",
|
||||
params=[
|
||||
("inc_dex", False),
|
||||
("cardset_id", cardset["id"]),
|
||||
("short_output", True),
|
||||
],
|
||||
)
|
||||
if p_query["count"] == 0:
|
||||
raise ValueError("No players returned from Paper Dynasty API")
|
||||
all_players = p_query["players"]
|
||||
|
||||
errors = []
|
||||
successes = []
|
||||
cxn_error = False
|
||||
count = 0
|
||||
max_count = limit or 9999
|
||||
start_time = datetime.datetime.now()
|
||||
|
||||
# First pass: Reset URLs for players with old sombaseball URLs
|
||||
for x in all_players:
|
||||
if "sombaseball" in x["image"]:
|
||||
if on_progress:
|
||||
on_progress(count, f"{x['p_name']} - fixing old URL")
|
||||
|
||||
release_dir = f"{start_time.year}-{start_time.month}-{start_time.day}"
|
||||
if x["pos_1"] in ["SP", "RP", "CP", "P"]:
|
||||
image_url = (
|
||||
f"{CARD_BASE_URL}/{x['player_id']}/pitchingcard"
|
||||
f"{urllib.parse.quote('?d=')}{release_dir}"
|
||||
)
|
||||
else:
|
||||
image_url = (
|
||||
f"{CARD_BASE_URL}/{x['player_id']}/battingcard"
|
||||
f"{urllib.parse.quote('?d=')}{release_dir}"
|
||||
)
|
||||
|
||||
await db_patch(
|
||||
"players", object_id=x["player_id"], params=[("image", image_url)]
|
||||
)
|
||||
else:
|
||||
count += 1
|
||||
if on_progress and count % 20 == 0:
|
||||
on_progress(count, f"{x['p_name']} - resetting")
|
||||
if count >= max_count:
|
||||
break
|
||||
|
||||
try:
|
||||
await db_post(f"players/{x['player_id']}/image-reset")
|
||||
except ConnectionError as e:
|
||||
if cxn_error:
|
||||
raise e
|
||||
cxn_error = True
|
||||
errors.append((x, e))
|
||||
except ValueError as e:
|
||||
errors.append((x, e))
|
||||
|
||||
# Second pass: Fetch images to trigger regeneration
|
||||
count = 0
|
||||
for x in all_players:
|
||||
if count >= max_count:
|
||||
break
|
||||
|
||||
if html_cards:
|
||||
card_url = f"{x['image']}&html=true"
|
||||
timeout = 2
|
||||
else:
|
||||
card_url = x["image"]
|
||||
timeout = 6
|
||||
|
||||
try:
|
||||
logger.info(f"Fetching card URL: {card_url}")
|
||||
await url_get(card_url, timeout=timeout)
|
||||
except ConnectionError as e:
|
||||
if cxn_error:
|
||||
raise e
|
||||
cxn_error = True
|
||||
errors.append((x, e))
|
||||
except ValueError as e:
|
||||
errors.append((x, e))
|
||||
else:
|
||||
# Handle image2
|
||||
if x["image2"] is not None:
|
||||
if html_cards:
|
||||
card_url2 = f"{x['image2']}&html=true"
|
||||
else:
|
||||
card_url2 = x["image2"]
|
||||
|
||||
if "sombaseball" in x["image2"]:
|
||||
errors.append((x, f"Bad card url: {x['image2']}"))
|
||||
else:
|
||||
try:
|
||||
await url_get(card_url2, timeout=6)
|
||||
successes.append(x)
|
||||
except ConnectionError as e:
|
||||
if cxn_error:
|
||||
raise e
|
||||
cxn_error = True
|
||||
errors.append((x, e))
|
||||
except ValueError as e:
|
||||
errors.append((x, e))
|
||||
else:
|
||||
successes.append(x)
|
||||
|
||||
count += 1
|
||||
|
||||
return {"errors": errors, "successes": successes, "cardset": cardset}
|
||||
|
||||
|
||||
async def check_card_images(
|
||||
cardset_name: str, limit: Optional[int] = None, on_progress: callable = None
|
||||
) -> dict:
|
||||
"""
|
||||
Check and validate card images without uploading.
|
||||
|
||||
Args:
|
||||
cardset_name: Name of the cardset to check
|
||||
limit: Maximum number of cards to check
|
||||
on_progress: Callback function for progress updates
|
||||
|
||||
Returns:
|
||||
Dict with counts of errors and successes
|
||||
"""
|
||||
# Look up cardset
|
||||
c_query = await db_get("cardsets", params=[("name", cardset_name)])
|
||||
if not c_query or c_query["count"] == 0:
|
||||
raise ValueError(f'Cardset "{cardset_name}" not found')
|
||||
cardset = c_query["cardsets"][0]
|
||||
|
||||
# Get all players
|
||||
p_query = await db_get(
|
||||
"players",
|
||||
params=[
|
||||
("inc_dex", False),
|
||||
("cardset_id", cardset["id"]),
|
||||
("short_output", True),
|
||||
],
|
||||
)
|
||||
if not p_query or p_query["count"] == 0:
|
||||
raise ValueError("No players returned from Paper Dynasty API")
|
||||
all_players = p_query["players"]
|
||||
|
||||
# Generate release date for cache busting (include timestamp for same-day updates)
|
||||
now = datetime.datetime.now()
|
||||
timestamp = int(now.timestamp())
|
||||
release_date = f"{now.year}-{now.month}-{now.day}-{timestamp}"
|
||||
PD_API_URL = "https://pd.manticorum.com/api"
|
||||
|
||||
errors = []
|
||||
successes = []
|
||||
cxn_error = False
|
||||
count = 0
|
||||
max_count = limit or 9999
|
||||
|
||||
for x in all_players:
|
||||
if count >= max_count:
|
||||
break
|
||||
|
||||
if "sombaseball" in x["image"]:
|
||||
errors.append((x, f"Bad card url: {x['image']}"))
|
||||
continue
|
||||
|
||||
count += 1
|
||||
if on_progress and count % 20 == 0:
|
||||
on_progress(count, x["p_name"])
|
||||
|
||||
card_type = "pitching" if "pitching" in x["image"] else "batting"
|
||||
card_url = (
|
||||
f"{PD_API_URL}/v2/players/{x['player_id']}/{card_type}card?d={release_date}"
|
||||
)
|
||||
|
||||
try:
|
||||
logger.info(f"Checking card URL: {card_url}")
|
||||
await url_get(card_url, timeout=6)
|
||||
successes.append(x)
|
||||
except ConnectionError as e:
|
||||
if cxn_error:
|
||||
raise e
|
||||
cxn_error = True
|
||||
errors.append((x, e))
|
||||
except ValueError as e:
|
||||
errors.append((x, e))
|
||||
|
||||
return {
|
||||
"errors": errors,
|
||||
"successes": successes,
|
||||
"cardset": cardset,
|
||||
"release_date": release_date,
|
||||
}
|
||||
@ -1,21 +1,23 @@
|
||||
name: Adm Ball Traits
|
||||
player_type: batter
|
||||
hand: R
|
||||
target_ops: 0.8
|
||||
target_ops: 0.830 # This character is -0.2 from the standard thresholds in favor of defense and speed
|
||||
cardset_id: 29
|
||||
player_id: 13361
|
||||
batting_card_id: 6244
|
||||
positions:
|
||||
SS:
|
||||
range: 1
|
||||
error: 10
|
||||
innings: 500
|
||||
3B:
|
||||
range: 2
|
||||
error: 8
|
||||
SS:
|
||||
range: 3
|
||||
error: 10
|
||||
innings: 250
|
||||
baserunning:
|
||||
steal_jump: 0.083333
|
||||
steal_jump: 0.1388888
|
||||
steal_high: 15
|
||||
steal_low: 9
|
||||
steal_low: 12
|
||||
steal_auto: true
|
||||
running: 13
|
||||
hit_and_run: B
|
||||
@ -27,13 +29,13 @@ ratings:
|
||||
triple: 2.75
|
||||
double_three: 0.0
|
||||
double_two: 2.0
|
||||
double_pull: 3.75
|
||||
double_pull: 5.25
|
||||
single_two: 6.5
|
||||
single_one: 5.0
|
||||
single_one: 4.0
|
||||
single_center: 7.75
|
||||
bp_single: 5.0
|
||||
walk: 12.0
|
||||
hbp: 4.0
|
||||
hbp: 3.5
|
||||
strikeout: 16.0
|
||||
lineout: 4.0
|
||||
popout: 2.0
|
||||
@ -41,9 +43,9 @@ ratings:
|
||||
flyout_bq: 2.0
|
||||
flyout_lf_b: 4.0
|
||||
flyout_rf_b: 4.0
|
||||
groundout_a: 9.0
|
||||
groundout_b: 11.0
|
||||
groundout_c: 6.25
|
||||
groundout_a: 6.0
|
||||
groundout_b: 9.0
|
||||
groundout_c: 11.25
|
||||
pull_rate: 0.2
|
||||
center_rate: 0.5
|
||||
slap_rate: 0.3
|
||||
@ -54,22 +56,22 @@ ratings:
|
||||
double_three: 0.0
|
||||
double_two: 4.5
|
||||
double_pull: 4.5
|
||||
single_two: 5.0
|
||||
single_two: 6.25
|
||||
single_one: 8.0
|
||||
single_center: 5.5
|
||||
single_center: 5.75
|
||||
bp_single: 5.0
|
||||
walk: 11.0
|
||||
hbp: 1.0
|
||||
strikeout: 14.5
|
||||
strikeout: 13.0
|
||||
lineout: 2.0
|
||||
popout: 2.0
|
||||
flyout_a: 0.0
|
||||
flyout_bq: 3.0
|
||||
flyout_lf_b: 5.0
|
||||
flyout_rf_b: 8.0
|
||||
groundout_a: 5.0
|
||||
groundout_b: 12.0
|
||||
groundout_c: 10.5
|
||||
flyout_lf_b: 6.0
|
||||
flyout_rf_b: 9.0
|
||||
groundout_a: 3.0
|
||||
groundout_b: 9.0
|
||||
groundout_c: 13.5
|
||||
pull_rate: 0.18
|
||||
center_rate: 0.42
|
||||
slap_rate: 0.4
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
name: Kalin Young
|
||||
hand: R
|
||||
target_ops: 0.880
|
||||
target_ops: 0.920
|
||||
cardset_id: 29
|
||||
player_id: 13009
|
||||
batting_card_id: 6072
|
||||
@ -13,14 +13,12 @@ batting_card_id: 6072
|
||||
positions:
|
||||
RF:
|
||||
range: 2
|
||||
error: 6
|
||||
arm: 0
|
||||
fielding_pct: null
|
||||
error: 4
|
||||
arm: -1
|
||||
LF:
|
||||
range: 4
|
||||
error: 6
|
||||
arm: 0
|
||||
fielding_pct: null
|
||||
error: 4
|
||||
arm: -1
|
||||
|
||||
baserunning:
|
||||
steal_jump: 0.22222
|
||||
@ -35,53 +33,53 @@ ratings:
|
||||
# Updated ratings from submit_kalin_young.py
|
||||
vs_L:
|
||||
homerun: 2.05
|
||||
bp_homerun: 2.00
|
||||
bp_homerun: 3.00
|
||||
triple: 0.00
|
||||
double_three: 0.00
|
||||
double_two: 6.00
|
||||
double_pull: 2.35
|
||||
single_two: 5.05
|
||||
single_one: 5.10
|
||||
single_center: 5.80 # +1.55 from original
|
||||
double_two: 6.50
|
||||
double_pull: 4.35
|
||||
single_two: 5.55
|
||||
single_one: 2.10
|
||||
single_center: 4.80 # +1.55 from original
|
||||
bp_single: 5.00
|
||||
walk: 16.05 # +1.55 from original
|
||||
walk: 16.55 # +1.55 from original
|
||||
hbp: 1.00
|
||||
strikeout: 15.40 # -3.10 from original
|
||||
lineout: 15.00
|
||||
strikeout: 16.40 # -3.10 from original
|
||||
lineout: 8.00
|
||||
popout: 0.00
|
||||
flyout_a: 0.00
|
||||
flyout_bq: 0.75
|
||||
flyout_lf_b: 3.65
|
||||
flyout_rf_b: 3.95
|
||||
flyout_bq: 1.75
|
||||
flyout_lf_b: 5.65
|
||||
flyout_rf_b: 5.95
|
||||
groundout_a: 3.95
|
||||
groundout_b: 10.00
|
||||
groundout_c: 4.90
|
||||
groundout_b: 9.50
|
||||
groundout_c: 5.90
|
||||
pull_rate: 0.33
|
||||
center_rate: 0.37
|
||||
slap_rate: 0.30
|
||||
|
||||
vs_R:
|
||||
homerun: 2.10
|
||||
bp_homerun: 1.00
|
||||
bp_homerun: 3.00
|
||||
triple: 1.25
|
||||
double_three: 0.00
|
||||
double_two: 6.35
|
||||
double_pull: 1.80
|
||||
double_two: 5.35
|
||||
double_pull: 3.80
|
||||
single_two: 5.40
|
||||
single_one: 4.95
|
||||
single_one: 3.95
|
||||
single_center: 6.05 # +1.55 from original
|
||||
bp_single: 5.00
|
||||
walk: 14.55 # +1.55 from original
|
||||
hbp: 2.00
|
||||
strikeout: 17.90 # -3.10 from original
|
||||
strikeout: 16.90 # -3.10 from original
|
||||
lineout: 12.00
|
||||
popout: 1.00
|
||||
flyout_a: 0.00
|
||||
flyout_bq: 0.50
|
||||
flyout_lf_b: 4.10
|
||||
flyout_rf_b: 4.00
|
||||
groundout_a: 4.00
|
||||
groundout_b: 5.00
|
||||
groundout_a: 3.50
|
||||
groundout_b: 4.50
|
||||
groundout_c: 9.05
|
||||
pull_rate: 0.25
|
||||
center_rate: 0.40
|
||||
|
||||
93
pd_cards/custom/profiles/sippie_swartzel.yaml
Normal file
93
pd_cards/custom/profiles/sippie_swartzel.yaml
Normal file
@ -0,0 +1,93 @@
|
||||
# Sippie Swartzel - Custom Player
|
||||
# Target OPS: 0.850
|
||||
# Hand: Left-handed batter
|
||||
# Profile: Contact-oriented hitter with good plate discipline
|
||||
|
||||
name: Sippie Swartzel
|
||||
hand: L
|
||||
target_ops: 0.820
|
||||
cardset_id: 29
|
||||
player_id: 13475
|
||||
batting_card_id: 6304
|
||||
|
||||
positions:
|
||||
# Primary position - fill in based on character concept
|
||||
SS:
|
||||
range: 3
|
||||
error: 14
|
||||
RF:
|
||||
range: 4
|
||||
error: 5
|
||||
arm: -1
|
||||
|
||||
baserunning:
|
||||
steal_jump: 0.0833333 # Decent speed
|
||||
steal_high: 20
|
||||
steal_low: 9
|
||||
steal_auto: 0
|
||||
running: 12
|
||||
hit_and_run: B
|
||||
bunting: C
|
||||
|
||||
ratings:
|
||||
# vs_L = performance vs left-handed pitchers (same-side)
|
||||
# Typically weaker split for LHB
|
||||
# Target: Lower OPS vs LHP
|
||||
# Total must = 108
|
||||
vs_L:
|
||||
homerun: 0.00
|
||||
bp_homerun: 0.00
|
||||
triple: 3.00
|
||||
double_three: 0.00
|
||||
double_two: 5.00
|
||||
double_pull: 1.50
|
||||
single_two: 5.50
|
||||
single_one: 5.50
|
||||
single_center: 6.50
|
||||
bp_single: 5.00
|
||||
walk: 11.00
|
||||
hbp: 1.00
|
||||
strikeout: 19.00
|
||||
lineout: 14.50
|
||||
popout: 1.00
|
||||
flyout_a: 0.00
|
||||
flyout_bq: 0.50
|
||||
flyout_lf_b: 5.00
|
||||
flyout_rf_b: 5.00
|
||||
groundout_a: 4.00
|
||||
groundout_b: 9.00
|
||||
groundout_c: 6.00
|
||||
pull_rate: 0.28
|
||||
center_rate: 0.38
|
||||
slap_rate: 0.34
|
||||
|
||||
# vs_R = performance vs right-handed pitchers (opposite-side)
|
||||
# Stronger split for LHB
|
||||
# Target: Higher OPS vs RHP
|
||||
# Total must = 108
|
||||
vs_R:
|
||||
homerun: 0.00
|
||||
bp_homerun: 0.00
|
||||
triple: 4.50
|
||||
double_three: 0.00
|
||||
double_two: 5.50
|
||||
double_pull: 2.00
|
||||
single_two: 6.00
|
||||
single_one: 6.00
|
||||
single_center: 7.75
|
||||
bp_single: 5.00
|
||||
walk: 12.75
|
||||
hbp: 1.00
|
||||
strikeout: 16.50
|
||||
lineout: 12.50
|
||||
popout: 0.50
|
||||
flyout_a: 0.00
|
||||
flyout_bq: 0.50
|
||||
flyout_lf_b: 5.00
|
||||
flyout_rf_b: 5.00
|
||||
groundout_a: 4.00
|
||||
groundout_b: 7.50
|
||||
groundout_c: 6.00
|
||||
pull_rate: 0.30
|
||||
center_rate: 0.38
|
||||
slap_rate: 0.32
|
||||
Loading…
Reference in New Issue
Block a user