# Custom Card Creation ## Purpose Create fictional player cards using baseball archetypes (interactive tool) or manually create custom player database records via API calls. ## Interactive Creator **Script**: `/mnt/NV2/Development/paper-dynasty/card-creation/custom_cards/interactive_creator.py` **Supporting**: `archetype_definitions.py`, `archetype_calculator.py` ```bash cd /mnt/NV2/Development/paper-dynasty/card-creation source venv/bin/activate python -m custom_cards.interactive_creator ``` The interactive creator handles: cardset setup, archetype selection, rating calculation, review/tweak, database creation, S3 upload. ### Archetypes **Batter**: Power Slugger, Contact Hitter, Speedster, Balanced Star, Patient Walker, Slap Hitter, Three True Outcomes, Defensive Specialist **Pitcher**: Ace, Power Pitcher, Finesse Pitcher, Groundball Specialist, Dominant Closer, Setup Man, Swingman, Lefty Specialist --- ## Manual Creation: Database Submission Template When creating a custom player without the interactive tool: ```python import asyncio from db_calls import db_post, db_put, db_patch, db_get from creation_helpers import mlbteam_and_franchise # 1. Create Player (ASK USER for cost and rarity_id - NEVER make up values) mlb_team_id, franchise_id = mlbteam_and_franchise('FA') player_payload = { 'p_name': 'Player Name', 'cost': '88', # STRING - user specifies 'image': 'change-me', 'mlbclub': mlb_team_id, 'franchise': franchise_id, 'cardset_id': 29, 'set_num': 99999, 'rarity_id': 3, # INT - user specifies (see rarity table) 'pos_1': '1B', 'description': '2005 Custom', 'bbref_id': 'custom_playerp01', 'fangr_id': 0, 'mlbplayer_id': None # None, not 0 } player = await db_post('players', payload=player_payload) player_id = player['player_id'] # 2. Create BattingCard batting_card = { 'player_id': player_id, 'key_bbref': 'custom_playerp01', 'key_fangraphs': 0, 'key_mlbam': 0, 'key_retro': '', 'name_first': 'Player', 'name_last': 'Name', 'steal_low': 7, # 0-20 scale (2d10) 'steal_high': 11, # 0-20 scale (2d10) 'steal_auto': 0, # 0 or 1 'steal_jump': 0.055, # 0.0-1.0 (fraction of 36) 'hit_and_run': 'A', # A, B, C, or D 'running': 12, # 8-17 scale 'hand': 'R' # R, L, or S } await db_put('battingcards', payload={'cards': [batting_card]}, timeout=10) bc_result = await db_get('battingcards', params=[('player_id', player_id)]) battingcard_id = bc_result['cards'][0]['id'] # 3. Create BattingCardRatings (one per opposing hand — see full field ref below) rating_vl = { 'battingcard_id': battingcard_id, 'bat_hand': 'R', 'vs_hand': 'L', # Hits 'homerun': 0.55, 'bp_homerun': 1.00, # MUST be whole number (0, 1, 2, or 3) 'triple': 0.80, 'double_three': 0.00, # Usually 0.00 (reserved) 'double_two': 5.60, 'double_pull': 2.95, 'single_two': 10.35, 'single_one': 4.95, 'single_center': 8.45, 'bp_single': 5.00, # MUST be whole number (usually 5.0) # On-base 'walk': 8.15, 'hbp': 0.90, # Outs 'strikeout': 13.05, 'lineout': 11.60, 'popout': 0.00, # Usually 0.00 (added during image gen) 'flyout_a': 0.00, # Only 1.0 for power hitters (HR% > 10%) 'flyout_bq': 5.30, 'flyout_lf_b': 4.55, 'flyout_rf_b': 3.70, 'groundout_a': 9.45, # Double play balls 'groundout_b': 6.55, 'groundout_c': 5.10, # Percentages 'hard_rate': 0.33, 'med_rate': 0.50, 'soft_rate': 0.17, 'pull_rate': 0.38, 'center_rate': 0.36, 'slap_rate': 0.26 } # Create matching rating_vr with vs_hand='R' and different values await db_put('battingcardratings', payload={'ratings': [rating_vl, rating_vr]}, timeout=10) # 4. Create CardPositions (use db_put, NOT db_post) await db_put('cardpositions', payload={'positions': [ {'player_id': player_id, 'position': 'LF', 'range': 3, 'error': 7, 'arm': 2}, {'player_id': player_id, 'position': '2B', 'range': 4, 'error': 12} ]}) # 5. Generate card image, upload to S3, update player await db_patch('players', object_id=player_id, params=[('image', s3_url)]) ``` --- ## Rating Constraints - All D20 chances must be multiples of **0.05** - Total chances must equal exactly **108.00** (D20 x 5.4) - Apply **+/-0.5 randomization** to avoid mechanical-looking cards - **bp_homerun** rules: - hr_count < 0.5: BP-HR = 0, HR = 0 - hr_count <= 1.0: BP-HR = 1, HR = 0 - hr_count < 3: BP-HR = 1, HR = hr_count - 1 - hr_count < 6: BP-HR = 2, HR = hr_count - 2 - hr_count >= 6: BP-HR = 3, HR = hr_count - 3 - **NEVER allow negative regular HR values** - When removing HRs to lower OPS, redistribute to **singles** (not doubles) for more effective SLG reduction ### Ballpark (BP) Result Calculations - BP-HR and BP-Single multiply by **0.5** for AVG/OBP - BP results use **full value** for SLG (BP-HR = 2 bases, BP-Single = 1 base) ```python # AVG/OBP total_hits = homerun + (bp_homerun * 0.5) + triple + ... + (bp_single * 0.5) avg = total_hits / 108 # SLG total_bases = (homerun * 4) + (bp_homerun * 2) + (triple * 3) + ... + bp_single slg = total_bases / 108 ``` ### Total OPS Formula - **Batters**: `(OPS_vR + OPS_vL + min(OPS_vL, OPS_vR)) / 3` (weaker split double-counted) - **Pitchers**: `(OPS_vR + OPS_vL + max(OPS_vL, OPS_vR)) / 3` (stronger split double-counted) --- ## Database Field Reference ### Player Table (ALL fields required) `p_name`, `bbref_id`, `hand`, `mlbclub`, `franchise`, `cardset_id`, `description`, `is_custom`, `image`, `set_num`, `pos_1`, `cost` (STRING), `rarity_id` (INT), `mlbplayer_id` (None, not 0), `fangr_id` (0 for custom) ### Rarity IDs (Batters) | ID | Rarity | OPS Threshold | |----|--------|---------------| | 99 | Hall of Fame | 1.200+ | | 1 | Diamond | 1.000+ | | 2 | All-Star | 0.900+ | | 3 | Starter | 0.800+ | | 4 | Reserve | 0.700+ | | 5 | Replacement | remainder | **NEVER make up rarity IDs or cost values** — always ask the user. ### CardPosition Fields ```python { 'player_id': int, 'variant': 0, 'position': str, # C, 1B, 2B, 3B, SS, LF, CF, RF, DH, P 'innings': 1, 'range': int, # NOT fielding_rating 'error': int, # NOT fielding_error 'arm': int, # NOT fielding_arm — required for C, LF, CF, RF 'pb': int, # Catchers only (NOT catcher_pb) 'overthrow': int # Catchers only (NOT catcher_throw) } ``` ### MLBPlayer Field Names Use `first_name`/`last_name` (NOT `name_first`/`name_last`) ### DB Operation Methods | Endpoint | Method | |----------|--------| | `players`, `mlbplayers` | `db_post` | | `battingcards`, `pitchingcards` | `db_put` | | `battingcardratings`, `pitchingcardratings` | `db_put` | | `cardpositions` | `db_put` (NOT db_post) | --- ## Common Mistakes - **mlbplayer_id = 0**: Use `None`, not `0` — avoids FK constraint issues - **db_post for cardpositions**: Must use `db_put` — endpoint doesn't support POST - **Missing required fields**: ALL Player fields listed above are required - **Wrong field names**: `range`/`error`/`arm` for positions, `first_name`/`last_name` for MLBPlayer - **Making up rarity/cost**: NEVER assume — always ask the user --- ## Handling Existing Records ```python # Check for existing MLBPlayer by bbref_id, then fall back to name mlb_query = await db_get('mlbplayers', params=[('key_bbref', bbref_id)]) if not mlb_query or mlb_query.get('count', 0) == 0: mlb_query = await db_get('mlbplayers', params=[ ('first_name', name_first), ('last_name', name_last) ]) # Check for existing Player before creating p_query = await db_get('players', params=[('bbref_id', bbref_id), ('cardset_id', cardset_id)]) if p_query and p_query.get('count', 0) > 0: player_id = p_query['players'][0]['player_id'] # Use existing ``` --- ## Custom Player Conventions - **bbref_id**: `custom_{lastname}{firstinitial}01` (e.g., `custom_smithj01`) - **Org**: `mlbclub` and `franchise` = "Custom Ballplayers" - **Default cardset**: 29 | **set_num**: 9999 - **Flags**: `is_custom: True`, `fangraphs_id: 0`, `mlbam_id: 0` - **Reference impl**: `/mnt/NV2/Development/paper-dynasty/card-creation/create_valerie_theolia.py` ## Verification - **Dev**: `https://pddev.manticorum.com/api/v2/players/{player_id}/battingcard` - **Prod**: `https://pd.manticorum.com/api/v2/players/{player_id}/battingcard` - Add `?html=true` for HTML preview instead of PNG --- **Last Updated**: 2026-02-12 **Version**: 3.0 (Merged custom-player-database-creation.md; trimmed redundant content)