feat: overhaul hype scoring algorithm
- Period base: playoff OT escalates indefinitely (P4=600, P5=750…), reg season P4=5-min OT (600), P5=shootout (700) - Time priority range increased (max ~300 vs old ~120), calibrated to period length so 5-min reg season OT reads correctly - Matchup multiplier inverted: higher period = less weight (any OT is exciting regardless of teams) - Replace unconditional score_total with closeness bonus: rewards tight games regardless of goal volume (5-4 == 1-0 at same diff) - Power play bonus: 30 (P1/P2) → 50/100/150 (P3 by time) → 200 (OT) - Comeback bonus: one-time pulse (+50/75/100 by period) when trailing team scores to within 2 goals; keyed on team names, clears after firing, skips intermission Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
149
app/games.py
149
app/games.py
@@ -9,6 +9,10 @@ EASTERN = ZoneInfo("America/New_York")
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Maps (home_team_name, away_team_name) -> (home_score, away_score)
|
||||||
|
# Used to detect when the trailing team just scored.
|
||||||
|
_score_cache: dict[tuple[str, str], tuple[int, int]] = {}
|
||||||
|
|
||||||
|
|
||||||
def format_record(record):
|
def format_record(record):
|
||||||
if record == "N/A":
|
if record == "N/A":
|
||||||
@@ -39,6 +43,7 @@ def parse_games(scoreboard_data):
|
|||||||
"Home Logo": game["homeTeam"]["logo"],
|
"Home Logo": game["homeTeam"]["logo"],
|
||||||
"Away Logo": game["awayTeam"]["logo"],
|
"Away Logo": game["awayTeam"]["logo"],
|
||||||
"Game State": game_state,
|
"Game State": game_state,
|
||||||
|
"Game Type": game.get("gameType", 2),
|
||||||
"Period": get_period(game),
|
"Period": get_period(game),
|
||||||
"Time Remaining": get_time_remaining(game),
|
"Time Remaining": get_time_remaining(game),
|
||||||
"Time Running": game["clock"]["running"]
|
"Time Running": game["clock"]["running"]
|
||||||
@@ -47,7 +52,7 @@ def parse_games(scoreboard_data):
|
|||||||
"Intermission": game["clock"]["inIntermission"]
|
"Intermission": game["clock"]["inIntermission"]
|
||||||
if game_state == "LIVE"
|
if game_state == "LIVE"
|
||||||
else "N/A",
|
else "N/A",
|
||||||
"Priority": calculate_game_priority(game),
|
"Priority": calculate_game_priority(game) + get_comeback_bonus(game),
|
||||||
"Start Time": get_start_time(game),
|
"Start Time": get_start_time(game),
|
||||||
"Home Record": format_record(game["homeTeam"]["record"])
|
"Home Record": format_record(game["homeTeam"]["record"])
|
||||||
if game["gameState"] in ["PRE", "FUT"]
|
if game["gameState"] in ["PRE", "FUT"]
|
||||||
@@ -75,6 +80,52 @@ def parse_games(scoreboard_data):
|
|||||||
return sorted(extracted_info, key=lambda x: x["Priority"], reverse=True)
|
return sorted(extracted_info, key=lambda x: x["Priority"], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_comeback_bonus(game):
|
||||||
|
"""
|
||||||
|
Returns a one-time bonus when the trailing team just scored and the game
|
||||||
|
is still within reach (score_diff <= 2). Updates _score_cache.
|
||||||
|
Returns 0 for intermission, non-live games, or no cache entry yet.
|
||||||
|
"""
|
||||||
|
if game["gameState"] not in ("LIVE", "CRIT"):
|
||||||
|
return 0
|
||||||
|
if game["clock"]["inIntermission"]:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
home_name = game["homeTeam"]["name"]["default"]
|
||||||
|
away_name = game["awayTeam"]["name"]["default"]
|
||||||
|
key = (home_name, away_name)
|
||||||
|
|
||||||
|
home_score = game["homeTeam"]["score"]
|
||||||
|
away_score = game["awayTeam"]["score"]
|
||||||
|
period = game.get("periodDescriptor", {}).get("number", 0)
|
||||||
|
|
||||||
|
bonus = 0
|
||||||
|
if key in _score_cache:
|
||||||
|
prev_home, prev_away = _score_cache[key]
|
||||||
|
prev_diff = abs(prev_home - prev_away)
|
||||||
|
new_diff = abs(home_score - away_score)
|
||||||
|
|
||||||
|
trailing_scored = (
|
||||||
|
new_diff < prev_diff
|
||||||
|
and new_diff <= 2
|
||||||
|
and (
|
||||||
|
(prev_home < prev_away and home_score > prev_home)
|
||||||
|
or (prev_away < prev_home and away_score > prev_away)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if trailing_scored:
|
||||||
|
if period >= 4:
|
||||||
|
bonus = 100
|
||||||
|
elif period == 3:
|
||||||
|
bonus = 75
|
||||||
|
else:
|
||||||
|
bonus = 50
|
||||||
|
|
||||||
|
_score_cache[key] = (home_score, away_score)
|
||||||
|
return bonus
|
||||||
|
|
||||||
|
|
||||||
def convert_game_state(game_state):
|
def convert_game_state(game_state):
|
||||||
state_mapping = {"OFF": "FINAL", "CRIT": "LIVE", "FUT": "PRE"}
|
state_mapping = {"OFF": "FINAL", "CRIT": "LIVE", "FUT": "PRE"}
|
||||||
return state_mapping.get(game_state, game_state)
|
return state_mapping.get(game_state, game_state)
|
||||||
@@ -123,55 +174,59 @@ def get_game_outcome(game, game_state):
|
|||||||
|
|
||||||
|
|
||||||
def calculate_game_priority(game):
|
def calculate_game_priority(game):
|
||||||
# Return 0 if game is in certain states
|
|
||||||
if game["gameState"] in ["FINAL", "OFF", "PRE", "FUT"]:
|
if game["gameState"] in ["FINAL", "OFF", "PRE", "FUT"]:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Get period, time remaining, scores, and other relevant data
|
|
||||||
period = game.get("periodDescriptor", {}).get("number", 0)
|
period = game.get("periodDescriptor", {}).get("number", 0)
|
||||||
time_remaining = game.get("clock", {}).get("secondsRemaining", 0)
|
time_remaining = game.get("clock", {}).get("secondsRemaining", 0)
|
||||||
home_score = game["homeTeam"]["score"]
|
home_score = game["homeTeam"]["score"]
|
||||||
away_score = game["awayTeam"]["score"]
|
away_score = game["awayTeam"]["score"]
|
||||||
score_difference = abs(home_score - away_score)
|
score_difference = abs(home_score - away_score)
|
||||||
score_total = (home_score + away_score) * 20
|
is_playoff = game.get("gameType", 2) == 3
|
||||||
|
|
||||||
# Get standings for home and away teams
|
# ── 1. Base priority by period ────────────────────────────────────────
|
||||||
home_team_standings = get_team_standings(game["homeTeam"]["name"]["default"])
|
if is_playoff:
|
||||||
away_team_standings = get_team_standings(game["awayTeam"]["name"]["default"])
|
# Playoffs: P4+ are full 20-min OTs that escalate indefinitely
|
||||||
|
base_priority = {1: 150, 2: 200, 3: 350}.get(period, 600 + (period - 4) * 150)
|
||||||
|
else:
|
||||||
|
# Regular season: P4=5-min OT, P5=shootout
|
||||||
|
base_priority = {1: 150, 2: 200, 3: 350, 4: 600, 5: 700}.get(period, 150)
|
||||||
|
|
||||||
# Calculate total values of leagueSequence + leagueL10Sequence for each team
|
# ── 2. Period length for time calculations ────────────────────────────
|
||||||
|
if period >= 4:
|
||||||
|
period_length = 1200 if is_playoff else 300
|
||||||
|
else:
|
||||||
|
period_length = 1200
|
||||||
|
|
||||||
|
# ── 3. Standings-quality matchup adjustment ───────────────────────────
|
||||||
|
home_standings = get_team_standings(game["homeTeam"]["name"]["default"])
|
||||||
|
away_standings = get_team_standings(game["awayTeam"]["name"]["default"])
|
||||||
home_total = (
|
home_total = (
|
||||||
home_team_standings["league_sequence"]
|
home_standings["league_sequence"] + home_standings["league_l10_sequence"]
|
||||||
+ home_team_standings["league_l10_sequence"]
|
|
||||||
)
|
)
|
||||||
away_total = (
|
away_total = (
|
||||||
away_team_standings["league_sequence"]
|
away_standings["league_sequence"] + away_standings["league_l10_sequence"]
|
||||||
+ away_team_standings["league_l10_sequence"]
|
|
||||||
)
|
)
|
||||||
|
# Higher period = matchup matters less (any OT is exciting regardless of teams)
|
||||||
# Calculate the matchup adjustment factor
|
matchup_multiplier = {1: 2.0, 2: 1.65, 3: 1.50, 4: 1.0}.get(period, 1.0)
|
||||||
matchup_multiplier = {5: 1, 4: 1, 3: 1.50, 2: 1.65, 1: 2}.get(period)
|
|
||||||
matchup_adjustment = (home_total + away_total) * matchup_multiplier
|
matchup_adjustment = (home_total + away_total) * matchup_multiplier
|
||||||
|
|
||||||
# Calculate the base priority based on period
|
# ── 4. Score-differential penalty ────────────────────────────────────
|
||||||
base_priority = {5: 650, 4: 600, 3: 300, 2: 200}.get(period, 150)
|
if score_difference > 3:
|
||||||
|
score_differential_adjustment = 500
|
||||||
# Adjust base priority based on score difference
|
elif score_difference > 2:
|
||||||
|
score_differential_adjustment = 350
|
||||||
|
elif score_difference > 1:
|
||||||
|
score_differential_adjustment = 100
|
||||||
|
else:
|
||||||
score_differential_adjustment = 0
|
score_differential_adjustment = 0
|
||||||
|
|
||||||
if score_difference > 3:
|
|
||||||
score_differential_adjustment += 500
|
|
||||||
elif score_difference > 2:
|
|
||||||
score_differential_adjustment += 350
|
|
||||||
elif score_difference > 1:
|
|
||||||
score_differential_adjustment += 100
|
|
||||||
|
|
||||||
if period == 3 and time_remaining <= 300:
|
if period == 3 and time_remaining <= 300:
|
||||||
score_differential_adjustment = score_differential_adjustment * 2
|
score_differential_adjustment *= 2
|
||||||
|
|
||||||
base_priority -= score_differential_adjustment
|
base_priority -= score_differential_adjustment
|
||||||
|
|
||||||
# Adjust base priority based on certain conditions
|
# ── 5. Late-3rd urgency bonus ─────────────────────────────────────────
|
||||||
if period == 3 and time_remaining <= 720:
|
if period == 3 and time_remaining <= 720:
|
||||||
if score_difference == 0:
|
if score_difference == 0:
|
||||||
base_priority += 100
|
base_priority += 100
|
||||||
@@ -184,24 +239,46 @@ def calculate_game_priority(game):
|
|||||||
elif score_difference == 1:
|
elif score_difference == 1:
|
||||||
base_priority += 30
|
base_priority += 30
|
||||||
|
|
||||||
# Calculate time priority
|
# ── 6. Closeness bonus (replaces unconditional score_total) ──────────
|
||||||
time_multiplier = {4: 2, 3: 2, 2: 1.5}.get(period, 0.75)
|
# Rewards tight games regardless of total goals scored
|
||||||
time_priority = ((1200 - time_remaining) / 20) * time_multiplier
|
closeness_bonus = {0: 80, 1: 50, 2: 20}.get(score_difference, 0)
|
||||||
|
|
||||||
|
# ── 7. Time priority ─────────────────────────────────────────────────
|
||||||
|
# Calibrated to period length so deep-into-period signal is meaningful
|
||||||
|
# for both 5-min reg season OT and 20-min playoff OT
|
||||||
|
time_multiplier = {4: 5, 3: 5, 2: 3}.get(period, 2.0 if period >= 5 else 1.5)
|
||||||
|
time_priority = ((period_length - time_remaining) / 20) * time_multiplier
|
||||||
|
|
||||||
|
# ── 8. Power play bonus ───────────────────────────────────────────────
|
||||||
|
pp_bonus = 0
|
||||||
|
situation = game.get("situation", {})
|
||||||
|
if "PP" in situation.get("situationDescriptions", []):
|
||||||
|
if period >= 4:
|
||||||
|
pp_bonus = 200
|
||||||
|
elif period == 3 and time_remaining <= 300:
|
||||||
|
pp_bonus = 150
|
||||||
|
elif period == 3 and time_remaining <= 720:
|
||||||
|
pp_bonus = 100
|
||||||
|
elif period == 3:
|
||||||
|
pp_bonus = 50
|
||||||
|
else:
|
||||||
|
pp_bonus = 30
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"priority components — base: %s, time: %s, matchup: %s, score_total: %s",
|
"priority components — base: %s, time: %.0f, matchup: %.0f, "
|
||||||
|
"closeness: %s, pp: %s",
|
||||||
base_priority,
|
base_priority,
|
||||||
time_priority,
|
time_priority,
|
||||||
matchup_adjustment,
|
matchup_adjustment,
|
||||||
score_total,
|
closeness_bonus,
|
||||||
|
pp_bonus,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate the final priority
|
|
||||||
final_priority = int(
|
final_priority = int(
|
||||||
base_priority + time_priority - matchup_adjustment + score_total
|
base_priority + time_priority - matchup_adjustment + closeness_bonus + pp_bonus
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pushes the games that are in intermission to the bottom, but retains their sort
|
# Pushes intermission games to the bottom, retains relative sort order
|
||||||
if game["clock"]["inIntermission"]:
|
if game["clock"]["inIntermission"]:
|
||||||
return -2000 - time_remaining
|
return -2000 - time_remaining
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ def make_game(
|
|||||||
start_time_utc="2024-04-10T23:00:00Z",
|
start_time_utc="2024-04-10T23:00:00Z",
|
||||||
home_record="40-25-10",
|
home_record="40-25-10",
|
||||||
away_record="38-27-09",
|
away_record="38-27-09",
|
||||||
|
game_type=2,
|
||||||
|
situation=None,
|
||||||
):
|
):
|
||||||
clock = {
|
clock = {
|
||||||
"timeRemaining": f"{seconds_remaining // 60:02d}:{seconds_remaining % 60:02d}",
|
"timeRemaining": f"{seconds_remaining // 60:02d}:{seconds_remaining % 60:02d}",
|
||||||
@@ -43,6 +45,8 @@ def make_game(
|
|||||||
"record": away_record,
|
"record": away_record,
|
||||||
},
|
},
|
||||||
"gameOutcome": {"lastPeriodType": "REG"},
|
"gameOutcome": {"lastPeriodType": "REG"},
|
||||||
|
"gameType": game_type,
|
||||||
|
**({"situation": situation} if situation is not None else {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import app.games
|
||||||
from tests.conftest import make_game
|
from tests.conftest import make_game
|
||||||
from app.games import (
|
from app.games import (
|
||||||
calculate_game_priority,
|
calculate_game_priority,
|
||||||
convert_game_state,
|
convert_game_state,
|
||||||
format_record,
|
format_record,
|
||||||
|
get_comeback_bonus,
|
||||||
get_game_outcome,
|
get_game_outcome,
|
||||||
get_period,
|
get_period,
|
||||||
get_power_play_info,
|
get_power_play_info,
|
||||||
@@ -224,3 +226,190 @@ class TestCalculateGamePriority:
|
|||||||
)
|
)
|
||||||
result = calculate_game_priority(game)
|
result = calculate_game_priority(game)
|
||||||
assert isinstance(result, int)
|
assert isinstance(result, int)
|
||||||
|
|
||||||
|
def test_playoff_ot_escalates_per_period(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||||
|
)
|
||||||
|
p4 = make_game(game_state="LIVE", period=4, seconds_remaining=600, game_type=3)
|
||||||
|
p5 = make_game(game_state="LIVE", period=5, seconds_remaining=600, game_type=3)
|
||||||
|
assert calculate_game_priority(p5) > calculate_game_priority(p4)
|
||||||
|
|
||||||
|
def test_regular_season_shootout_p5_above_p4(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||||
|
)
|
||||||
|
p4 = make_game(game_state="LIVE", period=4, seconds_remaining=150, game_type=2)
|
||||||
|
p5 = make_game(game_state="LIVE", period=5, seconds_remaining=0, game_type=2)
|
||||||
|
assert calculate_game_priority(p5) > calculate_game_priority(p4)
|
||||||
|
|
||||||
|
def test_playoff_p4_higher_than_regular_season_p4(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||||
|
)
|
||||||
|
reg = make_game(game_state="LIVE", period=4, seconds_remaining=150, game_type=2)
|
||||||
|
playoff = make_game(
|
||||||
|
game_state="LIVE", period=4, seconds_remaining=600, game_type=3
|
||||||
|
)
|
||||||
|
assert calculate_game_priority(playoff) > calculate_game_priority(reg)
|
||||||
|
|
||||||
|
def test_closeness_bonus_tied_beats_one_goal(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||||
|
)
|
||||||
|
tied = self._live_game(home_score=2, away_score=2)
|
||||||
|
one_goal = self._live_game(home_score=2, away_score=1)
|
||||||
|
assert calculate_game_priority(tied) > calculate_game_priority(one_goal)
|
||||||
|
|
||||||
|
def test_5_4_same_priority_as_1_0(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||||
|
)
|
||||||
|
high_scoring = self._live_game(home_score=5, away_score=4)
|
||||||
|
low_scoring = self._live_game(home_score=1, away_score=0)
|
||||||
|
assert calculate_game_priority(high_scoring) == calculate_game_priority(
|
||||||
|
low_scoring
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_pp_in_ot_adds_200(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||||
|
)
|
||||||
|
base = make_game(game_state="LIVE", period=4, seconds_remaining=600)
|
||||||
|
with_pp = make_game(
|
||||||
|
game_state="LIVE",
|
||||||
|
period=4,
|
||||||
|
seconds_remaining=600,
|
||||||
|
situation={"situationDescriptions": ["PP"], "timeRemaining": "1:30"},
|
||||||
|
)
|
||||||
|
assert calculate_game_priority(with_pp) - calculate_game_priority(base) == 200
|
||||||
|
|
||||||
|
def test_pp_late_p3_adds_150(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||||
|
)
|
||||||
|
base = self._live_game(period=3, seconds_remaining=240)
|
||||||
|
with_pp = make_game(
|
||||||
|
game_state="LIVE",
|
||||||
|
period=3,
|
||||||
|
seconds_remaining=240,
|
||||||
|
situation={"situationDescriptions": ["PP"], "timeRemaining": "1:30"},
|
||||||
|
)
|
||||||
|
assert calculate_game_priority(with_pp) - calculate_game_priority(base) == 150
|
||||||
|
|
||||||
|
def test_pp_mid_p3_adds_100(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||||
|
)
|
||||||
|
base = self._live_game(period=3, seconds_remaining=600)
|
||||||
|
with_pp = make_game(
|
||||||
|
game_state="LIVE",
|
||||||
|
period=3,
|
||||||
|
seconds_remaining=600,
|
||||||
|
situation={"situationDescriptions": ["PP"], "timeRemaining": "1:30"},
|
||||||
|
)
|
||||||
|
assert calculate_game_priority(with_pp) - calculate_game_priority(base) == 100
|
||||||
|
|
||||||
|
def test_pp_early_p3_adds_50(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||||
|
)
|
||||||
|
base = self._live_game(period=3, seconds_remaining=900)
|
||||||
|
with_pp = make_game(
|
||||||
|
game_state="LIVE",
|
||||||
|
period=3,
|
||||||
|
seconds_remaining=900,
|
||||||
|
situation={"situationDescriptions": ["PP"], "timeRemaining": "1:30"},
|
||||||
|
)
|
||||||
|
assert calculate_game_priority(with_pp) - calculate_game_priority(base) == 50
|
||||||
|
|
||||||
|
def test_pp_p1_adds_30(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||||
|
)
|
||||||
|
base = self._live_game(period=1, seconds_remaining=600)
|
||||||
|
with_pp = make_game(
|
||||||
|
game_state="LIVE",
|
||||||
|
period=1,
|
||||||
|
seconds_remaining=600,
|
||||||
|
situation={"situationDescriptions": ["PP"], "timeRemaining": "1:30"},
|
||||||
|
)
|
||||||
|
assert calculate_game_priority(with_pp) - calculate_game_priority(base) == 30
|
||||||
|
|
||||||
|
def test_time_priority_increases_as_clock_runs(self, mocker):
|
||||||
|
mocker.patch(
|
||||||
|
"app.games.get_team_standings",
|
||||||
|
return_value={"league_sequence": 0, "league_l10_sequence": 0},
|
||||||
|
)
|
||||||
|
early = self._live_game(period=3, seconds_remaining=1100)
|
||||||
|
late = self._live_game(period=3, seconds_remaining=200)
|
||||||
|
assert calculate_game_priority(late) > calculate_game_priority(early)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetComebackBonus:
|
||||||
|
def setup_method(self):
|
||||||
|
app.games._score_cache.clear()
|
||||||
|
|
||||||
|
def test_returns_zero_on_first_call(self):
|
||||||
|
game = make_game(home_score=2, away_score=1)
|
||||||
|
assert get_comeback_bonus(game) == 0
|
||||||
|
|
||||||
|
def test_cache_populated_after_first_call(self):
|
||||||
|
game = make_game(home_score=2, away_score=1)
|
||||||
|
get_comeback_bonus(game)
|
||||||
|
assert ("Maple Leafs", "Bruins") in app.games._score_cache
|
||||||
|
|
||||||
|
def test_no_bonus_when_leading_team_scores(self):
|
||||||
|
app.games._score_cache[("Maple Leafs", "Bruins")] = (2, 1)
|
||||||
|
game = make_game(home_score=3, away_score=1) # leading home scored again
|
||||||
|
assert get_comeback_bonus(game) == 0
|
||||||
|
|
||||||
|
def test_bonus_when_trailing_home_scores_p3(self):
|
||||||
|
app.games._score_cache[("Maple Leafs", "Bruins")] = (1, 3)
|
||||||
|
game = make_game(home_score=2, away_score=3, period=3)
|
||||||
|
assert get_comeback_bonus(game) == 75
|
||||||
|
|
||||||
|
def test_bonus_when_trailing_away_scores_p2(self):
|
||||||
|
app.games._score_cache[("Maple Leafs", "Bruins")] = (3, 1)
|
||||||
|
game = make_game(home_score=3, away_score=2, period=2)
|
||||||
|
assert get_comeback_bonus(game) == 50
|
||||||
|
|
||||||
|
def test_no_bonus_when_diff_still_above_2(self):
|
||||||
|
# 5-1 → 5-2: diff 4→3, still > 2, no bonus
|
||||||
|
app.games._score_cache[("Maple Leafs", "Bruins")] = (1, 5)
|
||||||
|
game = make_game(home_score=2, away_score=5)
|
||||||
|
assert get_comeback_bonus(game) == 0
|
||||||
|
|
||||||
|
def test_bonus_fires_once_then_clears(self):
|
||||||
|
app.games._score_cache[("Maple Leafs", "Bruins")] = (1, 3)
|
||||||
|
game = make_game(home_score=2, away_score=3, period=3)
|
||||||
|
assert get_comeback_bonus(game) == 75
|
||||||
|
# Same score on next call — cache now (2, 3), no change
|
||||||
|
assert get_comeback_bonus(game) == 0
|
||||||
|
|
||||||
|
def test_no_bonus_in_intermission(self):
|
||||||
|
app.games._score_cache[("Maple Leafs", "Bruins")] = (1, 3)
|
||||||
|
game = make_game(home_score=2, away_score=3, in_intermission=True)
|
||||||
|
assert get_comeback_bonus(game) == 0
|
||||||
|
# Cache should NOT be updated
|
||||||
|
assert app.games._score_cache[("Maple Leafs", "Bruins")] == (1, 3)
|
||||||
|
|
||||||
|
def test_no_bonus_for_non_live_state(self):
|
||||||
|
app.games._score_cache[("Maple Leafs", "Bruins")] = (1, 3)
|
||||||
|
game = make_game(game_state="OFF", home_score=2, away_score=3)
|
||||||
|
assert get_comeback_bonus(game) == 0
|
||||||
|
|
||||||
|
def test_ot_comeback_bonus_is_100(self):
|
||||||
|
app.games._score_cache[("Maple Leafs", "Bruins")] = (1, 3)
|
||||||
|
game = make_game(home_score=2, away_score=3, period=4)
|
||||||
|
assert get_comeback_bonus(game) == 100
|
||||||
|
|||||||
Reference in New Issue
Block a user