From 8d5bbebc54b543c8a07cfe71d36ad8e5b7e51edd Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:35:28 +0300 Subject: [PATCH] Fix tournament match API missing played map results mid-set --- app/db/seed/factories/TournamentFactory.ts | 12 +++- .../routes/tournament-match.$id.test.ts | 71 +++++++++++++++++++ .../api-public/routes/tournament-match.$id.ts | 63 ++++++++-------- app/features/api-public/schema.ts | 2 + ...2026-10-11-api-match-played-map-results.md | 5 ++ 5 files changed, 121 insertions(+), 32 deletions(-) create mode 100644 app/features/api-public/routes/tournament-match.$id.test.ts create mode 100644 changelog/2026-10-11-api-match-played-map-results.md diff --git a/app/db/seed/factories/TournamentFactory.ts b/app/db/seed/factories/TournamentFactory.ts index 509e5324b..9934eea58 100644 --- a/app/db/seed/factories/TournamentFactory.ts +++ b/app/db/seed/factories/TournamentFactory.ts @@ -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); diff --git a/app/features/api-public/routes/tournament-match.$id.test.ts b/app/features/api-public/routes/tournament-match.$id.test.ts new file mode 100644 index 000000000..bd12aa974 --- /dev/null +++ b/app/features/api-public/routes/tournament-match.$id.test.ts @@ -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({ 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, + ]); + }); +}); diff --git a/app/features/api-public/routes/tournament-match.$id.ts b/app/features/api-public/routes/tournament-match.$id.ts index 8ba00fad2..ac2af7604 100644 --- a/app/features/api-public/routes/tournament-match.$id.ts +++ b/app/features/api-public/routes/tournament-match.$id.ts @@ -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 } = diff --git a/app/features/api-public/schema.ts b/app/features/api-public/schema.ts index df130fbd6..98f08699a 100644 --- a/app/features/api-public/schema.ts +++ b/app/features/api-public/schema.ts @@ -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 | 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 | null; /** (round robin only) whether the map ended in a knockout. `null` if not tracked. */ diff --git a/changelog/2026-10-11-api-match-played-map-results.md b/changelog/2026-10-11-api-match-played-map-results.md new file mode 100644 index 000000000..38df9dc32 --- /dev/null +++ b/changelog/2026-10-11-api-match-played-map-results.md @@ -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