claude-configs/skills/paper-dynasty/workflows/custom-card-creation.md
Cal Corum 8a1d15911f Initial commit: Claude Code configuration backup
Version control Claude Code configuration including:
- Global instructions (CLAUDE.md)
- User settings (settings.json)
- Custom agents (architect, designer, engineer, etc.)
- Custom skills (create-skill templates and workflows)

Excludes session data, secrets, cache, and temporary files per .gitignore.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 16:34:21 -06:00

19 KiB
Raw Blame History

Custom Card Creation Workflow

Purpose

Create fictional player cards for Paper Dynasty using baseball archetypes without needing real statistics.

When to Use

  • Creating promotional/event cards for fictional players
  • Building custom rosters for special game modes
  • Testing new card mechanics
  • Creating themed card sets (e.g., "Legends", "Future Stars", "Fantasy Heroes")

Overview

This workflow provides an interactive system to:

  1. Select baseball archetypes (e.g., "Power Slugger", "Ace Pitcher", "Speedster")
  2. Define player details (name, team, positions, handedness)
  3. Review calculated D20 ratings based on the archetype
  4. Iteratively tweak ratings to achieve desired player profile
  5. Automatically create all database records (Player, Card, Ratings, Positions)

Location

Script: /mnt/NV2/Development/paper-dynasty/card-creation/custom_cards/interactive_creator.py

Supporting Files:

  • archetype_definitions.py - Baseball archetype profiles
  • archetype_calculator.py - Converts archetypes to D20 game mechanics
  • __init__.py - Package exports

Quick Start

Option 1: Run Script Directly

cd /mnt/NV2/Development/paper-dynasty/card-creation
source venv/bin/activate
python -m custom_cards.interactive_creator

Option 2: Via Paper Dynasty Skill

Simply say: "Create custom cards" or "I want to make fictional player cards"

Claude will:

  1. Navigate to the card-creation directory
  2. Activate the virtual environment
  3. Launch the interactive workflow
  4. Guide you through the process

Workflow Steps

1. Cardset Setup

  • Specify target cardset name (e.g., "Custom Players 2025")
  • System searches for existing cardset or creates new one
  • Configure season year and player description prefix

2. Player Creation Loop

For each player:

A. Select Player Type

  • Batter or Pitcher

B. Choose Archetype

Batter Archetypes:

  • Power Slugger - High HR, lower average, high K rate
  • Contact Hitter - High average, low K, gap power
  • Speedster - Elite speed, slap hitter, excellent defense
  • Balanced Star - Well-rounded five-tool player
  • Patient Walker - Elite plate discipline, high OBP
  • Slap Hitter - Opposite field, puts ball in play
  • Three True Outcomes - Extreme power/walks, very high K
  • Defensive Specialist - Elite defense, weak bat

Pitcher Archetypes:

  • Ace - Elite starter, dominates both sides
  • Power Pitcher - High velocity, high K, some walks
  • Finesse Pitcher - Command specialist, weak contact
  • Groundball Specialist - Induces grounders, low HR
  • Dominant Closer - Elite short relief, shuts down late innings
  • Setup Man - Solid late-inning reliever
  • Swingman - Can start or relieve, versatile
  • Lefty Specialist - LOOGY type, dominates lefties

C. Player Information

  • First and last name
  • Batting/throwing hand (R, L, or S for switch)
  • MLB team affiliation
  • Defensive positions (batters only)

D. Review & Tweak

System displays calculated ratings:

  • AVG / OBP / SLG / OPS for each split (vs L, vs R)
  • Hit distribution (HR, 3B, 2B, 1B)
  • Walk/strikeout rates
  • Batted ball distribution
  • Total OPS (using Paper Dynasty formula)
  • Baserunning ratings (batters only)

Options:

  1. Accept these ratings (proceed to create)
  2. Tweak archetype percentages (adjust power, contact, etc.)
  3. Manually adjust specific D20 ratings
  4. Start over with different archetype

E. Database Creation

