Remove unused participant code + slim down tournament response

This commit is contained in:
Kalle 2024-06-09 23:53:31 +03:00
parent 4248cca5b1
commit dcf738f6d3
29 changed files with 225 additions and 2794 deletions

View File

@ -14,7 +14,7 @@ import type { BracketMapCounts } from "./toMapList";
interface CreateBracketArgs {
id: number;
preview: boolean;
data: ValueToArray<DataTypes>;
data: Omit<ValueToArray<DataTypes>, "participant">;
type: Tables["TournamentStage"]["type"];
canBeStarted?: boolean;
name: string;
@ -25,10 +25,7 @@ interface CreateBracketArgs {
bracketIdx: number;
placements: number[];
}[];
seeding?: {
id: number;
name: string;
}[];
seeding?: number[];
}
export interface Standing {
@ -219,6 +216,18 @@ export abstract class Bracket {
throw new Error("not implemented");
}
get participantTournamentTeamIds() {
// if (this.seeding) {
// return this.seeding.map((seed) => seed.id);
// }
return removeDuplicates(
this.data.match
.flatMap((match) => [match.opponent1?.id, match.opponent2?.id])
.filter(Boolean),
) as number[];
}
currentStandings(_includeUnfinishedGroups: boolean) {
return this.standings;
}
@ -272,7 +281,10 @@ export abstract class Bracket {
}
get enoughTeams() {
return this.data.participant.length >= TOURNAMENT.ENOUGH_TEAMS_TO_START;
return (
this.participantTournamentTeamIds.length >=
TOURNAMENT.ENOUGH_TEAMS_TO_START
);
}
canCheckIn(user: OptionalIdObject) {
@ -287,14 +299,14 @@ export abstract class Bracket {
source(_placements: number[]): {
relevantMatchesFinished: boolean;
teams: { id: number; name: string }[];
teams: number[];
} {
throw new Error("not implemented");
}
teamsWithNames(teams: { id: number }[]) {
return teams.map((team) => {
const name = this.data.participant.find(
const name = this.tournament.ctx.teams.find(
(participant) => participant.id === team.id,
)?.name;
invariant(name, `Team name not found for id: ${team.id}`);
@ -420,7 +432,7 @@ class SingleEliminationBracket extends Bracket {
}
const teamCountWhoDidntLoseYet =
this.data.participant.length - teams.length;
this.participantTournamentTeamIds.length - teams.length;
const result: Standing[] = [];
for (const roundId of removeDuplicates(teams.map((team) => team.lostAt))) {
@ -443,13 +455,13 @@ class SingleEliminationBracket extends Bracket {
}
if (teamCountWhoDidntLoseYet === 1) {
const winner = this.data.participant.find((participant) =>
result.every(({ team }) => team.id !== participant.id),
const winnerId = this.participantTournamentTeamIds.find((participantId) =>
result.every(({ team }) => team.id !== participantId),
);
invariant(winner, "No winner identified");
invariant(winnerId, "No winner identified");
const winnerTeam = this.tournament.teamById(winner.id);
invariant(winnerTeam, `Winner team not found for id: ${winner.id}`);
const winnerTeam = this.tournament.teamById(winnerId);
invariant(winnerTeam, `Winner team not found for id: ${winnerId}`);
result.push({
team: winnerTeam,
@ -573,7 +585,7 @@ class DoubleEliminationBracket extends Bracket {
}
const teamCountWhoDidntLoseInLosersYet =
this.data.participant.length - teams.length;
this.participantTournamentTeamIds.length - teams.length;
const result: Standing[] = [];
for (const roundId of removeDuplicates(teams.map((team) => team.lostAt))) {
@ -691,13 +703,15 @@ class DoubleEliminationBracket extends Bracket {
}
source(placements: number[]) {
const resolveLosersGroupId = (data: ValueToArray<DataTypes>) => {
const resolveLosersGroupId = (
data: Omit<ValueToArray<DataTypes>, "participant">,
) => {
const minGroupId = Math.min(...data.round.map((round) => round.group_id));
return minGroupId + 1;
};
const placementsToRoundsIds = (
data: ValueToArray<DataTypes>,
data: Omit<ValueToArray<DataTypes>, "participant">,
losersGroupId: number,
) => {
const firstRoundIsOnlyByes = () => {
@ -742,7 +756,7 @@ class DoubleEliminationBracket extends Bracket {
(a, b) => b - a,
);
const teams: { id: number }[] = [];
const teams: number[] = [];
let relevantMatchesFinished = true;
for (const roundId of sourceRoundsIds) {
const roundsMatches = this.data.match.filter(
@ -766,13 +780,13 @@ class DoubleEliminationBracket extends Bracket {
match.opponent1?.result === "win" ? match.opponent2 : match.opponent1;
invariant(loser?.id, "Loser id not found");
teams.push({ id: loser.id });
teams.push(loser.id);
}
}
return {
relevantMatchesFinished,
teams: this.teamsWithNames(teams),
teams,
};
}
}
@ -788,14 +802,14 @@ class RoundRobinBracket extends Bracket {
source(placements: number[]): {
relevantMatchesFinished: boolean;
teams: { id: number; name: string }[];
teams: number[];
} {
if (placements.some((p) => p < 0)) {
throw new Error("Negative placements not implemented");
}
const standings = this.standings;
const relevantMatchesFinished =
standings.length === this.data.participant.length;
standings.length === this.participantTournamentTeamIds.length;
const uniquePlacements = removeDuplicates(
standings.map((s) => s.placement),
@ -810,7 +824,7 @@ class RoundRobinBracket extends Bracket {
relevantMatchesFinished,
teams: standings
.filter((s) => placements.includes(placementNormalized(s.placement)))
.map((s) => ({ id: s.team.id, name: s.team.name })),
.map((s) => s.team.id),
};
}
@ -1060,7 +1074,7 @@ class SwissBracket extends Bracket {
source(placements: number[]): {
relevantMatchesFinished: boolean;
teams: { id: number; name: string }[];
teams: number[];
} {
if (placements.some((p) => p < 0)) {
throw new Error("Negative placements not implemented");
@ -1101,7 +1115,7 @@ class SwissBracket extends Bracket {
relevantMatchesFinished,
teams: standings
.filter((s) => placements.includes(placementNormalized(s.placement)))
.map((s) => ({ id: s.team.id, name: s.team.name })),
.map((s) => s.team.id),
};
}

View File

@ -7,13 +7,9 @@ import { nullFilledArray } from "~/utils/arrays";
import type { Bracket, Standing } from "./Bracket";
import type { TournamentRepositoryInsertableMatch } from "~/features/tournament/TournamentRepository.server";
type SwissSeeding = { id: number; name: string };
interface CreateArgs extends Omit<InputStage, "type" | "seeding" | "number"> {
seeding: readonly SwissSeeding[];
}
export function create(args: CreateArgs): ValueToArray<DataTypes> {
export function create(
args: Omit<InputStage, "type" | "number">,
): ValueToArray<DataTypes> {
const swissSettings = args.settings?.swiss;
const groupCount = swissSettings?.groupCount ?? 1;
@ -29,11 +25,6 @@ export function create(args: CreateArgs): ValueToArray<DataTypes> {
return {
group,
match: firstRoundMatches({ seeding: args.seeding, groupCount, roundCount }),
participant: args.seeding.map((p) => ({
id: p.id,
name: p.name,
tournament_id: args.tournamentId,
})),
round: group.flatMap((g) =>
nullFilledArray(roundCount).map((_, i) => ({
id: roundId++,
@ -60,7 +51,7 @@ function firstRoundMatches({
groupCount,
roundCount,
}: {
seeding: CreateArgs["seeding"];
seeding: InputStage["seeding"];
groupCount: number;
roundCount: number;
}): Match[] {
@ -106,10 +97,10 @@ function firstRoundMatches({
round_id: roundId,
number: i + 1,
opponent1: {
id: upper.id,
id: upper,
},
opponent2: {
id: lower.id,
id: lower,
},
status: 2,
});
@ -123,7 +114,7 @@ function firstRoundMatches({
round_id: roundId,
number: upperHalf.length + 1,
opponent1: {
id: bye.id,
id: bye,
},
opponent2: null,
status: 2,
@ -137,11 +128,11 @@ function firstRoundMatches({
if (!seeding) return [];
if (groupCount === 1) return [[...seeding]];
const groups: SwissSeeding[][] = nullFilledArray(groupCount).map(() => []);
const groups: number[][] = nullFilledArray(groupCount).map(() => []);
for (let i = 0; i < seeding.length; i++) {
const groupIndex = i % groupCount;
groups[groupIndex].push(seeding[i]);
groups[groupIndex].push(seeding[i]!);
}
return groups;

View File

@ -23,22 +23,18 @@ FollowUp("includes correct teams in the top cut", () => {
for (const tournamentTeamId of [892, 882, 881]) {
assert.ok(
tournamentPP257.brackets[1].seeding?.some(
(team) => team.id === tournamentTeamId,
(team) => team === tournamentTeamId,
),
);
}
});
FollowUp("underground bracket includes a checked in team", () => {
assert.ok(
tournamentPP257.brackets[2].seeding?.some((team) => team.id === 902),
);
assert.ok(tournamentPP257.brackets[2].seeding?.some((team) => team === 902));
});
FollowUp("underground bracket doesn't include a non checked in team", () => {
assert.ok(
tournamentPP257.brackets[2].seeding?.some((team) => team.id === 902),
);
assert.ok(tournamentPP257.brackets[2].seeding?.some((team) => team === 902));
});
FollowUp("underground bracket includes checked in teams (DE->SE)", () => {
@ -141,7 +137,7 @@ FollowUp("avoids rematches in RR -> SE (PP 255)", () => {
validateNoRematches(rrMatches, topCutMatches);
});
FollowUp("avoids rematches in RR -> SE (PP 255)", () => {
FollowUp("avoids rematches in RR -> SE (PP 255) - only minimum swap", () => {
const oldTopCutMatches = PADDLING_POOL_255_TOP_CUT_INITIAL_MATCHES();
const newTopCutMatches = tournamentPP255.brackets[1].data.match;
@ -164,7 +160,7 @@ FollowUp("avoids rematches in RR -> SE (PP 255)", () => {
}
// 1 team should get swapped meaning two matches are now different
assert.equal(different, 2);
assert.equal(different, 2, "Amount of different matches is incorrect");
});
FollowUp.run();

View File

@ -86,7 +86,7 @@ export class Tournament {
return a.createdAt - b.createdAt;
}
private initBrackets(data: ValueToArray<DataTypes>) {
private initBrackets(data: Omit<ValueToArray<DataTypes>, "participant">) {
for (const [
bracketIdx,
{ type, name, sources },
@ -99,11 +99,6 @@ export class Tournament {
const match = data.match.filter(
(match) => match.stage_id === inProgressStage.id,
);
const participants = new Set(
match
.flatMap((match) => [match.opponent1?.id, match.opponent2?.id])
.filter((id) => typeof id === "number"),
);
this.brackets.push(
Bracket.create({
@ -115,9 +110,6 @@ export class Tournament {
createdAt: inProgressStage.createdAt,
data: {
...data,
participant: data.participant.filter((participant) =>
participants.has(participant.id),
),
group: data.group.filter(
(group) => group.stage_id === inProgressStage.id,
),
@ -136,7 +128,7 @@ export class Tournament {
const { teams, relevantMatchesFinished } = sources
? this.resolveTeamsFromSources(sources)
: {
teams: this.ctx.teams,
teams: this.ctx.teams.map((team) => team.id),
relevantMatchesFinished: true,
};
@ -166,7 +158,7 @@ export class Tournament {
checkedInTeams.length >= TOURNAMENT.ENOUGH_TEAMS_TO_START &&
(sources ? relevantMatchesFinished : this.regularCheckInHasEnded),
teamsPendingCheckIn:
bracketIdx !== 0 ? notCheckedInTeams.map((t) => t.id) : undefined,
bracketIdx !== 0 ? notCheckedInTeams : undefined,
}),
);
} else {
@ -174,7 +166,7 @@ export class Tournament {
const { teams, relevantMatchesFinished } = sources
? this.resolveTeamsFromSources(sources)
: {
teams: this.ctx.teams,
teams: this.ctx.teams.map((team) => team.id),
relevantMatchesFinished: true,
};
@ -225,7 +217,7 @@ export class Tournament {
TOURNAMENT.ENOUGH_TEAMS_TO_START &&
(sources ? relevantMatchesFinished : this.regularCheckInHasEnded),
teamsPendingCheckIn:
bracketIdx !== 0 ? notCheckedInTeams.map((t) => t.id) : undefined,
bracketIdx !== 0 ? notCheckedInTeams : undefined,
}),
);
}
@ -235,7 +227,7 @@ export class Tournament {
private resolveTeamsFromSources(
sources: NonNullable<TournamentBracketProgression[number]["sources"]>,
) {
const teams: { id: number; name: string }[] = [];
const teams: number[] = [];
let allRelevantMatchesFinished = true;
for (const { bracketIdx, placements } of sources) {
@ -254,10 +246,7 @@ export class Tournament {
}
private avoidReplaysOfPreviousBracketOpponent(
teams: {
id: number;
name: string;
}[],
teams: number[],
bracket: {
sources: TournamentBracketProgression[number]["sources"];
type: TournamentBracketProgression[number]["type"];
@ -304,12 +293,7 @@ export class Tournament {
new Map() as Map<number, number[]>,
);
const bracketReplays = (
candidateTeams: {
id: number;
name: string;
}[],
) => {
const bracketReplays = (candidateTeams: number[]) => {
const manager = getTournamentManager();
manager.create({
tournamentId: this.ctx.id,
@ -358,12 +342,12 @@ export class Tournament {
const [oneId, twoId] = replays[0];
const lowerSeedId =
newOrder.findIndex((t) => t.id === oneId) <
newOrder.findIndex((t) => t.id === twoId)
newOrder.findIndex((t) => t === oneId) <
newOrder.findIndex((t) => t === twoId)
? twoId
: oneId;
if (!potentialSwitchCandidates.some((t) => t.id === lowerSeedId)) {
if (!potentialSwitchCandidates.some((t) => t === lowerSeedId)) {
logger.warn(
`Avoiding replays failed, no potential switch candidates found in match: ${oneId} vs. ${twoId}`,
);
@ -373,10 +357,10 @@ export class Tournament {
for (const candidate of potentialSwitchCandidates) {
// can't switch place with itself
if (candidate.id === lowerSeedId) continue;
if (candidate === lowerSeedId) continue;
const candidateIdx = newOrder.findIndex((t) => t.id === candidate.id);
const otherIdx = newOrder.findIndex((t) => t.id === lowerSeedId);
const candidateIdx = newOrder.findIndex((t) => t === candidate);
const otherIdx = newOrder.findIndex((t) => t === lowerSeedId);
const temp = newOrder[candidateIdx];
newOrder[candidateIdx] = newOrder[otherIdx];
@ -403,12 +387,12 @@ export class Tournament {
teams,
bracketIdx,
}: {
teams: { id: number; name: string }[];
teams: number[];
bracketIdx: number;
}) {
return teams.reduce(
(acc, cur) => {
const team = this.teamById(cur.id);
const team = this.teamById(cur);
invariant(team, "Team not found");
const usesRegularCheckIn = bracketIdx === 0;
@ -431,8 +415,8 @@ export class Tournament {
return acc;
},
{ checkedInTeams: [], notCheckedInTeams: [] } as {
checkedInTeams: { id: number; name: string }[];
notCheckedInTeams: { id: number; name: string }[];
checkedInTeams: number[];
notCheckedInTeams: number[];
},
);
}
@ -887,9 +871,11 @@ export class Tournament {
}
const participantInAnotherBracket = ongoingFollowUpBrackets
.flatMap((b) => b.data.participant)
.flatMap((b) => b.participantTournamentTeamIds)
.some(
(p) => p.id === match.opponent1?.id || p.id === match.opponent2?.id,
(tournamentTeamId) =>
tournamentTeamId === match.opponent1?.id ||
tournamentTeamId === match.opponent2?.id,
);
return participantInAnotherBracket;

View File

@ -1,7 +1,6 @@
// this file offers database functions specifically for the crud.server.ts file
import type {
Participant,
Stage as StageType,
Group as GroupType,
Round as RoundType,
@ -9,42 +8,15 @@ import type {
} from "~/modules/brackets-model";
import { sql } from "~/db/sql";
import type {
Tournament,
TournamentGroup,
TournamentMatch,
TournamentRound,
TournamentStage,
TournamentTeam,
} from "~/db/types";
import { nanoid } from "nanoid";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import type { TournamentRoundMaps } from "~/db/tables";
const team_getByTournamentIdStm = sql.prepare(/*sql*/ `
select
*
from
"TournamentTeam"
where
"TournamentTeam"."tournamentId" = @tournamentId
`);
export class Team {
static #convertTeam(rawTeam: TournamentTeam): Participant {
return {
id: rawTeam.id,
name: rawTeam.name,
tournament_id: rawTeam.tournamentId,
};
}
static getByTournamentId(tournamentId: Tournament["id"]): Participant[] {
return (team_getByTournamentIdStm.all({ tournamentId }) as any[]).map(
this.#convertTeam,
);
}
}
const stage_getByIdStm = sql.prepare(/*sql*/ `
select
*
@ -137,7 +109,7 @@ export class Stage {
return this.#convertStage(stage);
}
static getByTournamentId(tournamentId: Tournament["id"]): Participant[] {
static getByTournamentId(tournamentId: number): StageType[] {
return (stage_getByTournamentIdStm.all({ tournamentId }) as any[]).map(
this.#convertStage,
);

View File

@ -142,28 +142,6 @@ export const FOUR_TEAMS_RR = (): ValueToArray<DataTypes> => ({
},
},
],
participant: [
{
id: 0,
tournament_id: 1,
name: "Team 1",
},
{
id: 1,
tournament_id: 1,
name: "Team 2",
},
{
id: 2,
tournament_id: 1,
name: "Team 3",
},
{
id: 3,
tournament_id: 1,
name: "Team 4",
},
],
});
export const FIVE_TEAMS_RR = (): ValueToArray<DataTypes> => ({
@ -383,33 +361,6 @@ export const FIVE_TEAMS_RR = (): ValueToArray<DataTypes> => ({
},
},
],
participant: [
{
id: 0,
tournament_id: 3,
name: "Team 1",
},
{
id: 1,
tournament_id: 3,
name: "Team 2",
},
{
id: 2,
tournament_id: 3,
name: "Team 3",
},
{
id: 3,
tournament_id: 3,
name: "Team 4",
},
{
id: 4,
tournament_id: 3,
name: "Team 5",
},
],
});
export const SIX_TEAMS_TWO_GROUPS_RR = (): ValueToArray<DataTypes> => ({
@ -576,38 +527,6 @@ export const SIX_TEAMS_TWO_GROUPS_RR = (): ValueToArray<DataTypes> => ({
},
},
],
participant: [
{
id: 0,
tournament_id: 3,
name: "Team 1",
},
{
id: 1,
tournament_id: 3,
name: "Team 2",
},
{
id: 2,
tournament_id: 3,
name: "Team 3",
},
{
id: 3,
tournament_id: 3,
name: "Team 4",
},
{
id: 4,
tournament_id: 3,
name: "Team 5",
},
{
id: 5,
tournament_id: 3,
name: "Team 6",
},
],
});
export const PADDLING_POOL_257 = () =>
@ -2056,183 +1975,6 @@ export const PADDLING_POOL_257 = () =>
lastGameFinishedAt: 1709752657,
},
],
participant: [
{
id: 819,
name: "Heaven sent Lunatics",
tournament_id: 27,
},
{
id: 828,
name: "4Ever",
tournament_id: 27,
},
{
id: 838,
name: "G Gaming Gaming",
tournament_id: 27,
},
{
id: 839,
name: "1HP",
tournament_id: 27,
},
{
id: 843,
name: "Rogueport Rascals",
tournament_id: 27,
},
{
id: 845,
name: "Smoking Moais ",
tournament_id: 27,
},
{
id: 847,
name: "Big Tommy and the Flops",
tournament_id: 27,
},
{
id: 867,
name: "NEVER BACK DOWN NEVER WHAT?",
tournament_id: 27,
},
{
id: 869,
name: "Müll🚮",
tournament_id: 27,
},
{
id: 870,
name: "Shade",
tournament_id: 27,
},
{
id: 871,
name: "ASC Niji",
tournament_id: 27,
},
{
id: 872,
name: "Intrusive thoughts ",
tournament_id: 27,
},
{
id: 873,
name: "Préférence Pêche ",
tournament_id: 27,
},
{
id: 874,
name: "Squid Emoji",
tournament_id: 27,
},
{
id: 875,
name: "Chippeur arrête de Chipper",
tournament_id: 27,
},
{
id: 878,
name: "SAN DIMAS HS FOOTBALL RULES!",
tournament_id: 27,
},
{
id: 879,
name: "ASC Shokkai ",
tournament_id: 27,
},
{
id: 881,
name: "New Generation",
tournament_id: 27,
},
{
id: 882,
name: "Chaos Control",
tournament_id: 27,
},
{
id: 883,
name: "Second Try",
tournament_id: 27,
},
{
id: 884,
name: "Hazard",
tournament_id: 27,
},
{
id: 886,
name: "Seaya",
tournament_id: 27,
},
{
id: 887,
name: "AquaSonix",
tournament_id: 27,
},
{
id: 888,
name: "Naw, Id win",
tournament_id: 27,
},
{
id: 889,
name: "DistInkt",
tournament_id: 27,
},
{
id: 890,
name: "better gaming chair",
tournament_id: 27,
},
{
id: 891,
name: "Mafia mbappe",
tournament_id: 27,
},
{
id: 892,
name: "Le classique à Cam",
tournament_id: 27,
},
{
id: 893,
name: "Ink Souls Maria",
tournament_id: 27,
},
{
id: 896,
name: "<_>Placeholder",
tournament_id: 27,
},
{
id: 897,
name: "Splash Mirrors 3",
tournament_id: 27,
},
{
id: 898,
name: "Atsub",
tournament_id: 27,
},
{
id: 900,
name: "Theres a snake in my boot 🐍",
tournament_id: 27,
},
{
id: 901,
name: "Pickup oder so ig",
tournament_id: 27,
},
{
id: 902,
name: "Blaze",
tournament_id: 27,
},
],
},
ctx: {
id: 27,
@ -7940,183 +7682,6 @@ export const PADDLING_POOL_255 = () =>
lastGameFinishedAt: 1708541417,
},
],
participant: [
{
id: 672,
name: "Müll🚮",
tournament_id: 18,
},
{
id: 673,
name: "Grandma Sicko Mode",
tournament_id: 18,
},
{
id: 674,
name: "Smoking Moais ",
tournament_id: 18,
},
{
id: 675,
name: "Yoghurt Party",
tournament_id: 18,
},
{
id: 676,
name: "NEVER BACK DOWN NEVER WHAT",
tournament_id: 18,
},
{
id: 677,
name: "4Ever",
tournament_id: 18,
},
{
id: 678,
name: "Amoura",
tournament_id: 18,
},
{
id: 695,
name: "Hisense RL170D4BWE Freestanding ",
tournament_id: 18,
},
{
id: 697,
name: "Atlass",
tournament_id: 18,
},
{
id: 698,
name: "Please don't forfeit. ",
tournament_id: 18,
},
{
id: 699,
name: "Stream easy by lesserafim",
tournament_id: 18,
},
{
id: 700,
name: "Fresh takos ",
tournament_id: 18,
},
{
id: 701,
name: "Hazard",
tournament_id: 18,
},
{
id: 702,
name: "ici ça bzzz",
tournament_id: 18,
},
{
id: 704,
name: "Second Try",
tournament_id: 18,
},
{
id: 705,
name: "Flutter Mane Fanclub",
tournament_id: 18,
},
{
id: 706,
name: "DistInkt",
tournament_id: 18,
},
{
id: 707,
name: "Chaos Control",
tournament_id: 18,
},
{
id: 708,
name: "91 c'est la Champions League",
tournament_id: 18,
},
{
id: 709,
name: "ASC Shokkai ",
tournament_id: 18,
},
{
id: 710,
name: "Bloody Wave",
tournament_id: 18,
},
{
id: 712,
name: "Revenge ",
tournament_id: 18,
},
{
id: 713,
name: "https://youtu.be/Euq7uTeYCP0?si=",
tournament_id: 18,
},
{
id: 714,
name: "youtube.com/watch?v=dQw4w9WgXcQ",
tournament_id: 18,
},
{
id: 715,
name: "Rule them",
tournament_id: 18,
},
{
id: 716,
name: "allo kayora ?",
tournament_id: 18,
},
{
id: 717,
name: "ASC Niji~K",
tournament_id: 18,
},
{
id: 718,
name: "Enperries 200p",
tournament_id: 18,
},
{
id: 719,
name: "Gambawaffeln",
tournament_id: 18,
},
{
id: 720,
name: "Blaze",
tournament_id: 18,
},
{
id: 721,
name: "Squid Emoji",
tournament_id: 18,
},
{
id: 722,
name: "iPad jaune",
tournament_id: 18,
},
{
id: 723,
name: "We Are Innocent Caterpillars",
tournament_id: 18,
},
{
id: 724,
name: "menoks!",
tournament_id: 18,
},
{
id: 725,
name: "Ink Souls Maria",
tournament_id: 18,
},
],
},
ctx: {
id: 18,
@ -14162,178 +13727,6 @@ export const IN_THE_ZONE_32 = () =>
lastGameFinishedAt: null,
},
],
participant: [
{
id: 499,
name: "Grougrou ",
tournament_id: 11,
},
{
id: 500,
name: "Hades",
tournament_id: 11,
},
{
id: 507,
name: "Celeste",
tournament_id: 11,
},
{
id: 509,
name: "New Generation ",
tournament_id: 11,
},
{
id: 546,
name: "Moonlight",
tournament_id: 11,
},
{
id: 547,
name: "atomic bomb ",
tournament_id: 11,
},
{
id: 548,
name: "Jackpot",
tournament_id: 11,
},
{
id: 551,
name: "Zoneando",
tournament_id: 11,
},
{
id: 552,
name: "Starburst",
tournament_id: 11,
},
{
id: 560,
name: "Yaotl Teotl",
tournament_id: 11,
},
{
id: 564,
name: "Black Lotus ",
tournament_id: 11,
},
{
id: 589,
name: "11:11; Make a Wish",
tournament_id: 11,
},
{
id: 590,
name: "ASC Tenshi",
tournament_id: 11,
},
{
id: 591,
name: "BEt",
tournament_id: 11,
},
{
id: 592,
name: "As you wish",
tournament_id: 11,
},
{
id: 593,
name: "Whaa",
tournament_id: 11,
},
{
id: 594,
name: "FOAMS 34",
tournament_id: 11,
},
{
id: 595,
name: "Hypernova",
tournament_id: 11,
},
{
id: 596,
name: "metal pipe",
tournament_id: 11,
},
{
id: 597,
name: "OVERTIME!!",
tournament_id: 11,
},
{
id: 598,
name: "Gen BOB Ten-Piece Chicken Nugget",
tournament_id: 11,
},
{
id: 599,
name: "Hazard",
tournament_id: 11,
},
{
id: 600,
name: "Gentlemates",
tournament_id: 11,
},
{
id: 601,
name: "Zenith",
tournament_id: 11,
},
{
id: 603,
name: "JumpingCatapult ",
tournament_id: 11,
},
{
id: 604,
name: "Joga Bonito",
tournament_id: 11,
},
{
id: 605,
name: "all my homie hate pencil ",
tournament_id: 11,
},
{
id: 606,
name: "Peaky P-Key ",
tournament_id: 11,
},
{
id: 608,
name: "Hiro le tournevis",
tournament_id: 11,
},
{
id: 609,
name: "UK MAFIA",
tournament_id: 11,
},
{
id: 610,
name: "Smoking Moais ",
tournament_id: 11,
},
{
id: 611,
name: "Replay",
tournament_id: 11,
},
{
id: 612,
name: "delulu ",
tournament_id: 11,
},
{
id: 614,
name: "Reputation (Taylor's Version)",
tournament_id: 11,
},
],
},
ctx: {
id: 11,

View File

@ -1,289 +0,0 @@
import { suite } from "uvu";
import { FIVE_TEAMS_RR, FOUR_TEAMS_RR, SIX_TEAMS_TWO_GROUPS_RR } from "./mocks";
import { adjustResults, testTournament } from "./test-utils";
import * as assert from "uvu/assert";
import { BRACKET_NAMES } from "~/features/tournament/tournament-constants";
import type { TournamentData } from "../Tournament.server";
const RoundRobinStandings = suite("Round Robin Standings");
const roundRobinTournamentCtx: Partial<TournamentData["ctx"]> = {
settings: {
bracketProgression: [{ name: BRACKET_NAMES.GROUPS, type: "round_robin" }],
},
inProgressBrackets: [
{ id: 0, type: "round_robin", name: BRACKET_NAMES.GROUPS, createdAt: 0 },
],
};
RoundRobinStandings("resolves standings from points", () => {
const tournament = testTournament(
adjustResults(FOUR_TEAMS_RR(), [
{ ids: [0, 3], score: [2, 0] },
{ ids: [2, 1], score: [0, 2] },
{ ids: [1, 3], score: [2, 0] },
{ ids: [0, 2], score: [2, 0] },
{ ids: [2, 3], score: [2, 0] },
{ ids: [1, 0], score: [0, 2] },
]),
roundRobinTournamentCtx,
);
const standings = tournament.bracketByIdx(0)!.standings;
assert.equal(standings.length, 4);
assert.equal(standings[0].team.id, 0);
assert.equal(standings[0].placement, 1);
assert.equal(standings[1].team.id, 1);
assert.equal(standings[2].team.id, 2);
assert.equal(standings[3].team.id, 3);
});
RoundRobinStandings("tiebreaker via head-to-head", () => {
// id 0 = WWWL
// id 1 = WWWL
// id 2 = WWLL
// id 3 = WWLL but won against 2
// id 4 = LLLL
const tournament = testTournament(
adjustResults(FIVE_TEAMS_RR(), [
{
ids: [4, 1],
score: [0, 2],
},
{
ids: [0, 2],
score: [2, 0],
},
{
ids: [4, 3],
score: [0, 2],
},
{
ids: [1, 3],
score: [2, 0],
},
{
ids: [0, 4],
score: [2, 0],
},
{
ids: [2, 4],
score: [2, 0],
},
{
ids: [1, 0],
score: [2, 0],
},
{
ids: [3, 0],
score: [0, 2],
},
{
ids: [2, 1],
score: [2, 0],
},
{
ids: [3, 2],
score: [2, 0],
},
]),
roundRobinTournamentCtx,
);
const standings = tournament.bracketByIdx(0)!.standings;
assert.equal(standings.length, 5);
assert.equal(standings[2].team.id, 3);
assert.equal(standings[2].placement, 3);
assert.equal(standings[3].team.id, 2);
assert.equal(standings[3].placement, 4);
});
RoundRobinStandings("tiebreaker via maps won", () => {
// id 0 = WWWW
// id 1 = WWLL
// id 2 = WWLL
// id 3 = WWLL
const tournament = testTournament(
adjustResults(FOUR_TEAMS_RR(), [
{ ids: [0, 3], score: [2, 0] },
{ ids: [2, 1], score: [0, 2] },
{ ids: [1, 3], score: [0, 2] },
{ ids: [0, 2], score: [2, 0] },
{ ids: [2, 3], score: [2, 1] },
{ ids: [1, 0], score: [0, 2] },
]),
roundRobinTournamentCtx,
);
const standings = tournament.bracketByIdx(0)!.standings;
// they won the most maps out of the 3 tied teams
assert.equal(standings[1].team.id, 3);
});
RoundRobinStandings("three way tiebreaker via points scored", () => {
// id 0 = LLL
// id 1 = WWL
// id 2 = WWL
// id 3 = WWL
const tournament = testTournament(
adjustResults(FOUR_TEAMS_RR(), [
{ ids: [0, 3], score: [0, 2], points: [0, 200] },
{ ids: [2, 1], score: [0, 2], points: [50, 100] },
{ ids: [1, 3], score: [0, 2], points: [0, 200] },
{ ids: [0, 2], score: [0, 2], points: [0, 200] },
{ ids: [2, 3], score: [2, 0], points: [150, 149] },
{ ids: [1, 0], score: [2, 0], points: [200, 0] },
]),
roundRobinTournamentCtx,
);
const standings = tournament.bracketByIdx(0)!.standings;
assert.equal(standings[0].team.id, 3);
assert.equal(standings[1].team.id, 2);
});
RoundRobinStandings("if everything is tied, uses seeds as tiebreaker", () => {
// id 0 = LLL
// id 1 = WWL
// id 2 = WWL
// id 3 = WWL
const tournament = testTournament(
adjustResults(FOUR_TEAMS_RR(), [
{ ids: [0, 3], score: [0, 2], points: [0, 200] },
{ ids: [2, 1], score: [0, 2], points: [0, 200] },
{ ids: [1, 3], score: [0, 2], points: [0, 200] },
{ ids: [0, 2], score: [0, 2], points: [0, 200] },
{ ids: [2, 3], score: [2, 0], points: [200, 0] },
{ ids: [1, 0], score: [2, 0], points: [200, 0] },
]),
roundRobinTournamentCtx,
);
const standings = tournament.bracketByIdx(0)!.standings;
assert.equal(standings[0].team.id, 1);
assert.equal(standings[1].team.id, 2);
});
RoundRobinStandings("if two groups finished, standings for both groups", () => {
const tournament = testTournament(
adjustResults(SIX_TEAMS_TWO_GROUPS_RR(), [
{
ids: [4, 3],
score: [2, 0],
},
{
ids: [0, 4],
score: [2, 0],
},
{
ids: [3, 0],
score: [2, 0],
},
{
ids: [5, 2],
score: [2, 0],
},
{
ids: [1, 5],
score: [2, 0],
},
{
ids: [2, 1],
score: [2, 0],
},
]),
roundRobinTournamentCtx,
);
const standings = tournament.bracketByIdx(0)!.standings;
assert.equal(standings.length, 6);
assert.equal(standings.filter((s) => s.placement === 1).length, 2);
});
RoundRobinStandings(
"if one group finished and other ongoing, standings for just one group",
() => {
const tournament = testTournament(
adjustResults(SIX_TEAMS_TWO_GROUPS_RR(), [
{
ids: [4, 3],
score: [2, 0],
},
{
ids: [0, 4],
score: [2, 0],
},
{
ids: [3, 0],
score: [2, 0],
},
{
ids: [5, 2],
score: [2, 0],
},
{
ids: [1, 5],
score: [2, 0],
},
{
ids: [2, 1],
score: [0, 0],
},
]),
roundRobinTournamentCtx,
);
const standings = tournament.bracketByIdx(0)!.standings;
assert.equal(standings.length, 3);
assert.equal(standings.filter((s) => s.placement === 1).length, 1);
},
);
RoundRobinStandings(
"teams with same placements are ordered by group id",
() => {
const base = SIX_TEAMS_TWO_GROUPS_RR();
const tournament = testTournament(
adjustResults({ ...base, group: base.group.reverse() }, [
{
ids: [4, 3],
score: [2, 0],
},
{
ids: [0, 4],
score: [0, 2],
},
{
ids: [3, 0],
score: [2, 0],
},
{
ids: [5, 2],
score: [2, 0],
},
{
ids: [1, 5],
score: [2, 0],
},
{
ids: [2, 1],
score: [2, 0],
},
]),
roundRobinTournamentCtx,
);
const standings = tournament.bracketByIdx(0)!.standings;
assert.equal(standings[0].team.id, 4);
},
);
RoundRobinStandings.run();

View File

@ -2,6 +2,7 @@ import { BRACKET_NAMES } from "~/features/tournament/tournament-constants";
import { Tournament } from "../Tournament";
import type { TournamentData } from "../Tournament.server";
import type { DataTypes, ValueToArray } from "~/modules/brackets-manager/types";
import { removeDuplicates } from "~/utils/arrays";
const tournamentCtxTeam = (
teamId: number,
@ -37,6 +38,12 @@ export const testTournament = (
data: ValueToArray<DataTypes>,
partialCtx?: Partial<TournamentData["ctx"]>,
) => {
const participant = removeDuplicates(
data.match
.flatMap((m) => [m.opponent1?.id, m.opponent2?.id])
.filter(Boolean),
) as number[];
return new Tournament({
data,
ctx: {
@ -68,10 +75,7 @@ export const testTournament = (
createdAt: 0,
})),
castedMatchesInfo: null,
teams: nTeams(
data.participant.length,
Math.min(...data.participant.map((p) => p.id)),
),
teams: nTeams(participant.length, Math.min(...participant)),
author: {
chatNameColor: null,
customUrl: null,

View File

@ -357,7 +357,7 @@ export default function TournamentBracketsPage() {
bracketIdx === 0
? tournament.ctx.teams.length
: (bracket.teamsPendingCheckIn ?? []).length +
bracket.data.participant.length;
bracket.participantTournamentTeamIds.length;
return (
<div>
@ -401,7 +401,7 @@ export default function TournamentBracketsPage() {
alertClassName="tournament-bracket__start-bracket-alert"
textClassName="stack horizontal md items-center"
>
{bracket.data.participant.length}/
{bracket.participantTournamentTeamIds.length}/
{totalTeamsAvailableForTheBracket()} teams checked in
{bracket.canBeStarted ? (
<BracketStarter bracket={bracket} bracketIdx={bracketIdx} />
@ -506,7 +506,11 @@ function MiniCheckinInfoBanner({
if (!teamMemberOf) return null;
if (bracket.data.participant.some((p) => p.id === teamMemberOf.id)) {
if (
bracket.participantTournamentTeamIds.some(
(tournamentTeamId) => tournamentTeamId === teamMemberOf.id,
)
) {
return (
<div className="tournament-bracket__mini-alert">
Your team is checked in to the bracket (ask the TO for a check-out if

View File

@ -1,107 +1,11 @@
import type {
Match,
Seeding,
Stage,
GroupType,
} from "~/modules/brackets-model";
import type { GroupType, Match, Stage } from "~/modules/brackets-model";
import { Status } from "~/modules/brackets-model";
import type { DeepPartial, ParticipantSlot, Side } from "../types";
import type { SetNextOpponent } from "../helpers";
import { ordering } from "../ordering";
import { Create } from "../create";
import { BaseGetter } from "./getter";
import { Get } from "../get";
import * as helpers from "../helpers";
import type { DeepPartial, Side } from "../types";
import { BaseGetter } from "./getter";
export class BaseUpdater extends BaseGetter {
/**
* Updates or resets the seeding of a stage.
*
* @param stageId ID of the stage.
* @param seeding A new seeding or `null` to reset the existing seeding.
*/
protected updateSeeding(stageId: number, seeding: Seeding | null): void {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
const create = new Create(this.storage, {
name: stage.name,
tournamentId: stage.tournament_id,
type: stage.type,
settings: stage.settings,
seeding: seeding || undefined,
});
create.setExisting(stageId, false);
const method = BaseGetter.getSeedingOrdering(stage.type, create);
const slots = create.getSlots();
const matches = this.getSeedingMatches(stage.id, stage.type);
if (!matches)
throw Error("Error getting matches associated to the seeding.");
const ordered = ordering[method](slots);
BaseUpdater.assertCanUpdateSeeding(matches, ordered);
create.run();
}
/**
* Confirms the current seeding of a stage.
*
* @param stageId ID of the stage.
*/
protected confirmCurrentSeeding(stageId: number): void {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
const get = new Get(this.storage);
const currentSeeding = get.seeding(stageId);
const newSeeding = helpers.convertSlotsToSeeding(
currentSeeding.map(helpers.convertTBDtoBYE),
);
const create = new Create(this.storage, {
name: stage.name,
tournamentId: stage.tournament_id,
type: stage.type,
settings: stage.settings,
seeding: newSeeding,
});
create.setExisting(stageId, true);
create.run();
}
/**
* Throws an error if a match is locked and the new seeding will change this match's participants.
*
* @param matches The matches stored in the database.
* @param slots The slots to check from the new seeding.
*/
protected static assertCanUpdateSeeding(
matches: Match[],
slots: ParticipantSlot[],
): void {
let index = 0;
for (const match of matches) {
const opponent1 = slots[index++];
const opponent2 = slots[index++];
const locked = helpers.isMatchParticipantLocked(match);
if (!locked) continue;
if (
match.opponent1?.id !== opponent1?.id ||
match.opponent2?.id !== opponent2?.id
)
throw Error("A match is locked.");
}
}
/**
* Updates the matches related (previous and next) to a match.
*

View File

@ -2,10 +2,9 @@ import type {
Group,
InputStage,
Match,
Participant,
Round,
Seeding,
SeedOrdering,
Seeding,
Stage,
} from "~/modules/brackets-model";
import { defaultMinorOrdering, ordering } from "./ordering";
@ -569,37 +568,7 @@ export class Create {
this.stage.settings.size,
);
if (helpers.isSeedingWithIds(this.stage.seeding))
return this.getSlotsUsingIds(this.stage.seeding, positions);
return this.getSlotsUsingNames(this.stage.seeding, positions);
}
/**
* Returns the list of slots with a seeding containing names. Participants may be added to database.
*
* @param seeding The seeding (names).
* @param positions An optional list of positions (seeds) for a manual ordering.
*/
private getSlotsUsingNames(
seeding: Seeding,
positions?: number[],
): ParticipantSlot[] {
const participants = helpers.extractParticipantsFromSeeding(
this.stage.tournamentId,
seeding,
);
if (!this.registerParticipants(participants))
throw Error("Error registering the participants.");
// Get participants back with IDs.
const added = this.storage.select("participant", {
tournament_id: this.stage.tournamentId,
});
if (!added) throw Error("Error getting registered participant.");
return helpers.mapParticipantsNamesToDatabase(seeding, added, positions);
return this.getSlotsUsingIds(this.stage.seeding, positions);
}
/**
@ -612,16 +581,21 @@ export class Create {
seeding: Seeding,
positions?: number[],
): ParticipantSlot[] {
const participants = this.storage.select("participant", {
tournament_id: this.stage.tournamentId,
});
if (!participants) throw Error("No available participants.");
if (positions && positions.length !== seeding.length) {
throw Error(
"Not enough seeds in at least one group of the manual ordering.",
);
}
return helpers.mapParticipantsIdsToDatabase(
seeding,
participants,
positions,
);
const slots = seeding.map((slot, i) => {
if (slot === null) return null; // BYE.
return { id: slot, position: i + 1 };
});
if (!positions) return slots;
return positions.map((position) => slots[position - 1]);
}
/**
@ -851,31 +825,6 @@ export class Create {
return existing.id;
}
/**
* Inserts missing participants.
*
* @param participants The list of participants to process.
*/
private registerParticipants(participants: OmitId<Participant>[]): boolean {
const existing = this.storage.select("participant", {
tournament_id: this.stage.tournamentId,
});
// Insert all if nothing.
if (!existing || existing.length === 0)
return this.storage.insert("participant", participants);
// Insert only missing otherwise.
for (const participant of participants) {
if (existing.some((value) => value.name === participant.name)) continue;
const result = this.storage.insert("participant", participant);
if (result === -1) return false;
}
return true;
}
/**
* Creates a new stage.
*/

View File

@ -1,12 +1,6 @@
import type {
Stage,
Group,
Round,
Match,
Participant,
} from "~/modules/brackets-model";
import type { Stage, Group, Round, Match } from "~/modules/brackets-model";
import { Status } from "~/modules/brackets-model";
import type { Database, FinalStandingsItem, ParticipantSlot } from "./types";
import type { Database, ParticipantSlot } from "./types";
import { BaseGetter } from "./base/getter";
import * as helpers from "./helpers";
@ -19,17 +13,11 @@ export class Get extends BaseGetter {
public stageData(stageId: number): Database {
const stageData = this.getStageSpecificData(stageId);
const participants = this.storage.select("participant", {
tournament_id: stageData.stage.tournament_id,
});
if (!participants) throw Error("Error getting participants.");
return {
stage: [stageData.stage],
group: stageData.groups,
round: stageData.rounds,
match: stageData.matches,
participant: participants,
};
}
@ -48,11 +36,6 @@ export class Get extends BaseGetter {
this.getStageSpecificData(stage.id),
);
const participants = this.storage.select("participant", {
tournament_id: tournamentId,
});
if (!participants) throw Error("Error getting participants.");
return {
stage: stages,
group: stagesData.reduce(
@ -67,7 +50,6 @@ export class Get extends BaseGetter {
(acc, data) => [...acc, ...data.matches],
[] as Match[],
),
participant: participants,
};
}
@ -214,30 +196,6 @@ export class Get extends BaseGetter {
return this.eliminationSeeding(stage).map(pickRelevantProps);
}
/**
* Returns the final standings of a stage.
*
* @param stageId ID of the stage.
*/
public finalStandings(stageId: number): FinalStandingsItem[] {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
switch (stage.type) {
case "round_robin":
throw Error("A round-robin stage does not have standings.");
case "single_elimination":
return this.singleEliminationStandings(stageId);
case "double_elimination":
if (stage.settings.size === 2) {
return this.singleEliminationStandings(stageId);
}
return this.doubleEliminationStandings(stageId);
default:
throw Error("Unknown stage type.");
}
}
/**
* Returns the seeding of a round-robin stage.
*
@ -282,134 +240,6 @@ export class Get extends BaseGetter {
return helpers.convertMatchesToSeeding(matches);
}
/**
* Returns the final standings of a single elimination stage.
*
* @param stageId ID of the stage.
*/
private singleEliminationStandings(stageId: number): FinalStandingsItem[] {
const grouped: Participant[][] = [];
const {
stage: stages,
group: groups,
match: matches,
participant: participants,
} = this.stageData(stageId);
const [stage] = stages;
const [singleBracket, finalGroup] = groups;
const final = matches
.filter((match) => match.group_id === singleBracket.id)
.pop();
if (!final) throw Error("Final not found.");
// 1st place: Final winner.
grouped[0] = [
helpers.findParticipant(participants, getFinalWinnerIfDefined(final)),
];
// Rest: every loser in reverse order.
const losers = helpers.getLosers(
participants,
matches.filter((match) => match.group_id === singleBracket.id),
);
grouped.push(...losers.reverse());
if (stage.settings?.consolationFinal) {
const consolationFinal = matches
.filter((match) => match.group_id === finalGroup.id)
.pop();
if (!consolationFinal) throw Error("Consolation final not found.");
const consolationFinalWinner = helpers.findParticipant(
participants,
getFinalWinnerIfDefined(consolationFinal),
);
const consolationFinalLoser = helpers.findParticipant(
participants,
helpers.getLoser(consolationFinal),
);
// Overwrite semi-final losers with the consolation final results.
grouped.splice(2, 1, [consolationFinalWinner], [consolationFinalLoser]);
}
return helpers.makeFinalStandings(grouped);
}
/**
* Returns the final standings of a double elimination stage.
*
* @param stageId ID of the stage.
*/
private doubleEliminationStandings(stageId: number): FinalStandingsItem[] {
const grouped: Participant[][] = [];
const {
stage: stages,
group: groups,
match: matches,
participant: participants,
} = this.stageData(stageId);
const [stage] = stages;
const [winnerBracket, loserBracket, finalGroup] = groups;
if (stage.settings?.grandFinal === "none") {
const finalWB = matches
.filter((match) => match.group_id === winnerBracket.id)
.pop();
if (!finalWB) throw Error("WB final not found.");
const finalLB = matches
.filter((match) => match.group_id === loserBracket.id)
.pop();
if (!finalLB) throw Error("LB final not found.");
// 1st place: WB Final winner.
grouped[0] = [
helpers.findParticipant(participants, getFinalWinnerIfDefined(finalWB)),
];
// 2nd place: LB Final winner.
grouped[1] = [
helpers.findParticipant(participants, getFinalWinnerIfDefined(finalLB)),
];
} else {
const grandFinalMatches = matches.filter(
(match) => match.group_id === finalGroup.id,
);
const decisiveMatch = helpers.getGrandFinalDecisiveMatch(
stage.settings?.grandFinal || "none",
grandFinalMatches,
);
// 1st place: Grand Final winner.
grouped[0] = [
helpers.findParticipant(
participants,
getFinalWinnerIfDefined(decisiveMatch),
),
];
// 2nd place: Grand Final loser.
grouped[1] = [
helpers.findParticipant(participants, helpers.getLoser(decisiveMatch)),
];
}
// Rest: every loser in reverse order.
const losers = helpers.getLosers(
participants,
matches.filter((match) => match.group_id === loserBracket.id),
);
grouped.push(...losers.reverse());
return helpers.makeFinalStandings(grouped);
}
/**
* Returns only the data specific to the given stage (without the participants).
*
@ -441,9 +271,3 @@ export class Get extends BaseGetter {
};
}
}
const getFinalWinnerIfDefined = (match: Match): ParticipantSlot => {
const winner = helpers.getWinner(match);
if (!winner) throw Error("The final match does not have a winner.");
return winner;
};

View File

@ -2,9 +2,7 @@ import type {
GrandFinalType,
Match,
MatchResults,
Participant,
ParticipantResult,
CustomParticipant,
Result,
RoundRobinMode,
Seeding,
@ -19,16 +17,15 @@ import type {
Database,
DeepPartial,
Duel,
FinalStandingsItem,
IdMapping,
Nullable,
OmitId,
ParitySplit,
ParticipantSlot,
Scores,
Side,
} from "./types";
import { ordering } from "./ordering";
import invariant from "~/utils/invariant";
/**
* Splits an array of objects based on their values at a given key.
@ -234,7 +231,6 @@ export function balanceByes(
*/
export function normalizeIds(data: Database): Database {
const mappings = {
participant: makeNormalizedIdMapping(data.participant),
stage: makeNormalizedIdMapping(data.stage),
group: makeNormalizedIdMapping(data.group),
round: makeNormalizedIdMapping(data.round),
@ -242,10 +238,6 @@ export function normalizeIds(data: Database): Database {
};
return {
participant: data.participant.map((value) => ({
...value,
id: mappings.participant[value.id],
})),
stage: data.stage.map((value) => ({
...value,
id: mappings.stage[value.id],
@ -267,8 +259,8 @@ export function normalizeIds(data: Database): Database {
stage_id: mappings.stage[value.stage_id],
group_id: mappings.group[value.group_id],
round_id: mappings.round[value.round_id],
opponent1: normalizeParticipant(value.opponent1, mappings.participant),
opponent2: normalizeParticipant(value.opponent2, mappings.participant),
opponent1: value.opponent1,
opponent2: value.opponent2,
})),
};
}
@ -912,11 +904,8 @@ export function getOriginPosition(match: Match, side: Side): number {
* @param participants The list of participants.
* @param matches A list of matches to get losers of.
*/
export function getLosers(
participants: Participant[],
matches: Match[],
): Participant[][] {
const losers: Participant[][] = [];
export function getLosers(matches: Match[]): number[][] {
const losers: number[][] = [];
let currentRound: number | null = null;
let roundIndex = -1;
@ -931,38 +920,13 @@ export function getLosers(
const loser = getLoser(match);
if (loser === null) continue;
losers[roundIndex].push(findParticipant(participants, loser));
invariant(loser.id, "Loser id not found");
losers[roundIndex].push(loser.id);
}
return losers;
}
/**
* Makes final standings based on participants grouped by ranking.
*
* @param grouped A list of participants grouped by ranking.
*/
export function makeFinalStandings(
grouped: Participant[][],
): FinalStandingsItem[] {
const standings: FinalStandingsItem[] = [];
let rank = 1;
for (const group of grouped) {
for (const participant of group) {
standings.push({
id: participant.id,
name: participant.name,
rank,
});
}
rank++;
}
return standings;
}
/**
* Returns the decisive match of a Grand Final.
*
@ -986,24 +950,6 @@ export function getGrandFinalDecisiveMatch(
throw Error("The Grand Final is disabled.");
}
/**
* Finds a participant in a list.
*
* @param participants The list of participants.
* @param slot The slot of the participant to find.
*/
export function findParticipant(
participants: Participant[],
slot: ParticipantSlot,
): Participant {
if (!slot) throw Error("Cannot find a BYE participant.");
const participant = participants.find(
(participant) => participant.id === slot?.id,
);
if (!participant) throw Error("Participant not found.");
return participant;
}
/**
* Gets the side the winner of the current match will go to in the next match.
*
@ -1226,115 +1172,6 @@ export function setResults(
}
}
/**
* Indicates if a seeding is filled with participants' IDs.
*
* @param seeding The seeding.
*/
export function isSeedingWithIds(seeding: Seeding): boolean {
return seeding.some((value) => typeof value === "number");
}
/**
* Extracts participants from a seeding, without the BYEs.
*
* @param tournamentId ID of the tournament.
* @param seeding The seeding (no IDs).
*/
export function extractParticipantsFromSeeding(
tournamentId: number,
seeding: Seeding,
): OmitId<Participant>[] {
const withoutByes = seeding.filter(
(name): name is /* number */ string | CustomParticipant => name !== null,
);
const participants = withoutByes.map<OmitId<Participant>>((item) => {
if (typeof item === "string") {
return {
tournament_id: tournamentId,
name: item,
};
}
return {
...item,
tournament_id: tournamentId,
name: item.name,
};
});
return participants;
}
/**
* Returns participant slots mapped to the instances stored in the database thanks to their name.
*
* @param seeding The seeding.
* @param database The participants stored in the database.
* @param positions An optional list of positions (seeds) for a manual ordering.
*/
export function mapParticipantsNamesToDatabase(
seeding: Seeding,
database: Participant[],
positions?: number[],
): ParticipantSlot[] {
return mapParticipantsToDatabase("name", seeding, database, positions);
}
/**
* Returns participant slots mapped to the instances stored in the database thanks to their id.
*
* @param seeding The seeding.
* @param database The participants stored in the database.
* @param positions An optional list of positions (seeds) for a manual ordering.
*/
export function mapParticipantsIdsToDatabase(
seeding: Seeding,
database: Participant[],
positions?: number[],
): ParticipantSlot[] {
return mapParticipantsToDatabase("id", seeding, database, positions);
}
/**
* Returns participant slots mapped to the instances stored in the database thanks to a property of theirs.
*
* @param prop The property to search participants with.
* @param seeding The seeding.
* @param database The participants stored in the database.
* @param positions An optional list of positions (seeds) for a manual ordering.
*/
export function mapParticipantsToDatabase(
prop: "id" | "name",
seeding: Seeding,
database: Participant[],
positions?: number[],
): ParticipantSlot[] {
const slots = seeding.map((slot, i) => {
if (slot === null) return null; // BYE.
const found = database.find((participant) =>
typeof slot === "object"
? participant[prop] === slot[prop]
: participant[prop] === slot,
);
if (!found) throw Error(`Participant ${prop} not found in database.`);
return { id: found.id, position: i + 1 };
});
if (!positions) return slots;
if (positions.length !== slots.length)
throw Error(
"Not enough seeds in at least one group of the manual ordering.",
);
return positions.map((position) => slots[position - 1]); // Because `position` is `i + 1`.
}
/**
* Converts a list of matches to a seeding.
*

View File

@ -78,11 +78,6 @@ export class BracketsManager {
public import(data: Database, normalizeIds = false): void {
if (normalizeIds) data = helpers.normalizeIds(data);
if (!this.storage.delete("participant"))
throw Error("Could not empty the participant table.");
if (!this.storage.insert("participant", data.participant))
throw Error("Could not import participants.");
if (!this.storage.delete("stage"))
throw Error("Could not empty the stage table.");
if (!this.storage.insert("stage", data.stage))
@ -108,9 +103,6 @@ export class BracketsManager {
* Exports data from the database.
*/
public export(): Database {
const participants = this.storage.select("participant");
if (!participants) throw Error("Error getting participants.");
const stages = this.storage.select("stage");
if (!stages) throw Error("Error getting stages.");
@ -124,7 +116,6 @@ export class BracketsManager {
if (!matches) throw Error("Error getting matches.");
return {
participant: participants,
stage: stages,
group: groups,
round: rounds,

View File

@ -51,13 +51,4 @@ export class Reset extends BaseUpdater {
if (!helpers.isRoundRobin(stage) && !helpers.isSwiss(stage))
this.updateRelatedMatches(stored, true, true);
}
/**
* Resets the seeding of a stage.
*
* @param stageId ID of the stage.
*/
public seeding(stageId: number): void {
this.updateSeeding(stageId, null);
}
}

View File

@ -2,68 +2,10 @@ import * as assert from "uvu/assert";
import { BracketsManager } from "../manager";
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { suite } from "uvu";
import type { InputStage } from "~/modules/brackets-model";
const storage = new InMemoryDatabase();
const manager = new BracketsManager(storage);
const createTournament = (tournamentType: any): InputStage => ({
name: "Amateur",
tournamentId: 0,
type: tournamentType,
seeding: [
{ name: "Team 1", nationality: "US", tournament_id: 0, id: 0 },
{ name: "Team 2", nationality: "US", tournament_id: 0, id: 1 },
{ name: "Team 3", nationality: "US", tournament_id: 0, id: 2 },
{ name: "Team 4", nationality: "US", tournament_id: 0, id: 3 },
{ name: "Team 5", nationality: "US", tournament_id: 0, id: 4 },
{ name: "Team 6", nationality: "US", tournament_id: 0, id: 5 },
{ name: "Team 7", nationality: "US", tournament_id: 0, id: 6 },
{ name: "Team 8", nationality: "US", tournament_id: 0, id: 7 },
{ name: "Team 9", nationality: "US", tournament_id: 0, id: 8 },
{ name: "Team 10", nationality: "US", tournament_id: 0, id: 9 },
{ name: "Team 11", nationality: "US", tournament_id: 0, id: 10 },
{ name: "Team 12", nationality: "US", tournament_id: 0, id: 11 },
{ name: "Team 13", nationality: "US", tournament_id: 0, id: 12 },
{ name: "Team 14", nationality: "US", tournament_id: 0, id: 13 },
{ name: "Team 15", nationality: "US", tournament_id: 0, id: 14 },
{ name: "Team 16", nationality: "US", tournament_id: 0, id: 15 },
],
settings:
tournamentType === "round_robin"
? { groupCount: 2 }
: { seedOrdering: ["natural"] },
});
const CustomSeeding = suite("Create tournaments with custom seeding");
CustomSeeding.before.each(() => {
storage.reset();
});
CustomSeeding("should create single elimination with custom seeding", () => {
manager.create(createTournament("single_elimination"));
const stageData = manager.get.stageData(0);
assert.equal((stageData.participant[0] as any).nationality, "US");
assert.equal(stageData.participant.length, 16);
});
CustomSeeding("should create double elimination with custom seeding", () => {
manager.create(createTournament("double_elimination"));
const stageData = manager.get.stageData(0);
assert.equal((stageData.participant[0] as any).nationality, "US");
assert.equal(stageData.participant.length, 16);
});
CustomSeeding("should create round robin with custom seeding", () => {
manager.create(createTournament("round_robin"));
const stageData = manager.get.stageData(0);
assert.equal((stageData.participant[0] as any).nationality, "US");
assert.equal(stageData.participant.length, 16);
});
const ExtraFields = suite("Update results with extra fields");
ExtraFields.before.each(() => {
@ -75,7 +17,7 @@ ExtraFields("Extra fields when updating a match", () => {
name: "Amateur",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
});
manager.update.match({
@ -127,5 +69,4 @@ ExtraFields("Extra fields when updating a match", () => {
});
});
CustomSeeding.run();
ExtraFields.run();

View File

@ -17,7 +17,7 @@ DeleteStage("should delete a stage and all its linked data", () => {
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
});
manager.delete.stage(0);
@ -38,14 +38,14 @@ DeleteStage("should delete one stage and only its linked data", () => {
name: "Example 1",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
});
manager.create({
name: "Example 2",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
});
manager.delete.stage(0);
@ -72,14 +72,14 @@ DeleteStage("should delete all stages of the tournament", () => {
name: "Example 1",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
});
manager.create({
name: "Example 2",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
});
manager.delete.tournament(0);

View File

@ -18,24 +18,7 @@ CreateDoubleElimination("should create a double elimination stage", () => {
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
"Team 9",
"Team 10",
"Team 11",
"Team 12",
"Team 13",
"Team 14",
"Team 15",
"Team 16",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
});
@ -48,30 +31,6 @@ CreateDoubleElimination("should create a double elimination stage", () => {
assert.equal(storage.select<any>("match")!.length, 30);
});
CreateDoubleElimination(
"should create a double elimination stage with only two participants",
() => {
// This is an edge case. No lower bracket nor grand final will be created.
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
settings: { size: 2 },
});
assert.equal(storage.select<any>("group")!.length, 1);
assert.equal(storage.select<any>("round")!.length, 1);
assert.equal(storage.select<any>("match")!.length, 1);
// Ensure update works.
manager.update.seeding(0, ["Team 1", "Team 2"]);
manager.update.match({
id: 0,
opponent1: { result: "win" },
});
},
);
CreateDoubleElimination(
"should create a tournament with 256+ tournaments",
() => {
@ -91,16 +50,7 @@ CreateDoubleElimination(
name: "Example with double grand final",
tournamentId: 0,
type: "double_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: { grandFinal: "double", seedOrdering: ["natural"] },
});
@ -125,24 +75,7 @@ MatchUpdateDoubleElimination(
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
"Team 9",
"Team 10",
"Team 11",
"Team 12",
"Team 13",
"Team 14",
"Team 15",
"Team 16",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
});
@ -196,7 +129,7 @@ MatchUpdateDoubleElimination(
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: ["Team 1", "Team 2", "Team 3", null],
seeding: [1, 2, 3, null],
settings: { grandFinal: "simple" },
});
@ -234,7 +167,7 @@ MatchUpdateDoubleElimination("should determine matches in grand final", () => {
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
settings: { grandFinal: "double" },
});
@ -306,7 +239,7 @@ MatchUpdateDoubleElimination(
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
settings: { grandFinal: "double" },
});
@ -338,24 +271,7 @@ MatchUpdateDoubleElimination(
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
"Team 9",
"Team 10",
"Team 11",
"Team 12",
"Team 13",
"Team 14",
"Team 15",
"Team 16",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: {
seedOrdering: ["natural", "reverse", "reverse"],
grandFinal: "simple",
@ -397,16 +313,7 @@ MatchUpdateDoubleElimination(
name: "Example with inner_outer loser ordering",
tournamentId: 0,
type: "double_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
seedOrdering: ["inner_outer", "inner_outer"],
},
@ -423,7 +330,7 @@ MatchUpdateDoubleElimination(
opponent1: { result: "win" }, // Loser id: 7.
});
assert.equal(storage.select<any>("match", 7).opponent1.id, 7);
assert.equal(storage.select<any>("match", 7).opponent1.id, 8);
// Match of position 2.
manager.update.match({
@ -431,7 +338,7 @@ MatchUpdateDoubleElimination(
opponent1: { result: "win" }, // Loser id: 4.
});
assert.equal(storage.select<any>("match", 8).opponent1.id, 4);
assert.equal(storage.select<any>("match", 8).opponent1.id, 5);
// Match of position 3.
manager.update.match({
@ -439,7 +346,7 @@ MatchUpdateDoubleElimination(
opponent1: { result: "win" }, // Loser id: 6.
});
assert.equal(storage.select<any>("match", 8).opponent2.id, 6);
assert.equal(storage.select<any>("match", 8).opponent2.id, 7);
// Match of position 4.
manager.update.match({
@ -447,7 +354,7 @@ MatchUpdateDoubleElimination(
opponent1: { result: "win" }, // Loser id: 5.
});
assert.equal(storage.select<any>("match", 7).opponent2.id, 5);
assert.equal(storage.select<any>("match", 7).opponent2.id, 6);
},
);
@ -460,24 +367,7 @@ SkipFirstRoundDoubleElimination.before.each(() => {
name: "Example with double grand final",
tournamentId: 0,
type: "double_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
"Team 9",
"Team 10",
"Team 11",
"Team 12",
"Team 13",
"Team 14",
"Team 15",
"Team 16",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: {
seedOrdering: ["natural"],
skipFirstRound: true,
@ -498,8 +388,8 @@ SkipFirstRoundDoubleElimination(
assert.equal(storage.select<any>("round", 0).number, 1); // Even though the "real" first round is skipped, the stored first round's number should be 1.
assert.equal(storage.select<any>("match", 0).opponent1.id, 0); // First match of WB.
assert.equal(storage.select<any>("match", 7).opponent1.id, 1); // First match of LB.
assert.equal(storage.select<any>("match", 0).opponent1.id, 1); // First match of WB.
assert.equal(storage.select<any>("match", 7).opponent1.id, 2); // First match of LB.
},
);
@ -507,31 +397,31 @@ SkipFirstRoundDoubleElimination(
"should choose the correct previous and next matches",
() => {
manager.update.match({ id: 0, opponent1: { result: "win" } });
assert.equal(storage.select<any>("match", 7).opponent1.id, 1); // First match of LB Round 1 (must stay).
assert.equal(storage.select<any>("match", 12).opponent1.id, 2); // First match of LB Round 2 (must be updated).
assert.equal(storage.select<any>("match", 7).opponent1.id, 2); // First match of LB Round 1 (must stay).
assert.equal(storage.select<any>("match", 12).opponent1.id, 3); // First match of LB Round 2 (must be updated).
manager.update.match({ id: 1, opponent1: { result: "win" } });
assert.equal(storage.select<any>("match", 7).opponent2.id, 3); // First match of LB Round 1 (must stay).
assert.equal(storage.select<any>("match", 11).opponent1.id, 6); // Second match of LB Round 2 (must be updated).
assert.equal(storage.select<any>("match", 7).opponent2.id, 4); // First match of LB Round 1 (must stay).
assert.equal(storage.select<any>("match", 11).opponent1.id, 7); // Second match of LB Round 2 (must be updated).
manager.update.match({ id: 4, opponent1: { result: "win" } }); // First match of WB Round 2.
assert.equal(storage.select<any>("match", 18).opponent1.id, 4); // First match of LB Round 4.
assert.equal(storage.select<any>("match", 18).opponent1.id, 5); // First match of LB Round 4.
manager.update.match({ id: 7, opponent1: { result: "win" } }); // First match of LB Round 1.
assert.equal(storage.select<any>("match", 11).opponent2.id, 1); // First match of LB Round 2.
assert.equal(storage.select<any>("match", 11).opponent2.id, 2); // First match of LB Round 2.
for (let i = 2; i < 21; i++)
manager.update.match({ id: i, opponent1: { result: "win" } });
assert.equal(storage.select<any>("match", 15).opponent1.id, 6); // First match of LB Round 3.
assert.equal(storage.select<any>("match", 15).opponent1.id, 7); // First match of LB Round 3.
assert.equal(storage.select<any>("match", 21).opponent1.id, 0); // GF Round 1.
assert.equal(storage.select<any>("match", 21).opponent2.id, 8); // GF Round 1.
assert.equal(storage.select<any>("match", 21).opponent1.id, 1); // GF Round 1.
assert.equal(storage.select<any>("match", 21).opponent2.id, 9); // GF Round 1.
manager.update.match({ id: 21, opponent2: { result: "win" } });
assert.equal(storage.select<any>("match", 21).opponent1.id, 0); // GF Round 2.
assert.equal(storage.select<any>("match", 22).opponent2.id, 8); // GF Round 2.
assert.equal(storage.select<any>("match", 21).opponent1.id, 1); // GF Round 2.
assert.equal(storage.select<any>("match", 22).opponent2.id, 9); // GF Round 2.
manager.update.match({ id: 22, opponent2: { result: "win" } });
},

View File

@ -19,16 +19,7 @@ FindSingleElimination("should find previous matches", () => {
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
});
const beforeFirst = manager.find.previousMatches(0);
@ -55,16 +46,7 @@ FindSingleElimination("should find next matches", () => {
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
});
const afterFirst = manager.find.nextMatches(0);
@ -86,33 +68,24 @@ FindSingleElimination(
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
seedOrdering: ["natural"],
},
});
manager.update.match({ id: 0, opponent1: { result: "loss" } });
const afterFirstEliminated = manager.find.nextMatches(0, 0);
const afterFirstEliminated = manager.find.nextMatches(0, 1);
assert.equal(afterFirstEliminated.length, 0);
const afterFirstContinued = manager.find.nextMatches(0, 1);
const afterFirstContinued = manager.find.nextMatches(0, 2);
assert.equal(afterFirstContinued.length, 1);
manager.update.match({ id: 1, opponent1: { result: "win" } });
const beforeSemi1Up = manager.find.previousMatches(4, 1);
const beforeSemi1Up = manager.find.previousMatches(4, 2);
assert.equal(beforeSemi1Up.length, 1);
assert.equal(beforeSemi1Up[0].id, 0);
const beforeSemi1Down = manager.find.previousMatches(4, 2);
const beforeSemi1Down = manager.find.previousMatches(4, 3);
assert.equal(beforeSemi1Down.length, 1);
assert.equal(beforeSemi1Down[0].id, 1);
},
@ -131,16 +104,7 @@ FindDoubleElimination("should find previous matches", () => {
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
});
const beforeFirstWB = manager.find.previousMatches(0);
@ -187,16 +151,7 @@ FindDoubleElimination("should find next matches", () => {
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
});
const afterFirstWB = manager.find.nextMatches(0);
@ -236,33 +191,33 @@ FindDoubleElimination(
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
settings: {
seedOrdering: ["natural"],
},
});
manager.update.match({ id: 0, opponent1: { result: "loss" } });
const afterFirstEliminated = manager.find.nextMatches(0, 0);
const afterFirstEliminated = manager.find.nextMatches(0, 1);
assert.equal(afterFirstEliminated.length, 1);
assert.equal(afterFirstEliminated[0].id, 3);
const afterFirstContinued = manager.find.nextMatches(0, 1);
const afterFirstContinued = manager.find.nextMatches(0, 2);
assert.equal(afterFirstContinued.length, 1);
assert.equal(afterFirstContinued[0].id, 2);
manager.update.match({ id: 1, opponent1: { result: "win" } });
const beforeSemi1Up = manager.find.previousMatches(2, 1);
const beforeSemi1Up = manager.find.previousMatches(2, 2);
assert.equal(beforeSemi1Up.length, 1);
assert.equal(beforeSemi1Up[0].id, 0);
const beforeSemi1Down = manager.find.previousMatches(2, 2);
const beforeSemi1Down = manager.find.previousMatches(2, 3);
assert.equal(beforeSemi1Down.length, 1);
assert.equal(beforeSemi1Down[0].id, 1);
manager.update.match({ id: 3, opponent1: { result: "loss" } });
const afterLowerBracketEliminated = manager.find.nextMatches(3, 0);
const afterLowerBracketEliminated = manager.find.nextMatches(3, 1);
assert.equal(afterLowerBracketEliminated.length, 0);
const afterLowerBracketContinued = manager.find.nextMatches(3, 3);
const afterLowerBracketContinued = manager.find.nextMatches(3, 4);
assert.equal(afterLowerBracketContinued.length, 1);
assert.equal(afterLowerBracketContinued[0].id, 4);

View File

@ -17,11 +17,11 @@ BYEHandling("should propagate BYEs through the brackets", () => {
name: "Example with BYEs",
tournamentId: 0,
type: "double_elimination",
seeding: ["Team 1", null, null, null],
seeding: [1, null, null, null],
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
});
assert.equal(storage.select<any>("match", 2).opponent1.id, 0);
assert.equal(storage.select<any>("match", 2).opponent1.id, 1);
assert.equal(storage.select<any>("match", 2).opponent2, null);
assert.equal(storage.select<any>("match", 3).opponent1, null);
@ -30,7 +30,7 @@ BYEHandling("should propagate BYEs through the brackets", () => {
assert.equal(storage.select<any>("match", 4).opponent1, null);
assert.equal(storage.select<any>("match", 4).opponent2, null);
assert.equal(storage.select<any>("match", 5).opponent1.id, 0);
assert.equal(storage.select<any>("match", 5).opponent1.id, 1);
assert.equal(storage.select<any>("match", 5).opponent2, null);
});
@ -39,7 +39,7 @@ BYEHandling("should handle incomplete seeding during creation", () => {
name: "Example with BYEs",
tournamentId: 0,
type: "double_elimination",
seeding: ["Team 1", "Team 2"],
seeding: [1, 2],
settings: {
seedOrdering: ["natural"],
balanceByes: false, // Default value.
@ -47,8 +47,8 @@ BYEHandling("should handle incomplete seeding during creation", () => {
},
});
assert.equal(storage.select<any>("match", 0).opponent1.id, 0);
assert.equal(storage.select<any>("match", 0).opponent2.id, 1);
assert.equal(storage.select<any>("match", 0).opponent1.id, 1);
assert.equal(storage.select<any>("match", 0).opponent2.id, 2);
assert.equal(storage.select<any>("match", 1).opponent1, null);
assert.equal(storage.select<any>("match", 1).opponent2, null);
@ -59,7 +59,7 @@ BYEHandling("should balance BYEs in the seeding", () => {
name: "Example with BYEs",
tournamentId: 0,
type: "double_elimination",
seeding: ["Team 1", "Team 2"],
seeding: [1, 2],
settings: {
seedOrdering: ["natural"],
balanceByes: true,
@ -67,10 +67,10 @@ BYEHandling("should balance BYEs in the seeding", () => {
},
});
assert.equal(storage.select<any>("match", 0).opponent1.id, 0);
assert.equal(storage.select<any>("match", 0).opponent1.id, 1);
assert.equal(storage.select<any>("match", 0).opponent2, null);
assert.equal(storage.select<any>("match", 1).opponent1.id, 1);
assert.equal(storage.select<any>("match", 1).opponent1.id, 2);
assert.equal(storage.select<any>("match", 1).opponent2, null);
});
@ -131,42 +131,6 @@ SpecialCases.before.each(() => {
storage.reset();
});
SpecialCases(
"should create a stage and add participants IDs in seeding",
() => {
const teams = [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
];
const participants = teams.map((name) => ({
tournament_id: 0,
name,
}));
// Simulation of external database filling for participants.
storage.insert("participant", participants);
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
settings: { size: 8 },
});
// Update seeding with already existing IDs.
manager.update.seeding(0, [0, 1, 2, 3, 4, 5, 6, 7]);
assert.equal(storage.select<any>("match", 0).opponent1.id, 0);
},
);
SpecialCases("should throw if the name of the stage is not provided", () => {
assert.throws(
() =>
@ -203,15 +167,7 @@ SpecialCases(
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
],
seeding: [1, 2, 3, 4, 5, 6, 7],
}),
"The library only supports a participant count which is a power of two.",
);
@ -267,24 +223,7 @@ SeedingAndOrderingInElimination.before.each(() => {
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
"Team 9",
"Team 10",
"Team 11",
"Team 12",
"Team 13",
"Team 14",
"Team 15",
"Team 16",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: {
seedOrdering: [
"inner_outer",
@ -429,7 +368,7 @@ ResetMatchAndMatchGames("should reset results of a match", () => {
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2"],
seeding: [1, 2],
settings: {
seedOrdering: ["natural"],
size: 8,
@ -478,7 +417,7 @@ ResetMatchAndMatchGames(
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
settings: {
seedOrdering: ["natural"],
},
@ -517,7 +456,7 @@ ImportExport("should import data in the storage", () => {
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
settings: {
seedOrdering: ["natural"],
},
@ -552,90 +491,12 @@ ImportExport("should import data in the storage", () => {
assert.equal(storage.select<any>("match", 1).opponent1.result, undefined);
});
ImportExport("should import data in the storage with normalized IDs", () => {
storage.insert("participant", { name: "Unused team" });
manager.create({
name: "Example 1",
tournamentId: 0,
type: "round_robin",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
settings: {
groupCount: 1,
},
});
manager.create({
name: "Example 2",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 5", "Team 6", "Team 7", "Team 8"],
settings: {
seedOrdering: ["natural"],
},
});
const initialData = manager.get.stageData(1);
assert.equal(initialData.stage[0].id, 1);
assert.equal(initialData.participant[0], {
id: 1,
tournament_id: 0,
name: "Team 1",
});
assert.equal(initialData.group[0], { id: 1, stage_id: 1, number: 1 });
assert.equal(initialData.round[0], {
id: 3,
stage_id: 1,
group_id: 1,
number: 1,
});
assert.equal(initialData.match[0], {
id: 6,
stage_id: 1,
group_id: 1,
round_id: 3,
opponent1: { id: 5, position: 1 },
opponent2: { id: 6, position: 2 },
number: 1,
status: 2,
});
manager.import(initialData, true);
const data = manager.get.stageData(0);
assert.equal(data.stage[0].id, 0);
assert.equal(data.participant[0], {
id: 0,
tournament_id: 0,
name: "Team 1",
});
assert.equal(data.group[0], { id: 0, stage_id: 0, number: 1 });
assert.equal(data.round[0], {
id: 0,
stage_id: 0,
group_id: 0,
number: 1,
});
assert.equal(data.match[0], {
id: 0,
stage_id: 0,
group_id: 0,
round_id: 0,
opponent1: { id: 4, position: 1 },
opponent2: { id: 5, position: 2 },
number: 1,
status: 2,
});
});
ImportExport("should export data from the storage", () => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
settings: {
seedOrdering: ["natural"],
},
@ -643,11 +504,10 @@ ImportExport("should export data from the storage", () => {
const data = manager.export();
for (const key of ["participant", "stage", "group", "round", "match"]) {
for (const key of ["stage", "group", "round", "match"]) {
assert.ok(Object.keys(data).includes(key));
}
assert.equal(storage.select<any>("participant"), data.participant);
assert.equal(storage.select<any>("stage"), data.stage);
assert.equal(storage.select<any>("group"), data.group);
assert.equal(storage.select<any>("round"), data.round);

View File

@ -6,195 +6,6 @@ import * as assert from "uvu/assert";
const storage = new InMemoryDatabase();
const manager = new BracketsManager(storage);
const GetFinalStandings = suite("Get final standings");
GetFinalStandings.before.each(() => {
storage.reset();
});
GetFinalStandings(
"should get the final standings for a single elimination stage with consolation final",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
settings: { consolationFinal: true },
});
for (let i = 0; i < 8; i++) {
manager.update.match({
id: i,
...(i % 2 === 0
? { opponent1: { result: "win" } }
: { opponent2: { result: "win" } }),
});
}
const finalStandings = manager.get.finalStandings(0);
assert.equal(finalStandings, [
{ id: 0, name: "Team 1", rank: 1 },
{ id: 5, name: "Team 6", rank: 2 },
// The consolation final has inverted those ones (rank 3).
{ id: 1, name: "Team 2", rank: 3 },
{ id: 4, name: "Team 5", rank: 4 },
{ id: 7, name: "Team 8", rank: 5 },
{ id: 3, name: "Team 4", rank: 5 },
{ id: 6, name: "Team 7", rank: 5 },
{ id: 2, name: "Team 3", rank: 5 },
]);
},
);
GetFinalStandings(
"should get the final standings for a single elimination stage without consolation final",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
settings: { consolationFinal: false },
});
for (let i = 0; i < 7; i++) {
manager.update.match({
id: i,
...(i % 2 === 0
? { opponent1: { result: "win" } }
: { opponent2: { result: "win" } }),
});
}
const finalStandings = manager.get.finalStandings(0);
assert.equal(finalStandings, [
{ id: 0, name: "Team 1", rank: 1 },
{ id: 5, name: "Team 6", rank: 2 },
// Here, they are not inverted (rank 3).
{ id: 4, name: "Team 5", rank: 3 },
{ id: 1, name: "Team 2", rank: 3 },
{ id: 7, name: "Team 8", rank: 4 },
{ id: 3, name: "Team 4", rank: 4 },
{ id: 6, name: "Team 7", rank: 4 },
{ id: 2, name: "Team 3", rank: 4 },
]);
},
);
GetFinalStandings(
"should get the final standings for a double elimination stage with a grand final",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
settings: { grandFinal: "double" },
});
for (let i = 0; i < 15; i++) {
manager.update.match({
id: i,
...(i % 2 === 0
? { opponent1: { result: "win" } }
: { opponent2: { result: "win" } }),
});
}
const finalStandings = manager.get.finalStandings(0);
assert.equal(finalStandings, [
{ id: 0, name: "Team 1", rank: 1 },
{ id: 5, name: "Team 6", rank: 2 },
{ id: 4, name: "Team 5", rank: 3 },
{ id: 3, name: "Team 4", rank: 4 },
{ id: 1, name: "Team 2", rank: 5 },
{ id: 6, name: "Team 7", rank: 5 },
{ id: 7, name: "Team 8", rank: 6 },
{ id: 2, name: "Team 3", rank: 6 },
]);
},
);
GetFinalStandings(
"should get the final standings for a double elimination stage without a grand final",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
settings: { grandFinal: "none" },
});
for (let i = 0; i < 13; i++) {
manager.update.match({
id: i,
// The parity is reversed here, just to have different results.
...(i % 2 === 1
? { opponent1: { result: "win" } }
: { opponent2: { result: "win" } }),
});
}
const finalStandings = manager.get.finalStandings(0);
assert.equal(finalStandings, [
{ id: 6, name: "Team 7", rank: 1 },
{ id: 2, name: "Team 3", rank: 2 },
{ id: 3, name: "Team 4", rank: 3 },
{ id: 5, name: "Team 6", rank: 4 },
{ id: 0, name: "Team 1", rank: 5 },
{ id: 7, name: "Team 8", rank: 5 },
{ id: 4, name: "Team 5", rank: 6 },
{ id: 1, name: "Team 2", rank: 6 },
]);
},
);
const GetSeeding = suite("Get seeding");
GetSeeding("should get the seeding of a round-robin stage", () => {
@ -228,44 +39,13 @@ GetSeeding("should get the seeding of a round-robin stage with BYEs", () => {
groupCount: 2,
size: 8,
},
seeding: ["Team 1", null, null, null, null, null, null, null],
seeding: [1, null, null, null, null, null, null, null],
});
const seeding = manager.get.seeding(0);
assert.equal(seeding.length, 8);
});
GetSeeding(
"should get the seeding of a round-robin stage with BYEs after update",
() => {
storage.reset();
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
settings: {
groupCount: 2,
size: 8,
},
});
manager.update.seeding(0, [
"Team 1",
null,
null,
null,
null,
null,
null,
null,
]);
const seeding = manager.get.seeding(0);
assert.equal(seeding.length, 8);
},
);
GetSeeding("should get the seeding of a single elimination stage", () => {
storage.reset();
@ -289,16 +69,7 @@ GetSeeding("should get the seeding with BYEs", () => {
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [
"Team 1",
null,
"Team 3",
"Team 4",
"Team 5",
null,
null,
"Team 8",
],
seeding: [1, null, 2, 3, 4, null, null, 5],
settings: {
seedOrdering: ["inner_outer"],
},
@ -307,16 +78,15 @@ GetSeeding("should get the seeding with BYEs", () => {
const seeding = manager.get.seeding(0);
assert.equal(seeding.length, 8);
assert.equal(seeding, [
{ id: 0, position: 1 },
{ id: 1, position: 1 },
null,
{ id: 1, position: 3 },
{ id: 2, position: 4 },
{ id: 3, position: 5 },
{ id: 2, position: 3 },
{ id: 3, position: 4 },
{ id: 4, position: 5 },
null,
null,
{ id: 4, position: 8 },
{ id: 5, position: 8 },
]);
});
GetFinalStandings.run();
GetSeeding.run();

View File

@ -17,16 +17,7 @@ CreateRoundRobinStage("should create a round-robin stage", () => {
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: { groupCount: 2 },
} as any;
@ -48,16 +39,7 @@ CreateRoundRobinStage(
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
groupCount: 2,
manualOrdering: [
@ -92,16 +74,7 @@ CreateRoundRobinStage(
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
groupCount: 2,
manualOrdering: [[1, 4, 6, 7]],
@ -116,16 +89,7 @@ CreateRoundRobinStage(
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
groupCount: 2,
manualOrdering: [
@ -146,16 +110,7 @@ CreateRoundRobinStage(
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
null,
null,
null,
],
seeding: [1, 2, 3, 4, 5, null, null, null],
settings: { groupCount: 2 },
} as any;
@ -192,24 +147,15 @@ CreateRoundRobinStage(
name: "Example with effort balanced",
tournamentId: 0,
type: "round_robin",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
groupCount: 2,
seedOrdering: ["groups.seed_optimized"],
},
});
assert.equal(storage.select<any>("match", 0).opponent1.id, 0);
assert.equal(storage.select<any>("match", 0).opponent2.id, 7);
assert.equal(storage.select<any>("match", 0).opponent1.id, 1);
assert.equal(storage.select<any>("match", 0).opponent2.id, 8);
},
);
@ -253,7 +199,7 @@ UpdateRoundRobinScores.before.each(() => {
name: "Example scores",
tournamentId: 0,
type: "round_robin",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
settings: { groupCount: 1 },
});
});
@ -269,7 +215,7 @@ ExampleUseCase.before.each(() => {
name: "Example scores",
tournamentId: 0,
type: "round_robin",
seeding: ["POCEBLO", "twitch.tv/mrs_fly", "Ballec Squad", "AQUELLEHEURE?!"],
seeding: [1, 2, 3, 4],
settings: { groupCount: 1 },
});
});

View File

@ -18,24 +18,7 @@ CreateSingleEliminationStage("should create a single elimination stage", () => {
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
"Team 9",
"Team 10",
"Team 11",
"Team 12",
"Team 13",
"Team 14",
"Team 15",
"Team 16",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: { seedOrdering: ["natural"] },
} as any;
@ -57,20 +40,11 @@ CreateSingleEliminationStage(
name: "Example with BYEs",
tournamentId: 0,
type: "single_elimination",
seeding: [
"Team 1",
null,
"Team 3",
"Team 4",
null,
null,
"Team 7",
"Team 8",
],
seeding: [1, null, 3, 4, null, null, 7, 8],
settings: { seedOrdering: ["natural"] },
});
assert.equal(storage.select<any>("match", 4).opponent1.id, 0); // Determined because of opponent's BYE.
assert.equal(storage.select<any>("match", 4).opponent1.id, 1); // Determined because of opponent's BYE.
assert.equal(storage.select<any>("match", 4).opponent2.id, null); // To be determined.
assert.equal(storage.select<any>("match", 5).opponent1, null); // BYE propagated.
assert.equal(storage.select<any>("match", 5).opponent2.id, null); // To be determined.
@ -84,16 +58,7 @@ CreateSingleEliminationStage(
name: "Example with consolation final",
tournamentId: 0,
type: "single_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: { consolationFinal: true, seedOrdering: ["natural"] },
});
@ -110,21 +75,12 @@ CreateSingleEliminationStage(
name: "Example with consolation final and BYEs",
tournamentId: 0,
type: "single_elimination",
seeding: [
null,
null,
null,
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [null, null, null, 4, 5, 6, 7, 8],
settings: { consolationFinal: true, seedOrdering: ["natural"] },
});
assert.equal(storage.select<any>("match", 4).opponent1, null);
assert.equal(storage.select<any>("match", 4).opponent2.id, 0);
assert.equal(storage.select<any>("match", 4).opponent2.id, 4);
// Consolation final
assert.equal(storage.select<any>("match", 7).opponent1, null);
@ -139,16 +95,7 @@ CreateSingleEliminationStage(
name: "Example with Bo3 matches",
tournamentId: 0,
type: "single_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: { seedOrdering: ["natural"] },
});
@ -258,10 +205,10 @@ CreateSingleEliminationStage(
tournamentId: 0,
type: "single_elimination",
seeding: [
"Team 1",
"Team 1", // Duplicate
"Team 3",
"Team 4",
1,
1, // Duplicate
3,
4,
],
}),
"The seeding has a duplicate participant.",
@ -276,7 +223,7 @@ CreateSingleEliminationStage(
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
});
assert.throws(
@ -303,7 +250,7 @@ PreviousAndNextMatchUpdate(
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
settings: { consolationFinal: true },
});
@ -341,7 +288,7 @@ PreviousAndNextMatchUpdate(
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
settings: { consolationFinal: true },
});

View File

@ -11,24 +11,7 @@ const example = {
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
"Team 9",
"Team 10",
"Team 11",
"Team 12",
"Team 13",
"Team 14",
"Team 15",
"Team 16",
],
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: { seedOrdering: ["natural"] },
} as any;
@ -67,7 +50,7 @@ UpdateMatches(
assert.equal(after.opponent1.score, 2);
// Name should stay. It shouldn't be overwritten.
assert.equal(after.opponent1.id, 0);
assert.equal(after.opponent1.id, 1);
},
);
@ -94,7 +77,7 @@ UpdateMatches(
opponent1: { result: "win" },
});
assert.equal(storage.select<any>("match", 8).opponent1.id, 0);
assert.equal(storage.select<any>("match", 8).opponent1.id, 1);
manager.update.match({
id: 0,
@ -108,7 +91,7 @@ UpdateMatches(
const nextMatch = storage.select<any>("match", 8);
assert.equal(nextMatch.status, Status.Waiting);
assert.equal(nextMatch.opponent1.id, 1);
assert.equal(nextMatch.opponent1.id, 2);
},
);
@ -241,7 +224,7 @@ GiveOpponentIds.before.each(() => {
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: ["Team 1", "Team 2", "Team 3", "Team 4"],
seeding: [1, 2, 3, 4],
settings: { seedOrdering: ["natural"] },
});
});
@ -250,11 +233,11 @@ GiveOpponentIds("should update the right opponents based on their IDs", () => {
manager.update.match({
id: 0,
opponent1: {
id: 1,
id: 2,
score: 10,
},
opponent2: {
id: 0,
id: 1,
score: 5,
},
});
@ -271,7 +254,7 @@ GiveOpponentIds(
manager.update.match({
id: 0,
opponent1: {
id: 1,
id: 2,
score: 10,
},
});
@ -291,7 +274,7 @@ GiveOpponentIds(
manager.update.match({
id: 0,
opponent1: {
id: 2, // Belongs to match id 1.
id: 3, // Belongs to match id 1.
score: 10,
},
}),
@ -330,387 +313,6 @@ LockedMatches(
},
);
const Seeding = suite("Seeding");
Seeding.before.each(() => {
storage.reset();
manager.create({
name: "Without participants",
tournamentId: 0,
type: "double_elimination",
settings: {
size: 8,
seedOrdering: ["natural"],
},
});
});
Seeding("should update the seeding in a stage without any participant", () => {
manager.update.seeding(0, [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
]);
assert.equal(storage.select<any>("match", 0).opponent1.id, 0);
assert.equal(storage.select<any>("participant")!.length, 8);
});
Seeding("should update the seeding to remove participants", () => {
manager.update.seeding(0, [
"Team 1",
"Team 2",
null,
"Team 4",
"Team 5",
"Team 6",
"Team 7",
null,
]);
assert.equal(storage.select<any>("match", 0).opponent1.id, 0);
// In this context, a `null` value is not a BYE, but a TDB (to be determined)
// because we consider the tournament might have been started.
// If it's not and you prefer BYEs, just recreate the tournament.
assert.equal(storage.select<any>("match", 1).opponent1.id, null);
assert.equal(storage.select<any>("match", 3).opponent2.id, null);
});
Seeding("should handle incomplete seeding during seeding update", () => {
manager.update.seeding(0, ["Team 1", "Team 2"]);
assert.equal(storage.select<any>("match", 0).opponent1.id, 0);
assert.equal(storage.select<any>("match", 0).opponent2.id, 1);
// Same here, see comments above.
assert.equal(storage.select<any>("match", 1).opponent1.id, null);
assert.equal(storage.select<any>("match", 1).opponent2.id, null);
});
Seeding("should update BYE to TBD during seeding update", () => {
storage.reset();
manager.create({
name: "With participants and BYEs",
tournamentId: 0,
type: "double_elimination",
seeding: [
null,
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
],
settings: {
seedOrdering: ["natural"],
},
});
assert.equal(storage.select<any>("match", 0).opponent1, null);
manager.update.seeding(0, [
null,
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
]);
// To stay consistent with the fact that `update.seeding()` uses TBD and not BYE,
// the BYE should be updated to TDB here.
assert.equal(storage.select<any>("match", 0).opponent1.id, null);
});
Seeding("should reset the seeding of a stage", () => {
manager.update.seeding(0, [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
]);
manager.reset.seeding(0);
assert.equal(storage.select<any>("match", 0).opponent1.id, null);
assert.equal(storage.select<any>("participant")!.length, 8); // Participants aren't removed.
});
Seeding(
"should update the seeding in a stage with participants already",
() => {
manager.update.seeding(0, [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
]);
manager.update.seeding(0, [
"Team A",
"Team B",
"Team C",
"Team D",
"Team E",
"Team F",
"Team G",
"Team H",
]);
assert.equal(storage.select<any>("match", 0).opponent1.id, 8);
assert.equal(storage.select<any>("participant")!.length, 16);
},
);
Seeding(
"should update the seeding in a stage by registering only one missing participant",
() => {
manager.update.seeding(0, [
"Team A",
"Team B",
"Team C",
"Team D",
"Team E",
"Team F",
"Team G",
"Team H",
]);
manager.update.seeding(0, [
"Team A",
"Team B", // Match 0.
"Team C",
"Team D", // Match 1.
"Team E",
"Team F", // Match 2.
"Team G",
"Team Z", // Match 3.
]);
assert.equal(storage.select<any>("match", 0).opponent1.id, 0);
assert.equal(storage.select<any>("match", 3).opponent2.id, 8);
assert.equal(storage.select<any>("participant")!.length, 9);
},
);
Seeding("should update the seeding in a stage on non-locked matches", () => {
manager.update.seeding(0, [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
]);
manager.update.match({
id: 2, // Any match id.
opponent1: { score: 1 },
opponent2: { score: 0 },
});
manager.update.seeding(0, [
"Team A",
"Team B", // Match 0.
"Team C",
"Team D", // Match 1.
"Team 5",
"Team 6", // Match 2. NO CHANGE.
"Team G",
"Team H", // Match 3.
]);
assert.equal(storage.select<any>("match", 0).opponent1.id, 8); // New id.
assert.equal(storage.select<any>("match", 2).opponent1.id, 4); // Still old id.
assert.equal(storage.select<any>("participant")!.length, 8 + 6);
});
Seeding(
"should update the seeding and keep completed matches completed",
() => {
manager.update.seeding(0, [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
]);
manager.update.match({
id: 0,
opponent1: { score: 1, result: "win" },
opponent2: { score: 0 },
});
manager.update.seeding(0, [
"Team 1",
"Team 2", // Keep this pair.
"Team 4",
"Team 3",
"Team 6",
"Team 5",
"Team 8",
"Team 7",
]);
const match = storage.select<any>("match", 0);
assert.equal(match.opponent1.result, "win");
assert.equal(match.status, Status.Completed);
},
);
Seeding(
"should throw if a match is completed and would have to be changed",
() => {
manager.update.seeding(0, [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
]);
manager.update.match({
id: 0,
opponent1: { score: 1, result: "win" },
opponent2: { score: 0 },
});
assert.throws(
() =>
manager.update.seeding(0, [
"Team 2",
"Team 1", // Change this pair.
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
]),
"A match is locked.",
);
},
);
Seeding(
"should throw if a match is locked and would have to be changed",
() => {
manager.update.seeding(0, [
"Team 1",
"Team 2",
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
]);
manager.update.match({
id: 2, // Any match id.
opponent1: { score: 1 },
opponent2: { score: 0 },
});
assert.throws(
() =>
manager.update.seeding(0, [
"Team A",
"Team B", // Match 0.
"Team C",
"Team D", // Match 1.
"WILL",
"THROW", // Match 2.
"Team G",
"Team H", // Match 3.
]),
"A match is locked.",
);
},
);
Seeding("should throw if the seeding has duplicate participants", () => {
assert.throws(
() =>
manager.update.seeding(0, [
"Team 1",
"Team 1", // Duplicate
"Team 3",
"Team 4",
"Team 5",
"Team 6",
"Team 7",
"Team 8",
]),
"The seeding has a duplicate participant.",
);
});
Seeding("should confirm the current seeding", () => {
manager.update.seeding(0, [
"Team 1",
"Team 2",
null,
"Team 4",
"Team 5",
null,
null,
null,
]);
assert.equal(storage.select<any>("match", 1).opponent1.id, null); // First, is a TBD.
assert.equal(storage.select<any>("match", 2).opponent2.id, null);
assert.equal(storage.select<any>("match", 3).opponent1.id, null);
assert.equal(storage.select<any>("match", 3).opponent2.id, null);
manager.update.confirmSeeding(0);
assert.equal(storage.select<any>("participant")!.length, 4);
assert.equal(storage.select<any>("match", 1).opponent1, null); // Should become a BYE.
assert.equal(storage.select<any>("match", 2).opponent2, null);
assert.equal(storage.select<any>("match", 3).opponent1, null);
assert.equal(storage.select<any>("match", 3).opponent2, null);
assert.equal(storage.select<any>("match", 5).opponent2, null); // A BYE should be propagated here.
assert.equal(storage.select<any>("match", 7).opponent2, null); // All of these too (in loser bracket).
assert.equal(storage.select<any>("match", 8).opponent1, null);
assert.equal(storage.select<any>("match", 8).opponent2, null);
assert.equal(storage.select<any>("match", 9).opponent1, null);
assert.equal(storage.select<any>("match", 10).opponent2, null);
});
UpdateMatches.run();
GiveOpponentIds.run();
LockedMatches.run();
Seeding.run();

View File

@ -1,7 +1,6 @@
import type {
Group,
Match,
Participant,
Round,
SeedOrdering,
Stage,
@ -90,7 +89,6 @@ export interface DataTypes {
group: Group;
round: Round;
match: Match;
participant: Participant;
}
/**

View File

@ -1,9 +1,4 @@
import type {
Match,
Round,
Seeding,
SeedOrdering,
} from "~/modules/brackets-model";
import type { Match, Round, SeedOrdering } from "~/modules/brackets-model";
import { Status } from "~/modules/brackets-model";
import { ordering } from "./ordering";
import { BaseUpdater } from "./base/updater";
@ -65,27 +60,6 @@ export class Update extends BaseUpdater {
this.updateRoundOrdering(round, method);
}
/**
* Updates the seeding of a stage.
*
* @param stageId ID of the stage.
* @param seeding The new seeding.
*/
public seeding(stageId: number, seeding: Seeding): void {
this.updateSeeding(stageId, seeding);
}
/**
* Confirms the seeding of a stage.
*
* This will convert TBDs to BYEs and propagate them.
*
* @param stageId ID of the stage.
*/
public confirmSeeding(stageId: number): void {
this.confirmCurrentSeeding(stageId);
}
/**
* Update the seed ordering of a round.
*

View File

@ -8,7 +8,6 @@ import type {
export class InMemoryDatabase implements CrudInterface {
protected data: Database = {
participant: [],
stage: [],
group: [],
round: [],
@ -40,7 +39,6 @@ export class InMemoryDatabase implements CrudInterface {
*/
reset(): void {
this.data = {
participant: [],
stage: [],
group: [],
round: [],

View File

@ -2,7 +2,6 @@
* Contains everything which is given by the user as input.
*-----------------------------------------------------------*/
import type { Participant } from "./storage";
import type {
GrandFinalType,
RoundRobinMode,
@ -10,12 +9,6 @@ import type {
StageType,
} from "./unions";
/**
* A participant as it would be persisted in the storage, but with extra fields.
*/
export type CustomParticipant<ExtraFields = Record<string, unknown>> =
Participant & ExtraFields;
/**
* The seeding for a stage.
*
@ -25,7 +18,7 @@ export type CustomParticipant<ExtraFields = Record<string, unknown>> =
* - Its ID.
* - Or a BYE: `null`.
*/
export type Seeding = (CustomParticipant | string | number | null)[];
export type Seeding = (number | null)[];
/**
* Used to create a stage.

View File

@ -10,16 +10,6 @@ import type { StageType } from "./unions";
/**
* A participant of a stage (team or individual).
*/
export interface Participant {
/** ID of the participant. */
id: number;
/** ID of the tournament this participant belongs to. */
tournament_id: number;
/** Name of the participant. */
name: string;
}
/**
* A stage, which can be a round-robin stage or a single/double elimination stage.