From 55fda6c742e0da4d6248ed3f1190eb75f993388d Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Wed, 30 Sep 2026 05:54:56 -0400 Subject: [PATCH 1/2] bug: season MVP only counts competitive matches Impact is measured against a player's own team, so in a Duel it is always 1.08 regardless of result, which was enough to win Season MVP from a single match. --- .../seasons/calculate_season_awards.sql | 3 ++ test/awards.spec.ts | 42 +++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/hasura/functions/seasons/calculate_season_awards.sql b/hasura/functions/seasons/calculate_season_awards.sql index f6488b5c8..7e2a1aac8 100644 --- a/hasura/functions/seasons/calculate_season_awards.sql +++ b/hasura/functions/seasons/calculate_season_awards.sql @@ -42,6 +42,9 @@ BEGIN INTO _mvp_steam_id FROM public.player_elo pe WHERE pe.season_id = _season_id + -- Impact is relative to teammates, so a Duel scores a flat 1.08 + -- regardless of result and would outrank every 5v5 regular. + AND pe."type" = 'Competitive' GROUP BY pe.steam_id HAVING COUNT(*) > 0 ORDER BY AVG(COALESCE(pe.impact, 1.0)) DESC, diff --git a/test/awards.spec.ts b/test/awards.spec.ts index 3da7cd7d9..567868971 100644 --- a/test/awards.spec.ts +++ b/test/awards.spec.ts @@ -771,7 +771,12 @@ describe("awards (SQL-driven)", () => { describe("season awards", () => { const seedSeasonElo = async ( seasonId: string, - rows: Array<{ steam: string; elo: number; impact: number }>, + rows: Array<{ + steam: string; + elo: number; + impact: number; + type?: string; + }>, ) => { for (const row of rows) { const { matchId } = await fx.bareMatch(); @@ -779,8 +784,15 @@ describe("awards (SQL-driven)", () => { `INSERT INTO player_elo (match_id, steam_id, season_id, type, current, change, impact, expected_score, actual_score, k_factor) - VALUES ($1, $2, $3, 'Competitive', $4, 0, $5, 0.5, 1.0, 32)`, - [matchId, row.steam, seasonId, row.elo, row.impact], + VALUES ($1, $2, $3, $6, $4, 0, $5, 0.5, 1.0, 32)`, + [ + matchId, + row.steam, + seasonId, + row.elo, + row.impact, + row.type ?? "Competitive", + ], ); } }; @@ -841,6 +853,30 @@ describe("awards (SQL-driven)", () => { expect(String(rows[3].player_steam_id)).toBe(String(players[2])); }); + it("picks the mvp from competitive matches only", async () => { + const [season] = await postgres.query>( + "INSERT INTO seasons (starts_at) VALUES (now()) RETURNING id", + ); + const players = await fx.players(2); + + await seedSeasonElo(season.id, [ + { steam: players[0], elo: 1200, impact: 1.05 }, + { steam: players[1], elo: 1100, impact: 1.08, type: "Duel" }, + { steam: players[1], elo: 1100, impact: 1.2, type: "Wingman" }, + ]); + + await postgres.query("SELECT calculate_season_awards($1)", [season.id]); + + const [mvp] = await postgres.query>( + `SELECT ar.player_steam_id + FROM award_recipients ar + JOIN awards a ON a.id = ar.award_id + WHERE ar.season_id = $1 AND a.system_key = 'season_mvp'`, + [season.id], + ); + expect(String(mvp.player_steam_id)).toBe(String(players[0])); + }); + it("counts season medals on the awards leaderboard", async () => { const [season] = await postgres.query>( "INSERT INTO seasons (starts_at) VALUES (now()) RETURNING id", From 53b75f7a7cb17bc4d697fb1613d151da9112b4cb Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Wed, 30 Sep 2026 06:02:30 -0400 Subject: [PATCH 2/2] bug: season MVP requires at least 5 competitive matches --- .../seasons/calculate_season_awards.sql | 2 +- test/awards.spec.ts | 109 +++++++++++++----- 2 files changed, 81 insertions(+), 30 deletions(-) diff --git a/hasura/functions/seasons/calculate_season_awards.sql b/hasura/functions/seasons/calculate_season_awards.sql index 7e2a1aac8..e719dc8df 100644 --- a/hasura/functions/seasons/calculate_season_awards.sql +++ b/hasura/functions/seasons/calculate_season_awards.sql @@ -46,7 +46,7 @@ BEGIN -- regardless of result and would outrank every 5v5 regular. AND pe."type" = 'Competitive' GROUP BY pe.steam_id - HAVING COUNT(*) > 0 + HAVING COUNT(*) >= 5 ORDER BY AVG(COALESCE(pe.impact, 1.0)) DESC, SUM(COALESCE(pe.impact, 1.0)) DESC, pe.steam_id ASC diff --git a/test/awards.spec.ts b/test/awards.spec.ts index 567868971..79089d030 100644 --- a/test/awards.spec.ts +++ b/test/awards.spec.ts @@ -776,27 +776,41 @@ describe("awards (SQL-driven)", () => { elo: number; impact: number; type?: string; + matches?: number; }>, ) => { for (const row of rows) { - const { matchId } = await fx.bareMatch(); - await postgres.query( - `INSERT INTO player_elo - (match_id, steam_id, season_id, type, current, change, impact, - expected_score, actual_score, k_factor) - VALUES ($1, $2, $3, $6, $4, 0, $5, 0.5, 1.0, 32)`, - [ - matchId, - row.steam, - seasonId, - row.elo, - row.impact, - row.type ?? "Competitive", - ], - ); + for (let i = 0; i < (row.matches ?? 1); i++) { + const { matchId } = await fx.bareMatch(); + await postgres.query( + `INSERT INTO player_elo + (match_id, steam_id, season_id, type, current, change, impact, + expected_score, actual_score, k_factor) + VALUES ($1, $2, $3, $6, $4, 0, $5, 0.5, 1.0, 32)`, + [ + matchId, + row.steam, + seasonId, + row.elo, + row.impact, + row.type ?? "Competitive", + ], + ); + } } }; + const seasonMvp = async (seasonId: string) => { + const [mvp] = await postgres.query>( + `SELECT ar.player_steam_id + FROM award_recipients ar + JOIN awards a ON a.id = ar.award_id + WHERE ar.season_id = $1 AND a.system_key = 'season_mvp'`, + [seasonId], + ); + return mvp ? String(mvp.player_steam_id) : null; + }; + it("ships the four season system awards", async () => { const rows = await postgres.query>( `SELECT system_key FROM awards @@ -822,7 +836,7 @@ describe("awards (SQL-driven)", () => { { steam: players[0], elo: 1400, impact: 1.0 }, { steam: players[1], elo: 1300, impact: 1.0 }, { steam: players[2], elo: 1200, impact: 1.0 }, - { steam: players[3], elo: 1100, impact: 1.9 }, + { steam: players[3], elo: 1100, impact: 1.9, matches: 5 }, ]); await postgres.query("SELECT calculate_season_awards($1)", [season.id]); @@ -860,21 +874,48 @@ describe("awards (SQL-driven)", () => { const players = await fx.players(2); await seedSeasonElo(season.id, [ - { steam: players[0], elo: 1200, impact: 1.05 }, - { steam: players[1], elo: 1100, impact: 1.08, type: "Duel" }, - { steam: players[1], elo: 1100, impact: 1.2, type: "Wingman" }, + { steam: players[0], elo: 1200, impact: 1.05, matches: 5 }, + { + steam: players[1], + elo: 1100, + impact: 1.08, + type: "Duel", + matches: 5, + }, + { + steam: players[1], + elo: 1100, + impact: 1.2, + type: "Wingman", + matches: 5, + }, ]); await postgres.query("SELECT calculate_season_awards($1)", [season.id]); - const [mvp] = await postgres.query>( - `SELECT ar.player_steam_id - FROM award_recipients ar - JOIN awards a ON a.id = ar.award_id - WHERE ar.season_id = $1 AND a.system_key = 'season_mvp'`, - [season.id], + expect(await seasonMvp(season.id)).toBe(String(players[0])); + }); + + it("requires five competitive matches for mvp", async () => { + const [season] = await postgres.query>( + "INSERT INTO seasons (starts_at) VALUES (now()) RETURNING id", ); - expect(String(mvp.player_steam_id)).toBe(String(players[0])); + const players = await fx.players(2); + + await seedSeasonElo(season.id, [ + { steam: players[0], elo: 1200, impact: 1.2, matches: 4 }, + { steam: players[1], elo: 1100, impact: 1.01, matches: 4 }, + ]); + + await postgres.query("SELECT calculate_season_awards($1)", [season.id]); + expect(await seasonMvp(season.id)).toBeNull(); + + await seedSeasonElo(season.id, [ + { steam: players[1], elo: 1100, impact: 1.01 }, + ]); + + await postgres.query("SELECT calculate_season_awards($1)", [season.id]); + expect(await seasonMvp(season.id)).toBe(String(players[1])); }); it("counts season medals on the awards leaderboard", async () => { @@ -884,7 +925,7 @@ describe("awards (SQL-driven)", () => { const players = await fx.players(3); await seedSeasonElo(season.id, [ - { steam: players[0], elo: 1400, impact: 1.0 }, + { steam: players[0], elo: 1400, impact: 1.0, matches: 5 }, { steam: players[1], elo: 1300, impact: 1.0 }, { steam: players[2], elo: 1200, impact: 1.0 }, ]); @@ -946,7 +987,12 @@ describe("awards (SQL-driven)", () => { const players = await fx.players(3); await seedSeasonElo( season.id, - players.map((steam, i) => ({ steam, elo: 1400 - i * 100, impact: 1 })), + players.map((steam, i) => ({ + steam, + elo: 1400 - i * 100, + impact: 1, + matches: 5, + })), ); const handId = await createAward("Season Community Pick"); @@ -979,7 +1025,12 @@ describe("awards (SQL-driven)", () => { const players = await fx.players(3); await seedSeasonElo( season.id, - players.map((steam, i) => ({ steam, elo: 1400 - i * 100, impact: 1 })), + players.map((steam, i) => ({ + steam, + elo: 1400 - i * 100, + impact: 1, + matches: 5, + })), ); const calculated = async () =>