Add tests and docs to the Swiss module

This commit is contained in:
Kalle
2025-07-02 22:13:32 +03:00
parent 4a0c48ba43
commit 009e7742a0
3 changed files with 125 additions and 1 deletions

View File

@@ -0,0 +1,103 @@
import { describe, expect, it } from "vitest";
import * as Swiss from "./Swiss";
describe("Swiss", () => {
const createArgsWithDefaults = (
args: Partial<Parameters<typeof Swiss.create>[0]> = {},
): Parameters<typeof Swiss.create>[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()", () => {});
});

View File

@@ -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<InputStage, "type" | "number">,
args: Omit<InputStage, "type" | "number" | "seeding"> & { 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,

View File

@@ -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,