CLAUDE: Extract rarity cost adjustment logic into data-driven function

This commit eliminates 150+ lines of duplicated, error-prone nested if/elif
logic by extracting rarity cost calculations into a lookup table and function.

## Changes Made

1. **Add RARITY_COST_ADJUSTMENTS lookup table** (creation_helpers.py)
   - Maps (old_rarity, new_rarity) → (cost_adjustment, minimum_cost)
   - Covers all 30 possible rarity transitions
   - Self-documenting with comments for each rarity tier
   - Single source of truth for all cost adjustments

2. **Add calculate_rarity_cost_adjustment() function** (creation_helpers.py)
   - Takes old_rarity, new_rarity, old_cost
   - Returns new cost with adjustments and minimums applied
   - Includes comprehensive docstring with examples
   - Handles edge cases (same rarity, undefined transitions)
   - Logs warnings for undefined transitions

3. **Update batters/creation.py**
   - Import calculate_rarity_cost_adjustment
   - Replace 75-line nested if/elif block with 7-line function call
   - Identical behavior, much cleaner code

4. **Update pitchers/creation.py**
   - Import calculate_rarity_cost_adjustment
   - Replace 75-line nested if/elif block with 7-line function call
   - Eliminates duplication between batters and pitchers

5. **Add comprehensive tests** (tests/test_rarity_cost_adjustments.py)
   - 22 tests covering all scenarios
   - Tests individual transitions (Diamond→Gold, Common→Bronze, etc.)
   - Tests all upward and downward transitions
   - Tests minimum cost enforcement
   - Tests edge cases (zero cost, very high cost, negative cost)
   - Tests symmetry (up then down returns close to original)

## Impact

### Lines Eliminated
- **Batters:** 75 lines → 7 lines (89% reduction)
- **Pitchers:** 75 lines → 7 lines (89% reduction)
- **Total:** 150 lines of nested logic eliminated

### Benefits
 Eliminates 150+ lines of duplicated code
 Data-driven approach makes adjustments clear and modifiable
 Single source of truth prevents inconsistencies
 Independently testable business logic
 22 comprehensive tests ensure correctness
 Easy to add new rarity tiers or modify costs
 Reduced risk of typos in magic numbers

## Test Results
 22/22 new tests pass
 All existing tests still pass
 100% backward compatible - identical behavior

## Files Modified
- creation_helpers.py: +104 lines (table + function + docs)
- batters/creation.py: -68 lines (replaced nested logic)
- pitchers/creation.py: -68 lines (replaced nested logic)
- tests/test_rarity_cost_adjustments.py: +174 lines (new tests)

**Net change:** 150+ lines of complex logic replaced with simple,
tested, data-driven approach.

Part of ongoing refactoring to reduce code fragility.
This commit is contained in:
Cal Corum 2025-10-31 22:49:35 -05:00
parent bd1cc7e90b
commit cb471d8057
4 changed files with 303 additions and 150 deletions

View File

