mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-11-15 09:57:48 -06:00
Fix tournament match API missing played map results mid-set
This commit is contained in:
@@ -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);
|
||||
|
||||
71
app/features/api-public/routes/tournament-match.$id.test.ts
Normal file
71
app/features/api-public/routes/tournament-match.$id.test.ts
Normal 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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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 } =
|
||||
|
||||
@@ -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. */
|
||||
|
||||
5
changelog/2026-10-11-api-match-played-map-results.md
Normal file
5
changelog/2026-10-11-api-match-played-map-results.md
Normal 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
|
||||
Reference in New Issue
Block a user