paper-dynasty-card-creation/docs/architecture/CARD_BUILDER_REDESIGN.md
Cal Corum 9a121d370f Add card builder architecture redesign documentation
Documents proposed architecture for moving card-building logic upstream
to Python, including:
- Executive summary with problem statement and migration path
- CardBuilder sketch with contract system for pluggable placement strategies
- Support for different card "personalities" (Standard, Clutch, Power Heavy, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 10:56:55 -06:00

210 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Card Creation Architecture Redesign - Executive Summary
**Date:** 2026-01-22
**Project:** Paper Dynasty Card Generation System
**Author:** Cal Corum + Claude (Jarvis)
---
## Problem Statement
The current card creation pipeline has an architectural inconsistency:
1. **Python generates continuous chance values** (e.g., 4.78 out of 108)
2. **Database fits to discrete card mechanics** (2d6 × d20 combinations)
3. **Database saves fitted values back** - what you send ≠ what gets stored
This results in subtle discrepancies (e.g., sending 4.75, card shows 4.65) because the card mechanics only support specific discrete probability values.
### Root Cause
The card uses 2d6 (rows 2-12) combined with d20 splits. Valid chance values are constrained by:
- Row frequencies: 1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1 (totaling 36)
- D20 subdivisions: 1-20 range creates fractional chances
Python generates values ignorant of these constraints; the database must "round" to fit.
---
## Proposed Solution
**Move the card-building logic upstream to Python**, making it the single source of truth.
### New Architecture
```
BEFORE:
Python: stats → continuous chances → POST raw values
Database: receives → fits to card → saves DIFFERENT values → renders
AFTER:
Python: stats → continuous chances → CardBuilder → discrete card structure → POST
Database: receives → stores → renders (no fitting needed)
```
### Key Components
| Component | Responsibility |
|-----------|----------------|
| `CardBuilder` | Fits continuous chances to discrete card structure |
| `CardContract` | Defines placement strategy (which rows for which plays) |
| `BuiltCard` | Structured output ready for storage/rendering |
| `RawBattingChances` / `RawPitchingChances` | Input from existing stat calculations |
---
## Contract System
A major enhancement: **pluggable placement strategies** that give cards different "personalities" using the same raw stats.
### Available Contracts
| Contract | Behavior | Use Case |
|----------|----------|----------|
| **Standard** | On-base results on middle rows (6-8) | Default behavior |
| **Clutch** | On-base results on edge rows (2-4, 10-12) | Role players, postseason heroes |
| **Power Heavy** | HR/3B/2B on prime rows, singles on edges | HR leaders, sluggers |
| **Contact First** | Singles on prime rows, power on edges | Leadoff hitters, high-AVG players |
| **Groundball Pitcher** | Groundouts on middle rows | Sinkerballers |
| **Flyball Pitcher** | Strikeouts/flyouts on middle rows | Power pitchers |
### Example: Same Stats, Different Cards
```
Power Heavy Contract:
Row 7 (most likely): HOMERUN
Row 12 (least likely): SINGLE
Contact First Contract:
Row 7 (most likely): SINGLE
Row 12 (least likely): HOMERUN
```
### Contract Interface
```python
class CardContract:
def get_row_preference(self, category: PlayCategory) -> List[int]
def get_play_priority(self) -> List[PlayCategory]
def should_use_splits(self, category: PlayCategory) -> bool
def max_splits_per_column(self) -> int
```
---
## Benefits
### Immediate
1. **Deterministic output** - What Python calculates is exactly what appears on the card
2. **No step 7** - Database doesn't modify/re-save values
3. **Local preview** - Validate cards without hitting the API
4. **Single source of truth** - Fitting logic in one place
### Strategic
1. **Card personalities** - Same player can have different "feels" via contracts
2. **Testable** - Unit test card building without database
3. **Extensible** - Add new contracts without touching core logic
4. **Data-driven** - Store contract name in DB, apply at generation time
---
## Migration Path
### Phase 1: Extract & Validate
- Extract current database fitting logic to shared module
- Run old and new in parallel, compare outputs
- Fix discrepancies
### Phase 2: Python Adoption
- Modify Python card-creation to use `CardBuilder`
- POST fitted card structures instead of raw chances
- Keep raw chances in separate field for debugging
### Phase 3: Database Simplification
- Database receives pre-fitted card structures
- Remove fitting logic from database
- Eliminate step 7 (no re-saving)
### Phase 4: Enhancements
- Add preview endpoint (returns structure without saving)
- Implement contract selection in card creation workflow
- Build custom contract UI
---
## Technical Details
### Discrete Probability Space
Valid chance values are defined by `EXACT_CHANCES`:
```python
[5.7, 5.4, 5.1, 4.8, 4.75, 4.5, 4.25, 4.2, 3.9, 3.8, 3.75, ...]
```
Plus whole numbers 1-6 (full row allocations).
### Pitching Card Specifics
- **X-checks**: Redirect to batter's card (e.g., `GB (ss) X`)
- **Batter Power**: `bp_homerun`, `bp_single` redirect to batter
- **Secondary plays**: D20 splits pair primary with complementary outcome
- **Bolding**: Strikeouts bolded (good for pitcher), hits not bolded
### Row Frequencies
```
Row: 2 3 4 5 6 7 8 9 10 11 12
Freq: 1 2 3 4 5 6 5 4 3 2 1
```
Row 7 is most valuable (6 chances = 16.67% of outcomes).
---
## Files Produced
| File | Description |
|------|-------------|
| `card_builder_sketch.py` | Main module: CardBuilder, contracts, data structures |
| `contracts.py` | Standalone contracts reference (superseded by integration) |
| `EXECUTIVE_SUMMARY.md` | This document |
---
## Open Questions
1. **Shared package or copy?** Extract to pip-installable package both repos import, or maintain synced copies?
2. **Keep raw chances?** Store both raw (for analysis) and fitted (for rendering), or just fitted?
3. **Contract storage** - Where to store contract selection? Player level? Cardset level? Both?
4. **Custom contracts** - Allow users to create custom contracts via UI?
---
## Recommendation
**Proceed with the shared card-building module approach.**
The contract system adds significant value by enabling card personalities without changing the underlying statistics. This addresses the original consistency problem while opening new design possibilities.
**Suggested next step:** Create a proof-of-concept by implementing `CardBuilder` against a single real player's stats and comparing output to current database behavior.
---
## Appendix: Code Location
All sketch files are in:
```
~/.claude/scratchpad/2026-01-22-card-builder-sketch/
```
To test:
```bash
cd ~/.claude/scratchpad/2026-01-22-card-builder-sketch
python card_builder_sketch.py
```