feat: expose hype score breakdown in /scoreboard response
Adds a "Hype Breakdown" dict to every game in the API response with the individual components that sum to Priority: base period score, time priority, matchup penalty, closeness bonus, power play bonus, comeback bonus, and importance sub-components (season weight, playoff relevance, rivalry multiplier). Achieved by extracting private _priority_components() and _importance_components() helpers; public function signatures and all tests unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
89
app/games.py
89
app/games.py
@@ -30,6 +30,10 @@ def parse_games(scoreboard_data):
|
|||||||
extracted_info = []
|
extracted_info = []
|
||||||
for game in scoreboard_data.get("games", []):
|
for game in scoreboard_data.get("games", []):
|
||||||
game_state = convert_game_state(game["gameState"])
|
game_state = convert_game_state(game["gameState"])
|
||||||
|
priority_comps = _priority_components(game)
|
||||||
|
comeback = get_comeback_bonus(game)
|
||||||
|
importance_comps = _importance_components(game)
|
||||||
|
total_priority = priority_comps["total"] + comeback + importance_comps["total"]
|
||||||
extracted_info.append(
|
extracted_info.append(
|
||||||
{
|
{
|
||||||
"Home Team": game["homeTeam"]["name"]["default"],
|
"Home Team": game["homeTeam"]["name"]["default"],
|
||||||
@@ -52,9 +56,22 @@ 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": total_priority,
|
||||||
+ get_comeback_bonus(game)
|
"Hype Breakdown": {
|
||||||
+ calculate_game_importance(game),
|
"base": priority_comps["base"],
|
||||||
|
"time": priority_comps["time"],
|
||||||
|
"matchup": priority_comps["matchup"],
|
||||||
|
"closeness": priority_comps["closeness"],
|
||||||
|
"power_play": priority_comps["power_play"],
|
||||||
|
"comeback": comeback,
|
||||||
|
"importance": importance_comps["total"],
|
||||||
|
"importance_season_weight": importance_comps["season_weight"],
|
||||||
|
"importance_playoff_relevance": importance_comps[
|
||||||
|
"playoff_relevance"
|
||||||
|
],
|
||||||
|
"importance_rivalry": importance_comps["rivalry"],
|
||||||
|
"total": total_priority,
|
||||||
|
},
|
||||||
"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"]
|
||||||
@@ -175,9 +192,19 @@ def get_game_outcome(game, game_state):
|
|||||||
return game["gameOutcome"]["lastPeriodType"] if game_state == "FINAL" else "N/A"
|
return game["gameOutcome"]["lastPeriodType"] if game_state == "FINAL" else "N/A"
|
||||||
|
|
||||||
|
|
||||||
def calculate_game_priority(game):
|
def _priority_components(game):
|
||||||
|
"""Return a dict of all priority components plus the final total."""
|
||||||
|
_zero = {
|
||||||
|
"base": 0,
|
||||||
|
"time": 0,
|
||||||
|
"matchup": 0,
|
||||||
|
"closeness": 0,
|
||||||
|
"power_play": 0,
|
||||||
|
"total": 0,
|
||||||
|
}
|
||||||
|
|
||||||
if game["gameState"] in ["FINAL", "OFF", "PRE", "FUT"]:
|
if game["gameState"] in ["FINAL", "OFF", "PRE", "FUT"]:
|
||||||
return 0
|
return _zero
|
||||||
|
|
||||||
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)
|
||||||
@@ -188,17 +215,12 @@ def calculate_game_priority(game):
|
|||||||
|
|
||||||
# ── 1. Base priority by period ────────────────────────────────────────
|
# ── 1. Base priority by period ────────────────────────────────────────
|
||||||
if is_playoff:
|
if is_playoff:
|
||||||
# Playoffs: P4+ are full 20-min OTs that escalate indefinitely
|
|
||||||
base_priority = {1: 150, 2: 200, 3: 350}.get(period, 600 + (period - 4) * 150)
|
base_priority = {1: 150, 2: 200, 3: 350}.get(period, 600 + (period - 4) * 150)
|
||||||
else:
|
else:
|
||||||
# Regular season: P4=5-min OT, P5=shootout
|
|
||||||
base_priority = {1: 150, 2: 200, 3: 350, 4: 600, 5: 700}.get(period, 150)
|
base_priority = {1: 150, 2: 200, 3: 350, 4: 600, 5: 700}.get(period, 150)
|
||||||
|
|
||||||
# ── 2. Period length for time calculations ────────────────────────────
|
# ── 2. Period length for time calculations ────────────────────────────
|
||||||
if period >= 4:
|
period_length = (1200 if is_playoff else 300) if period >= 4 else 1200
|
||||||
period_length = 1200 if is_playoff else 300
|
|
||||||
else:
|
|
||||||
period_length = 1200
|
|
||||||
|
|
||||||
# ── 3. Standings-quality matchup adjustment ───────────────────────────
|
# ── 3. Standings-quality matchup adjustment ───────────────────────────
|
||||||
home_standings = get_team_standings(game["homeTeam"]["name"]["default"])
|
home_standings = get_team_standings(game["homeTeam"]["name"]["default"])
|
||||||
@@ -209,7 +231,6 @@ def calculate_game_priority(game):
|
|||||||
away_total = (
|
away_total = (
|
||||||
away_standings["league_sequence"] + away_standings["league_l10_sequence"]
|
away_standings["league_sequence"] + away_standings["league_l10_sequence"]
|
||||||
)
|
)
|
||||||
# Higher period = matchup matters less (any OT is exciting regardless of teams)
|
|
||||||
matchup_multiplier = {1: 2.0, 2: 1.65, 3: 1.50, 4: 1.0}.get(period, 1.0)
|
matchup_multiplier = {1: 2.0, 2: 1.65, 3: 1.50, 4: 1.0}.get(period, 1.0)
|
||||||
matchup_adjustment = (home_total + away_total) * matchup_multiplier
|
matchup_adjustment = (home_total + away_total) * matchup_multiplier
|
||||||
|
|
||||||
@@ -241,13 +262,10 @@ def calculate_game_priority(game):
|
|||||||
elif score_difference == 1:
|
elif score_difference == 1:
|
||||||
base_priority += 30
|
base_priority += 30
|
||||||
|
|
||||||
# ── 6. Closeness bonus (replaces unconditional score_total) ──────────
|
# ── 6. Closeness bonus ───────────────────────────────────────────────
|
||||||
# Rewards tight games regardless of total goals scored
|
|
||||||
closeness_bonus = {0: 80, 1: 50, 2: 20}.get(score_difference, 0)
|
closeness_bonus = {0: 80, 1: 50, 2: 20}.get(score_difference, 0)
|
||||||
|
|
||||||
# ── 7. Time priority ─────────────────────────────────────────────────
|
# ── 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_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
|
time_priority = ((period_length - time_remaining) / 20) * time_multiplier
|
||||||
|
|
||||||
@@ -282,9 +300,20 @@ def calculate_game_priority(game):
|
|||||||
|
|
||||||
# Pushes intermission games to the bottom, retains relative sort order
|
# Pushes intermission games to the bottom, retains relative sort order
|
||||||
if game["clock"]["inIntermission"]:
|
if game["clock"]["inIntermission"]:
|
||||||
return -2000 - time_remaining
|
return {**_zero, "total": -2000 - time_remaining}
|
||||||
|
|
||||||
return final_priority
|
return {
|
||||||
|
"base": base_priority,
|
||||||
|
"time": int(time_priority),
|
||||||
|
"matchup": int(matchup_adjustment),
|
||||||
|
"closeness": closeness_bonus,
|
||||||
|
"power_play": pp_bonus,
|
||||||
|
"total": final_priority,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_game_priority(game):
|
||||||
|
return _priority_components(game)["total"]
|
||||||
|
|
||||||
|
|
||||||
def get_team_standings(team_name):
|
def get_team_standings(team_name):
|
||||||
@@ -321,13 +350,14 @@ def get_team_standings(team_name):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def calculate_game_importance(game):
|
def _importance_components(game):
|
||||||
# Playoff games already have elevated priorities; don't double-count
|
"""Return a dict of all importance components plus the final total."""
|
||||||
|
_zero = {"season_weight": 0.0, "playoff_relevance": 0.0, "rivalry": 1.0, "total": 0}
|
||||||
|
|
||||||
if game.get("gameType", 2) != 2:
|
if game.get("gameType", 2) != 2:
|
||||||
return 0
|
return _zero
|
||||||
# FINAL/OFF games must sort below LIVE and PRE games
|
|
||||||
if game["gameState"] in ("FINAL", "OFF"):
|
if game["gameState"] in ("FINAL", "OFF"):
|
||||||
return 0
|
return _zero
|
||||||
|
|
||||||
home_st = get_team_standings(game["homeTeam"]["name"]["default"])
|
home_st = get_team_standings(game["homeTeam"]["name"]["default"])
|
||||||
away_st = get_team_standings(game["awayTeam"]["name"]["default"])
|
away_st = get_team_standings(game["awayTeam"]["name"]["default"])
|
||||||
@@ -368,7 +398,7 @@ def calculate_game_importance(game):
|
|||||||
rivalry_multiplier = 1.0
|
rivalry_multiplier = 1.0
|
||||||
|
|
||||||
raw = season_weight * playoff_relevance * rivalry_multiplier
|
raw = season_weight * playoff_relevance * rivalry_multiplier
|
||||||
importance = int((raw / 1.4) * 150)
|
importance = max(0, min(int((raw / 1.4) * 150), 150))
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"importance components — season_weight: %.3f, playoff_relevance: %.2f, "
|
"importance components — season_weight: %.3f, playoff_relevance: %.2f, "
|
||||||
@@ -379,7 +409,16 @@ def calculate_game_importance(game):
|
|||||||
importance,
|
importance,
|
||||||
)
|
)
|
||||||
|
|
||||||
return max(0, min(importance, 150))
|
return {
|
||||||
|
"season_weight": round(season_weight, 3),
|
||||||
|
"playoff_relevance": playoff_relevance,
|
||||||
|
"rivalry": rivalry_multiplier,
|
||||||
|
"total": importance,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_game_importance(game):
|
||||||
|
return _importance_components(game)["total"]
|
||||||
|
|
||||||
|
|
||||||
def utc_to_eastern(utc_time):
|
def utc_to_eastern(utc_time):
|
||||||
|
|||||||
Reference in New Issue
Block a user