System automatically creates:

  • MLBPlayer record (if needed)
  • Player record with image URL (image="https://pd.manticorum.com/api/v2/players/{player_id}/<batt|pitch>ingcard?d=<YYYY-MM-DD>")
  • BattingCard or PitchingCard
  • BattingCardRatings or PitchingCardRatings (vs L and vs R)
  • CardPosition records

F. Image Generation & AWS Upload

After database creation, system automatically:

  1. Trigger Card Generation: GET request to Player.image URL to render the card PNG
  2. Fetch Card Image: Download the generated PNG image bytes from API
  3. Upload to S3: POST image to AWS S3 bucket at cards/cardset-{cardset_id:03d}/player-{player_id}/{batting|pitching}card.png?d={release_date}
  4. Update Player Record: PATCH player with new S3 URL (replaces API URL with S3 CDN URL)

S3 Configuration (from check_cards_and_upload.py):

  • Bucket: paper-dynasty
  • Region: us-east-1
  • Cache-Control: public, max-age=300 (5 minute cache)
  • Content-Type: image/png
  • URL format includes cache-busting query parameter with date

Why This Matters:

  • S3 provides fast CDN delivery for card images
  • Reduces API server load (nginx gateway caches S3 URLs)
  • Consistent URL structure for all card images
  • Cache-busting ensures updated cards display immediately

3. Continue or Exit

After each player, choose to create another or finish.

Technical Details

Archetype System

Archetypes define traditional baseball stats:

  • Batting: AVG, OBP, SLG, K%, BB%, power distribution, batted ball profile, spray chart
  • Pitching: Stats against (AVG, OBP, SLG, K%, BB%), batted ball profile, role ratings

Rating Calculation

The calculator converts traditional stats to D20 game mechanics:

  • All ratings sum to 108 chances (D20 * 5.4 for 20-sided die system)
  • Distributes hits among HR, 3B, 2B, 1B based on power profile
  • Allocates outs among K, lineouts, flyouts, groundouts
  • Applies spray chart distribution (pull/center/opposite field)
  • Calculates baserunning from speed ratings

Total OPS Formula

  • Batters: (OPS_vR + OPS_vL + min(OPS_vL, OPS_vR)) / 3
  • Pitchers: (OPS_vR + OPS_vL + max(OPS_vL, OPS_vR)) / 3

This formula determines rarity and card cost.

Custom Player Identifiers

  • Custom players use synthetic IDs: custom_lastnamefirstinitial01
  • Example: "John Smith" → custom_smithj01
  • Marked with is_custom: True flag in database
  • No real baseball IDs (fangraphs_id=0, mlbam_id=0)

Environment Requirements

Python Environment

Must be run with virtual environment activated:

source /mnt/NV2/Development/paper-dynasty/card-creation/venv/bin/activate

API Access

Requires API authentication via db_calls.py:

  • Production: https://pd.manticorum.com/api
  • Development: https://pddev.manticorum.com/api

Set via alt_database variable in db_calls.py

Database Permissions

Script performs:

  • GET (cardsets, players, battingcards, pitchingcards)
  • POST (mlbplayers, players, cardpositions)
  • PUT (battingcards/ratings, pitchingcards/ratings)

Example Session

PAPER DYNASTY - CUSTOM CARD CREATOR
==================================================================
Create fictional player cards using baseball archetypes.

CARDSET SETUP
------------------------------------------------------------------
Enter cardset name: Custom Heroes 2025
Cardset not found. Create new cardset? yes
Enter season year: 2025
Ranked legal? no
Available in packs? yes
✓ Created new cardset: Custom Heroes 2025 (ID: 30)

PLAYER TYPE
------------------------------------------------------------------
1. Batter
2. Pitcher
3. Quit
Select player type: 1

BATTER ARCHETYPES
==================================================================
1. Power Slugger (power_slugger)
   High power, lots of home runs, lower average, high strikeout rate
   vs RHP: 0.240/0.320/0.480 (OPS: 0.800)
   vs LHP: 0.250/0.330/0.500 (OPS: 0.830)
   Speed: 4/10  |  Positions: LF, RF, 1B, DH
