diff --git a/app/features/tournament-bracket/core/Swiss.test.ts b/app/features/tournament-bracket/core/Swiss.test.ts new file mode 100644 index 000000000..1991cd474 --- /dev/null +++ b/app/features/tournament-bracket/core/Swiss.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from "vitest"; +import * as Swiss from "./Swiss"; + +describe("Swiss", () => { + const createArgsWithDefaults = ( + args: Partial[0]> = {}, + ): Parameters[0] => { + return { + name: "Swiss Tournament", + seeding: [1, 2, 3, 4], + settings: {}, + tournamentId: 1, + ...args, + }; + }; + + describe("create()", () => { + it("attaches the correct tournament id to the data", () => { + const data = Swiss.create(createArgsWithDefaults()); + + expect(data.stage[0].tournament_id).toBe(1); + }); + + it("creates a swiss bracket with correct amount of initial matches", () => { + const data = Swiss.create(createArgsWithDefaults()); + + expect(data.match).toHaveLength(2); + }); + + it("creates a swiss bracket with correct amount of rounds as default", () => { + const data = Swiss.create(createArgsWithDefaults()); + + expect(data.round).toHaveLength(5); + }); + + it("creates a swiss bracket with correct amount of rounds as parameter", () => { + const data = Swiss.create( + createArgsWithDefaults({ + settings: { + swiss: { + groupCount: 1, + roundCount: 4, + }, + }, + }), + ); + + expect(data.round).toHaveLength(4); + }); + + it("creates a swiss bracket with two groups", () => { + const data = Swiss.create( + createArgsWithDefaults({ + settings: { + swiss: { + groupCount: 2, + roundCount: 5, + }, + }, + }), + ); + + expect(data.round).toHaveLength(10); + + const matchGroupIds = data.match.map((m) => m.group_id); + expect(matchGroupIds).toContain(0); + expect(matchGroupIds).toContain(1); + }); + + it("every team has a match", () => { + const data = Swiss.create(createArgsWithDefaults()); + + for (const teamId of [1, 2, 3, 4]) { + expect( + data.match.some( + (match) => + match.opponent1?.id === teamId || match.opponent2?.id === teamId, + ), + ).toBe(true); + } + }); + + it("assigns a BYE if odd number of teams", () => { + const data = Swiss.create( + createArgsWithDefaults({ + seeding: [1, 2, 3, 4, 5], + }), + ); + + const byes = data.match.filter((match) => match.opponent2 === null); + expect(byes).toHaveLength(1); + }); + + it("throws if not enough teams are provided", () => { + expect(() => { + Swiss.create(createArgsWithDefaults({ seeding: [1] })); + }).toThrow("Not enough teams for Swiss tournament"); + }); + }); + + // TODO: + // describe("generateMatchUps()", () => {}); +}); diff --git a/app/features/tournament-bracket/core/Swiss.ts b/app/features/tournament-bracket/core/Swiss.ts index 2fd5625f4..c7e373372 100644 --- a/app/features/tournament-bracket/core/Swiss.ts +++ b/app/features/tournament-bracket/core/Swiss.ts @@ -8,9 +8,16 @@ import { nullFilledArray } from "~/utils/arrays"; import invariant from "~/utils/invariant"; import type { Bracket, Standing } from "./Bracket"; +/** + * Creates a Swiss tournament data set (initial matches) based on the provided arguments. Mimics bracket-manager module's interfaces. + */ export function create( - args: Omit, + args: Omit & { seeding: number[] }, ): TournamentManagerDataSet { + if (args.seeding.length < 2) { + throw new Error("Not enough teams for Swiss tournament"); + } + const swissSettings = args.settings?.swiss; const groupCount = @@ -142,6 +149,13 @@ function firstRoundMatches({ } } +/** + * Generates the next round of matchups for a Swiss tournament bracket within a specific group. + * + * Considers only the matches and teams within the specified group. Teams that have dropped out are excluded from the pairing process. + * If the group has an uneven number of teams, the lowest standing team that has not already received a bye will receive one. + * Matches are generated such that teams do not replay previous opponents if possible. + */ export function generateMatchUps({ bracket, groupId, diff --git a/app/features/tournament/core/Standings.ts b/app/features/tournament/core/Standings.ts index 707748fd4..a255af285 100644 --- a/app/features/tournament/core/Standings.ts +++ b/app/features/tournament/core/Standings.ts @@ -86,6 +86,13 @@ export function matchesPlayed({ }); } +/** + * Computes the standings for a given tournament by aggregating results from relevant brackets. + * + * For example if the tournament format is round robin (where 2 out of 4 teams per group advance) to single elimination, + * the top teams are decided by the single elimination bracket, and the teams who failed to make the bracket are ordered + * by their performance in the round robin group stage. + */ export function tournamentStandings(tournament: Tournament): Standing[] { const bracketIdxs = Progression.bracketIdxsForStandings( tournament.ctx.settings.bracketProgression,