Fix tournament team page score ordering edge cases

This commit is contained in:
Kalle
2026-08-26 18:51:11 +03:00
parent 4b71c7734b
commit 09122de16b
4 changed files with 164 additions and 21 deletions

View File

@@ -66,4 +66,23 @@ describe("findByTournamentTeamId", () => {
finalMatch.id,
]);
});
test("resolves which side of the match the team is on", async () => {
const tournament = await TournamentFactory.createPlayed(
{ authorId: users.id(1), minMembersPerTeam: 1 },
{ teamRosters: [[users.id(1)], [users.id(2)]] },
);
const match = tournament.matches[0];
const [winnerSet] = await TournamentMatchRepository.findByTournamentTeamId(
match.winnerTeamId,
);
const [loserSet] = await TournamentMatchRepository.findByTournamentTeamId(
match.loserTeamId,
);
expect(winnerSet.teamSide).toBe(winnerSet.winnerSide);
expect(loserSet.teamSide).not.toBe(loserSet.winnerSide);
expect(winnerSet.teamSide).not.toBe(loserSet.teamSide);
});
});

View File

@@ -431,6 +431,10 @@ export function findByTournamentTeamId(tournamentTeamId: number) {
)
.select(({ eb }) => [
"TournamentMatch.id as tournamentMatchId",
"TournamentMatch.winnerSide",
sql<Side>`iif(${opponentOneId} = ${tournamentTeamId}, 'opponent1', 'opponent2')`.as(
"teamSide",
),
opponentOneScore.as("opponentOneScore"),
opponentTwoScore.as("opponentTwoScore"),
"otherTeam.name as otherTeamName",

View File

@@ -1,5 +1,10 @@
import { describe, expect, test } from "vitest";
import { winCounts } from "./sets.server";
import type { FindByTournamentTeamIdItem } from "~/features/tournament-match/TournamentMatchRepository.server";
import {
type AllRoundsItem,
tournamentTeamSets,
winCounts,
} from "./sets.server";
describe("winCounts", () => {
test("returns 0% (not NaN) when there are no played sets", () => {
@@ -8,4 +13,124 @@ describe("winCounts", () => {
expect(result.sets.percentage).toBe(0);
expect(result.maps.percentage).toBe(0);
});
test("counts a set the team won on the bracket but lost on maps as a win", () => {
// e.g. a set the opponent forfeited after winning games, awarded 2-1 by the
// organizer — the bracket, and so the set score, says the team won it
const result = winCounts([
{
tournamentMatchId: 1,
score: [2, 1],
result: "win",
round: { type: "winners", round: 1 },
stageName: "Main bracket",
maps: [
{ stageId: 1, modeShort: "SZ", result: "loss", source: "BOTH" },
{ stageId: 2, modeShort: "TC", result: "loss", source: "BOTH" },
{ stageId: 3, modeShort: "RM", result: "win", source: "BOTH" },
],
opponent: { id: 2, name: "Opponent", roster: [] },
},
]);
expect(result.sets.won).toBe(1);
});
test("counts a set that ended early with the maps split as a win", () => {
const result = winCounts([
{
tournamentMatchId: 1,
score: [1, 1],
result: "win",
round: { type: "winners", round: 1 },
stageName: "Main bracket",
maps: [
{ stageId: 1, modeShort: "SZ", result: "win", source: "BOTH" },
{ stageId: 2, modeShort: "TC", result: "loss", source: "BOTH" },
],
opponent: { id: 2, name: "Opponent", roster: [] },
},
]);
expect(result.sets.won).toBe(1);
});
test("counts a set the team lost on the bracket but won on maps as a loss", () => {
const result = winCounts([
{
tournamentMatchId: 1,
score: [2, 1],
result: "loss",
round: { type: "winners", round: 1 },
stageName: "Main bracket",
maps: [
{ stageId: 1, modeShort: "SZ", result: "win", source: "BOTH" },
{ stageId: 2, modeShort: "TC", result: "loss", source: "BOTH" },
{ stageId: 3, modeShort: "RM", result: "win", source: "BOTH" },
],
opponent: { id: 2, name: "Opponent", roster: [] },
},
]);
expect(result.sets.won).toBe(0);
expect(result.maps.won).toBe(2);
});
});
const ALL_ROUNDS: AllRoundsItem[] = [
{
stageId: 1,
stageName: "Main bracket",
stageType: "single_elimination",
roundNumber: 1,
groupNumber: 1,
},
];
/** A played Bo3 the team being viewed lost 1-2 on the maps. */
function playedSetRow(
overrides: Partial<FindByTournamentTeamIdItem>,
): FindByTournamentTeamIdItem {
return {
tournamentMatchId: 1,
winnerSide: "opponent1",
teamSide: "opponent1",
opponentOneScore: 1,
opponentTwoScore: 2,
otherTeamId: 2,
otherTeamName: "Opponent",
roundNumber: 1,
stageId: 1,
groupNumber: 1,
matches: [
{ mode: "SZ", stageId: 1, source: "BOTH", wasWinner: 1 },
{ mode: "TC", stageId: 2, source: "BOTH", wasWinner: 0 },
{ mode: "RM", stageId: 3, source: "BOTH", wasWinner: 0 },
],
players: [],
...overrides,
};
}
describe("tournamentTeamSets", () => {
test("orders the score by the slot the team being viewed is in", () => {
const [set] = tournamentTeamSets({
sets: [playedSetRow({ teamSide: "opponent2" })],
allRounds: ALL_ROUNDS,
});
expect(set.score).toEqual([2, 1]);
});
test("takes the set result from the bracket winner even when the maps disagree", () => {
// organizer overrode the winner after the games were reported, so the team
// won the set on the bracket while losing 1-2 on the maps
const [set] = tournamentTeamSets({
sets: [playedSetRow({ teamSide: "opponent1", winnerSide: "opponent1" })],
allRounds: ALL_ROUNDS,
});
expect(set.result).toBe("win");
expect(set.score).toEqual([1, 2]);
});
});

View File

@@ -16,6 +16,12 @@ export interface AllRoundsItem {
export interface PlayedSet {
tournamentMatchId: number;
score: [teamBeingViewed: number, opponent: number];
/**
* Who won the set according to the bracket. Can disagree with the maps and
* the score e.g. when an organizer overrode the winner after games were
* already reported.
*/
result: "win" | "loss";
round: {
type: "winners" | "losers" | "single_elim" | "round_robin" | "swiss";
round: number | "finals" | "grand_finals" | "bracket_reset";
@@ -63,7 +69,7 @@ export function winCounts(sets: PlayedSet[]) {
}
totalSets++;
if (mapsWonThisSet > totalMapsThisSet / 2) {
if (set.result === "win") {
setsWon++;
}
@@ -140,7 +146,8 @@ export function tournamentTeamSets({
result: match.wasWinner ? "win" : "loss",
source: parseMaplistSource(match.source),
})),
score: flipScoreIfNeeded(set),
result: set.winnerSide === set.teamSide ? "win" : "loss",
score: scoreFromTeamPerspective(set),
opponent: {
id: set.otherTeamId,
name: set.otherTeamName,
@@ -150,24 +157,12 @@ export function tournamentTeamSets({
});
}
function flipScoreIfNeeded(set: FindByTournamentTeamIdItem): [number, number] {
const score: [number, number] = [
set.opponentOneScore ?? 0,
set.opponentTwoScore ?? 0,
];
const wonTheSet =
set.matches.reduce((acc, cur) => cur.wasWinner + acc, 0) >
set.matches.length / 2;
if (
(wonTheSet && score[0] < score[1]) ||
(!wonTheSet && score[0] > score[1])
) {
return [score[1], score[0]];
}
return score;
function scoreFromTeamPerspective(
set: FindByTournamentTeamIdItem,
): [number, number] {
return set.teamSide === "opponent1"
? [set.opponentOneScore ?? 0, set.opponentTwoScore ?? 0]
: [set.opponentTwoScore ?? 0, set.opponentOneScore ?? 0];
}
function resolveRoundType({