paper-dynasty-card-creation/rarity_thresholds.py
Cal Corum feedfe951f feat: add RARITY_LADDER, rarity_is_downgrade, and next_rarity helpers (#59)
Closes #59

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 09:01:59 -05:00

201 lines
6.0 KiB
Python

"""
Rarity threshold configurations for card creation.
This module defines OPS thresholds for assigning rarity tiers to player cards.
Different seasons may have different thresholds based on league-wide performance.
"""
from dataclasses import dataclass
@dataclass
class PitcherRarityThresholds:
"""OPS-against thresholds for pitcher rarity assignment."""
# Starter thresholds (OPS-against, lower is better)
starter_hof: float # Hall of Fame (99)
starter_diamond: float # Diamond (1)
starter_gold: float # Gold (2)
starter_silver: float # Silver (3)
starter_bronze: float # Bronze (4)
# Reliever thresholds (OPS-against, lower is better)
reliever_hof: float # Hall of Fame (99)
reliever_diamond: float # Diamond (1)
reliever_gold: float # Gold (2)
reliever_silver: float # Silver (3)
reliever_bronze: float # Bronze (4)
def get_rarity_for_starter(self, total_ops: float) -> int:
"""Returns rarity ID for a starting pitcher based on OPS-against."""
if total_ops <= self.starter_hof:
return 99
elif total_ops <= self.starter_diamond:
return 1
elif total_ops <= self.starter_gold:
return 2
elif total_ops <= self.starter_silver:
return 3
elif total_ops <= self.starter_bronze:
return 4
else:
return 5 # Common
def get_rarity_for_reliever(self, total_ops: float) -> int:
"""Returns rarity ID for a relief pitcher based on OPS-against."""
if total_ops <= self.reliever_hof:
return 99
elif total_ops <= self.reliever_diamond:
return 1
elif total_ops <= self.reliever_gold:
return 2
elif total_ops <= self.reliever_silver:
return 3
elif total_ops <= self.reliever_bronze:
return 4
else:
return 5 # Common
@dataclass
class BatterRarityThresholds:
"""OPS thresholds for batter rarity assignment."""
# Batter thresholds (OPS, higher is better)
hof: float # Hall of Fame (99)
diamond: float # Diamond (1)
gold: float # Gold (2)
silver: float # Silver (3)
bronze: float # Bronze (4)
def get_rarity(self, total_ops: float) -> int:
"""Returns rarity ID for a batter based on OPS."""
# For batters, higher OPS is better, so we check >= instead of <=
if total_ops >= self.hof:
return 99
elif total_ops >= self.diamond:
return 1
elif total_ops >= self.gold:
return 2
elif total_ops >= self.silver:
return 3
elif total_ops >= self.bronze:
return 4
else:
return 5 # Common
# 2024 Season Thresholds (Original values)
PITCHER_THRESHOLDS_2024 = PitcherRarityThresholds(
starter_hof=0.4,
starter_diamond=0.475,
starter_gold=0.53,
starter_silver=0.6,
starter_bronze=0.675,
reliever_hof=0.325,
reliever_diamond=0.4,
reliever_gold=0.475,
reliever_silver=0.55,
reliever_bronze=0.625,
)
BATTER_THRESHOLDS_2024 = BatterRarityThresholds(
hof=1.2,
diamond=1.0,
gold=0.9,
silver=0.8,
bronze=0.7,
)
# 2025 Season Thresholds (Adjusted based on data analysis)
PITCHER_THRESHOLDS_2025 = PitcherRarityThresholds(
starter_hof=0.300, # Top 5%
starter_diamond=0.354, # Top 15%
starter_gold=0.384, # Top 30%
starter_silver=0.441, # Top 50%
starter_bronze=0.487, # Top 70%
reliever_hof=0.270, # Top 5%
reliever_diamond=0.319, # Top 15%
reliever_gold=0.370, # Top 30%
reliever_silver=0.436, # Top 50%
reliever_bronze=0.503, # Top 70%
)
BATTER_THRESHOLDS_2025 = BatterRarityThresholds(
hof=1.2,
diamond=1.0,
gold=0.9,
silver=0.8,
bronze=0.7,
)
def get_pitcher_thresholds(season: int) -> PitcherRarityThresholds:
"""Get pitcher rarity thresholds for a specific season."""
if season >= 2025:
return PITCHER_THRESHOLDS_2025
else:
return PITCHER_THRESHOLDS_2024
def get_batter_thresholds(season: int) -> BatterRarityThresholds:
"""Get batter rarity thresholds for a specific season."""
if season >= 2025:
return BATTER_THRESHOLDS_2025
else:
return BATTER_THRESHOLDS_2024
# Ordered from least to most prestigious. Used for ladder comparisons (T4
# rarity upgrade, downgrade guard). Do not change the order.
RARITY_LADDER: list[int] = [
5,
4,
3,
2,
1,
99,
] # Common → Bronze → Silver → Gold → Diamond → HoF
def rarity_is_downgrade(current_rarity_id: int, new_rarity_id: int) -> bool:
"""Return True if new_rarity_id is a less prestigious tier than current_rarity_id.
Uses the RARITY_LADDER ordering. Unknown IDs are treated as position 0
(worst), so an unknown current rarity will never trigger a downgrade guard.
An unknown new_rarity_id also returns False.
"""
try:
current_pos = RARITY_LADDER.index(current_rarity_id)
except ValueError:
return False
try:
new_pos = RARITY_LADDER.index(new_rarity_id)
except ValueError:
return False
return current_pos > new_pos
def next_rarity(current_rarity_id: int) -> int | None:
"""Return the next more-prestigious rarity ID, or None if already at HoF.
Uses the RARITY_LADDER ordering. Returns None when current_rarity_id is
Hall of Fame (99) — the T4 rarity upgrade is a no-op at the top tier.
Returns None for any unrecognised rarity ID.
Examples:
next_rarity(5) → 4 (Common → Bronze)
next_rarity(4) → 3 (Bronze → Silver)
next_rarity(3) → 2 (Silver → Gold)
next_rarity(2) → 1 (Gold → Diamond)
next_rarity(1) → 99 (Diamond → HoF)
next_rarity(99) → None (HoF: already at max)
"""
try:
pos = RARITY_LADDER.index(current_rarity_id)
except ValueError:
return None
if pos == len(RARITY_LADDER) - 1:
return None # Already at HoF
return RARITY_LADDER[pos + 1]