Fix tournament match API missing played map results mid-set

This commit is contained in:
Kalle
2026-10-11 17:35:28 +03:00
parent 832e7b9862
commit 8d5bbebc54
5 changed files with 121 additions and 32 deletions

View File

@@ -82,6 +82,8 @@ type StartBracketArgs = {
type PlayMatchesFilter = {
roundNumbers?: number[];
matchIds?: number[];
/** Stop after this many maps, leaving unfinished sets in progress. */
mapCount?: number;
};
/** Bracket idx(s) in the progression, or `"all"` = every bracket then finalize. */
@@ -317,7 +319,7 @@ export async function playMatches(
const played = playableMatches(tournament, filter);
for (const match of played) {
await setActiveRosters(tournamentId, match);
await playOutMatch(tournamentId, match);
await playOutMatch(tournamentId, match, filter.mapCount);
}
clearTournamentDataCache(tournamentId);
@@ -516,11 +518,15 @@ async function setActiveRosters(tournamentId: number, match: PlayedMatch) {
}
}
async function playOutMatch(tournamentId: number, match: PlayedMatch) {
async function playOutMatch(
tournamentId: number,
match: PlayedMatch,
mapCount = Number.POSITIVE_INFINITY,
) {
let position = 0;
let setOver = false;
while (!setOver) {
while (!setOver && position < mapCount) {
// rehydrated per map, the previous report having moved the score on
const tournament = await tournamentFromDB(tournamentId);
const matchRow = await findMatch(match.id);

View File

@@ -0,0 +1,71 @@
import { beforeEach, describe, expect, test } from "vitest";
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory";
import * as UserFactory from "~/db/seed/factories/UserFactory";
import { wrappedLoader } from "~/utils/Test";
import type { GetTournamentMatchResponse } from "../schema";
import { loader } from "./tournament-match.$id";
const users = UserFactory.pool();
const matchLoader = wrappedLoader<Response>({ loader });
const fetchMatch = async (matchId: number) => {
const response = await matchLoader({ params: { id: String(matchId) } });
return (await response.json()) as GetTournamentMatchResponse;
};
/** Four one-player teams in a single elimination bracket of SZ Bo3 sets, the first set played `mapCount` maps in. */
const tournamentWithFirstSetPlayed = async (mapCount?: number) => {
const tournament = await TournamentFactory.create({
authorId: users.id(1),
minMembersPerTeam: 1,
});
for (const userId of users.ids()) {
await TournamentTeamFactory.create(
{ tournamentId: tournament.id, memberUserIds: [userId] },
{ isCheckedIn: true },
);
}
const [firstMatch] = await TournamentFactory.startBracket(tournament.id);
const [played] = await TournamentFactory.playMatches(tournament.id, {
matchIds: [firstMatch.id],
mapCount,
});
return played;
};
describe("GET /api/tournament-match/:id", () => {
beforeEach(async () => {
await users.create(4);
});
test("lists the maps already played of a set in progress with their results", async () => {
const played = await tournamentWithFirstSetPlayed(1);
const { mapList } = await fetchMatch(played.id);
expect(mapList?.map((map) => map.winnerTeamId)).toEqual([
played.winnerTeamId,
null,
null,
]);
expect(mapList?.[0].participatedUserIds).toHaveLength(2);
expect(mapList?.[1].participatedUserIds).toBeNull();
});
test("lists only the played maps of a finished set", async () => {
const played = await tournamentWithFirstSetPlayed();
const { mapList } = await fetchMatch(played.id);
expect(mapList?.map((map) => map.winnerTeamId)).toEqual([
played.winnerTeamId,
played.winnerTeamId,
]);
});
});

View File

@@ -77,21 +77,21 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
return null;
}
if (match.winnerSide) {
return match.playedMapList.map((playedMap) => ({
map: {
mode: playedMap.mode,
stage: {
id: playedMap.stageId,
name: t(`game-misc:STAGE_${playedMap.stageId}`),
},
const playedMaps = match.playedMapList.map((playedMap) => ({
map: {
mode: playedMap.mode,
stage: {
id: playedMap.stageId,
name: t(`game-misc:STAGE_${playedMap.stageId}`),
},
participatedUserIds: playedMap.participants.map((p) => p.userId),
winnerTeamId: playedMap.winnerTeamId,
source: parseMaplistSource(playedMap.source),
ko: playedMap.ko !== null ? Boolean(playedMap.ko) : null,
}));
}
},
participatedUserIds: playedMap.participants.map((p) => p.userId),
winnerTeamId: playedMap.winnerTeamId,
source: parseMaplistSource(playedMap.source),
ko: playedMap.ko !== null ? Boolean(playedMap.ko) : null,
}));
if (match.winnerSide) return playedMaps;
const pickBanEvents = match.maps?.pickBan
? await TournamentRepository.findPickBanEventsByMatchId(match.id)
@@ -102,7 +102,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
opponentTwo.id,
]);
return resolveMapList({
const upcomingMaps = resolveMapList({
tournamentId: match.tournamentId,
matchId,
teams: [opponentOne.id, opponentTwo.id],
@@ -122,21 +122,26 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
return [];
})
: undefined,
}).map((mapListMap) => {
return {
map: {
mode: mapListMap.mode,
stage: {
id: mapListMap.stageId,
name: t(`game-misc:STAGE_${mapListMap.stageId}`),
})
.filter((mapListMap) => !mapListMap.bannedByTournamentTeamId)
.slice(playedMaps.length)
.map((mapListMap) => {
return {
map: {
mode: mapListMap.mode,
stage: {
id: mapListMap.stageId,
name: t(`game-misc:STAGE_${mapListMap.stageId}`),
},
},
},
participatedUserIds: null,
winnerTeamId: null,
source: mapListMap.source,
ko: null,
};
});
participatedUserIds: null,
winnerTeamId: null,
source: mapListMap.source,
ko: null,
};
});
return [...playedMaps, ...upcomingMaps];
};
const { bracketName, roundNameWithoutMatchIdentifier } =

View File

@@ -241,6 +241,7 @@ export interface GetTournamentMatchResponse {
bracketName: string | null;
/** Name of the round this match belongs to. @example "Grand Finals" */
roundName: string | null;
/** Maps already played (with their results) followed by the upcoming ones. Once the set is over, only the played maps. `null` until both teams are known. */
mapList: Array<MapListMap> | null;
/** @example "https://sendou.ink/to/9/matches/695" */
url: string;
@@ -408,6 +409,7 @@ export type MapListMap = {
| "TO"
| "COUNTERPICK"
| "ROLL";
/** `null` if the map has not been played yet. */
winnerTeamId: number | null;
participatedUserIds: Array<number> | null;
/** (round robin only) whether the map ended in a knockout. `null` if not tracked. */

View File

@@ -0,0 +1,5 @@
---
navItem: medal
type: bug
---
Tournament match API now includes the results of maps already played while the set is still in progress