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>
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, not0— 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/armfor positions,first_name/last_namefor 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:
mlbclubandfranchise= "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=truefor HTML preview instead of PNG
Last Updated: 2026-02-12 Version: 3.0 (Merged custom-player-database-creation.md; trimmed redundant content)