From c9f5c7c929e384428f70cd7e2155bbb5978c5dfe Mon Sep 17 00:00:00 2001 From: josh Date: Sun, 29 Mar 2026 19:13:05 -0400 Subject: [PATCH] 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 --- app/games.py | 89 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/app/games.py b/app/games.py index 26236e0..6e7f835 100644 --- a/app/games.py +++ b/app/games.py @@ -30,6 +30,10 @@ def parse_games(scoreboard_data): extracted_info = [] for game in scoreboard_data.get("games", []): 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( { "Home Team": game["homeTeam"]["name"]["default"], @@ -52,9 +56,22 @@ def parse_games(scoreboard_data): "Intermission": game["clock"]["inIntermission"] if game_state == "LIVE" else "N/A", - "Priority": calculate_game_priority(game) - + get_comeback_bonus(game) - + calculate_game_importance(game), + "Priority": total_priority, + "Hype Breakdown": { + "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), "Home Record": format_record(game["homeTeam"]["record"]) 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" -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"]: - return 0 + return _zero period = game.get("periodDescriptor", {}).get("number", 0) time_remaining = game.get("clock", {}).get("secondsRemaining", 0) @@ -188,17 +215,12 @@ def calculate_game_priority(game): # ── 1. Base priority by period ──────────────────────────────────────── 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) else: - # Regular season: P4=5-min OT, P5=shootout base_priority = {1: 150, 2: 200, 3: 350, 4: 600, 5: 700}.get(period, 150) # ── 2. Period length for time calculations ──────────────────────────── - if period >= 4: - period_length = 1200 if is_playoff else 300 - else: - period_length = 1200 + period_length = (1200 if is_playoff else 300) if period >= 4 else 1200 # ── 3. Standings-quality matchup adjustment ─────────────────────────── home_standings = get_team_standings(game["homeTeam"]["name"]["default"]) @@ -209,7 +231,6 @@ def calculate_game_priority(game): away_total = ( 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_adjustment = (home_total + away_total) * matchup_multiplier @@ -241,13 +262,10 @@ def calculate_game_priority(game): elif score_difference == 1: base_priority += 30 - # ── 6. Closeness bonus (replaces unconditional score_total) ────────── - # Rewards tight games regardless of total goals scored + # ── 6. Closeness bonus ─────────────────────────────────────────────── 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 @@ -282,9 +300,20 @@ def calculate_game_priority(game): # Pushes intermission games to the bottom, retains relative sort order 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): @@ -321,13 +350,14 @@ def get_team_standings(team_name): } -def calculate_game_importance(game): - # Playoff games already have elevated priorities; don't double-count +def _importance_components(game): + """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: - return 0 - # FINAL/OFF games must sort below LIVE and PRE games + return _zero if game["gameState"] in ("FINAL", "OFF"): - return 0 + return _zero home_st = get_team_standings(game["homeTeam"]["name"]["default"]) away_st = get_team_standings(game["awayTeam"]["name"]["default"]) @@ -368,7 +398,7 @@ def calculate_game_importance(game): rivalry_multiplier = 1.0 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( "importance components — season_weight: %.3f, playoff_relevance: %.2f, " @@ -379,7 +409,16 @@ def calculate_game_importance(game): 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):