...

Select archetype: 1

PLAYER INFORMATION
------------------------------------------------------------------
First name: Zeus
Last name: Thunder
Batting hand: 1 (Right)
MLB Team: 15 (New York Yankees)
Positions: RF DH

REVIEW & TWEAK RATINGS
==================================================================
Zeus Thunder (R) - NYY
----------------------------------------------------------------------
VS LHP:
  AVG: 0.250  OBP: 0.330  SLG: 0.500  OPS: 0.830
  Hits: 32.4  (HR: 5.2  3B: 0.3  2B: 8.1  1B: 18.8)
  BB: 12.9  HBP: 1.4  K: 24.2

VS RHP:
  AVG: 0.240  OBP: 0.320  SLG: 0.480  OPS: 0.800
  Hits: 31.1  (HR: 5.0  3B: 0.3  2B: 7.8  1B: 18.0)
  BB: 11.7  HBP: 1.3  K: 27.2

Total OPS: 0.810

Baserunning:
  Steal: 8-5 (Auto: 0, Jump: 4)
  Running: 5/10  Hit-and-Run: 4/10

Options:
1. Accept these ratings
2. Tweak archetype percentages
3. Manually adjust specific ratings
4. Start over with different archetype

Select: 1

CREATING DATABASE RECORDS
------------------------------------------------------------------
✓ Created MLBPlayer record (ID: 1534)
✓ Created Player record (ID: 8421)
✓ Created batting card
✓ Created batting ratings
✓ Created 2 position record(s)

GENERATING & UPLOADING CARD IMAGE
------------------------------------------------------------------
✓ Triggered card generation at API
✓ Fetched card image (42.3 KB)
✓ Uploaded to S3: cards/cardset-030/player-8421/battingcard.png
✓ Updated player record with S3 URL

✓ Zeus Thunder created successfully!

Create another custom player? no

Custom card creation complete!

Future Enhancements

  • Implement archetype percentage tweaking (option 2)
  • Implement manual D20 rating adjustments (option 3)
  • Add defensive rating calculator for each position
  • Import/export player profiles as JSON
  • Batch creation from CSV file
  • Visual card preview during review
  • Save favorite custom archetypes

Quick Reference: Minimal Database Submission Template

When creating a custom player manually (not using interactive_creator.py), use this template:

import asyncio
from db_calls import db_post, db_put, db_patch
from creation_helpers import mlbteam_and_franchise
from custom_cards.archetype_calculator import BatterRatingCalculator
import boto3
from datetime import datetime

# 1. Calculate ratings using BatterRatingCalculator
# 2. Get team info
mlb_team_id, franchise_id = mlbteam_and_franchise('FA')

# 3. Create Player (ASK USER for cost and rarity_id - NEVER make up values)
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']

# 4. Create BattingCard/PitchingCard (use db_put)
await db_put('battingcards', payload={'cards': [...]}, timeout=10)

# 5. Create Ratings (use db_put)
await db_put('battingcardratings', payload={'ratings': [...]}, timeout=10)

# 6. Create Positions (use db_put, NOT db_post)
await db_put('cardpositions', payload={'positions': [...]}, timeout=10)

# 7. Generate card image and upload to S3
# 8. Update player with S3 URL
await db_patch('players', object_id=player_id, params=[('image', s3_url)])

Common Mistakes & Lessons Learned

Making Up Rarity IDs or Cost Values

NEVER assume or calculate rarity_id or cost values. These are specified by the user based on game balance decisions, not algorithmic thresholds.

Using mlbplayer_id = 0 When Skipping MLBPlayer

ALWAYS use mlbplayer_id = None (not 0) when skipping MLBPlayer creation. Using 0 may cause foreign key constraint issues.

Using db_post for CardPositions

ALWAYS use db_put('cardpositions', ...) not db_post. The endpoint doesn't support POST method.

