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>
19 KiB
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:
- Select baseball archetypes (e.g., "Power Slugger", "Ace Pitcher", "Speedster")
- Define player details (name, team, positions, handedness)
- Review calculated D20 ratings based on the archetype
- Iteratively tweak ratings to achieve desired player profile
- 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 profilesarchetype_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:
- Navigate to the card-creation directory
- Activate the virtual environment
- Launch the interactive workflow
- 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:
- Accept these ratings (proceed to create)
- Tweak archetype percentages (adjust power, contact, etc.)
- Manually adjust specific D20 ratings
- 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:
- Trigger Card Generation: GET request to
Player.imageURL to render the card PNG - Fetch Card Image: Download the generated PNG image bytes from API
- 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} - 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: Trueflag 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(NOTname_first/name_last) - Player: Use
mlbclub/franchise(NOTmlb_team_id/franchise_id) - CardPosition: Use
range/error/arm(NOTfielding_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
armvalue (cannot be None) - C requires
pbandoverthrowvalues (cannot be None)
Database Operation Methods
cardpositions: Usedb_put(NOTdb_post)battingcards,pitchingcards: Usedb_putplayers,mlbplayers: Usedb_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.,
029not29) - Include cache-busting query parameter:
?d={YYYY-M-D}
Upload Steps
- Fetch card image from API:
GET /players/{id}/battingcard?d={date} - 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' ) - 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}01fangraphs_id: 0mlbam_id: 0strat_code: 0is_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
Related Workflows
- 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)