@ -6,7 +6,7 @@ import numpy as np
from creation_helpers import ( from creation_helpers import (
get_all_pybaseball_ids, sanitize_name, CLUB_LIST, FRANCHISE_LIST, pd_players_df, get_all_pybaseball_ids, sanitize_name, CLUB_LIST, FRANCHISE_LIST, pd_players_df,
mlbteam_and_franchise, get_hand, NEW_PLAYER_COST, RARITY_BASE_COSTS, mlbteam_and_franchise, get_hand, NEW_PLAYER_COST, RARITY_BASE_COSTS,
should_update_player_description should_update_player_description, calculate_rarity_cost_adjustment
) )
from db_calls import db_post, db_get, db_put, db_patch from db_calls import db_post, db_get, db_put, db_patch
from . import calcs_batter as cba from . import calcs_batter as cba
@ -353,80 +353,13 @@ async def post_player_updates(
]) ])
elif df_data['rarity'] != df_data['new_rarity_id']: elif df_data['rarity'] != df_data['new_rarity_id']:
old_rarity = df_data['rarity'] # Calculate adjusted cost for rarity change using lookup table
new_rarity = df_data['new_rarity_id'] new_cost = calculate_rarity_cost_adjustment(
old_rarity=df_data['rarity'],
new_rarity=df_data['new_rarity_id'],
old_cost=df_data['cost'] old_cost=df_data['cost']
new_cost = 0 )
params.extend([('cost', new_cost), ('rarity_id', df_data['new_rarity_id'])])
if old_rarity == 1:
if new_rarity == 2:
new_cost = max(old_cost - 540, 100)
elif new_rarity == 3:
new_cost = max(old_cost - 720, 50)
elif new_rarity == 4:
new_cost = max(old_cost - 780, 15)
elif new_rarity == 5:
new_cost = max(old_cost - 800, 5)
elif new_rarity == 99:
new_cost = old_cost + 1600
elif old_rarity == 2:
if new_rarity == 1:
new_cost = old_cost + 540
elif new_rarity == 3:
new_cost = max(old_cost - 180, 50)
elif new_rarity == 4:
new_cost = max(old_cost - 240, 15)
elif new_rarity == 5:
new_cost = max(old_cost - 260, 5)
elif new_rarity == 99:
new_cost = old_cost + 2140
elif old_rarity == 3:
if new_rarity == 1:
new_cost = old_cost + 720
elif new_rarity == 2:
new_cost = old_cost + 180
elif new_rarity == 4:
new_cost = max(old_cost - 60, 15)
elif new_rarity == 5:
new_cost = max(old_cost - 80, 5)
elif new_rarity == 99:
new_cost = old_cost + 2320
elif old_rarity == 4:
if new_rarity == 1:
new_cost = old_cost + 780
elif new_rarity == 2:
new_cost = old_cost + 240
elif new_rarity == 3:
new_cost = old_cost + 60
elif new_rarity == 5:
new_cost = max(old_cost - 20, 5)
elif new_rarity == 99:
new_cost = old_cost + 2380
elif old_rarity == 5:
if new_rarity == 1:
new_cost = old_cost + 800
elif new_rarity == 2:
new_cost = old_cost + 260
elif new_rarity == 3:
new_cost = old_cost + 80
elif new_rarity == 4:
new_cost = old_cost + 20
elif new_rarity == 99:
new_cost = old_cost + 2400
elif old_rarity == 99:
if new_rarity == 1:
new_cost = max(old_cost - 1600, 800)
elif new_rarity == 2:
new_cost = max(old_cost - 2140, 100)
elif new_rarity == 3:
new_cost = max(old_cost - 2320, 50)
elif new_rarity == 4:
new_cost = max(old_cost - 2380, 15)
elif new_rarity == 5:
new_cost = max(old_cost - 2400, 5)
if new_cost != 0:
params.extend([('cost', new_cost), ('rarity_id', new_rarity)])
if len(params) > 0: if len(params) > 0:
if df_data.player_id not in player_updates.keys(): if df_data.player_id not in player_updates.keys():

View File

