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>
This commit is contained in:
parent
a2f4d02b18
commit
9a121d370f
209
docs/architecture/CARD_BUILDER_REDESIGN.md
Normal file
209
docs/architecture/CARD_BUILDER_REDESIGN.md
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
1589
docs/architecture/card_builder_sketch.py
Normal file
1589
docs/architecture/card_builder_sketch.py
Normal file
File diff suppressed because it is too large
Load Diff
589
docs/architecture/contracts.py
Normal file
589
docs/architecture/contracts.py
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
"""
|
||||||
|
Card Building Contracts - Pluggable Placement Strategies
|
||||||
|
|
||||||
|
A "contract" defines the rules for how plays are placed on a card:
|
||||||
|
- Which rows to prefer for different play types
|
||||||
|
- Priority ordering of plays
|
||||||
|
- Secondary play pairing rules
|
||||||
|
- Split vs whole number preferences
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
This enables different card "personalities" while using the same fitting engine.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from decimal import Decimal
|
||||||
|
from enum import Enum
|
||||||
|
from typing import List, Dict, Optional, Callable, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ROW PREFERENCE STRATEGIES
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class RowPreference(Enum):
|
||||||
|
"""Built-in row ordering strategies."""
|
||||||
|
MIDDLE_FIRST = "middle_first" # 7, 6, 8, 5, 9, 4, 10, 3, 11, 2, 12
|
||||||
|
EDGES_FIRST = "edges_first" # 2, 12, 3, 11, 4, 10, 5, 9, 6, 8, 7
|
||||||
|
HIGH_TO_LOW = "high_to_low" # 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2
|
||||||
|
LOW_TO_HIGH = "low_to_high" # 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
|
||||||
|
SYMMETRIC_OUT = "symmetric_out" # 7, 6, 8, 5, 9, 4, 10, 3, 11, 2, 12 (same as middle)
|
||||||
|
SYMMETRIC_IN = "symmetric_in" # 2, 12, 3, 11, 4, 10, 5, 9, 6, 8, 7 (same as edges)
|
||||||
|
BY_FREQUENCY_DESC = "freq_desc" # 7, 6, 8, 5, 9, 4, 10, 3, 11, 2, 12 (by chance count)
|
||||||
|
BY_FREQUENCY_ASC = "freq_asc" # 2, 12, 3, 11, 4, 10, 5, 9, 6, 8, 7
|
||||||
|
|
||||||
|
|
||||||
|
ROW_ORDERINGS = {
|
||||||
|
RowPreference.MIDDLE_FIRST: [7, 6, 8, 5, 9, 4, 10, 3, 11, 2, 12],
|
||||||
|
RowPreference.EDGES_FIRST: [2, 12, 3, 11, 4, 10, 5, 9, 6, 8, 7],
|
||||||
|
RowPreference.HIGH_TO_LOW: [12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2],
|
||||||
|
RowPreference.LOW_TO_HIGH: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||||
|
RowPreference.SYMMETRIC_OUT: [7, 6, 8, 5, 9, 4, 10, 3, 11, 2, 12],
|
||||||
|
RowPreference.SYMMETRIC_IN: [2, 12, 3, 11, 4, 10, 5, 9, 6, 8, 7],
|
||||||
|
RowPreference.BY_FREQUENCY_DESC: [7, 6, 8, 5, 9, 4, 10, 3, 11, 2, 12],
|
||||||
|
RowPreference.BY_FREQUENCY_ASC: [2, 12, 3, 11, 4, 10, 5, 9, 6, 8, 7],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PLAY CATEGORY DEFINITIONS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class PlayCategory(Enum):
|
||||||
|
"""Categories of plays for contract rules."""
|
||||||
|
ON_BASE = "on_base" # Walks, HBP, singles, doubles, triples, HR
|
||||||
|
POWER = "power" # HR, triples, doubles
|
||||||
|
CONTACT = "contact" # Singles
|
||||||
|
PATIENCE = "patience" # Walks, HBP
|
||||||
|
STRIKEOUT = "strikeout"
|
||||||
|
GROUNDOUT = "groundout"
|
||||||
|
FLYOUT = "flyout"
|
||||||
|
XCHECK = "xcheck" # Pitcher X-checks
|
||||||
|
BATTER_POWER = "batter_power" # bp_homerun, bp_single
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# THE CONTRACT INTERFACE
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlacementRule:
|
||||||
|
"""A single rule for how to place a category of plays."""
|
||||||
|
category: PlayCategory
|
||||||
|
row_preference: RowPreference
|
||||||
|
priority: int = 50 # Lower = placed earlier (0-100)
|
||||||
|
prefer_splits: bool = True # Use d20 splits when possible?
|
||||||
|
max_per_column: Optional[int] = None # Limit instances per column
|
||||||
|
secondary_pool: List[str] = field(default_factory=list) # What can fill split remainder
|
||||||
|
|
||||||
|
|
||||||
|
class CardContract(ABC):
|
||||||
|
"""
|
||||||
|
Abstract base class for card building contracts.
|
||||||
|
|
||||||
|
A contract defines the "personality" of a card - how plays are distributed
|
||||||
|
across the 2d6 probability space.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Human-readable contract name."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def description(self) -> str:
|
||||||
|
"""Describe what this contract does."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_row_preference(self, category: PlayCategory) -> List[int]:
|
||||||
|
"""
|
||||||
|
Return ordered list of preferred rows for this play category.
|
||||||
|
First row in list is most preferred.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_play_priority(self) -> List[PlayCategory]:
|
||||||
|
"""
|
||||||
|
Return categories in priority order (placed first to last).
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_secondary_pool(self, category: PlayCategory) -> List[PlayCategory]:
|
||||||
|
"""
|
||||||
|
Return categories that can fill d20 split remainders for this category.
|
||||||
|
Default: flyouts and groundouts.
|
||||||
|
"""
|
||||||
|
return [PlayCategory.FLYOUT, PlayCategory.GROUNDOUT]
|
||||||
|
|
||||||
|
def should_use_splits(self, category: PlayCategory) -> bool:
|
||||||
|
"""Whether this category should use d20 splits. Default: True."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def max_splits_per_column(self) -> int:
|
||||||
|
"""Maximum d20 splits allowed per column. Default: 4."""
|
||||||
|
return 4
|
||||||
|
|
||||||
|
def validate(self, card_data: dict) -> List[str]:
|
||||||
|
"""
|
||||||
|
Validate a built card against this contract's rules.
|
||||||
|
Returns list of violations (empty if valid).
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CONCRETE CONTRACT IMPLEMENTATIONS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class StandardContract(CardContract):
|
||||||
|
"""
|
||||||
|
The default contract - matches current database behavior.
|
||||||
|
|
||||||
|
- On-base results favor middle rows (more likely to occur)
|
||||||
|
- Outs fill remaining rows
|
||||||
|
- Standard priority ordering
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "Standard"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
return "Default card layout - on-base results on middle rows (6-8)"
|
||||||
|
|
||||||
|
def get_row_preference(self, category: PlayCategory) -> List[int]:
|
||||||
|
if category in [PlayCategory.ON_BASE, PlayCategory.POWER,
|
||||||
|
PlayCategory.CONTACT, PlayCategory.PATIENCE]:
|
||||||
|
# Good outcomes on likely rows
|
||||||
|
return ROW_ORDERINGS[RowPreference.MIDDLE_FIRST]
|
||||||
|
else:
|
||||||
|
# Outs on whatever's left (edges tend to be available)
|
||||||
|
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
|
||||||
|
|
||||||
|
def get_play_priority(self) -> List[PlayCategory]:
|
||||||
|
return [
|
||||||
|
PlayCategory.BATTER_POWER, # bp_hr, bp_single first
|
||||||
|
PlayCategory.PATIENCE, # Walks, HBP
|
||||||
|
PlayCategory.XCHECK, # X-checks
|
||||||
|
PlayCategory.POWER, # HR, 3B, 2B
|
||||||
|
PlayCategory.CONTACT, # Singles
|
||||||
|
PlayCategory.STRIKEOUT,
|
||||||
|
PlayCategory.GROUNDOUT,
|
||||||
|
PlayCategory.FLYOUT,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ClutchContract(CardContract):
|
||||||
|
"""
|
||||||
|
"Clutch" card personality - on-base results on EDGE rows.
|
||||||
|
|
||||||
|
This makes hits less likely overall, but when they happen,
|
||||||
|
the player "came through in the clutch" (rare but impactful).
|
||||||
|
|
||||||
|
Good for: Role players, clutch hitters, postseason heroes
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "Clutch"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
return "On-base results on edge rows (2-4, 10-12) - less frequent but memorable"
|
||||||
|
|
||||||
|
def get_row_preference(self, category: PlayCategory) -> List[int]:
|
||||||
|
if category in [PlayCategory.ON_BASE, PlayCategory.POWER,
|
||||||
|
PlayCategory.CONTACT, PlayCategory.PATIENCE]:
|
||||||
|
# Good outcomes on unlikely rows
|
||||||
|
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
|
||||||
|
else:
|
||||||
|
# Outs on middle rows (more common)
|
||||||
|
return ROW_ORDERINGS[RowPreference.MIDDLE_FIRST]
|
||||||
|
|
||||||
|
def get_play_priority(self) -> List[PlayCategory]:
|
||||||
|
# Same priority, different placement
|
||||||
|
return StandardContract().get_play_priority()
|
||||||
|
|
||||||
|
|
||||||
|
class ConsistentContract(CardContract):
|
||||||
|
"""
|
||||||
|
"Consistent" card personality - spread results evenly.
|
||||||
|
|
||||||
|
Distributes on-base results across all rows proportionally,
|
||||||
|
making the player more predictable/reliable.
|
||||||
|
|
||||||
|
Good for: Contact hitters, batting average leaders
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "Consistent"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
return "Spread results evenly across all rows - predictable performance"
|
||||||
|
|
||||||
|
def get_row_preference(self, category: PlayCategory) -> List[int]:
|
||||||
|
# Alternate between middle and edges to spread out
|
||||||
|
return [7, 2, 8, 12, 6, 3, 9, 11, 5, 4, 10]
|
||||||
|
|
||||||
|
def get_play_priority(self) -> List[PlayCategory]:
|
||||||
|
return StandardContract().get_play_priority()
|
||||||
|
|
||||||
|
|
||||||
|
class PowerHeavyContract(CardContract):
|
||||||
|
"""
|
||||||
|
Power-focused card - extra-base hits get prime real estate.
|
||||||
|
|
||||||
|
HR and triples on row 7 (most likely), doubles on 6/8,
|
||||||
|
singles pushed to edges.
|
||||||
|
|
||||||
|
Good for: Power hitters, HR leaders
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "Power Heavy"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
return "Power hits (HR, 3B, 2B) on prime rows, singles on edges"
|
||||||
|
|
||||||
|
def get_row_preference(self, category: PlayCategory) -> List[int]:
|
||||||
|
if category == PlayCategory.POWER:
|
||||||
|
# Power on the most likely rows
|
||||||
|
return [7, 6, 8, 5, 9]
|
||||||
|
elif category == PlayCategory.CONTACT:
|
||||||
|
# Singles pushed to edges
|
||||||
|
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
|
||||||
|
elif category == PlayCategory.PATIENCE:
|
||||||
|
# Walks in the middle-ish
|
||||||
|
return [5, 9, 4, 10, 6, 8]
|
||||||
|
else:
|
||||||
|
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
|
||||||
|
|
||||||
|
def get_play_priority(self) -> List[PlayCategory]:
|
||||||
|
# Power comes before patience
|
||||||
|
return [
|
||||||
|
PlayCategory.BATTER_POWER,
|
||||||
|
PlayCategory.POWER, # HR, 3B, 2B FIRST
|
||||||
|
PlayCategory.PATIENCE, # Then walks
|
||||||
|
PlayCategory.XCHECK,
|
||||||
|
PlayCategory.CONTACT,
|
||||||
|
PlayCategory.STRIKEOUT,
|
||||||
|
PlayCategory.GROUNDOUT,
|
||||||
|
PlayCategory.FLYOUT,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ContactFirstContract(CardContract):
|
||||||
|
"""
|
||||||
|
Contact-focused card - singles get prime placement.
|
||||||
|
|
||||||
|
Singles on middle rows, power hits on edges.
|
||||||
|
More base hits, but less extra-base damage.
|
||||||
|
|
||||||
|
Good for: Contact hitters, leadoff types, high-average players
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "Contact First"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
return "Singles on prime rows, power on edges - consistent base hits"
|
||||||
|
|
||||||
|
def get_row_preference(self, category: PlayCategory) -> List[int]:
|
||||||
|
if category == PlayCategory.CONTACT:
|
||||||
|
return ROW_ORDERINGS[RowPreference.MIDDLE_FIRST]
|
||||||
|
elif category == PlayCategory.POWER:
|
||||||
|
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
|
||||||
|
elif category == PlayCategory.PATIENCE:
|
||||||
|
return [5, 9, 4, 10]
|
||||||
|
else:
|
||||||
|
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
|
||||||
|
|
||||||
|
def get_play_priority(self) -> List[PlayCategory]:
|
||||||
|
return [
|
||||||
|
PlayCategory.BATTER_POWER,
|
||||||
|
PlayCategory.CONTACT, # Singles FIRST
|
||||||
|
PlayCategory.PATIENCE,
|
||||||
|
PlayCategory.POWER,
|
||||||
|
PlayCategory.XCHECK,
|
||||||
|
PlayCategory.STRIKEOUT,
|
||||||
|
PlayCategory.GROUNDOUT,
|
||||||
|
PlayCategory.FLYOUT,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class GroundballPitcherContract(CardContract):
|
||||||
|
"""
|
||||||
|
Groundball pitcher personality.
|
||||||
|
|
||||||
|
Groundouts on middle rows, flyouts on edges.
|
||||||
|
X-checks to corner infielders get priority.
|
||||||
|
|
||||||
|
Good for: Sinkerballers, ground ball specialists
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "Groundball Pitcher"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
return "Groundouts on likely rows, X-checks favor corner infielders"
|
||||||
|
|
||||||
|
def get_row_preference(self, category: PlayCategory) -> List[int]:
|
||||||
|
if category == PlayCategory.GROUNDOUT:
|
||||||
|
return ROW_ORDERINGS[RowPreference.MIDDLE_FIRST]
|
||||||
|
elif category == PlayCategory.FLYOUT:
|
||||||
|
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
|
||||||
|
elif category == PlayCategory.STRIKEOUT:
|
||||||
|
return [5, 9, 4, 10] # Mid-edges
|
||||||
|
else:
|
||||||
|
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
|
||||||
|
|
||||||
|
def get_play_priority(self) -> List[PlayCategory]:
|
||||||
|
return [
|
||||||
|
PlayCategory.BATTER_POWER,
|
||||||
|
PlayCategory.PATIENCE, # Walks/HBP
|
||||||
|
PlayCategory.XCHECK, # Corner IF X-checks
|
||||||
|
PlayCategory.GROUNDOUT, # Groundouts prioritized
|
||||||
|
PlayCategory.STRIKEOUT,
|
||||||
|
PlayCategory.POWER,
|
||||||
|
PlayCategory.CONTACT,
|
||||||
|
PlayCategory.FLYOUT,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class FlyballPitcherContract(CardContract):
|
||||||
|
"""
|
||||||
|
Flyball pitcher personality.
|
||||||
|
|
||||||
|
Flyouts and strikeouts on middle rows.
|
||||||
|
X-checks to outfielders get priority.
|
||||||
|
|
||||||
|
Good for: Power pitchers, high-K guys
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "Flyball Pitcher"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
return "Flyouts and strikeouts on likely rows, X-checks favor outfielders"
|
||||||
|
|
||||||
|
def get_row_preference(self, category: PlayCategory) -> List[int]:
|
||||||
|
if category in [PlayCategory.FLYOUT, PlayCategory.STRIKEOUT]:
|
||||||
|
return ROW_ORDERINGS[RowPreference.MIDDLE_FIRST]
|
||||||
|
elif category == PlayCategory.GROUNDOUT:
|
||||||
|
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
|
||||||
|
else:
|
||||||
|
return ROW_ORDERINGS[RowPreference.EDGES_FIRST]
|
||||||
|
|
||||||
|
def get_play_priority(self) -> List[PlayCategory]:
|
||||||
|
return [
|
||||||
|
PlayCategory.BATTER_POWER,
|
||||||
|
PlayCategory.PATIENCE,
|
||||||
|
PlayCategory.STRIKEOUT, # K's prioritized
|
||||||
|
PlayCategory.FLYOUT, # Flyouts prioritized
|
||||||
|
PlayCategory.XCHECK,
|
||||||
|
PlayCategory.POWER,
|
||||||
|
PlayCategory.CONTACT,
|
||||||
|
PlayCategory.GROUNDOUT,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CUSTOM CONTRACT BUILDER
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CustomContract(CardContract):
|
||||||
|
"""
|
||||||
|
Build a custom contract from individual rules.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
contract = CustomContract(
|
||||||
|
name="My Custom Card",
|
||||||
|
description="Power on edges, walks in middle",
|
||||||
|
rules={
|
||||||
|
PlayCategory.POWER: PlacementRule(
|
||||||
|
category=PlayCategory.POWER,
|
||||||
|
row_preference=RowPreference.EDGES_FIRST,
|
||||||
|
priority=10
|
||||||
|
),
|
||||||
|
PlayCategory.PATIENCE: PlacementRule(
|
||||||
|
category=PlayCategory.PATIENCE,
|
||||||
|
row_preference=RowPreference.MIDDLE_FIRST,
|
||||||
|
priority=5
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
_name: str
|
||||||
|
_description: str
|
||||||
|
rules: Dict[PlayCategory, PlacementRule] = field(default_factory=dict)
|
||||||
|
default_row_preference: RowPreference = RowPreference.MIDDLE_FIRST
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
return self._description
|
||||||
|
|
||||||
|
def get_row_preference(self, category: PlayCategory) -> List[int]:
|
||||||
|
if category in self.rules:
|
||||||
|
return ROW_ORDERINGS[self.rules[category].row_preference]
|
||||||
|
return ROW_ORDERINGS[self.default_row_preference]
|
||||||
|
|
||||||
|
def get_play_priority(self) -> List[PlayCategory]:
|
||||||
|
# Sort by priority value (lower = first)
|
||||||
|
sorted_rules = sorted(
|
||||||
|
self.rules.items(),
|
||||||
|
key=lambda x: x[1].priority
|
||||||
|
)
|
||||||
|
return [cat for cat, _ in sorted_rules]
|
||||||
|
|
||||||
|
def get_secondary_pool(self, category: PlayCategory) -> List[PlayCategory]:
|
||||||
|
if category in self.rules and self.rules[category].secondary_pool:
|
||||||
|
# Convert strings to PlayCategory if needed
|
||||||
|
return [PlayCategory(s) if isinstance(s, str) else s
|
||||||
|
for s in self.rules[category].secondary_pool]
|
||||||
|
return super().get_secondary_pool(category)
|
||||||
|
|
||||||
|
def should_use_splits(self, category: PlayCategory) -> bool:
|
||||||
|
if category in self.rules:
|
||||||
|
return self.rules[category].prefer_splits
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CONTRACT REGISTRY
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class ContractRegistry:
|
||||||
|
"""Registry of available contracts."""
|
||||||
|
|
||||||
|
_contracts: Dict[str, CardContract] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls, contract: CardContract):
|
||||||
|
"""Register a contract by name."""
|
||||||
|
cls._contracts[contract.name.lower()] = contract
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, name: str) -> Optional[CardContract]:
|
||||||
|
"""Get a contract by name (case-insensitive)."""
|
||||||
|
return cls._contracts.get(name.lower())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def list_all(cls) -> List[str]:
|
||||||
|
"""List all registered contract names."""
|
||||||
|
return list(cls._contracts.keys())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_default(cls) -> CardContract:
|
||||||
|
"""Get the default contract."""
|
||||||
|
return StandardContract()
|
||||||
|
|
||||||
|
|
||||||
|
# Register built-in contracts
|
||||||
|
ContractRegistry.register(StandardContract())
|
||||||
|
ContractRegistry.register(ClutchContract())
|
||||||
|
ContractRegistry.register(ConsistentContract())
|
||||||
|
ContractRegistry.register(PowerHeavyContract())
|
||||||
|
ContractRegistry.register(ContactFirstContract())
|
||||||
|
ContractRegistry.register(GroundballPitcherContract())
|
||||||
|
ContractRegistry.register(FlyballPitcherContract())
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# USAGE EXAMPLE
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def example_contract_usage():
|
||||||
|
"""
|
||||||
|
How contracts would integrate with the CardBuilder.
|
||||||
|
"""
|
||||||
|
from card_builder_sketch import CardBuilder, RawBattingChances
|
||||||
|
|
||||||
|
# Get a contract
|
||||||
|
standard = ContractRegistry.get("standard")
|
||||||
|
clutch = ContractRegistry.get("clutch")
|
||||||
|
|
||||||
|
print("Available contracts:")
|
||||||
|
for name in ContractRegistry.list_all():
|
||||||
|
contract = ContractRegistry.get(name)
|
||||||
|
print(f" - {contract.name}: {contract.description}")
|
||||||
|
|
||||||
|
print("\nRow preferences for ON_BASE category:")
|
||||||
|
print(f" Standard: {standard.get_row_preference(PlayCategory.ON_BASE)[:5]}...")
|
||||||
|
print(f" Clutch: {clutch.get_row_preference(PlayCategory.ON_BASE)[:5]}...")
|
||||||
|
|
||||||
|
# Build a custom contract
|
||||||
|
custom = CustomContract(
|
||||||
|
_name="My Custom",
|
||||||
|
_description="Walks on edges, power in middle",
|
||||||
|
rules={
|
||||||
|
PlayCategory.PATIENCE: PlacementRule(
|
||||||
|
category=PlayCategory.PATIENCE,
|
||||||
|
row_preference=RowPreference.EDGES_FIRST,
|
||||||
|
priority=1
|
||||||
|
),
|
||||||
|
PlayCategory.POWER: PlacementRule(
|
||||||
|
category=PlayCategory.POWER,
|
||||||
|
row_preference=RowPreference.MIDDLE_FIRST,
|
||||||
|
priority=2
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\nCustom contract rows for PATIENCE: {custom.get_row_preference(PlayCategory.PATIENCE)[:5]}...")
|
||||||
|
print(f"Custom contract rows for POWER: {custom.get_row_preference(PlayCategory.POWER)[:5]}...")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# INTEGRATION WITH CARD BUILDER
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
"""
|
||||||
|
To integrate contracts with CardBuilder, modify the builder to accept a contract:
|
||||||
|
|
||||||
|
class CardBuilder:
|
||||||
|
def __init__(self, contract: CardContract = None):
|
||||||
|
self.contract = contract or StandardContract()
|
||||||
|
self.max_splits = self.contract.max_splits_per_column()
|
||||||
|
|
||||||
|
def _get_available_rows(self, column, category: PlayCategory) -> List[int]:
|
||||||
|
# Instead of returning rows in default order,
|
||||||
|
# use the contract's preference for this category
|
||||||
|
preferred_order = self.contract.get_row_preference(category)
|
||||||
|
return [r for r in preferred_order if column.rows[r].is_empty]
|
||||||
|
|
||||||
|
def build_batting_card(self, ...):
|
||||||
|
# Use contract's priority order
|
||||||
|
for category in self.contract.get_play_priority():
|
||||||
|
plays = self._get_plays_for_category(category, chances)
|
||||||
|
for play, raw_chance in plays:
|
||||||
|
# Use contract's row preference when placing
|
||||||
|
available = self._get_available_rows(column, category)
|
||||||
|
...
|
||||||
|
|
||||||
|
This makes the fitting algorithm contract-aware without changing its core logic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
example_contract_usage()
|
||||||
Loading…
Reference in New Issue
Block a user