brackets-manager
Version:
A simple library to manage tournament brackets (round-robin, single elimination, double elimination)
365 lines • 16.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Get = void 0;
const brackets_model_1 = require("brackets-model");
const getter_1 = require("./base/getter");
const helpers = require("./helpers");
class Get extends getter_1.BaseGetter {
/**
* Returns the data needed to display a stage.
*
* @param stageId ID of the stage.
*/
async stageData(stageId) {
const stage = await this.storage.select('stage', stageId);
if (!stage)
throw Error('Stage not found.');
const stageData = await this.getStageSpecificData(stage.id);
const participants = await this.storage.select('participant', { tournament_id: stage.tournament_id });
if (!participants)
throw Error('Error getting participants.');
return {
stage: [stage],
group: stageData.groups,
round: stageData.rounds,
match: stageData.matches,
match_game: stageData.matchGames,
participant: participants,
};
}
/**
* Returns the data needed to display a whole tournament with all its stages.
*
* @param tournamentId ID of the tournament.
*/
async tournamentData(tournamentId) {
const stages = await this.storage.select('stage', { tournament_id: tournamentId });
if (!stages)
throw Error('Error getting stages.');
const stagesData = await Promise.all(stages.map(stage => this.getStageSpecificData(stage.id)));
const participants = await this.storage.select('participant', { tournament_id: tournamentId });
if (!participants)
throw Error('Error getting participants.');
return {
stage: stages,
group: stagesData.reduce((acc, data) => [...acc, ...data.groups], []),
round: stagesData.reduce((acc, data) => [...acc, ...data.rounds], []),
match: stagesData.reduce((acc, data) => [...acc, ...data.matches], []),
match_game: stagesData.reduce((acc, data) => [...acc, ...data.matchGames], []),
participant: participants,
};
}
/**
* Returns the match games associated to a list of matches.
*
* @param matches A list of matches.
*/
async matchGames(matches) {
const parentMatches = matches.filter(match => match.child_count > 0);
const matchGamesQueries = await Promise.all(parentMatches.map(match => this.storage.select('match_game', { parent_id: match.id })));
if (matchGamesQueries.some(game => game === null))
throw Error('Error getting match games.');
return helpers.getNonNull(matchGamesQueries).flat();
}
/**
* Returns the stage that is not completed yet, because of uncompleted matches.
* If all matches are completed in this tournament, there is no "current stage", so `null` is returned.
*
* @param tournamentId ID of the tournament.
*/
async currentStage(tournamentId) {
const stages = await this.storage.select('stage', { tournament_id: tournamentId });
if (!stages)
throw Error('Error getting stages.');
for (const stage of stages) {
const matches = await this.storage.select('match', { stage_id: stage.id });
if (!matches)
throw Error('Error getting matches.');
if (matches.every(match => match.status >= brackets_model_1.Status.Completed))
continue;
return stage;
}
return null;
}
/**
* Returns the round that is not completed yet, because of uncompleted matches.
* If all matches are completed in this stage of a tournament, there is no "current round", so `null` is returned.
*
* Note: The consolation final of single elimination and the grand final of double elimination will be in a different `Group`.
*
* @param stageId ID of the stage.
* @example
* If you don't know the stage id, you can first get the current stage.
* ```js
* const tournamentId = 3;
* const currentStage = await manager.get.currentStage(tournamentId);
* const currentRound = await manager.get.currentRound(currentStage.id);
* ```
*/
async currentRound(stageId) {
const matches = await this.storage.select('match', { stage_id: stageId });
if (!matches)
throw Error('Error getting matches.');
const matchesByRound = helpers.splitBy(matches, 'round_id');
for (const roundMatches of matchesByRound) {
if (roundMatches.every(match => match.status >= brackets_model_1.Status.Completed))
continue;
const round = await this.storage.select('round', roundMatches[0].round_id);
if (!round)
throw Error('Round not found.');
return round;
}
return null;
}
/**
* Returns the matches that can currently be played in parallel.
* If the stage doesn't contain any, an empty array is returned.
*
* Note:
* - Returned matches are ongoing (i.e. ready or running).
* - Returned matches can be from different rounds.
*
* @param stageId ID of the stage.
* @example
* If you don't know the stage id, you can first get the current stage.
* ```js
* const tournamentId = 3;
* const currentStage = await manager.get.currentStage(tournamentId);
* const currentMatches = await manager.get.currentMatches(currentStage.id);
* ```
*/
async currentMatches(stageId) {
const stage = await this.storage.select('stage', stageId);
if (!stage)
throw Error('Stage not found.');
// TODO: Implement this for all stage types.
// - For round robin, 1 round per group can be played in parallel at their own pace.
// - For double elimination, 1 round per bracket (upper and lower) can be played in parallel at their own pace.
if (stage.type !== 'single_elimination')
throw Error('Not implemented for round robin and double elimination. Ask if needed.');
const matches = await this.storage.select('match', { stage_id: stageId });
if (!matches)
throw Error('Error getting matches.');
const matchesByRound = helpers.splitBy(matches, 'round_id');
const roundCount = helpers.getUpperBracketRoundCount(stage.settings.size);
// Save multiple queries for `round`.
let currentRoundIndex = -1;
const currentMatches = [];
for (const roundMatches of matchesByRound) {
currentRoundIndex++;
if (stage.settings.consolationFinal && currentRoundIndex === roundCount - 1) {
const [final] = roundMatches;
const [consolationFinal] = matchesByRound[currentRoundIndex + 1];
const finals = [final, consolationFinal];
if (finals.every(match => !helpers.isMatchOngoing(match)))
return currentMatches;
return finals.filter(match => helpers.isMatchOngoing(match));
}
if (roundMatches.every(match => !helpers.isMatchOngoing(match)))
continue;
currentMatches.push(...roundMatches.filter(match => helpers.isMatchOngoing(match)));
}
return currentMatches;
}
/**
* Returns the seeding of a stage.
*
* @param stageId ID of the stage.
*/
async seeding(stageId) {
const stage = await this.storage.select('stage', stageId);
if (!stage)
throw Error('Stage not found.');
const pickRelevantProps = (slot) => {
if (slot === null)
return null;
const { id, position } = slot;
return { id, position };
};
if (stage.type === 'round_robin')
return (await this.roundRobinSeeding(stage)).map(pickRelevantProps);
return (await this.eliminationSeeding(stage)).map(pickRelevantProps);
}
// eslint-disable-next-line jsdoc/require-jsdoc
async finalStandings(stageId, roundRobinOptions) {
const stage = await this.storage.select('stage', stageId);
if (!stage)
throw Error('Stage not found.');
switch (stage.type) {
case 'round_robin': {
if (!roundRobinOptions)
throw Error('Round-robin options are required for round-robin stages.');
return this.roundRobinStandings(stage, roundRobinOptions);
}
case 'single_elimination': {
if (roundRobinOptions)
throw Error('Round-robin options are not supported for elimination stages.');
return this.singleEliminationStandings(stage);
}
case 'double_elimination': {
if (roundRobinOptions)
throw Error('Round-robin options are not supported for elimination stages.');
return this.doubleEliminationStandings(stage);
}
default:
throw Error('Unknown stage type.');
}
}
/**
* Returns the seeding of a round-robin stage.
*
* @param stage The stage.
*/
async roundRobinSeeding(stage) {
if (stage.settings.size === undefined)
throw Error('The size of the seeding is undefined.');
const matches = await this.storage.select('match', { stage_id: stage.id });
if (!matches)
throw Error('Error getting matches.');
const slots = helpers.convertMatchesToSeeding(matches);
// BYE vs. BYE matches of a round-robin stage are removed
// when the stage is created. We need to add them back temporarily.
if (slots.length < stage.settings.size) {
const diff = stage.settings.size - slots.length;
for (let i = 0; i < diff; i++)
slots.push(null);
}
const unique = helpers.uniqueBy(slots, item => item && item.position);
const seeding = helpers.setArraySize(unique, stage.settings.size, null);
return seeding;
}
/**
* Returns the seeding of an elimination stage.
*
* @param stage The stage.
*/
async eliminationSeeding(stage) {
const firstRound = await this.storage.selectFirst('round', { stage_id: stage.id, number: 1 }, false);
if (!firstRound)
throw Error('Error getting the first round.');
const matches = await this.storage.select('match', { round_id: firstRound.id });
if (!matches)
throw Error('Error getting matches.');
return helpers.convertMatchesToSeeding(matches);
}
/**
* Returns the final standings of a round-robin stage.
*
* @param stage The stage.
* @param roundRobinOptions The options for the round-robin standings.
*/
async roundRobinStandings(stage, roundRobinOptions) {
const participants = await this.storage.select('participant', { tournament_id: stage.tournament_id });
if (!participants)
throw Error('Error getting participants.');
const matches = await this.storage.select('match', { stage_id: stage.id });
if (!matches)
throw Error('Error getting matches.');
const matchesByGroup = helpers.splitBy(matches, 'group_id');
const unsortedRanking = matchesByGroup.flatMap(groupMatches => {
const groupRanking = helpers.getRanking(groupMatches, roundRobinOptions.rankingFormula);
const qualifiedOnly = groupRanking.slice(0, roundRobinOptions.maxQualifiedParticipantsPerGroup);
return qualifiedOnly.map(item => ({
...item,
groupId: groupMatches[0].group_id,
name: helpers.findParticipant(participants, item).name,
}));
});
return unsortedRanking.sort((a, b) => a.rank - b.rank);
}
/**
* Returns the final standings of a single elimination stage.
*
* @param stage The stage.
*/
async singleEliminationStandings(stage) {
var _a;
const grouped = [];
const { group: groups, match: matches, participant: participants } = await this.stageData(stage.id);
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 ((_a = stage.settings) === null || _a === void 0 ? void 0 : _a.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 stage The stage.
*/
async doubleEliminationStandings(stage) {
var _a, _b;
const grouped = [];
const { group: groups, match: matches, participant: participants } = await this.stageData(stage.id);
const [winnerBracket, loserBracket, finalGroup] = groups;
if (((_a = stage.settings) === null || _a === void 0 ? void 0 : _a.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(((_b = stage.settings) === null || _b === void 0 ? void 0 : _b.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).
*
* @param stageId ID of the stage.
*/
async getStageSpecificData(stageId) {
const groups = await this.storage.select('group', { stage_id: stageId });
if (!groups)
throw Error('Error getting groups.');
const rounds = await this.storage.select('round', { stage_id: stageId });
if (!rounds)
throw Error('Error getting rounds.');
const matches = await this.storage.select('match', { stage_id: stageId });
if (!matches)
throw Error('Error getting matches.');
const matchGames = await this.matchGames(matches);
return {
groups,
rounds,
matches,
matchGames,
};
}
}
exports.Get = Get;
const getFinalWinnerIfDefined = (match) => {
const winner = helpers.getWinner(match);
if (!winner)
throw Error('The final match does not have a winner.');
return winner;
};
//# sourceMappingURL=get.js.map