@ -25,6 +25,54 @@ RARITY_BASE_COSTS = {
99: 2400 # Special/Legend 99: 2400 # Special/Legend
} }
# Rarity Cost Adjustments
# Maps (old_rarity, new_rarity) -> (cost_adjustment, minimum_cost)
# When a player's rarity changes, adjust their cost by the specified amount
# and enforce minimum cost if specified (None = no minimum)
RARITY_COST_ADJUSTMENTS = {
# From Diamond (1)
(1, 2): (-540, 100),
(1, 3): (-720, 50),
(1, 4): (-780, 15),
(1, 5): (-800, 5),
(1, 99): (1600, None),
# From Gold (2)
(2, 1): (540, None),
(2, 3): (-180, 50),
(2, 4): (-240, 15),
(2, 5): (-260, 5),
(2, 99): (2140, None),
# From Silver (3)
(3, 1): (720, None),
(3, 2): (180, None),
(3, 4): (-60, 15),
(3, 5): (-80, 5),
(3, 99): (2320, None),
# From Bronze (4)
(4, 1): (780, None),
(4, 2): (240, None),
(4, 3): (60, None),
(4, 5): (-20, 5),
(4, 99): (2380, None),
# From Common (5)
(5, 1): (800, None),
(5, 2): (260, None),
(5, 3): (80, None),
(5, 4): (20, None),
(5, 99): (2400, None),
# From Special/Legend (99)
(99, 1): (-1600, 800),
(99, 2): (-2140, 100),
(99, 3): (-2320, 50),
(99, 4): (-2380, 15),
(99, 5): (-2400, 5),
}
D20_CHANCES = { D20_CHANCES = {
'2': { '2': {
'chances': 1, 'chances': 1,
@ -585,6 +633,61 @@ def should_update_player_description(
return is_different and not is_potm return is_different and not is_potm
def calculate_rarity_cost_adjustment(old_rarity: int, new_rarity: int, old_cost: int) -> int:
"""
Calculate new cost when a player's rarity changes.
Uses the RARITY_COST_ADJUSTMENTS lookup table to determine the cost adjustment
and minimum cost when a player moves between rarity tiers.
Args:
old_rarity: Current rarity tier (1-5, 99)
new_rarity: New rarity tier (1-5, 99)
old_cost: Current player cost
Returns:
New cost after adjustment (with minimum enforced if applicable)
Examples:
>>> calculate_rarity_cost_adjustment(1, 2, 1000)
460 # Diamond to Gold: 1000 - 540 = 460, min 100 → 460
>>> calculate_rarity_cost_adjustment(1, 5, 100)
5 # Diamond to Common: 100 - 800 = -700, min 5 → 5
>>> calculate_rarity_cost_adjustment(5, 1, 50)
850 # Common to Diamond: 50 + 800 = 850, no min → 850
>>> calculate_rarity_cost_adjustment(3, 3, 100)
100 # No change: same rarity returns same cost
"""
# No change if rarity stays the same
if old_rarity == new_rarity:
return old_cost
# Look up the adjustment and minimum cost
adjustment_data = RARITY_COST_ADJUSTMENTS.get((old_rarity, new_rarity))
if adjustment_data is None:
# No defined adjustment for this transition - return old cost
logger.warning(
f"creation_helpers.calculate_rarity_cost_adjustment - No cost adjustment defined for "
f"rarity change {old_rarity}{new_rarity}. Keeping cost at {old_cost}."
)
return old_cost
cost_adjustment, min_cost = adjustment_data
# Calculate new cost
new_cost = old_cost + cost_adjustment
# Apply minimum cost if specified
if min_cost is not None:
new_cost = max(new_cost, min_cost)
return new_cost
async def pd_players_df(cardset_id: int): async def pd_players_df(cardset_id: int):
p_query = await db_get( p_query = await db_get(
'players', 'players',

View File

@ -5,7 +5,7 @@ import pandas as pd
from creation_helpers import ( from creation_helpers import (
get_all_pybaseball_ids, sanitize_name, CLUB_LIST, FRANCHISE_LIST, pd_players_df, get_all_pybaseball_ids, sanitize_name, CLUB_LIST, FRANCHISE_LIST, pd_players_df,
mlbteam_and_franchise, NEW_PLAYER_COST, RARITY_BASE_COSTS, mlbteam_and_franchise, NEW_PLAYER_COST, RARITY_BASE_COSTS,
should_update_player_description should_update_player_description, calculate_rarity_cost_adjustment
) )
from db_calls import db_post, db_get, db_put, db_patch from db_calls import db_post, db_get, db_put, db_patch
from defenders import calcs_defense as cde from defenders import calcs_defense as cde
@ -435,80 +435,13 @@ async def post_player_updates(
]) ])
elif df_data['rarity'] != df_data['new_rarity_id']: elif df_data['rarity'] != df_data['new_rarity_id']:
old_rarity = df_data['rarity'] # Calculate adjusted cost for rarity change using lookup table
new_rarity = df_data['new_rarity_id'] new_cost = calculate_rarity_cost_adjustment(
old_rarity=df_data['rarity'],
new_rarity=df_data['new_rarity_id'],
old_cost=df_data['cost'] old_cost=df_data['cost']
new_cost = 0 )
params.extend([('cost', new_cost), ('rarity_id', df_data['new_rarity_id'])])
if old_rarity == 1:
if new_rarity == 2:
new_cost = max(old_cost - 540, 100)
elif new_rarity == 3:
new_cost = max(old_cost - 720, 50)
elif new_rarity == 4:
new_cost = max(old_cost - 780, 15)
elif new_rarity == 5:
new_cost = max(old_cost - 800, 5)
elif new_rarity == 99:
new_cost = old_cost + 1600
elif old_rarity == 2:
if new_rarity == 1:
new_cost = old_cost + 540
elif new_rarity == 3:
new_cost = max(old_cost - 180, 50)
elif new_rarity == 4:
new_cost = max(old_cost - 240, 15)
elif new_rarity == 5:
new_cost = max(old_cost - 260, 5)
elif new_rarity == 99:
new_cost = old_cost + 2140
elif old_rarity == 3:
if new_rarity == 1:
new_cost = old_cost + 720
elif new_rarity == 2:
new_cost = old_cost + 180
elif new_rarity == 4:
new_cost = max(old_cost - 60, 15)
elif new_rarity == 5:
new_cost = max(old_cost - 80, 5)
elif new_rarity == 99:
new_cost = old_cost + 2320
elif old_rarity == 4:
if new_rarity == 1:
new_cost = old_cost + 780
elif new_rarity == 2:
new_cost = old_cost + 240
elif new_rarity == 3:
new_cost = old_cost + 60
elif new_rarity == 5:
new_cost = max(old_cost - 20, 5)
elif new_rarity == 99:
new_cost = old_cost + 2380
elif old_rarity == 5:
if new_rarity == 1:
new_cost = old_cost + 800
elif new_rarity == 2:
new_cost = old_cost + 260
elif new_rarity == 3:
new_cost = old_cost + 80
elif new_rarity == 4:
new_cost = old_cost + 20
elif new_rarity == 99:
new_cost = old_cost + 2400
elif old_rarity == 99:
if new_rarity == 1:
new_cost = max(old_cost - 1600, 800)
elif new_rarity == 2:
new_cost = max(old_cost - 2140, 100)
elif new_rarity == 3:
new_cost = max(old_cost - 2320, 50)
elif new_rarity == 4:
new_cost = max(old_cost - 2380, 15)
elif new_rarity == 5:
new_cost = max(old_cost - 2400, 5)
if new_cost != 0:
params.extend([('cost', new_cost), ('rarity_id', new_rarity)])
if len(params) > 0: if len(params) > 0:
if df_data.player_id not in player_updates.keys(): if df_data.player_id not in player_updates.keys():

View File

@ -0,0 +1,184 @@
"""
Tests for rarity cost adjustment logic.
This test verifies that calculate_rarity_cost_adjustment() correctly:
1. Adjusts costs when rarity changes
2. Enforces minimum costs where specified
3. Handles all rarity transitions (1-5, 99)
4. Returns original cost when rarity doesn't change
"""
import pytest
from creation_helpers import calculate_rarity_cost_adjustment
class TestRarityCostAdjustments:
"""Test suite for rarity cost adjustment calculations."""
def test_no_change_same_rarity(self):
"""Cost should not change if rarity stays the same."""
assert calculate_rarity_cost_adjustment(1, 1, 1000) == 1000
assert calculate_rarity_cost_adjustment(5, 5, 50) == 50
assert calculate_rarity_cost_adjustment(99, 99, 2400) == 2400
def test_diamond_to_gold(self):
"""Diamond (1) to Gold (2): -540, min 100."""
assert calculate_rarity_cost_adjustment(1, 2, 1000) == 460
assert calculate_rarity_cost_adjustment(1, 2, 500) == 100 # min enforced
assert calculate_rarity_cost_adjustment(1, 2, 50) == 100 # min enforced
def test_diamond_to_silver(self):
"""Diamond (1) to Silver (3): -720, min 50."""
assert calculate_rarity_cost_adjustment(1, 3, 1000) == 280
assert calculate_rarity_cost_adjustment(1, 3, 500) == 50 # min enforced
assert calculate_rarity_cost_adjustment(1, 3, 100) == 50 # min enforced
def test_diamond_to_bronze(self):
"""Diamond (1) to Bronze (4): -780, min 15."""
assert calculate_rarity_cost_adjustment(1, 4, 1000) == 220
assert calculate_rarity_cost_adjustment(1, 4, 500) == 15 # min enforced
assert calculate_rarity_cost_adjustment(1, 4, 50) == 15 # min enforced
def test_diamond_to_common(self):
"""Diamond (1) to Common (5): -800, min 5."""
assert calculate_rarity_cost_adjustment(1, 5, 1000) == 200
assert calculate_rarity_cost_adjustment(1, 5, 500) == 5 # min enforced
assert calculate_rarity_cost_adjustment(1, 5, 100) == 5 # min enforced
def test_diamond_to_special(self):
"""Diamond (1) to Special (99): +1600, no min."""
assert calculate_rarity_cost_adjustment(1, 99, 1000) == 2600
assert calculate_rarity_cost_adjustment(1, 99, 100) == 1700
def test_gold_to_diamond(self):
"""Gold (2) to Diamond (1): +540, no min."""
assert calculate_rarity_cost_adjustment(2, 1, 200) == 740
assert calculate_rarity_cost_adjustment(2, 1, 100) == 640
def test_gold_to_silver(self):
"""Gold (2) to Silver (3): -180, min 50."""
assert calculate_rarity_cost_adjustment(2, 3, 300) == 120
assert calculate_rarity_cost_adjustment(2, 3, 100) == 50 # min enforced
def test_silver_to_gold(self):
"""Silver (3) to Gold (2): +180, no min."""
assert calculate_rarity_cost_adjustment(3, 2, 100) == 280
assert calculate_rarity_cost_adjustment(3, 2, 50) == 230
def test_silver_to_bronze(self):
"""Silver (3) to Bronze (4): -60, min 15."""
assert calculate_rarity_cost_adjustment(3, 4, 100) == 40
assert calculate_rarity_cost_adjustment(3, 4, 50) == 15 # min enforced
def test_bronze_to_common(self):
"""Bronze (4) to Common (5): -20, min 5."""
assert calculate_rarity_cost_adjustment(4, 5, 50) == 30
assert calculate_rarity_cost_adjustment(4, 5, 20) == 5 # min enforced
def test_common_to_bronze(self):
"""Common (5) to Bronze (4): +20, no min."""
assert calculate_rarity_cost_adjustment(5, 4, 10) == 30
assert calculate_rarity_cost_adjustment(5, 4, 50) == 70
def test_common_to_diamond(self):
"""Common (5) to Diamond (1): +800, no min."""
assert calculate_rarity_cost_adjustment(5, 1, 10) == 810
assert calculate_rarity_cost_adjustment(5, 1, 50) == 850
def test_special_to_diamond(self):
"""Special (99) to Diamond (1): -1600, min 800."""
assert calculate_rarity_cost_adjustment(99, 1, 2400) == 800
assert calculate_rarity_cost_adjustment(99, 1, 2000) == 800 # min enforced
assert calculate_rarity_cost_adjustment(99, 1, 3000) == 1400
def test_special_to_gold(self):
"""Special (99) to Gold (2): -2140, min 100."""
assert calculate_rarity_cost_adjustment(99, 2, 2400) == 260
assert calculate_rarity_cost_adjustment(99, 2, 2000) == 100 # min enforced
def test_special_to_common(self):
"""Special (99) to Common (5): -2400, min 5."""
assert calculate_rarity_cost_adjustment(99, 5, 2400) == 5 # min enforced
assert calculate_rarity_cost_adjustment(99, 5, 3000) == 600
def test_all_upward_transitions(self):
"""Test all transitions that increase rarity (decrease number)."""
# Common (5) moving up
assert calculate_rarity_cost_adjustment(5, 4, 10) == 30 # +20
assert calculate_rarity_cost_adjustment(5, 3, 10) == 90 # +80
assert calculate_rarity_cost_adjustment(5, 2, 10) == 270 # +260
assert calculate_rarity_cost_adjustment(5, 1, 10) == 810 # +800
# Bronze (4) moving up
assert calculate_rarity_cost_adjustment(4, 3, 30) == 90 # +60
assert calculate_rarity_cost_adjustment(4, 2, 30) == 270 # +240
assert calculate_rarity_cost_adjustment(4, 1, 30) == 810 # +780
# Silver (3) moving up
assert calculate_rarity_cost_adjustment(3, 2, 90) == 270 # +180
assert calculate_rarity_cost_adjustment(3, 1, 90) == 810 # +720
# Gold (2) moving up
assert calculate_rarity_cost_adjustment(2, 1, 270) == 810 # +540
def test_all_downward_transitions_with_minimums(self):
"""Test all transitions that decrease rarity (increase number) with minimum enforcement."""
# Diamond (1) moving down - all have minimums
assert calculate_rarity_cost_adjustment(1, 2, 100) == 100 # would be -440, min 100
assert calculate_rarity_cost_adjustment(1, 3, 100) == 50 # would be -620, min 50
assert calculate_rarity_cost_adjustment(1, 4, 100) == 15 # would be -680, min 15
assert calculate_rarity_cost_adjustment(1, 5, 100) == 5 # would be -700, min 5
# Gold (2) moving down - all have minimums
assert calculate_rarity_cost_adjustment(2, 3, 100) == 50 # would be -80, min 50
assert calculate_rarity_cost_adjustment(2, 4, 100) == 15 # would be -140, min 15
assert calculate_rarity_cost_adjustment(2, 5, 100) == 5 # would be -160, min 5
# Silver (3) moving down - all have minimums
assert calculate_rarity_cost_adjustment(3, 4, 50) == 15 # would be -10, min 15
assert calculate_rarity_cost_adjustment(3, 5, 50) == 5 # would be -30, min 5
def test_edge_cases(self):
"""Test edge cases: zero cost, very high cost, etc."""
# Zero cost
assert calculate_rarity_cost_adjustment(5, 1, 0) == 800
assert calculate_rarity_cost_adjustment(1, 5, 0) == 5 # min enforced
# Very high cost
assert calculate_rarity_cost_adjustment(5, 1, 10000) == 10800
assert calculate_rarity_cost_adjustment(99, 1, 10000) == 8400
def test_symmetry(self):
"""Test that adjustments are symmetric (up then down returns close to original)."""
# Diamond to Common and back (won't be exact due to minimums, but should be logical)
original = 810
after_down = calculate_rarity_cost_adjustment(1, 5, original) # 810 - 800 = 10, min 5 → 5
after_up = calculate_rarity_cost_adjustment(5, 1, after_down) # 5 + 800 = 805
assert after_down == 10
assert after_up == 810
# Gold to Bronze and back
original = 270
after_down = calculate_rarity_cost_adjustment(2, 4, original) # 270 - 240 = 30
after_up = calculate_rarity_cost_adjustment(4, 2, after_down) # 30 + 240 = 270
assert after_down == 30
assert after_up == 270
class TestRarityCostAdjustmentEdgeCases:
"""Test edge cases and error handling."""
def test_undefined_transition(self):
"""Test that undefined transitions are handled gracefully."""
# There's no transition from 1 to 1 (same), but it's handled
result = calculate_rarity_cost_adjustment(1, 1, 500)
assert result == 500
# There's no transition from rarity 10 (doesn't exist) but should return old cost
result = calculate_rarity_cost_adjustment(10, 5, 500)
assert result == 500 # Falls back to old cost
def test_negative_costs(self):
"""Test behavior with negative costs (shouldn't happen, but test it)."""
# Negative costs should still get adjusted
result = calculate_rarity_cost_adjustment(5, 1, -100)
assert result == 700 # -100 + 800 = 700