Missing Required Fields in Player Payload

ALL of these fields are required: p_name, cost, image, mlbclub, franchise, cardset_id, set_num, rarity_id, pos_1, description, bbref_id, fangr_id

Wrong Field Names

  • MLBPlayer: Use first_name/last_name (NOT name_first/name_last)
  • Player: Use mlbclub/franchise (NOT mlb_team_id/franchise_id)
  • CardPosition: Use range/error/arm (NOT fielding_rating/fielding_error/fielding_arm)

Troubleshooting

"ModuleNotFoundError: No module named 'creation_helpers'"

  • Ensure running from card-creation directory
  • Ensure virtual environment is activated

"Cardset not found"

  • Create new cardset when prompted
  • Verify cardset name spelling if expecting existing

"Total chances != 108"

  • Minor rounding errors (<0.1) are acceptable
  • Report if difference > 0.5

"Database connection error"

  • Verify API_TOKEN in db_calls.py
  • Check network connectivity
  • Verify correct database URL (prod vs dev)

Advanced Customization & Technical Details

Rating Calculation Nuances

Ballpark (BP) Result Calculations

  • BP-HR and BP-Single multiply by 0.5 for AVG/OBP calculations
  • BP results use FULL value for SLG calculations
    • BP-HR counts as 2 bases for slugging (not 0.5 × 2)
    • BP-Single counts as 1 base for slugging (not 0.5 × 1)

Example:

# AVG/OBP calculation
total_hits = homerun + (bp_homerun * 0.5) + triple + ... + (bp_single * 0.5)
avg = total_hits / 108

# SLG calculation
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
    • The weaker split is double-counted
  • Pitchers: (OPS_vR + OPS_vL + max(OPS_vL, OPS_vR)) / 3
    • The stronger split (worse for pitcher) is double-counted

Rating Constraints

  • All ratings must sum to exactly 108.0 (D20 × 5.4)
  • Use 0.05 increments for natural-looking ratings
  • Apply ±0.5 randomization to avoid mechanical-looking cards
  • CRITICAL: BP-HR MUST ALWAYS BE A WHOLE NUMBER (0, 1, 2, or 3)
    • If hr_count < 0.5: BP-HR = 0, regular HR = 0 (no power)
    • If hr_count ≤ 1.0: BP-HR = 1, regular HR = 0 (only ballpark homers)
    • If hr_count < 3: BP-HR = 1, regular HR = hr_count - 1
    • If hr_count < 6: BP-HR = 2, regular HR = hr_count - 2
    • If hr_count ≥ 6: BP-HR = 3, regular HR = hr_count - 3
    • NEVER allow negative regular HR values

Power Distribution Strategy

When removing home runs to lower OPS:

  • Redistribute to singles (not doubles) to reduce SLG more effectively
  • Redistributing to doubles maintains higher slugging than desired

Database Field Naming Reference

MLBPlayer Table

{
    'first_name': 'John',        # NOT name_first
    'last_name': 'Smith',        # NOT name_last
    'key_bbref': 'smithj01',
    'key_fangraphs': 0,
    'key_mlbam': 0,
    'key_retro': ''
}

Player Table (Required Fields)

{
    'p_name': 'John Smith',
    'bbref_id': 'custom_smithj01',
    'hand': 'R',
    'mlbclub': 'Custom Ballplayers',     # For custom players
    'franchise': 'Custom Ballplayers',   # For custom players
    'cardset_id': 29,
    'description': '05 Custom',
    'is_custom': True,                   # Flag for custom players
    'image': 'change-me',                # Initial placeholder (updated after S3 upload)
    'set_num': 9999,                     # Required (use 9999 for customs)
    'pos_1': 'C',                        # Required (primary position)
    'cost': '91',                        # STRING, not int
    'rarity_id': 3,                      # INT - see rarity table below
    'mlbplayer_id': None,                # Use None (not 0) when skipping MLBPlayer
    'fangr_id': 0                        # Required for custom players
}

