claude-configs/skills/paper-dynasty/workflows/custom-card-creation.md
Cal Corum 047ec745eb Add new skills, commands, scripts; update Paper Dynasty workflows
New:
- backlog, cognitive-memory, optimise-claude skills
- commands/ and scripts/ directories
- usage-data tracking

Updated:
- Paper Dynasty: consolidated workflows, updated API client and CLI
- .gitignore, CLAUDE.md, settings.json

Removed:
- Deprecated Paper Dynasty workflows (card-refresh, database-sync,
  discord-app-troubleshooting, gauntlet-cleanup, custom-player-db)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 14:10:21 -06:00

8.5 KiB

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

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:

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)
# 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

{
    '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

# 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)