Files
NHL-Scoreboard/tests/test_games.py
T
josh 930247b32f
CI / Lint (push) Successful in 8s
CI / Test (push) Successful in 10s
CI / Build & Push (push) Successful in 22s
style: apply ruff format and fix lint issues in playoff modules
- Rename single-letter `l` loop variables in bracket_view to satisfy E741
- Drop unused `json` import from test_playoff_cache (F401/F811)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 12:48:42 -04:00

992 lines
36 KiB
Python

import app.games
from tests.conftest import make_game, make_playoff_game
from app.games import (
_get_man_advantage,
calculate_game_importance,
calculate_game_priority,
convert_game_state,
format_record,
get_comeback_bonus,
get_game_outcome,
get_period,
get_power_play_info,
get_start_time,
get_time_remaining,
parse_games,
utc_to_eastern,
)
class TestConvertGameState:
def test_off_maps_to_final(self):
assert convert_game_state("OFF") == "FINAL"
def test_crit_maps_to_live(self):
assert convert_game_state("CRIT") == "LIVE"
def test_fut_maps_to_pre(self):
assert convert_game_state("FUT") == "PRE"
def test_unknown_state_passes_through(self):
assert convert_game_state("LIVE") == "LIVE"
class TestProcessRecord:
def test_na_returns_na(self):
assert format_record("N/A") == "N/A"
def test_pads_single_digit_parts(self):
assert format_record("5-3-1") == "05-03-01"
def test_already_padded_unchanged(self):
assert format_record("40-25-10") == "40-25-10"
class TestProcessPeriod:
def test_pre_game_returns_zero(self):
game = make_game(game_state="PRE")
assert get_period(game) == 0
def test_fut_game_returns_zero(self):
game = make_game(game_state="FUT")
assert get_period(game) == 0
def test_final_game_returns_na(self):
game = make_game(game_state="OFF")
assert get_period(game) == "N/A"
def test_live_game_returns_period_number(self):
game = make_game(game_state="LIVE", period=2)
assert get_period(game) == 2
class TestProcessTimeRemaining:
def test_pre_game_returns_2000(self):
game = make_game(game_state="FUT")
assert get_time_remaining(game) == "20:00"
def test_final_game_returns_0000(self):
game = make_game(game_state="OFF")
assert get_time_remaining(game) == "00:00"
def test_live_game_returns_clock(self):
game = make_game(game_state="LIVE", seconds_remaining=305)
assert get_time_remaining(game) == "05:05"
def test_live_game_at_zero_returns_end(self):
game = make_game(game_state="LIVE", seconds_remaining=0)
assert get_time_remaining(game) == "END"
class TestProcessStartTime:
def test_pre_game_returns_est_time(self):
game = make_game(game_state="FUT", start_time_utc="2024-04-10T23:00:00Z")
result = get_start_time(game)
assert result == "7:00 PM"
def test_pre_game_strips_leading_zero(self):
game = make_game(game_state="FUT", start_time_utc="2024-04-10T22:00:00Z")
result = get_start_time(game)
assert not result.startswith("0")
def test_live_game_returns_na(self):
game = make_game(game_state="LIVE")
assert get_start_time(game) == "N/A"
class TestGetGameOutcome:
def test_final_game_returns_last_period_type(self):
game = make_game(game_state="OFF")
assert get_game_outcome(game, "FINAL") == "REG"
def test_live_game_returns_na(self):
game = make_game(game_state="LIVE")
assert get_game_outcome(game, "LIVE") == "N/A"
class TestUtcToEstTime:
def test_converts_utc_to_edt(self):
# April is EDT (UTC-4): 23:00 UTC → 07:00 PM EDT
result = utc_to_eastern("2024-04-10T23:00:00Z")
assert result == "07:00 PM"
def test_converts_utc_to_est(self):
# January is EST (UTC-5): 23:00 UTC → 06:00 PM EST
result = utc_to_eastern("2024-01-15T23:00:00Z")
assert result == "06:00 PM"
class TestParseGames:
def test_returns_empty_list_for_none(self):
assert parse_games(None) == []
def test_returns_empty_list_for_empty_dict(self):
assert parse_games({}) == []
def test_pre_games_sorted_by_start_time_ascending(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={
"league_sequence": 16,
"league_l10_sequence": 16,
"division_abbrev": "ATL",
"conference_abbrev": "E",
"games_played": 40,
"wildcard_sequence": 16,
},
)
late = make_game(
game_state="FUT",
home_name="Rangers",
away_name="Devils",
start_time_utc="2024-04-10T22:00:00Z",
)
early = make_game(
game_state="FUT",
home_name="Bruins",
away_name="Canadiens",
start_time_utc="2024-04-10T19:00:00Z",
)
next_day = make_game(
game_state="FUT",
home_name="Kings",
away_name="Ducks",
start_time_utc="2024-04-11T00:30:00Z",
)
result = parse_games({"games": [late, next_day, early]})
pre_games = [g for g in result if g["Game State"] == "PRE"]
names = [g["Home Team"] for g in pre_games]
assert names == ["Bruins", "Rangers", "Kings"]
def test_live_games_still_sorted_by_priority_descending(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={
"league_sequence": 16,
"league_l10_sequence": 16,
"division_abbrev": "ATL",
"conference_abbrev": "E",
"games_played": 40,
"wildcard_sequence": 16,
},
)
# Late P3 tied — high priority
high = make_game(
game_state="LIVE",
home_name="Rangers",
away_name="Devils",
home_score=2,
away_score=2,
period=3,
seconds_remaining=60,
)
# Early P1 blowout — low priority
low = make_game(
game_state="LIVE",
home_name="Bruins",
away_name="Canadiens",
home_score=5,
away_score=0,
period=1,
seconds_remaining=900,
)
result = parse_games({"games": [low, high]})
live_games = [g for g in result if g["Game State"] == "LIVE"]
assert live_games[0]["Home Team"] == "Rangers"
assert live_games[1]["Home Team"] == "Bruins"
def test_pre_games_ignore_priority_even_if_nonzero(self, mocker):
# Give one team standings that maximize importance, another that minimize it
def fake_standings(name):
if name == "Bruins":
return {
"league_sequence": 1,
"league_l10_sequence": 1,
"division_abbrev": "ATL",
"conference_abbrev": "E",
"games_played": 80,
"wildcard_sequence": 18,
}
return {
"league_sequence": 32,
"league_l10_sequence": 32,
"division_abbrev": "PAC",
"conference_abbrev": "W",
"games_played": 10,
"wildcard_sequence": 30,
}
mocker.patch("app.games.get_team_standings", side_effect=fake_standings)
# Bruins game starts later but will have higher importance
high_hype_late = make_game(
game_state="FUT",
home_name="Bruins",
away_name="Maple Leafs",
start_time_utc="2024-04-10T23:00:00Z",
)
low_hype_early = make_game(
game_state="FUT",
home_name="Kings",
away_name="Ducks",
start_time_utc="2024-04-10T19:00:00Z",
)
result = parse_games({"games": [high_hype_late, low_hype_early]})
pre_games = [g for g in result if g["Game State"] == "PRE"]
# Lower-hype-but-earlier game must still appear first
assert pre_games[0]["Home Team"] == "Kings"
assert pre_games[1]["Home Team"] == "Bruins"
assert pre_games[1]["Priority"] > pre_games[0]["Priority"]
class TestGetPowerPlayInfo:
def test_returns_empty_when_no_situation(self):
game = make_game()
assert get_power_play_info(game, "Maple Leafs") == ""
def test_returns_pp_info_for_away_team(self):
game = make_game(away_name="Bruins")
game["situation"] = {
"awayTeam": {"situationDescriptions": ["PP"]},
"homeTeam": {"situationDescriptions": ["SH"]},
"timeRemaining": "1:30",
}
assert get_power_play_info(game, "Bruins") == "PP 1:30"
def test_returns_pp_info_for_home_team(self):
game = make_game(home_name="Maple Leafs", away_name="Bruins")
game["situation"] = {
"homeTeam": {"situationDescriptions": ["PP"]},
"awayTeam": {"situationDescriptions": ["SH"]},
"timeRemaining": "0:45",
}
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) == 140
def test_en_mid_p3_adds_100(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) == 100
def test_en_ot_adds_180(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) == 180
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 = 90, EN late P3 = 140, total = 230
assert delta == 230
class TestMultiManAdvantage:
def test_5v3_ot_pp_bonus_is_180(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",
},
)
# OT PP 5-on-3: 120 * 1.5 = 180
assert calculate_game_priority(with_5v3) - calculate_game_priority(base) == 180
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",
},
)
# OT PP 5-on-4: 120 base, no advantage mult
assert calculate_game_priority(with_pp) - calculate_game_priority(base) == 120
class TestCalculateGamePriority:
def _live_game(
self,
period=3,
seconds_remaining=300,
home_score=2,
away_score=1,
in_intermission=False,
):
return make_game(
game_state="LIVE",
period=period,
seconds_remaining=seconds_remaining,
home_score=home_score,
away_score=away_score,
in_intermission=in_intermission,
)
def test_returns_zero_for_final(self):
game = make_game(game_state="OFF")
assert calculate_game_priority(game) == 0
def test_returns_zero_for_pre(self):
game = make_game(game_state="FUT")
assert calculate_game_priority(game) == 0
def test_intermission_returns_negative(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
game = self._live_game(in_intermission=True, seconds_remaining=0)
assert calculate_game_priority(game) < 0
def test_score_diff_greater_than_3(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
game = self._live_game(home_score=5, away_score=0)
result = calculate_game_priority(game)
assert isinstance(result, int)
def test_score_diff_greater_than_2(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
game = self._live_game(home_score=4, away_score=1)
result = calculate_game_priority(game)
assert isinstance(result, int)
def test_score_diff_greater_than_1(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
game = self._live_game(home_score=3, away_score=1)
result = calculate_game_priority(game)
assert isinstance(result, int)
def test_late_3rd_tied_bonus(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
game = self._live_game(
period=3, seconds_remaining=600, home_score=2, away_score=2
)
result = calculate_game_priority(game)
assert isinstance(result, int)
def test_final_6_minutes_tied_bonus(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
game = self._live_game(
period=3, seconds_remaining=300, home_score=2, away_score=2
)
result = calculate_game_priority(game)
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_shootout_ranks_below_late_ot(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
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(
"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_beats_1_0_via_high_scoring_bonus(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)
# Same 1-goal diff, but 9 total goals earns the high-scoring bonus
assert calculate_game_priority(high_scoring) > calculate_game_priority(
low_scoring
)
def test_pp_in_ot_adds_120(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",
},
)
assert calculate_game_priority(with_pp) - calculate_game_priority(base) == 120
def test_pp_late_p3_adds_90(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={
"homeTeam": {"situationDescriptions": ["PP"]},
"awayTeam": {"situationDescriptions": ["SH"]},
"timeRemaining": "1:30",
},
)
assert calculate_game_priority(with_pp) - calculate_game_priority(base) == 90
def test_pp_mid_p3_adds_60(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={
"homeTeam": {"situationDescriptions": ["PP"]},
"awayTeam": {"situationDescriptions": ["SH"]},
"timeRemaining": "1:30",
},
)
assert calculate_game_priority(with_pp) - calculate_game_priority(base) == 60
def test_pp_early_p3_adds_35(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={
"homeTeam": {"situationDescriptions": ["PP"]},
"awayTeam": {"situationDescriptions": ["SH"]},
"timeRemaining": "1:30",
},
)
assert calculate_game_priority(with_pp) - calculate_game_priority(base) == 35
def test_pp_p1_adds_20(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={
"homeTeam": {"situationDescriptions": ["PP"]},
"awayTeam": {"situationDescriptions": ["SH"]},
"timeRemaining": "1:30",
},
)
assert calculate_game_priority(with_pp) - calculate_game_priority(base) == 20
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()
app.games._comeback_tracker.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_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_two_goal_recovery_in_p3(self):
# Was 0-2, now 2-2: recovery=2, base=50, period_mult=1.0, tie=20
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) == 70 # 50*1.0 + 20
def test_three_goal_recovery_in_p3(self):
# Was 0-3, now 3-3: recovery=3, base=90, period_mult=1.0, tie=20
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) == 110 # 90*1.0 + 20
def test_partial_recovery_in_p3(self):
# Was 0-3, now 2-3: recovery=2, base=50, 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) == 50 # 50*1.0
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 == 70
def test_period_multiplier_p1_lower(self):
# P1 recovery is less dramatic: base=50, period_mult=0.6, tie=20
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) == 50 # int(50*0.6 + 20)
def test_ot_multiplier_higher(self):
# OT: base=50, period_mult=1.2, tie=20
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) == 80 # int(50*1.2 + 20)
def test_no_bonus_in_intermission(self):
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
def test_no_bonus_for_non_live_state(self):
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_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 == 70 # 50*1.0 + 20
class TestPlayoffEnrichment:
_FULL_STANDINGS = {
"league_sequence": 16,
"league_l10_sequence": 16,
"division_abbrev": "ATL",
"conference_abbrev": "E",
"games_played": 40,
"wildcard_sequence": 16,
}
def test_regular_season_game_has_empty_playoff_fields(self, mocker):
mocker.patch("app.games.get_team_standings", return_value=self._FULL_STANDINGS)
result = parse_games({"games": [make_game()]})
g = result[0]
assert g["Is Playoff"] is False
assert g["Pinned"] is False
assert g["Playoff OT"] is False
assert g["Series Blurb"] == ""
assert g["Series Badges"] == []
def test_playoff_game_gets_series_fields(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
game = make_playoff_game(top_wins=2, bottom_wins=1, round_num=1)
result = parse_games({"games": [game]})
g = result[0]
assert g["Is Playoff"] is True
assert g["Pinned"] is False
assert "Game 4" in g["Series Blurb"]
assert "R1" in g["Series Badges"]
def test_game7_is_pinned(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
game = make_playoff_game(top_wins=3, bottom_wins=3)
result = parse_games({"games": [game]})
assert result[0]["Pinned"] is True
assert "GAME 7" in result[0]["Series Badges"]
def test_pinned_game_sorts_first(self, mocker):
mocker.patch("app.games.get_team_standings", return_value=self._FULL_STANDINGS)
# Low-priority Game 7 PRE should sort above a high-priority non-playoff LIVE
g7_pre = make_playoff_game(
top_wins=3,
bottom_wins=3,
game_state="FUT",
period=0,
seconds_remaining=1200,
start_time_utc="2026-04-20T23:00:00Z",
home_name="Kings",
away_name="Oilers",
)
hype_live = make_game(
game_state="LIVE",
home_name="Rangers",
away_name="Devils",
home_score=2,
away_score=2,
period=3,
seconds_remaining=60,
)
result = parse_games({"games": [hype_live, g7_pre]})
assert result[0]["Home Team"] == "Kings"
assert result[0]["Pinned"] is True
def test_playoff_ot_flagged(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={"league_sequence": 0, "league_l10_sequence": 0},
)
game = make_playoff_game(
top_wins=1,
bottom_wins=1,
period=4,
seconds_remaining=600,
game_state="LIVE",
)
result = parse_games({"games": [game]})
assert result[0]["Playoff OT"] is True
assert result[0]["OT Label"] == "OT"
def test_regular_season_ot_not_flagged_as_playoff_ot(self, mocker):
mocker.patch("app.games.get_team_standings", return_value=self._FULL_STANDINGS)
game = make_game(
game_state="LIVE", period=4, seconds_remaining=180, game_type=2
)
result = parse_games({"games": [game]})
assert result[0]["Playoff OT"] is False
assert result[0]["OT Label"] == ""
class TestCalculateGameImportance:
def _standings(
self,
league_seq=10,
l10_seq=10,
div="ATL",
conf="E",
gp=65,
wc=18,
):
return {
"league_sequence": league_seq,
"league_l10_sequence": l10_seq,
"division_abbrev": div,
"conference_abbrev": conf,
"games_played": gp,
"wildcard_sequence": wc,
}
def test_playoff_game_gets_fallback_importance(self):
game = make_game(game_type=3)
assert calculate_game_importance(game) == 60
def test_playoff_game7_cup_final_is_max(self):
game = make_game(
game_type=3,
series_status={"round": 4, "topSeedWins": 3, "bottomSeedWins": 3},
)
# Game 7 Cup Final: series_factor 1.0 * round 1.5 * 100 = 150
assert calculate_game_importance(game) == 150
def test_playoff_elimination_round1(self):
game = make_game(
game_type=3,
series_status={"round": 1, "topSeedWins": 3, "bottomSeedWins": 2},
)
# Elimination (3-x): 0.90 * 1.0 * 100 = 90
assert calculate_game_importance(game) == 90
def test_playoff_game1_round1_lowest(self):
game = make_game(
game_type=3,
series_status={"round": 1, "topSeedWins": 0, "bottomSeedWins": 0},
)
# Series factor 0.45 * round 1.0 * 100 = 45
assert calculate_game_importance(game) == 45
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")
assert calculate_game_importance(game) == 0
def test_near_zero_early_in_season(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value=self._standings(gp=10, wc=18),
)
game = make_game(game_state="FUT")
assert calculate_game_importance(game) <= 10
def test_max_bonus_late_season_bubble_division_game(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value=self._standings(gp=82, wc=18, div="ATL", conf="E"),
)
game = make_game(game_state="FUT")
# season_weight 1.0 * stakes 1.0 * rivalry 1.4 * 70 = 98
assert calculate_game_importance(game) == 98
def test_same_division_beats_same_conference(self, mocker):
home_st = self._standings(gp=70, wc=18, div="ATL", conf="E")
away_st_same_div = self._standings(gp=70, wc=18, div="ATL", conf="E")
away_st_diff_div = self._standings(gp=70, wc=18, div="MET", conf="E")
mocker.patch(
"app.games.get_team_standings",
side_effect=[home_st, away_st_same_div],
)
result_div = calculate_game_importance(make_game(game_state="FUT"))
mocker.patch(
"app.games.get_team_standings",
side_effect=[home_st, away_st_diff_div],
)
result_conf = calculate_game_importance(make_game(game_state="FUT"))
assert result_div > result_conf
def test_same_conference_beats_different_conference(self, mocker):
home_st = self._standings(gp=70, wc=18, div="ATL", conf="E")
away_same_conf = self._standings(gp=70, wc=18, div="MET", conf="E")
away_diff_conf = self._standings(gp=70, wc=18, div="PAC", conf="W")
mocker.patch(
"app.games.get_team_standings",
side_effect=[home_st, away_same_conf],
)
result_same = calculate_game_importance(make_game(game_state="FUT"))
mocker.patch(
"app.games.get_team_standings",
side_effect=[home_st, away_diff_conf],
)
result_diff = calculate_game_importance(make_game(game_state="FUT"))
assert result_same > result_diff
def test_bubble_teams_beat_safely_in_teams(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value=self._standings(gp=70, wc=18),
)
result_bubble = calculate_game_importance(make_game(game_state="FUT"))
mocker.patch(
"app.games.get_team_standings",
return_value=self._standings(gp=70, wc=5),
)
result_safe = calculate_game_importance(make_game(game_state="FUT"))
assert result_bubble > result_safe
def test_eliminated_teams_have_lowest_relevance(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value=self._standings(gp=70, wc=30),
)
assert calculate_game_importance(make_game(game_state="FUT")) < 30
def test_result_is_non_negative_int(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value=self._standings(gp=0, wc=32),
)
result = calculate_game_importance(make_game(game_state="FUT"))
assert isinstance(result, int)
assert result >= 0
def test_result_never_exceeds_100(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value=self._standings(gp=82, wc=18, div="ATL", conf="E"),
)
assert calculate_game_importance(make_game(game_state="FUT")) <= 100