UNPKG

brackets-manager

Version:

A simple library to manage tournament brackets (round-robin, single elimination, double elimination)

545 lines 23.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseGetter = void 0; const helpers = require("../helpers"); class BaseGetter { /** * Creates an instance of a Storage getter. * * @param storage The implementation of Storage. */ constructor(storage) { this.storage = storage; } /** * Gets all the rounds that contain ordered participants. * * @param stage The stage to get rounds from. */ async getOrderedRounds(stage) { if (!(stage === null || stage === void 0 ? void 0 : stage.settings.size)) throw Error('The stage has no size.'); if (stage.type === 'single_elimination') return this.getOrderedRoundsSingleElimination(stage.id); return this.getOrderedRoundsDoubleElimination(stage.id); } /** * Gets all the rounds that contain ordered participants in a single elimination stage. * * @param stageId ID of the stage. */ async getOrderedRoundsSingleElimination(stageId) { return [await this.getUpperBracketFirstRound(stageId)]; } /** * Gets all the rounds that contain ordered participants in a double elimination stage. * * @param stageId ID of the stage. */ async getOrderedRoundsDoubleElimination(stageId) { // Getting all rounds instead of cherry-picking them is the least expensive. const rounds = await this.storage.select('round', { stage_id: stageId }); if (!rounds) throw Error('Error getting rounds.'); const loserBracket = await this.getLoserBracket(stageId); if (!loserBracket) throw Error('Loser bracket not found.'); const firstRoundWB = rounds[0]; const roundsLB = rounds.filter(r => r.group_id === loserBracket.id); const orderedRoundsLB = roundsLB.filter(r => helpers.isOrderingSupportedLoserBracket(r.number, roundsLB.length)); return [firstRoundWB, ...orderedRoundsLB]; } /** * Gets the positional information (number in group and total number of rounds in group) of a round based on its id. * * @param roundId ID of the round. */ async getRoundPositionalInfo(roundId) { const round = await this.storage.select('round', roundId); if (!round) throw Error('Round not found.'); const rounds = await this.storage.select('round', { group_id: round.group_id }); if (!rounds) throw Error('Error getting rounds.'); return { roundNumber: round.number, roundCount: rounds.length, }; } /** * Gets the matches leading to the given match. * * @param match The current match. * @param matchLocation Location of the current match. * @param stage The parent stage. * @param roundNumber Number of the round. */ async getPreviousMatches(match, matchLocation, stage, roundNumber) { if (matchLocation === 'loser_bracket') return this.getPreviousMatchesLB(match, stage, roundNumber); if (matchLocation === 'final_group') return this.getPreviousMatchesFinal(match, stage, roundNumber); if (roundNumber === 1) return []; // The match is in the first round of an upper bracket. return this.getMatchesBeforeMajorRound(match, roundNumber); } /** * Gets the matches leading to the given match, which is in a final group (consolation final or grand final). * * @param match The current match. * @param stage The parent stage. * @param roundNumber Number of the current round. */ async getPreviousMatchesFinal(match, stage, roundNumber) { if (stage.type === 'single_elimination') return this.getPreviousMatchesFinalSingleElimination(match, stage); return this.getPreviousMatchesFinalDoubleElimination(match, roundNumber); } /** * Gets the matches leading to the given match, which is in a final group (consolation final). * * @param match The current match. * @param stage The parent stage. */ async getPreviousMatchesFinalSingleElimination(match, stage) { const upperBracket = await this.getUpperBracket(match.stage_id); const upperBracketRoundCount = helpers.getUpperBracketRoundCount(stage.settings.size); const semiFinalsRound = await this.storage.selectFirst('round', { group_id: upperBracket.id, number: upperBracketRoundCount - 1, // Second to last round }); if (!semiFinalsRound) throw Error('Semi finals round not found.'); const semiFinalMatches = await this.storage.select('match', { round_id: semiFinalsRound.id, }); if (!semiFinalMatches) throw Error('Error getting semi final matches.'); // In single elimination, both the final and consolation final have the same previous matches. return semiFinalMatches; } /** * Gets the matches leading to the given match, which is in a final group (grand final). * * @param match The current match. * @param roundNumber Number of the current round. */ async getPreviousMatchesFinalDoubleElimination(match, roundNumber) { if (roundNumber > 1) // Double grand final return [await this.findMatch(match.group_id, roundNumber - 1, 1)]; const winnerBracket = await this.getUpperBracket(match.stage_id); const lastRoundWB = await this.getLastRound(winnerBracket.id); const winnerBracketFinalMatch = await this.storage.selectFirst('match', { round_id: lastRoundWB.id, number: 1, }); if (!winnerBracketFinalMatch) throw Error('Match not found.'); const loserBracket = await this.getLoserBracket(match.stage_id); if (!loserBracket) throw Error('Loser bracket not found.'); const lastRoundLB = await this.getLastRound(loserBracket.id); const loserBracketFinalMatch = await this.storage.selectFirst('match', { round_id: lastRoundLB.id, number: 1, }); if (!loserBracketFinalMatch) throw Error('Match not found.'); return [winnerBracketFinalMatch, loserBracketFinalMatch]; } /** * Gets the matches leading to a given match from the loser bracket. * * @param match The current match. * @param stage The parent stage. * @param roundNumber Number of the round. */ async getPreviousMatchesLB(match, stage, roundNumber) { if (stage.settings.skipFirstRound && roundNumber === 1) return []; if (helpers.hasBye(match)) return []; // Shortcut because we are coming from propagateByes(). const winnerBracket = await this.getUpperBracket(match.stage_id); const actualRoundNumberWB = Math.ceil((roundNumber + 1) / 2); const roundNumberWB = stage.settings.skipFirstRound ? actualRoundNumberWB - 1 : actualRoundNumberWB; if (roundNumber === 1) return this.getMatchesBeforeFirstRoundLB(match, winnerBracket.id, roundNumberWB); if (helpers.isMajorRound(roundNumber)) return this.getMatchesBeforeMajorRound(match, roundNumber); return this.getMatchesBeforeMinorRoundLB(match, winnerBracket.id, roundNumber, roundNumberWB); } /** * Gets the matches leading to a given match in a major round (every round of upper bracket or specific ones in lower bracket). * * @param match The current match. * @param roundNumber Number of the round. */ async getMatchesBeforeMajorRound(match, roundNumber) { return [ await this.findMatch(match.group_id, roundNumber - 1, match.number * 2 - 1), await this.findMatch(match.group_id, roundNumber - 1, match.number * 2), ]; } /** * Gets the matches leading to a given match in the first round of the loser bracket. * * @param match The current match. * @param winnerBracketId ID of the winner bracket. * @param roundNumberWB The number of the previous round in the winner bracket. */ async getMatchesBeforeFirstRoundLB(match, winnerBracketId, roundNumberWB) { return [ await this.findMatch(winnerBracketId, roundNumberWB, helpers.getOriginPosition(match, 'opponent1')), await this.findMatch(winnerBracketId, roundNumberWB, helpers.getOriginPosition(match, 'opponent2')), ]; } /** * Gets the matches leading to a given match in a minor round of the loser bracket. * * @param match The current match. * @param winnerBracketId ID of the winner bracket. * @param roundNumber Number of the current round. * @param roundNumberWB The number of the previous round in the winner bracket. */ async getMatchesBeforeMinorRoundLB(match, winnerBracketId, roundNumber, roundNumberWB) { const matchNumber = helpers.getOriginPosition(match, 'opponent1'); return [ await this.findMatch(winnerBracketId, roundNumberWB, matchNumber), await this.findMatch(match.group_id, roundNumber - 1, match.number), ]; } /** * Gets the match(es) where the opponents of the current match will go just after. * * @param match The current match. * @param matchLocation Location of the current match. * @param stage The parent stage. * @param roundNumber The number of the current round. * @param roundCount Count of rounds. */ async getNextMatches(match, matchLocation, stage, roundNumber, roundCount) { switch (matchLocation) { case 'single_bracket': return this.getNextMatchesUpperBracket(match, stage, roundNumber, roundCount); case 'winner_bracket': return this.getNextMatchesWB(match, stage, roundNumber, roundCount); case 'loser_bracket': return this.getNextMatchesLB(match, stage, roundNumber, roundCount); case 'final_group': return this.getNextMatchesFinal(match, stage, roundNumber, roundCount); default: throw Error('Unknown bracket kind.'); } } /** * Gets the match(es) where the opponents of the current match of winner bracket will go just after. * * @param match The current match. * @param stage The parent stage. * @param roundNumber The number of the current round. * @param roundCount Count of rounds. */ async getNextMatchesWB(match, stage, roundNumber, roundCount) { const loserBracket = await this.getLoserBracket(match.stage_id); if (loserBracket === null) // Only one match in the stage, there is no loser bracket. return []; const actualRoundNumber = stage.settings.skipFirstRound ? roundNumber + 1 : roundNumber; const roundNumberLB = actualRoundNumber > 1 ? (actualRoundNumber - 1) * 2 : 1; const participantCount = stage.settings.size; const method = helpers.getLoserOrdering(stage.settings.seedOrdering, roundNumberLB); const actualMatchNumberLB = helpers.findLoserMatchNumber(participantCount, roundNumberLB, match.number, method); return [ ...await this.getNextMatchesUpperBracket(match, stage, roundNumber, roundCount), await this.findMatch(loserBracket.id, roundNumberLB, actualMatchNumberLB), ]; } /** * Gets the match(es) where the opponents of the current match of an upper bracket will go just after. * * @param match The current match. * @param stage The parent stage. * @param roundNumber The number of the current round. * @param roundCount Count of rounds. */ async getNextMatchesUpperBracket(match, stage, roundNumber, roundCount) { if (stage.type === 'single_elimination') return this.getNextMatchesUpperBracketSingleElimination(match, stage.type, roundNumber, roundCount); return this.getNextMatchesUpperBracketDoubleElimination(match, stage.type, roundNumber, roundCount); } /** * Gets the match(es) where the opponents of the current match of the unique bracket of a single elimination will go just after. * * @param match The current match. * @param stageType Type of the stage. * @param roundNumber The number of the current round. * @param roundCount Count of rounds. */ async getNextMatchesUpperBracketSingleElimination(match, stageType, roundNumber, roundCount) { if (roundNumber === roundCount - 1) { const finalGroupId = await this.getFinalGroupId(match.stage_id, stageType); const consolationFinal = await this.getFinalGroupFirstMatch(finalGroupId); return [ await this.getDiagonalMatch(match.group_id, roundNumber, match.number), ...consolationFinal ? [consolationFinal] : [], ]; } if (roundNumber === roundCount) return []; return [await this.getDiagonalMatch(match.group_id, roundNumber, match.number)]; } /** * Gets the match(es) where the opponents of the current match of the unique bracket of a double elimination will go just after. * * @param match The current match. * @param stageType Type of the stage. * @param roundNumber The number of the current round. * @param roundCount Count of rounds. */ async getNextMatchesUpperBracketDoubleElimination(match, stageType, roundNumber, roundCount) { if (roundNumber === roundCount) { const finalGroupId = await this.getFinalGroupId(match.stage_id, stageType); return [await this.getFinalGroupFirstMatch(finalGroupId)]; } return [await this.getDiagonalMatch(match.group_id, roundNumber, match.number)]; } /** * Gets the match(es) where the opponents of the current match of loser bracket will go just after. * * @param match The current match. * @param stage The parent stage. * @param roundNumber The number of the current round. * @param roundCount Count of rounds. */ async getNextMatchesLB(match, stage, roundNumber, roundCount) { if (roundNumber === roundCount - 1) { const finalGroupId = await this.getFinalGroupId(match.stage_id, stage.type); const consolationFinal = await this.getConsolationFinalMatchDoubleElimination(finalGroupId); return [ ...await this.getMatchAfterMajorRoundLB(match, roundNumber), ...consolationFinal ? [consolationFinal] : [], // Loser goes in consolation. ]; } if (roundNumber === roundCount) { const finalGroupId = await this.getFinalGroupId(match.stage_id, stage.type); const grandFinal = await this.getFinalGroupFirstMatch(finalGroupId); const consolationFinal = await this.getConsolationFinalMatchDoubleElimination(finalGroupId); return [ grandFinal, ...consolationFinal ? [consolationFinal] : [], // Returned array is length 1 if no consolation final. ]; } if (helpers.isMajorRound(roundNumber)) return this.getMatchAfterMajorRoundLB(match, roundNumber); return this.getMatchAfterMinorRoundLB(match, roundNumber); } /** * Gets the first match of the final group (consolation final or grand final). * * @param finalGroupId ID of the final group. */ async getFinalGroupFirstMatch(finalGroupId) { if (finalGroupId === null) return null; // `null` is required for `getNextMatchesWB()` because of how `applyToNextMatches()` works. return this.findMatch(finalGroupId, 1, 1); } /** * Gets the consolation final in a double elimination tournament. * * @param finalGroupId ID of the final group. */ async getConsolationFinalMatchDoubleElimination(finalGroupId) { if (finalGroupId === null) return null; return this.storage.selectFirst('match', { group_id: finalGroupId, number: 2, // Used to differentiate grand final and consolation final matches in the same final group. }); } /** * Gets the match following the current match, which is in the final group (consolation final or grand final). * * @param match The current match. * @param stage The parent stage. * @param roundNumber The number of the current round. * @param roundCount The count of rounds. */ async getNextMatchesFinal(match, stage, roundNumber, roundCount) { if (roundNumber === roundCount) return []; if (stage.settings.consolationFinal && match.number === 1 && roundNumber === roundCount - 1) return []; // Current match is the last grand final match. return [await this.findMatch(match.group_id, roundNumber + 1, 1)]; } /** * Gets the match where the opponents of the current match of a winner bracket's major round will go just after. * * @param match The current match. * @param roundNumber The number of the current round. */ async getMatchAfterMajorRoundLB(match, roundNumber) { return [await this.getParallelMatch(match.group_id, roundNumber, match.number)]; } /** * Gets the match where the opponents of the current match of a winner bracket's minor round will go just after. * * @param match The current match. * @param roundNumber The number of the current round. */ async getMatchAfterMinorRoundLB(match, roundNumber) { return [await this.getDiagonalMatch(match.group_id, roundNumber, match.number)]; } /** * Returns the good seeding ordering based on the stage's type. * * @param stageType The type of the stage. * @param create A reference to a Create instance. */ static getSeedingOrdering(stageType, create) { return stageType === 'round_robin' ? create.getRoundRobinOrdering() : create.getStandardBracketFirstRoundOrdering(); } /** * Returns the matches which contain the seeding of a stage based on its type. * * @param stageId ID of the stage. * @param stageType The type of the stage. */ async getSeedingMatches(stageId, stageType) { if (stageType === 'round_robin') return this.storage.select('match', { stage_id: stageId }); try { const firstRound = await this.getUpperBracketFirstRound(stageId); return this.storage.select('match', { round_id: firstRound.id }); } catch { return []; // The stage may have not been created yet. } } /** * Gets the first round of the upper bracket. * * @param stageId ID of the stage. */ async getUpperBracketFirstRound(stageId) { // Considering the database is ordered, this round will always be the first round of the upper bracket. const firstRound = await this.storage.selectFirst('round', { stage_id: stageId, number: 1 }, false); if (!firstRound) throw Error('Round not found.'); return firstRound; } /** * Gets the last round of a group. * * @param groupId ID of the group. */ async getLastRound(groupId) { const round = await this.storage.selectLast('round', { group_id: groupId }, false); if (!round) throw Error('Error getting rounds.'); return round; } /** * Returns the id of the final group (containing consolation final, or grand final, or both). * * @param stageId ID of the stage. * @param stageType Type of the stage. */ async getFinalGroupId(stageId, stageType) { const groupNumber = stageType === 'single_elimination' ? 2 /* single bracket + final */ : 3 /* winner bracket + loser bracket + final */; const finalGroup = await this.storage.selectFirst('group', { stage_id: stageId, number: groupNumber }); if (!finalGroup) return null; return finalGroup.id; } /** * Gets the upper bracket (the only bracket if single elimination or the winner bracket in double elimination). * * @param stageId ID of the stage. */ async getUpperBracket(stageId) { const winnerBracket = await this.storage.selectFirst('group', { stage_id: stageId, number: 1 }); if (!winnerBracket) throw Error('Winner bracket not found.'); return winnerBracket; } /** * Gets the loser bracket. * * @param stageId ID of the stage. */ async getLoserBracket(stageId) { return this.storage.selectFirst('group', { stage_id: stageId, number: 2 }); } /** * Gets the corresponding match in the next round ("diagonal match") the usual way. * * Just like from Round 1 to Round 2 in a single elimination stage. * * @param groupId ID of the group. * @param roundNumber Number of the round in its parent group. * @param matchNumber Number of the match in its parent round. */ async getDiagonalMatch(groupId, roundNumber, matchNumber) { return this.findMatch(groupId, roundNumber + 1, helpers.getDiagonalMatchNumber(matchNumber)); } /** * Gets the corresponding match in the next round ("parallel match") the "major round to minor round" way. * * Just like from Round 1 to Round 2 in the loser bracket of a double elimination stage. * * @param groupId ID of the group. * @param roundNumber Number of the round in its parent group. * @param matchNumber Number of the match in its parent round. */ async getParallelMatch(groupId, roundNumber, matchNumber) { return this.findMatch(groupId, roundNumber + 1, matchNumber); } /** * Finds a match in a given group. The match must have the given number in a round of which the number in group is given. * * **Example:** In group of id 1, give me the 4th match in the 3rd round. * * @param groupId ID of the group. * @param roundNumber Number of the round in its parent group. * @param matchNumber Number of the match in its parent round. */ async findMatch(groupId, roundNumber, matchNumber) { const round = await this.storage.selectFirst('round', { group_id: groupId, number: roundNumber, }); if (!round) throw Error('Round not found.'); const match = await this.storage.selectFirst('match', { round_id: round.id, number: matchNumber, }); if (!match) throw Error('Match not found.'); return match; } /** * Finds a match game based on its `id` or based on the combination of its `parent_id` and `number`. * * @param game Values to change in a match game. */ async findMatchGame(game) { if (game.id !== undefined) { const stored = await this.storage.select('match_game', game.id); if (!stored) throw Error('Match game not found.'); return stored; } if (game.parent_id !== undefined && game.number) { const stored = await this.storage.selectFirst('match_game', { parent_id: game.parent_id, number: game.number, }); if (!stored) throw Error('Match game not found.'); return stored; } throw Error('No match game id nor parent id and number given.'); } } exports.BaseGetter = BaseGetter; //# sourceMappingURL=getter.js.map