UNPKG

brackets-manager

Version:

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

323 lines 15.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseUpdater = void 0; const brackets_model_1 = require("brackets-model"); const ordering_1 = require("../ordering"); const creator_1 = require("./stage/creator"); const getter_1 = require("./getter"); const get_1 = require("../get"); const helpers = require("../helpers"); class BaseUpdater extends getter_1.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. * @param seeding.seeding Can contain names, IDs or BYEs. * @param seeding.seedingIds Can only contain IDs or BYEs. * @param keepSameSize Whether to keep the same size as before for the stage. */ async updateSeeding(stageId, { seeding, seedingIds }, keepSameSize) { var _a, _b; const stage = await this.storage.select('stage', stageId); if (!stage) throw Error('Stage not found.'); const newSize = keepSameSize ? stage.settings.size : (_b = (_a = (seedingIds || seeding)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0; const creator = new creator_1.StageCreator(this.storage, { name: stage.name, tournamentId: stage.tournament_id, type: stage.type, settings: { ...stage.settings, ...(newSize === 0 ? {} : { size: newSize }), // Just reset the seeding if the new size is going to be empty. }, ...((seedingIds ? { seedingIds } : { seeding: seeding !== null && seeding !== void 0 ? seeding : undefined })), }); creator.setExisting(stageId, false); const method = getter_1.BaseGetter.getSeedingOrdering(stage.type, creator); const slots = await creator.getSlots(); const matches = await this.getSeedingMatches(stage.id, stage.type); if (!matches) throw Error('Error getting matches associated to the seeding.'); const ordered = ordering_1.ordering[method](slots); BaseUpdater.assertCanUpdateSeeding(matches, ordered); await creator.run(); } /** * Confirms the current seeding of a stage. * * @param stageId ID of the stage. */ async confirmCurrentSeeding(stageId) { const stage = await this.storage.select('stage', stageId); if (!stage) throw Error('Stage not found.'); const get = new get_1.Get(this.storage); const currentSeeding = await get.seeding(stageId); const newSeeding = helpers.convertSlotsToSeeding(currentSeeding.map(helpers.convertTBDtoBYE)); const creator = new creator_1.StageCreator(this.storage, { name: stage.name, tournamentId: stage.tournament_id, type: stage.type, settings: stage.settings, seeding: newSeeding, }); creator.setExisting(stageId, true); await creator.run(); } /** * Updates a parent match based on its child games. * * @param parentId ID of the parent match. * @param inRoundRobin Indicates whether the parent match is in a round-robin stage. */ async updateParentMatch(parentId, inRoundRobin) { const storedParent = await this.storage.select('match', parentId); if (!storedParent) throw Error('Parent not found.'); const games = await this.storage.select('match_game', { parent_id: parentId }); if (!games) throw Error('No match games.'); const parentScores = helpers.getChildGamesResults(games); const parent = helpers.getParentMatchResults(storedParent, parentScores); helpers.setParentMatchCompleted(parent, storedParent.child_count, inRoundRobin); await this.updateMatch(storedParent, parent, true); } /** * 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. */ static assertCanUpdateSeeding(matches, slots) { var _a, _b; let index = 0; for (const match of matches) { // Changing the seeding would reset the matches of round >= 2, leaving the scores behind, with no participants. if (match.status === brackets_model_1.Status.Archived) throw Error('A match of round 1 is archived, which means round 2 was started.'); const opponent1 = slots[index++]; const opponent2 = slots[index++]; const isParticipantLocked = helpers.isMatchParticipantLocked(match); // The match is participant locked, and the participants would have to change. if (isParticipantLocked && (((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.id) !== (opponent1 === null || opponent1 === void 0 ? void 0 : opponent1.id) || ((_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.id) !== (opponent2 === null || opponent2 === void 0 ? void 0 : opponent2.id))) throw Error('A match is locked.'); } } /** * Updates the matches related (previous and next) to a match. * * @param match A match. * @param updatePrevious Whether to update the previous matches. * @param updateNext Whether to update the next matches. */ async updateRelatedMatches(match, updatePrevious, updateNext) { // This is a consolation match (doesn't have a `group_id`, nor a `round_id`). // It doesn't have any related matches from the POV of the library, because the // creation of consolation matches is handled by the user. if (match.round_id === undefined) return; const { roundNumber, roundCount } = await this.getRoundPositionalInfo(match.round_id); const stage = await this.storage.select('stage', match.stage_id); if (!stage) throw Error('Stage not found.'); const group = await this.storage.select('group', match.group_id); if (!group) throw Error('Group not found.'); const matchLocation = helpers.getMatchLocation(stage.type, group.number); updatePrevious && await this.updatePrevious(match, matchLocation, stage, roundNumber); updateNext && await this.updateNext(match, matchLocation, stage, roundNumber, roundCount); } /** * Updates a match based on a partial match. * * @param stored A reference to what will be updated in the storage. * @param match Input of the update. * @param force Whether to force update locked matches. */ async updateMatch(stored, match, force) { if (!force && helpers.isMatchUpdateLocked(stored)) throw Error('The match is locked.'); const stage = await this.storage.select('stage', stored.stage_id); if (!stage) throw Error('Stage not found.'); const inRoundRobin = helpers.isRoundRobin(stage); const { statusChanged, resultChanged } = helpers.setMatchResults(stored, match, inRoundRobin); await this.applyMatchUpdate(stored); // Don't update related matches if it's a simple score update. if (!statusChanged && !resultChanged) return; if (!helpers.isRoundRobin(stage)) await this.updateRelatedMatches(stored, statusChanged, resultChanged); } /** * Updates a match game based on a partial match game. * * @param stored A reference to what will be updated in the storage. * @param game Input of the update. */ async updateMatchGame(stored, game) { if (helpers.isMatchUpdateLocked(stored)) throw Error('The match game is locked.'); const stage = await this.storage.select('stage', stored.stage_id); if (!stage) throw Error('Stage not found.'); const inRoundRobin = helpers.isRoundRobin(stage); helpers.setMatchResults(stored, game, inRoundRobin); if (!await this.storage.update('match_game', stored.id, stored)) throw Error('Could not update the match game.'); await this.updateParentMatch(stored.parent_id, inRoundRobin); } /** * Updates the opponents and status of a match and its child games. * * @param match A match. */ async applyMatchUpdate(match) { if (!await this.storage.update('match', match.id, match)) throw Error('Could not update the match.'); if (match.child_count === 0) return; const updatedMatchGame = { opponent1: helpers.toResult(match.opponent1), opponent2: helpers.toResult(match.opponent2), }; // Only sync the child games' status with their parent's status when changing the parent match participants // (Locked, Waiting, Ready) or when archiving the parent match. if (match.status <= brackets_model_1.Status.Ready || match.status === brackets_model_1.Status.Archived) updatedMatchGame.status = match.status; if (!await this.storage.update('match_game', { parent_id: match.id }, updatedMatchGame)) throw Error('Could not update the match game.'); } /** * Updates the match(es) leading to the current match based on this match results. * * @param match Input of the update. * @param matchLocation Location of the current match. * @param stage The parent stage. * @param roundNumber Number of the round. */ async updatePrevious(match, matchLocation, stage, roundNumber) { const previousMatches = await this.getPreviousMatches(match, matchLocation, stage, roundNumber); if (previousMatches.length === 0) return; if (match.status >= brackets_model_1.Status.Running) await this.archiveMatches(previousMatches); else await this.resetMatchesStatus(previousMatches); } /** * Sets the status of a list of matches to archived. * * @param matches The matches to update. */ async archiveMatches(matches) { for (const match of matches) { if (match.status === brackets_model_1.Status.Archived) continue; match.status = brackets_model_1.Status.Archived; await this.applyMatchUpdate(match); } } /** * Resets the status of a list of matches to what it should currently be. * * @param matches The matches to update. */ async resetMatchesStatus(matches) { for (const match of matches) { match.status = helpers.getMatchStatus(match); await this.applyMatchUpdate(match); } } /** * Updates the match(es) following the current match based on this match results. * * @param match Input of the update. * @param matchLocation Location of the current match. * @param stage The parent stage. * @param roundNumber Number of the round. * @param roundCount Count of rounds. */ async updateNext(match, matchLocation, stage, roundNumber, roundCount) { const nextMatches = await this.getNextMatches(match, matchLocation, stage, roundNumber, roundCount); if (nextMatches.length === 0) { // Archive match if it doesn't have following matches and is completed. // When the stage is fully complete, all matches should be archived. if (match.status === brackets_model_1.Status.Completed) await this.archiveMatches([match]); return; } const winnerSide = helpers.getMatchResult(match); const actualRoundNumber = (stage.settings.skipFirstRound && matchLocation === 'winner_bracket') ? roundNumber + 1 : roundNumber; if (winnerSide) await this.applyToNextMatches(helpers.setNextOpponent, match, matchLocation, actualRoundNumber, roundCount, nextMatches, winnerSide); else await this.applyToNextMatches(helpers.resetNextOpponent, match, matchLocation, actualRoundNumber, roundCount, nextMatches); } /** * Applies a `SetNextOpponent` function to matches following the current match. * * - `nextMatches[0]` is assumed to be next match for the winner of the current match. * - `nextMatches[1]` is assumed to be next match for the loser of the current match. * * @param setNextOpponent The `SetNextOpponent` function. * @param match The current match. * @param matchLocation Location of the current match. * @param roundNumber Number of the current round. * @param roundCount Count of rounds. * @param nextMatches The matches following the current match. * @param winnerSide Side of the winner in the current match. */ async applyToNextMatches(setNextOpponent, match, matchLocation, roundNumber, roundCount, nextMatches, winnerSide) { if (matchLocation === 'final_group') { if (!nextMatches[0]) throw Error('First next match is null.'); setNextOpponent(nextMatches[0], 'opponent1', match, 'opponent1'); setNextOpponent(nextMatches[0], 'opponent2', match, 'opponent2'); await this.applyMatchUpdate(nextMatches[0]); return; } const nextSide = helpers.getNextSide(match.number, roundNumber, roundCount, matchLocation); // First next match if (nextMatches[0]) { setNextOpponent(nextMatches[0], nextSide, match, winnerSide); await this.propagateByeWinners(nextMatches[0]); } if (nextMatches.length !== 2) return; if (!nextMatches[1]) throw Error('Second next match is null.'); // Second next match if (matchLocation === 'single_bracket') { // Going into consolation final (single elimination) setNextOpponent(nextMatches[1], nextSide, match, winnerSide && helpers.getOtherSide(winnerSide)); await this.applyMatchUpdate(nextMatches[1]); } else if (matchLocation === 'winner_bracket') { // Going into loser bracket match (double elimination) const nextSideIntoLB = helpers.getNextSideLoserBracket(match.number, nextMatches[1], roundNumber); setNextOpponent(nextMatches[1], nextSideIntoLB, match, winnerSide && helpers.getOtherSide(winnerSide)); await this.propagateByeWinners(nextMatches[1]); } else if (matchLocation === 'loser_bracket') { // Going into consolation final (double elimination) const nextSideIntoConsolationFinal = helpers.getNextSideConsolationFinalDoubleElimination(roundNumber); setNextOpponent(nextMatches[1], nextSideIntoConsolationFinal, match, winnerSide && helpers.getOtherSide(winnerSide)); await this.propagateByeWinners(nextMatches[1]); } } /** * Propagates winner against BYEs in related matches. * * @param match The current match. */ async propagateByeWinners(match) { helpers.setMatchResults(match, match, false); // BYE propagation is only in non round-robin stages. await this.applyMatchUpdate(match); if (helpers.hasBye(match)) await this.updateRelatedMatches(match, true, true); } } exports.BaseUpdater = BaseUpdater; //# sourceMappingURL=updater.js.map