diff --git a/app/games.py b/app/games.py index ec5524f..1d5ba0c 100644 --- a/app/games.py +++ b/app/games.py @@ -10,9 +10,11 @@ EASTERN = ZoneInfo("America/New_York") 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]] = {} +# Maps (home_team_name, away_team_name) -> max score differential seen +_comeback_tracker: dict[tuple[str, str], int] = {} + def format_record(record): if record == "N/A": @@ -63,6 +65,7 @@ def parse_games(scoreboard_data): "matchup_bonus": priority_comps["matchup_bonus"], "closeness": priority_comps["closeness"], "power_play": priority_comps["power_play"], + "empty_net": priority_comps["empty_net"], "comeback": comeback, "importance": importance_comps["total"], "importance_season_weight": importance_comps["season_weight"], @@ -100,10 +103,11 @@ def parse_games(scoreboard_data): 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. + """Persistent comeback bonus that scales with deficit recovered. + + Tracks the maximum score differential seen in the game. A recovery of 2+ + goals earns a sustained bonus that persists as long as the game remains + close. One-goal swings are normal hockey and earn no bonus. """ if game["gameState"] not in ("LIVE", "CRIT"): return 0 @@ -116,33 +120,25 @@ def get_comeback_bonus(game): home_score = game["homeTeam"]["score"] away_score = game["awayTeam"]["score"] + current_diff = abs(home_score - away_score) period = game.get("periodDescriptor", {}).get("number", 0) - bonus = 0 + tracker_max = _comeback_tracker.get(key, 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 - + prev_diff = abs(_score_cache[key][0] - _score_cache[key][1]) + tracker_max = max(tracker_max, prev_diff) + _comeback_tracker[key] = tracker_max _score_cache[key] = (home_score, away_score) - return bonus + + recovery = tracker_max - current_diff + if recovery < 2 or tracker_max < 2: + return 0 + + base = {2: 60, 3: 120}.get(recovery, 160) + period_mult = {1: 0.6, 2: 0.8, 3: 1.0}.get(period, 1.2) + tie_bonus = 30 if current_diff == 0 else 0 + + return int(base * period_mult + tie_bonus) def convert_game_state(game_state): @@ -196,6 +192,17 @@ def get_game_outcome(game, game_state): return game["gameOutcome"]["lastPeriodType"] if game_state == "FINAL" else "N/A" +def _get_man_advantage(situation): + """Parse situationCode for player count difference. + Format: [away_goalie][away_skaters][home_skaters][home_goalie].""" + code = situation.get("situationCode", "") + if len(code) != 4 or not code.isdigit(): + return 1 + away_total = int(code[0]) + int(code[1]) + home_total = int(code[2]) + int(code[3]) + return abs(home_total - away_total) + + def _priority_components(game): """Return a dict of all priority components plus the final total.""" _zero = { @@ -204,6 +211,7 @@ def _priority_components(game): "matchup_bonus": 0, "closeness": 0, "power_play": 0, + "empty_net": 0, "total": 0, } @@ -238,21 +246,48 @@ def _priority_components(game): 33 - 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: 1.5, 2: 1.5, 3: 1.25, 4: 1.0}.get(period, 1.0) matchup_bonus = (home_quality + away_quality) * matchup_multiplier - # ── 4. Score-differential penalty ──────────────────────────────────── - if score_difference > 3: - score_differential_adjustment = 500 - elif score_difference > 2: - score_differential_adjustment = 350 - elif score_difference > 1: - score_differential_adjustment = 100 - else: - score_differential_adjustment = 0 + # ── Shootout: flat priority, no time component (rounds, not clock) ─── + if period == 5 and not is_playoff: + so_base = 550 + so_closeness = 80 + so_matchup = (home_quality + away_quality) * 1.0 + so_total = int(so_base + so_closeness + so_matchup) + return { + "base": so_base, + "time": 0, + "matchup_bonus": int(so_matchup), + "closeness": so_closeness, + "power_play": 0, + "empty_net": 0, + "total": so_total, + } - if period == 3 and time_remaining <= 300: - score_differential_adjustment *= 2 + # ── 4. Score-differential penalty (period-aware) ─────────────────────── + score_differential_adjustment = 0 + if period <= 2: + adj = {0: 0, 1: 0, 2: 60, 3: 200, 4: 350} + score_differential_adjustment = adj.get( + score_difference, 350 + (score_difference - 4) * 100 + ) + elif period == 3: + mins_left = time_remaining / 60 + if mins_left > 10: + adj = {0: 0, 1: 0, 2: 80, 3: 250, 4: 400} + elif mins_left > 5: + adj = {0: 0, 1: 0, 2: 120, 3: 350, 4: 500} + elif mins_left > 2: + # Goalie-pull zone: 2-goal penalty DECREASES + adj = {0: 0, 1: 0, 2: 80, 3: 450, 4: 600} + else: + # Final 2 min: 2-goal deficit with active goalie pull is exciting + adj = {0: 0, 1: 0, 2: 60, 3: 550, 4: 700} + score_differential_adjustment = adj.get( + score_difference, adj[4] + (score_difference - 4) * 100 + ) + # OT: always tied, no penalty needed base_priority -= score_differential_adjustment @@ -272,9 +307,14 @@ def _priority_components(game): # ── 6. Closeness bonus ─────────────────────────────────────────────── closeness_bonus = {0: 80, 1: 50, 2: 20}.get(score_difference, 0) - # ── 7. Time priority ───────────────────────────────────────────────── + # ── 7. Time priority (non-linear — final minutes weighted more) ───── 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 + elapsed_fraction = ( + max(0.0, (period_length - time_remaining) / period_length) + if period_length + else 0 + ) + time_priority = (elapsed_fraction**1.5) * (period_length / 20) * time_multiplier # ── 8. Power play bonus ─────────────────────────────────────────────── pp_bonus = 0 @@ -282,29 +322,49 @@ def _priority_components(game): home_descs = situation.get("homeTeam", {}).get("situationDescriptions", []) away_descs = situation.get("awayTeam", {}).get("situationDescriptions", []) if "PP" in home_descs or "PP" in away_descs: + man_advantage = _get_man_advantage(situation) + advantage_mult = 1.0 if man_advantage <= 1 else 1.6 if period >= 4: - pp_bonus = 200 + pp_bonus = int(200 * advantage_mult) elif period == 3 and time_remaining <= 300: - pp_bonus = 150 + pp_bonus = int(150 * advantage_mult) elif period == 3 and time_remaining <= 720: - pp_bonus = 100 + pp_bonus = int(100 * advantage_mult) elif period == 3: - pp_bonus = 50 + pp_bonus = int(50 * advantage_mult) else: - pp_bonus = 30 + pp_bonus = int(30 * advantage_mult) + + # ── 9. Empty net bonus ─────────────────────────────────────────────── + en_bonus = 0 + if "EN" in home_descs or "EN" in away_descs: + if period >= 4: + en_bonus = 250 + elif period == 3 and time_remaining <= 180: + en_bonus = 200 + elif period == 3 and time_remaining <= 360: + en_bonus = 150 + else: + en_bonus = 75 logger.debug( "priority components — base: %s, time: %.0f, matchup_bonus: %.0f, " - "closeness: %s, pp: %s", + "closeness: %s, pp: %s, en: %s", base_priority, time_priority, matchup_bonus, closeness_bonus, pp_bonus, + en_bonus, ) final_priority = int( - base_priority + time_priority + matchup_bonus + closeness_bonus + pp_bonus + base_priority + + time_priority + + matchup_bonus + + closeness_bonus + + pp_bonus + + en_bonus ) # Pushes intermission games to the bottom, retains relative sort order @@ -317,6 +377,7 @@ def _priority_components(game): "matchup_bonus": int(matchup_bonus), "closeness": closeness_bonus, "power_play": pp_bonus, + "empty_net": en_bonus, "total": final_priority, } @@ -359,14 +420,57 @@ def get_team_standings(team_name): } +def _playoff_importance(game): + """Importance for playoff games based on series context and round.""" + series = game.get("seriesStatus", {}) + if not series: + # No series data available — flat playoff bonus + return { + "season_weight": 1.0, + "playoff_relevance": 0.50, + "rivalry": 1.0, + "total": 100, + } + + round_num = series.get("round", 1) + top_wins = series.get("topSeedWins", 0) + bottom_wins = series.get("bottomSeedWins", 0) + max_wins = max(top_wins, bottom_wins) + min_wins = min(top_wins, bottom_wins) + + round_mult = {1: 1.0, 2: 1.15, 3: 1.30, 4: 1.50}.get(round_num, 1.0) + + if max_wins == 3 and min_wins == 3: + series_factor = 1.0 + elif max_wins == 3: + series_factor = 0.85 + elif max_wins == 2 and min_wins == 2: + series_factor = 0.70 + elif max_wins == 2: + series_factor = 0.55 + else: + series_factor = 0.40 + + importance = min(int(series_factor * round_mult * 200), 200) + + return { + "season_weight": round_mult, + "playoff_relevance": series_factor, + "rivalry": 1.0, + "total": importance, + } + + 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 _zero if game["gameState"] in ("FINAL", "OFF"): return _zero + if game.get("gameType", 2) == 3: + return _playoff_importance(game) + if game.get("gameType", 2) != 2: + return _zero home_st = get_team_standings(game["homeTeam"]["name"]["default"]) away_st = get_team_standings(game["awayTeam"]["name"]["default"]) diff --git a/app/static/script.js b/app/static/script.js index 90b66e9..2ce4b0c 100644 --- a/app/static/script.js +++ b/app/static/script.js @@ -157,10 +157,10 @@ function ppIndicator(game) { function updateGauges() { document.querySelectorAll('.gauge').forEach(el => { - const score = Math.min(700, Math.max(0, parseInt(el.dataset.score, 10))); - el.style.width = `${(score / 700) * 100}%`; - el.style.backgroundColor = score <= 300 ? '#4a90e2' - : score <= 550 ? '#f97316' + const score = Math.min(1000, Math.max(0, parseInt(el.dataset.score, 10))); + el.style.width = `${(score / 1000) * 100}%`; + el.style.backgroundColor = score <= 350 ? '#4a90e2' + : score <= 650 ? '#f97316' : '#ef4444'; }); } diff --git a/tests/conftest.py b/tests/conftest.py index b088677..5c1077d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,7 @@ def make_game( away_record="38-27-09", game_type=2, situation=None, + series_status=None, ): clock = { "timeRemaining": f"{seconds_remaining // 60:02d}:{seconds_remaining % 60:02d}", @@ -47,6 +48,7 @@ def make_game( "gameOutcome": {"lastPeriodType": "REG"}, "gameType": game_type, **({"situation": situation} if situation is not None else {}), + **({"seriesStatus": series_status} if series_status is not None else {}), } diff --git a/tests/test_games.py b/tests/test_games.py index aa98f10..097036a 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -1,6 +1,7 @@ import app.games from tests.conftest import make_game from app.games import ( + _get_man_advantage, calculate_game_importance, calculate_game_priority, convert_game_state, @@ -147,6 +148,146 @@ class TestGetPowerPlayInfo: assert get_power_play_info(game, "Maple Leafs") == "PP 0:45" +class TestGetManAdvantage: + def test_standard_5v4(self): + # 1451: away 1G+4S=5, home 5S+1G=6 → advantage=1 + assert _get_man_advantage({"situationCode": "1451"}) == 1 + + def test_5v3(self): + # 1351: away 1G+3S=4, home 5S+1G=6 → advantage=2 + assert _get_man_advantage({"situationCode": "1351"}) == 2 + + def test_4v3(self): + # 1341: away 1G+3S=4, home 4S+1G=5 → advantage=1 + assert _get_man_advantage({"situationCode": "1341"}) == 1 + + def test_even_strength(self): + # 1551: away 1G+5S=6, home 5S+1G=6 → advantage=0 + assert _get_man_advantage({"situationCode": "1551"}) == 0 + + def test_missing_code_defaults_to_1(self): + assert _get_man_advantage({}) == 1 + + def test_invalid_code_defaults_to_1(self): + assert _get_man_advantage({"situationCode": "abc"}) == 1 + + +class TestEmptyNetBonus: + def test_en_late_p3_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=3, seconds_remaining=90) + with_en = make_game( + game_state="LIVE", + period=3, + seconds_remaining=90, + situation={ + "homeTeam": {"situationDescriptions": ["EN"]}, + "awayTeam": {"situationDescriptions": []}, + "timeRemaining": "1:30", + }, + ) + assert calculate_game_priority(with_en) - calculate_game_priority(base) == 200 + + def test_en_mid_p3_adds_150(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=3, seconds_remaining=300) + with_en = make_game( + game_state="LIVE", + period=3, + seconds_remaining=300, + situation={ + "homeTeam": {"situationDescriptions": ["EN"]}, + "awayTeam": {"situationDescriptions": []}, + "timeRemaining": "5:00", + }, + ) + assert calculate_game_priority(with_en) - calculate_game_priority(base) == 150 + + def test_en_ot_adds_250(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_en = make_game( + game_state="LIVE", + period=4, + seconds_remaining=600, + situation={ + "homeTeam": {"situationDescriptions": ["EN"]}, + "awayTeam": {"situationDescriptions": []}, + "timeRemaining": "10:00", + }, + ) + assert calculate_game_priority(with_en) - calculate_game_priority(base) == 250 + + def test_en_stacks_with_pp(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=3, seconds_remaining=90) + with_both = make_game( + game_state="LIVE", + period=3, + seconds_remaining=90, + situation={ + "homeTeam": {"situationDescriptions": ["PP", "EN"]}, + "awayTeam": {"situationDescriptions": ["SH"]}, + "timeRemaining": "1:30", + }, + ) + delta = calculate_game_priority(with_both) - calculate_game_priority(base) + # PP late P3 = 150, EN late P3 = 200, total = 350 + assert delta == 350 + + +class TestMultiManAdvantage: + def test_5v3_ot_pp_bonus_is_320(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_5v3 = make_game( + game_state="LIVE", + period=4, + seconds_remaining=600, + situation={ + "homeTeam": {"situationDescriptions": ["PP"]}, + "awayTeam": {"situationDescriptions": ["SH"]}, + "timeRemaining": "1:30", + "situationCode": "1351", + }, + ) + assert calculate_game_priority(with_5v3) - calculate_game_priority(base) == 320 + + def test_standard_5v4_unchanged(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={ + "homeTeam": {"situationDescriptions": ["PP"]}, + "awayTeam": {"situationDescriptions": ["SH"]}, + "timeRemaining": "1:30", + "situationCode": "1451", + }, + ) + assert calculate_game_priority(with_pp) - calculate_game_priority(base) == 200 + + class TestCalculateGamePriority: def _live_game( self, @@ -239,14 +380,31 @@ class TestCalculateGamePriority: 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): + def test_shootout_ranks_below_late_ot(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) + ot = make_game(game_state="LIVE", period=4, seconds_remaining=150, game_type=2) + so = make_game(game_state="LIVE", period=5, seconds_remaining=0, game_type=2) + # Sudden-death OT is more exciting than a skills competition + assert calculate_game_priority(ot) > calculate_game_priority(so) + + def test_shootout_ranks_above_p2_blowout(self, mocker): + mocker.patch( + "app.games.get_team_standings", + return_value={"league_sequence": 0, "league_l10_sequence": 0}, + ) + so = make_game(game_state="LIVE", period=5, seconds_remaining=0, game_type=2) + blowout = make_game( + game_state="LIVE", + period=2, + seconds_remaining=600, + home_score=5, + away_score=1, + game_type=2, + ) + assert calculate_game_priority(so) > calculate_game_priority(blowout) def test_playoff_p4_higher_than_regular_season_p4(self, mocker): mocker.patch( @@ -382,6 +540,7 @@ class TestCalculateGamePriority: class TestGetComebackBonus: def setup_method(self): app.games._score_cache.clear() + app.games._comeback_tracker.clear() def test_returns_zero_on_first_call(self): game = make_game(home_score=2, away_score=1) @@ -392,50 +551,77 @@ class TestGetComebackBonus: 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 + def test_no_bonus_for_one_goal_swing(self): + # 1-goal swings are normal hockey, no bonus + app.games._score_cache[("Maple Leafs", "Bruins")] = (0, 1) + game = make_game(home_score=1, away_score=1, period=3) assert get_comeback_bonus(game) == 0 - def test_bonus_when_trailing_home_scores_p3(self): + def test_two_goal_recovery_in_p3(self): + # Was 0-2, now 2-2: recovery=2, base=60, period_mult=1.0, tie=30 + app.games._score_cache[("Maple Leafs", "Bruins")] = (0, 2) + app.games._comeback_tracker[("Maple Leafs", "Bruins")] = 2 + game = make_game(home_score=2, away_score=2, period=3) + assert get_comeback_bonus(game) == 90 # 60*1.0 + 30 + + def test_three_goal_recovery_in_p3(self): + # Was 0-3, now 3-3: recovery=3, base=120, period_mult=1.0, tie=30 + app.games._score_cache[("Maple Leafs", "Bruins")] = (2, 3) + app.games._comeback_tracker[("Maple Leafs", "Bruins")] = 3 + game = make_game(home_score=3, away_score=3, period=3) + assert get_comeback_bonus(game) == 150 # 120*1.0 + 30 + + def test_partial_recovery_in_p3(self): + # Was 0-3, now 2-3: recovery=2, base=60, period_mult=1.0, no tie app.games._score_cache[("Maple Leafs", "Bruins")] = (1, 3) + app.games._comeback_tracker[("Maple Leafs", "Bruins")] = 3 game = make_game(home_score=2, away_score=3, period=3) - assert get_comeback_bonus(game) == 75 + assert get_comeback_bonus(game) == 60 # 60*1.0 - 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_bonus_persists_across_polls(self): + # Set up a 2-goal recovery, then call again — bonus stays + app.games._score_cache[("Maple Leafs", "Bruins")] = (0, 2) + app.games._comeback_tracker[("Maple Leafs", "Bruins")] = 2 + game = make_game(home_score=2, away_score=2, period=3) + first = get_comeback_bonus(game) + second = get_comeback_bonus(game) + assert first == second == 90 - 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_period_multiplier_p1_lower(self): + # P1 recovery is less dramatic: base=60, period_mult=0.6, tie=30 + app.games._score_cache[("Maple Leafs", "Bruins")] = (0, 2) + app.games._comeback_tracker[("Maple Leafs", "Bruins")] = 2 + game = make_game(home_score=2, away_score=2, period=1) + assert get_comeback_bonus(game) == 66 # int(60*0.6 + 30) - 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_ot_multiplier_higher(self): + # OT: base=60, period_mult=1.2, tie=30 + app.games._score_cache[("Maple Leafs", "Bruins")] = (2, 2) + app.games._comeback_tracker[("Maple Leafs", "Bruins")] = 2 + game = make_game(home_score=2, away_score=2, period=4) + assert get_comeback_bonus(game) == 102 # int(60*1.2 + 30) 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) + app.games._score_cache[("Maple Leafs", "Bruins")] = (0, 2) + app.games._comeback_tracker[("Maple Leafs", "Bruins")] = 2 + game = make_game(home_score=2, away_score=2, 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) + app.games._score_cache[("Maple Leafs", "Bruins")] = (0, 3) + app.games._comeback_tracker[("Maple Leafs", "Bruins")] = 3 + game = make_game(game_state="OFF", home_score=3, 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 + def test_tracker_builds_max_deficit_over_time(self): + # Simulate progressive scoring: 0-1, 0-2, 1-2, 2-2 + key = ("Maple Leafs", "Bruins") + get_comeback_bonus(make_game(home_score=0, away_score=1, period=1)) + get_comeback_bonus(make_game(home_score=0, away_score=2, period=1)) + get_comeback_bonus(make_game(home_score=1, away_score=2, period=2)) + result = get_comeback_bonus(make_game(home_score=2, away_score=2, period=3)) + assert app.games._comeback_tracker[key] == 2 + assert result == 90 # 60*1.0 + 30 class TestCalculateGameImportance: @@ -457,9 +643,36 @@ class TestCalculateGameImportance: "wildcard_sequence": wc, } - def test_returns_zero_for_playoff_game(self): + def test_playoff_game_gets_fallback_importance(self): game = make_game(game_type=3) - assert calculate_game_importance(game) == 0 + assert calculate_game_importance(game) == 100 + + def test_playoff_game7_cup_final_is_max(self): + game = make_game( + game_type=3, + series_status={"round": 4, "topSeedWins": 3, "bottomSeedWins": 3}, + ) + assert calculate_game_importance(game) == 200 + + def test_playoff_elimination_round1(self): + game = make_game( + game_type=3, + series_status={"round": 1, "topSeedWins": 3, "bottomSeedWins": 2}, + ) + assert calculate_game_importance(game) == 170 + + def test_playoff_game1_round1_lowest(self): + game = make_game( + game_type=3, + series_status={"round": 1, "topSeedWins": 0, "bottomSeedWins": 0}, + ) + assert calculate_game_importance(game) == 80 + + def test_playoff_later_rounds_more_important(self): + series = {"topSeedWins": 2, "bottomSeedWins": 2} + r1 = make_game(game_type=3, series_status={**series, "round": 1}) + r3 = make_game(game_type=3, series_status={**series, "round": 3}) + assert calculate_game_importance(r3) > calculate_game_importance(r1) def test_returns_zero_for_final_game(self): game = make_game(game_state="OFF")