CRITICAL: ALL Required Fields Must Be Present Missing ANY of these fields will result in "field required" API errors.

Rarity ID Reference Table (Batters):

99 = Hall of Fame (1.200+ OPS)
1  = Diamond (1.000+ OPS)
2  = All-Star (0.900+ OPS)
3  = Starter (0.800+ OPS)
4  = Reserve (0.700+ OPS)
5  = Replacement (remainder)

⚠️ CRITICAL: NEVER MAKE UP RARITY IDs OR COST VALUES

  • These are the ONLY valid rarity_id values for batters
  • Cost is determined by user specification, not by OPS thresholds
  • When in doubt, ASK the user for cost and rarity_id values

CardPosition Table

{
    'player_id': 13008,
    'variant': 0,
    'position': 'C',
    'innings': 1,
    'range': 3,          # NOT fielding_rating
    'error': 5,          # NOT fielding_error
    'arm': 0,            # NOT fielding_arm or catcher_arm
    'pb': 6,             # NOT catcher_pb (catchers only)
    'overthrow': 3       # NOT catcher_throw (catchers only)
}

Position Validators:

  • C, LF, CF, RF require arm value (cannot be None)
  • C requires pb and overthrow values (cannot be None)

Database Operation Methods

  • cardpositions: Use db_put (NOT db_post)
  • battingcards, pitchingcards: Use db_put
  • players, mlbplayers: Use db_post

Handling Existing Records

MLBPlayer Conflicts

MLBPlayer records may exist even if bbref_id query returns empty:

# Try bbref_id first
mlb_query = await db_get('mlbplayers', params=[('key_bbref', bbref_id)])

# If that fails but creation errors with "already exists", try name-based
mlb_query = await db_get('mlbplayers', params=[
    ('first_name', name_first),
    ('last_name', name_last)
])

Player Record Best Practice

Always 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:
    # Use existing player
    player_id = p_query['players'][0]['player_id']
else:
    # Create new player
    new_player = await db_post('players', payload=player_payload)

AWS S3 Upload Process

S3 Path Structure

cards/cardset-{cardset_id:03d}/player-{player_id}/battingcard.png
  • Use zero-padded 3-digit cardset ID (e.g., 029 not 29)
  • Include cache-busting query parameter: ?d={YYYY-M-D}

Upload Steps

  1. Fetch card image from API: GET /players/{id}/battingcard?d={date}
  2. Upload to S3 with metadata:
    s3_client.put_object(
        Bucket='paper-dynasty',
        Key=f'cards/cardset-{cardset_id:03d}/player-{player_id}/battingcard.png',
        Body=image_bytes,
        ContentType='image/png',
        CacheControl='public, max-age=300'
    )
    
  3. Update player record with S3 URL:
    s3_url = f'https://paper-dynasty.s3.us-east-1.amazonaws.com/{s3_key}?d={date}'
    await db_patch('players', object_id=player_id, params=[('image', s3_url)])
    

Custom Player Conventions

Identifiers

  • bbref_id: custom_{lastname}{firstinitial}01
  • fangraphs_id: 0
  • mlbam_id: 0
  • strat_code: 0
  • is_custom: True

Organization

  • mlbclub: "Custom Ballplayers"
  • franchise: "Custom Ballplayers"
  • cardset_id: 29 (standard custom cardset)
  • set_num: 9999 (convention for custom players)

Example Reference Implementation

See: /mnt/NV2/Development/paper-dynasty/card-creation/create_valerie_theolia.py

This script demonstrates:

  • Manual rating customization with randomization
  • Proper field naming for all database tables
  • Handling existing records gracefully
  • S3 upload and URL updates
  • Complete end-to-end custom player creation
  • Weekly Card Generation: For real players from Retrosheet data
  • Gauntlet Cleanup: Managing temporary tournament teams
  • Pack Distribution: Reward systems and promotions

Last Updated: 2025-11-13 Maintainer: Cal Corum Version: 1.1 (Added Advanced Customization section)