paper-dynasty-card-creation/rarity_thresholds.py
Cal Corum aaa2eaa252 fix: guard against T4 rarity upgrade collision in live-series pipeline (#57)
Closes #57

- Add RARITY_LADDER and rarity_is_downgrade() to rarity_thresholds.py
- Add get_fully_evolved_players() to db_calls.py — queries a to-be-created
  database endpoint; returns empty set safely if endpoint is unavailable
- In batters/creation.py post_player_updates(): pre-flight check identifies
  players where OPS rarity would downgrade, then guards the rarity write to
  skip any downgrade for fully-evolved (T4) cards
- Same guard added to pitchers/creation.py post_player_updates()

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

169 lines
5.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 worst to best rarity. Used for ladder comparisons such as the
# T4 refractor guard — do not change the order.
RARITY_LADDER = [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.
"""
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