This commit is contained in:
Kalle 2026-07-30 15:02:47 +03:00
parent 419bd1ddef
commit aa9abe17fe
2 changed files with 69 additions and 123 deletions

View File

@ -31,8 +31,9 @@ const SINGLE_ELIMINATION: TournamentSettings["bracketProgression"] = [
},
];
/** Every round of a bracket the factory starts is played on these. A bracket can
* not be created without a map list, and picking one is the organizer's job. */
/** Every round of a bracket the factory starts is played on these, unless the
* caller passes a `maps` option. A bracket can not be created without a map list,
* and picking one is the organizer's job. */
const ROUND_MAPS = {
count: 3,
type: "BEST_OF",
@ -40,7 +41,10 @@ const ROUND_MAPS = {
mode: "SZ" as const,
stageId,
})),
} satisfies Omit<Engine.RoundMapsInput, "roundId">;
} satisfies RoundMaps;
/** The maps every round of a factory-started bracket is played on. */
type RoundMaps = Omit<Engine.RoundMapsInput, "roundId">;
/** The wrapping calendar event is not the caller's to choose, so it is not an argument. */
type InsertArgs = Omit<
@ -104,8 +108,13 @@ export async function createPlayed(
{
teamRosters,
playedOut = 0,
maps,
...options
}: Options & { teamRosters: number[][]; playedOut?: PlayedBrackets },
}: Options & {
teamRosters: number[][];
playedOut?: PlayedBrackets;
maps?: RoundMaps;
},
) {
const tournament = await create(overrides, options);
@ -119,7 +128,7 @@ export async function createPlayed(
);
}
const matches = await playOut(tournament.id, playedOut);
const matches = await playOut(tournament.id, playedOut, { maps });
return { ...tournament, teams, matches };
}
@ -136,6 +145,7 @@ export async function createPlayed(
export async function playOut(
tournamentId: number,
brackets: PlayedBrackets = 0,
{ maps }: { maps?: RoundMaps } = {},
): Promise<PlayedMatch[]> {
const tournament = await tournamentFromDB({ tournamentId, user: undefined });
const bracketIdxs =
@ -145,7 +155,7 @@ export async function playOut(
const matches: PlayedMatch[] = [];
for (const bracketIdx of bracketIdxs) {
await startBracket(tournamentId, { bracketIdx });
await startBracket(tournamentId, { bracketIdx, maps });
let playedThisPass: PlayedMatch[];
do {
@ -163,7 +173,8 @@ export async function playOut(
/**
* Starts one of the tournament's brackets, seeded by the teams that are in it
* the same teams the organizer would see offered on the bracket page.
* the same teams the organizer would see offered on the bracket page. Every round
* is played on `maps`, or the factory's default SZ Bo3 list.
*
* Later brackets of a progression are started by calling this again once the
* matches they source their teams from have been played.
@ -172,7 +183,10 @@ export async function playOut(
*/
export async function startBracket(
tournamentId: number,
{ bracketIdx = 0 }: { bracketIdx?: number } = {},
{
bracketIdx = 0,
maps = ROUND_MAPS,
}: { bracketIdx?: number; maps?: RoundMaps } = {},
) {
const tournament = await tournamentFromDB({ tournamentId, user: undefined });
@ -193,7 +207,7 @@ export async function startBracket(
name: bracket.name,
bracket: Engine.create({
...createInput,
maps: roundMapsFor(Engine.create(createInput), bracket.type),
maps: roundMapsFor(Engine.create(createInput), bracket.type, maps),
}),
});
@ -244,6 +258,7 @@ export async function playMatches(
function roundMapsFor(
bracket: Engine.BracketData,
type: Engine.StageType,
maps: RoundMaps,
): Engine.RoundMapsInput[] {
// round robin and swiss share one map list per round number across their groups
const rounds =
@ -251,7 +266,7 @@ function roundMapsFor(
? R.uniqueBy(bracket.round, (round) => round.number)
: bracket.round;
return rounds.map((round) => ({ roundId: round.id, ...ROUND_MAPS }));
return rounds.map((round) => ({ roundId: round.id, ...maps }));
}
function playableMatches(

View File

@ -18,8 +18,6 @@ import { TournamentSeedsPage } from "./pages/tournament/tournament-seeds-page";
import { TournamentTeamsPage } from "./pages/tournament/tournament-teams-page";
import { UserResultsPage } from "./pages/user/user-results-page";
// xxx: quite a lot of overlap (doing the same actions over and over again when they were already tested -> take shortcuts via factories to make tests faster?)
// xxx: tournament-bracket-elim, tournament-bracket-swiss etc. and pick ban as separate and general another?
const ROSTER_SIZE = 4;
@ -51,7 +49,7 @@ const ROUND_ROBIN: BracketProgression = [
/* Single group of 4 teams plays: R1 = matches 1 (team 1 vs. 4) & 2 (team 3 vs. 2),
* R2 = matches 3 (team 2 vs. 4) & 4 (team 1 vs. 3), R3 = matches 5 & 6 (team 2 vs. 1). */
const RR_TO_SE_WITH_UNDERGROUND: BracketProgression = [
const RR_TO_SE: BracketProgression = [
{
type: "round_robin",
name: "Groups stage",
@ -65,6 +63,10 @@ const RR_TO_SE_WITH_UNDERGROUND: BracketProgression = [
settings: {},
sources: [{ bracketIdx: 0, placements: [1, 2] }],
},
];
const RR_TO_SE_WITH_UNDERGROUND: BracketProgression = [
...RR_TO_SE,
{
type: "single_elimination",
name: "Underground bracket",
@ -348,35 +350,22 @@ test.describe("Tournament bracket", () => {
page,
factories,
}) => {
test.setTimeout(150_000);
const badges = await factories.BadgeFactory.createMany(2);
const tournament = await factories.TournamentFactory.create({
authorId: ADMIN_ID,
startTimes: startedTournamentTimes(),
bracketProgression: RR_TO_SE_WITH_UNDERGROUND,
mapPoolMaps: TO_MAP_POOL,
badges: badges.map((badge) => badge.id),
});
// groups of 3 and 4; playing every match with the alpha side winning drops
// teams 5, 6 and 7 into the underground bracket's placements
// groups of 3 and 4; the factory plays every match with the higher seed
// winning, dropping teams 5, 6 and 7 into the underground bracket's placements
const teams = await createTeams(factories, tournament.id, teamSeeds(7));
await impersonate(page);
const brackets = new TournamentBracketsPage(page);
await brackets.goto(tournament.id);
await brackets.finalize();
for (const matchId of [1, 2, 3, 4, 5, 6, 7, 8, 9]) {
const match = await brackets.openMatch(matchId);
await match.openTab("action");
await match.reportResult({ mapsToReport: 2 });
await match.backToBracket();
}
await factories.TournamentFactory.playOut(tournament.id);
// captain of one of the underground bracket teams
await impersonate(page, teams[4].ownerUserId);
const brackets = new TournamentBracketsPage(page);
await brackets.goto(tournament.id);
await brackets.bracketTab("Underground").click();
@ -388,22 +377,11 @@ test.describe("Tournament bracket", () => {
await admin.goto(tournament.id);
await admin.checkTeamInToBracket(teams[6].id, "Underground bracket");
await brackets.goto(tournament.id, 2);
await brackets.finalize();
// the checked in teams meet in the underground bracket; score reporting and
// starting brackets from the UI are covered by other tests
await factories.TournamentFactory.playOut(tournament.id, [2, 1]);
const undergroundMatch = await brackets.openMatch(10);
await undergroundMatch.openTab("action");
await undergroundMatch.reportResult({ mapsToReport: 3 });
await brackets.goto(tournament.id, 1);
await brackets.finalize();
for (const matchId of [11, 12, 13, 14]) {
const match = await brackets.openMatch(matchId);
await match.openTab("action");
await match.reportResult({ mapsToReport: 3 });
await match.backToBracket();
}
await brackets.goto(tournament.id);
const finalizeDialog = await brackets.openFinalizeTournamentDialog();
await finalizeDialog.assignBadgesLater();
await finalizeDialog.confirm();
@ -443,47 +421,28 @@ test.describe("Tournament bracket", () => {
page,
factories,
}) => {
test.slow();
const tournament = await factories.TournamentFactory.create({
name: "Swim or Sink 101",
authorId: ADMIN_ID,
startTimes: startedTournamentTimes(),
bracketProgression: SOS_BRACKETS,
mapPoolMaps: TO_MAP_POOL,
});
await createTeams(factories, tournament.id, teamSeeds(2));
// progression editing, score reporting and the finalize dialog are covered
// by other tests; only the result pages themselves are under test here
const players = await factories.UserFactory.createMany(ROSTER_SIZE * 2);
const teamRosters = [
players.slice(0, ROSTER_SIZE),
players.slice(ROSTER_SIZE),
].map((roster) => roster.map((player) => player.id));
const tournament = await factories.TournamentFactory.createPlayed(
{
name: "Swim or Sink 101",
authorId: ADMIN_ID,
startTimes: startedTournamentTimes(),
bracketProgression: RR_TO_SE,
},
{ teamRosters, playedOut: "all" },
);
await impersonate(page);
const admin = new TournamentAdminPage(page);
await admin.goto(tournament.id);
const eventEdit = await admin.editEventInfo();
for (let i = 0; i < 3; i++) {
await eventEdit.deleteLastBracket();
}
await eventEdit.fillLastPlacements("1,2");
await eventEdit.save();
const brackets = new TournamentBracketsPage(page);
await brackets.goto(tournament.id);
await brackets.finalize();
const groupsMatch = await brackets.openMatch(1);
await groupsMatch.openTab("action");
await groupsMatch.reportResult({ mapsToReport: 2 });
await groupsMatch.backToBracket();
await brackets.bracketTab("Great White").click();
await brackets.finalize();
const finalsMatch = await brackets.openMatch(2);
await finalsMatch.openTab("action");
await finalsMatch.reportResult({ mapsToReport: 3 });
await finalsMatch.backToBracket();
const finalizeDialog = await brackets.openFinalizeTournamentDialog();
await finalizeDialog.confirm();
const results = await brackets.nav.openResults();
await results.expandTeam(0);
@ -716,45 +675,26 @@ test.describe("Tournament bracket", () => {
page,
factories,
}) => {
test.slow();
const tournament = await factories.TournamentFactory.create({
authorId: ADMIN_ID,
startTimes: startedTournamentTimes(),
bracketProgression: ROUND_ROBIN,
mapPoolMaps: TO_MAP_POOL,
});
await createTeams(factories, tournament.id, teamSeeds(4));
// set situation where match A is completed and its participants also completed
// their follow up matches B & C and then we go back and change the winner of A:
// the two passes play rounds 1 (matches 1 & 2) and 2 (matches 3 & 4)
await factories.TournamentFactory.startBracket(tournament.id);
await factories.TournamentFactory.playMatches(tournament.id);
await factories.TournamentFactory.playMatches(tournament.id);
await impersonate(page);
const brackets = new TournamentBracketsPage(page);
await brackets.goto(tournament.id);
await brackets.finalize();
// needs also to be completed so the second round unlocks
let match = await brackets.openMatch(1);
await match.openTab("action");
await match.reportResult({ mapsToReport: 2 });
await match.backToBracket();
// set situation where match A is completed and its participants also completed their follow up matches B & C
// and then we go back and change the winner of A
match = await brackets.openMatch(2);
await match.openTab("action");
await match.reportResult({ mapsToReport: 2 });
await match.backToBracket();
match = await brackets.openMatch(3);
await match.openTab("action");
await match.reportResult({ mapsToReport: 2 });
await match.backToBracket();
match = await brackets.openMatch(4);
await match.openTab("action");
await match.reportResult({ mapsToReport: 2 });
await match.backToBracket();
match = await brackets.openMatch(2);
const match = await brackets.openMatch(2);
await match.openTab("admin");
await match.reopen();
// Wait for the reopen to be reflected before switching tabs: switching
@ -779,30 +719,21 @@ test.describe("Tournament bracket", () => {
authorId: ADMIN_ID,
startTimes: startedTournamentTimes(),
bracketProgression: ROUND_ROBIN,
mapPoolMaps: TO_MAP_POOL,
});
await createTeams(factories, tournament.id, teamSeeds(4));
// Complete R1 matches (1 and 2) to unlock R2 matches
await factories.TournamentFactory.startBracket(tournament.id);
await factories.TournamentFactory.playMatches(tournament.id);
await impersonate(page);
const brackets = new TournamentBracketsPage(page);
await brackets.goto(tournament.id);
await brackets.finalize();
// Complete R1 matches (1 and 2) to unlock R2 matches
let match = await brackets.openMatch(1);
await match.openTab("action");
await match.reportResult({ mapsToReport: 2 });
await match.backToBracket();
match = await brackets.openMatch(2);
await match.openTab("action");
await match.reportResult({ mapsToReport: 2 });
await match.backToBracket();
// Match 3 is R2 - should now be unlocked since R1 is complete
// Start it but don't complete it
match = await brackets.openMatch(3);
let match = await brackets.openMatch(3);
await match.openTab("action");
await match.reportResult({ mapsToReport: 1, setEnds: false });
await match.backToBracket();