brackets-manager
Version:
A simple library to manage tournament brackets (round-robin, single elimination, double elimination)
323 lines • 15.6 kB
JavaScript
"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