brackets-manager
Version:
A simple library to manage tournament brackets (round-robin, single elimination, double elimination)
1,307 lines • 68.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setExtraFields = exports.resetMatchResults = exports.setMatchResults = exports.getMatchStatus = exports.hasBye = exports.isMatchParticipantLocked = exports.isMatchUpdateLocked = exports.isMatchByeCompleted = exports.isMatchWinCompleted = exports.isMatchDrawCompleted = exports.isMatchResultCompleted = exports.isMatchForfeitCompleted = exports.isMatchStale = exports.isMatchOngoing = exports.isMatchCompleted = exports.isMatchStarted = exports.isMatchPending = exports.getOtherSide = exports.getSide = exports.isParticipantInMatch = exports.findPosition = exports.getMatchResult = exports.byeLoser = exports.byeWinnerToGrandFinal = exports.byeWinner = exports.getLoser = exports.getWinner = exports.toResultWithPosition = exports.toResult = exports.convertTBDtoBYE = exports.ensureNotTied = exports.ensureValidSize = exports.isPowerOfTwo = exports.fixSeeding = exports.ensureEquallySized = exports.ensureNoDuplicates = exports.ensureEvenSized = exports.makePairs = exports.setArraySize = exports.normalizeParticipant = exports.makeNormalizedIdMapping = exports.normalizeIds = exports.balanceByes = exports.makeGroups = exports.assertRoundRobin = exports.makeRoundRobinDistribution = exports.makeRoundRobinMatches = exports.splitByParity = exports.splitBy = exports.isDefined = void 0;
exports.getLowerBracketRoundCount = exports.getLoserOrdering = exports.getLoserCountFromWbForLbRound = exports.getLoserRoundMatchCount = exports.findLoserMatchNumber = exports.isDoubleEliminationNecessary = exports.getRoundPairCount = exports.getUpperBracketRoundCount = exports.isOrderingSupportedLoserBracket = exports.isOrderingSupportedUpperBracket = exports.ensureOrderingSupported = exports.getSeedCount = exports.getSeeds = exports.getChildGamesResults = exports.getUpdatedMatchResults = exports.getParentMatchResults = exports.setParentMatchCompleted = exports.transitionToMinor = exports.transitionToMajor = exports.isMinorRound = exports.isMajorRound = exports.uniqueBy = exports.getNonNull = exports.sortSeeding = exports.convertSlotsToSeeding = exports.convertMatchesToSeeding = exports.mapParticipantsToDatabase = exports.mapParticipantsIdsToDatabase = exports.mapParticipantsNamesToDatabase = exports.extractParticipantsFromSeeding = exports.isSeedingWithIds = exports.setForfeits = exports.setResults = exports.setCompleted = exports.getInferredResult = exports.setScores = exports.invertOpponents = exports.handleGivenStatus = exports.handleOpponentsInversion = exports.resetNextOpponent = exports.setNextOpponent = exports.getNextSideConsolationFinalDoubleElimination = exports.getNextSideLoserBracket = exports.getNextSide = exports.findParticipant = exports.getGrandFinalDecisiveMatch = exports.makeFinalStandings = exports.getLosers = exports.getOriginPosition = exports.getOpponentId = void 0;
exports.getRanking = exports.getFractionOfFinal = exports.getMatchLocation = exports.isFinalGroup = exports.isLoserBracket = exports.isWinnerBracket = exports.isRoundCompleted = exports.ensureNotRoundRobin = exports.isRoundRobin = exports.minScoreToWinBestOfX = exports.getNearestPowerOfTwo = exports.getDiagonalMatchNumber = void 0;
const brackets_model_1 = require("brackets-model");
const ordering_1 = require("./ordering");
/**
* Checks whether a value is defined (i.e. not null nor undefined).
*
* @param value The value to check.
*/
function isDefined(value) {
return value !== null && value !== undefined;
}
exports.isDefined = isDefined;
/**
* Splits an array of objects based on their values at a given key.
*
* @param objects The array to split.
* @param key The key of T.
*/
function splitBy(objects, key) {
const map = {};
for (const obj of objects) {
const commonValue = obj[key];
if (!map[commonValue])
map[commonValue] = [];
map[commonValue].push(obj);
}
return Object.values(map);
}
exports.splitBy = splitBy;
/**
* Splits an array in two parts: one with even indices and the other with odd indices.
*
* @param array The array to split.
*/
function splitByParity(array) {
return {
even: array.filter((_, i) => i % 2 === 0),
odd: array.filter((_, i) => i % 2 === 1),
};
}
exports.splitByParity = splitByParity;
/**
* Makes a list of rounds containing the matches of a round-robin group.
*
* @param participants The participants to distribute.
* @param mode The round-robin mode.
*/
function makeRoundRobinMatches(participants, mode = 'simple') {
const distribution = makeRoundRobinDistribution(participants);
if (mode === 'simple')
return distribution;
// Reverse rounds and their content.
const symmetry = distribution.map(round => [...round].reverse()).reverse();
return [...distribution, ...symmetry];
}
exports.makeRoundRobinMatches = makeRoundRobinMatches;
/**
* Distributes participants in rounds for a round-robin group.
*
* Conditions:
* - Each participant plays each other once.
* - Each participant plays once in each round.
*
* @param participants The participants to distribute.
*/
function makeRoundRobinDistribution(participants) {
const n = participants.length;
const n1 = n % 2 === 0 ? n : n + 1;
const roundCount = n1 - 1;
const matchPerRound = n1 / 2;
const rounds = [];
for (let roundId = 0; roundId < roundCount; roundId++) {
const matches = [];
for (let matchId = 0; matchId < matchPerRound; matchId++) {
if (matchId === 0 && n % 2 === 1)
continue;
const opponentsIds = [
(roundId - matchId - 1 + n1) % (n1 - 1),
matchId === 0 ? n1 - 1 : (roundId + matchId) % (n1 - 1),
];
matches.push([
participants[opponentsIds[0]],
participants[opponentsIds[1]],
]);
}
rounds.push(matches);
}
return rounds;
}
exports.makeRoundRobinDistribution = makeRoundRobinDistribution;
/**
* A helper to assert our generated round-robin is correct.
*
* @param input The input seeding.
* @param output The resulting distribution of seeds in groups.
*/
function assertRoundRobin(input, output) {
const n = input.length;
const matchPerRound = Math.floor(n / 2);
const roundCount = n % 2 === 0 ? n - 1 : n;
if (output.length !== roundCount)
throw Error('Round count is wrong');
if (!output.every(round => round.length === matchPerRound))
throw Error('Not every round has the good number of matches');
const checkAllOpponents = Object.fromEntries(input.map(element => [element, new Set()]));
for (const round of output) {
const checkUnique = new Set();
for (const match of round) {
if (match.length !== 2)
throw Error('One match is not a pair');
if (checkUnique.has(match[0]))
throw Error('This team is already playing');
checkUnique.add(match[0]);
if (checkUnique.has(match[1]))
throw Error('This team is already playing');
checkUnique.add(match[1]);
if (checkAllOpponents[match[0]].has(match[1]))
throw Error('The team has already matched this team');
checkAllOpponents[match[0]].add(match[1]);
if (checkAllOpponents[match[1]].has(match[0]))
throw Error('The team has already matched this team');
checkAllOpponents[match[1]].add(match[0]);
}
}
}
exports.assertRoundRobin = assertRoundRobin;
/**
* Distributes elements in groups of equal size.
*
* @param elements A list of elements to distribute in groups.
* @param groupCount The group count.
*/
function makeGroups(elements, groupCount) {
const groupSize = Math.ceil(elements.length / groupCount);
const result = [];
for (let i = 0; i < elements.length; i++) {
if (i % groupSize === 0)
result.push([]);
result[result.length - 1].push(elements[i]);
}
return result;
}
exports.makeGroups = makeGroups;
/**
* Balances BYEs to prevents having BYE against BYE in matches.
*
* @param seeding The seeding of the stage.
* @param participantCount The number of participants in the stage.
*/
function balanceByes(seeding, participantCount) {
seeding = seeding.filter(v => v !== null);
participantCount = participantCount || getNearestPowerOfTwo(seeding.length);
if (seeding.length < participantCount / 2) {
const flat = seeding.flatMap(v => [v, null]);
return setArraySize(flat, participantCount, null);
}
const nonNullCount = seeding.length;
const nullCount = participantCount - nonNullCount;
const againstEachOther = seeding.slice(0, nonNullCount - nullCount).filter((_, i) => i % 2 === 0).map((_, i) => [seeding[2 * i], seeding[2 * i + 1]]);
const againstNull = seeding.slice(nonNullCount - nullCount, nonNullCount).map(v => [v, null]);
const flat = [...againstEachOther.flat(), ...againstNull.flat()];
return setArraySize(flat, participantCount, null);
}
exports.balanceByes = balanceByes;
/**
* Normalizes IDs in a database.
*
* All IDs (and references to them) are remapped to consecutive IDs starting from 0.
*
* @param data Data to normalize.
*/
function normalizeIds(data) {
const mappings = {
participant: makeNormalizedIdMapping(data.participant),
stage: makeNormalizedIdMapping(data.stage),
group: makeNormalizedIdMapping(data.group),
round: makeNormalizedIdMapping(data.round),
match: makeNormalizedIdMapping(data.match),
match_game: makeNormalizedIdMapping(data.match_game),
};
return {
participant: data.participant.map(value => ({
...value,
id: mappings.participant[value.id],
})),
stage: data.stage.map(value => ({
...value,
id: mappings.stage[value.id],
})),
group: data.group.map(value => ({
...value,
id: mappings.group[value.id],
stage_id: mappings.stage[value.stage_id],
})),
round: data.round.map(value => ({
...value,
id: mappings.round[value.id],
stage_id: mappings.stage[value.stage_id],
group_id: mappings.group[value.group_id],
})),
match: data.match.map(value => ({
...value,
id: mappings.match[value.id],
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),
})),
match_game: data.match_game.map(value => ({
...value,
id: mappings.match_game[value.id],
stage_id: mappings.stage[value.stage_id],
parent_id: mappings.match[value.parent_id],
opponent1: normalizeParticipant(value.opponent1, mappings.participant),
opponent2: normalizeParticipant(value.opponent2, mappings.participant),
})),
};
}
exports.normalizeIds = normalizeIds;
/**
* Makes a mapping between old IDs and new normalized IDs.
*
* @param elements A list of elements with IDs.
*/
function makeNormalizedIdMapping(elements) {
let currentId = 0;
return elements.reduce((acc, current) => ({
...acc,
[current.id]: currentId++,
}), {});
}
exports.makeNormalizedIdMapping = makeNormalizedIdMapping;
/**
* Apply a normalizing mapping to a participant.
*
* @param participant The participant.
* @param mapping The mapping of IDs.
*/
function normalizeParticipant(participant, mapping) {
if (participant === null)
return null;
return {
...participant,
id: participant.id !== null ? mapping[participant.id] : null,
};
}
exports.normalizeParticipant = normalizeParticipant;
/**
* Sets the size of an array with a placeholder if the size is bigger.
*
* @param array The original array.
* @param length The new length.
* @param placeholder A placeholder to use to fill the empty space.
*/
function setArraySize(array, length, placeholder) {
return Array.from({ length }, (_, i) => array[i] || placeholder);
}
exports.setArraySize = setArraySize;
/**
* Makes pairs with each element and its next one.
*
* @example [1, 2, 3, 4] --> [[1, 2], [3, 4]]
* @param array A list of elements.
*/
function makePairs(array) {
return array.map((_, i) => (i % 2 === 0) ? [array[i], array[i + 1]] : []).filter((v) => v.length === 2);
}
exports.makePairs = makePairs;
/**
* Ensures that a list of elements has an even size.
*
* @param array A list of elements.
*/
function ensureEvenSized(array) {
if (array.length % 2 === 1)
throw Error('Array size must be even.');
}
exports.ensureEvenSized = ensureEvenSized;
/**
* Ensures there are no duplicates in a list of elements.
*
* @param array A list of elements.
*/
function ensureNoDuplicates(array) {
const nonNull = getNonNull(array);
const unique = nonNull.filter((item, index) => {
const stringifiedItem = JSON.stringify(item);
return nonNull.findIndex(obj => JSON.stringify(obj) === stringifiedItem) === index;
});
if (unique.length < nonNull.length)
throw new Error('The seeding has a duplicate participant.');
}
exports.ensureNoDuplicates = ensureNoDuplicates;
/**
* Ensures that two lists of elements have the same size.
*
* @param left The first list of elements.
* @param right The second list of elements.
*/
function ensureEquallySized(left, right) {
if (left.length !== right.length)
throw Error('Arrays\' size must be equal.');
}
exports.ensureEquallySized = ensureEquallySized;
/**
* Fixes the seeding by enlarging it if it's not complete.
*
* @param seeding The seeding of the stage.
* @param participantCount The number of participants in the stage.
*/
function fixSeeding(seeding, participantCount) {
if (seeding.length > participantCount)
throw Error('The seeding has more participants than the size of the stage.');
if (seeding.length < participantCount)
return setArraySize(seeding, participantCount, null);
return seeding;
}
exports.fixSeeding = fixSeeding;
/**
* Indicates whether a number is a power of two.
*
* @param number The number to test.
*/
function isPowerOfTwo(number) {
return Number.isInteger(Math.log2(number));
}
exports.isPowerOfTwo = isPowerOfTwo;
/**
* Ensures that the participant count is valid.
*
* @param stageType Type of the stage to test.
* @param participantCount The number to test.
*/
function ensureValidSize(stageType, participantCount) {
if (participantCount === 0)
throw Error('Impossible to create an empty stage. If you want an empty seeding, just set the size of the stage.');
if (participantCount < 2)
throw Error('Impossible to create a stage with less than 2 participants.');
if (stageType === 'round_robin') {
// Round robin supports any number of participants.
return;
}
if (!isPowerOfTwo(participantCount))
throw Error('The library only supports a participant count which is a power of two.');
}
exports.ensureValidSize = ensureValidSize;
/**
* Ensures that a match scores aren't tied.
*
* @param scores Two numbers which are scores.
*/
function ensureNotTied(scores) {
if (scores[0] === scores[1])
throw Error(`${scores[0]} and ${scores[1]} are tied. It cannot be.`);
}
exports.ensureNotTied = ensureNotTied;
/**
* Converts a TBD to a BYE.
*
* @param slot The slot to convert.
*/
function convertTBDtoBYE(slot) {
if (slot === null)
return null; // Already a BYE.
if ((slot === null || slot === void 0 ? void 0 : slot.id) === null)
return null; // It's a TBD: make it a BYE.
return slot; // It's a determined participant.
}
exports.convertTBDtoBYE = convertTBDtoBYE;
/**
* Converts a participant slot to a result stored in storage.
*
* @param slot A participant slot.
*/
function toResult(slot) {
return slot && {
id: slot.id,
};
}
exports.toResult = toResult;
/**
* Converts a participant slot to a result stored in storage, with the position the participant is coming from.
*
* @param slot A participant slot.
*/
function toResultWithPosition(slot) {
return slot && {
id: slot.id,
position: slot.position,
};
}
exports.toResultWithPosition = toResultWithPosition;
/**
* Returns the winner of a match.
*
* @param match The match.
*/
function getWinner(match) {
const winnerSide = getMatchResult(match);
if (!winnerSide)
return null;
return match[winnerSide];
}
exports.getWinner = getWinner;
/**
* Returns the loser of a match.
*
* @param match The match.
*/
function getLoser(match) {
const winnerSide = getMatchResult(match);
if (!winnerSide)
return null;
return match[getOtherSide(winnerSide)];
}
exports.getLoser = getLoser;
/**
* Returns the pre-computed winner for a match because of BYEs.
*
* @param opponents Two opponents.
*/
function byeWinner(opponents) {
if (opponents[0] === null && opponents[1] === null) // Double BYE.
return null; // BYE.
if (opponents[0] === null && opponents[1] !== null) // opponent1 BYE.
return { id: opponents[1].id }; // opponent2.
if (opponents[0] !== null && opponents[1] === null) // opponent2 BYE.
return { id: opponents[0].id }; // opponent1.
return { id: null }; // Normal.
}
exports.byeWinner = byeWinner;
/**
* Returns the pre-computed winner for a match because of BYEs in a lower bracket.
*
* @param opponents Two opponents.
*/
function byeWinnerToGrandFinal(opponents) {
const winner = byeWinner(opponents);
if (winner)
winner.position = 1;
return winner;
}
exports.byeWinnerToGrandFinal = byeWinnerToGrandFinal;
/**
* Returns the pre-computed loser for a match because of BYEs.
*
* Only used for loser bracket.
*
* @param opponents Two opponents.
* @param index The index of the duel in the round.
*/
function byeLoser(opponents, index) {
if (opponents[0] === null || opponents[1] === null) // At least one BYE.
return null; // BYE.
return { id: null, position: index + 1 }; // Normal.
}
exports.byeLoser = byeLoser;
/**
* Returns the winner side or `null` if no winner.
*
* @param match A match's results.
*/
function getMatchResult(match) {
var _a, _b;
if (!isMatchCompleted(match))
return null;
if (isMatchDrawCompleted(match))
return null;
if (match.opponent1 === null && match.opponent2 === null)
return null;
let winner = null;
if (((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.result) === 'win' || match.opponent2 === null || match.opponent2.forfeit)
winner = 'opponent1';
if (((_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.result) === 'win' || match.opponent1 === null || match.opponent1.forfeit) {
if (winner !== null)
throw Error('There are two winners.');
winner = 'opponent2';
}
return winner;
}
exports.getMatchResult = getMatchResult;
/**
* Finds a position in a list of matches.
*
* @param matches A list of matches to search into.
* @param position The position to find.
*/
function findPosition(matches, position) {
var _a, _b;
for (const match of matches) {
if (((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.position) === position)
return match.opponent1;
if (((_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.position) === position)
return match.opponent2;
}
return null;
}
exports.findPosition = findPosition;
/**
* Checks if a participant is involved in a given match.
*
* @param match A match.
* @param participantId ID of a participant.
*/
function isParticipantInMatch(match, participantId) {
return [match.opponent1, match.opponent2].some(m => (m === null || m === void 0 ? void 0 : m.id) === participantId);
}
exports.isParticipantInMatch = isParticipantInMatch;
/**
* Gets the side where the winner of the given match will go in the next match.
*
* @param matchNumber Number of the match.
*/
function getSide(matchNumber) {
return matchNumber % 2 === 1 ? 'opponent1' : 'opponent2';
}
exports.getSide = getSide;
/**
* Gets the other side of a match.
*
* @param side The side that we don't want.
*/
function getOtherSide(side) {
return side === 'opponent1' ? 'opponent2' : 'opponent1';
}
exports.getOtherSide = getOtherSide;
/**
* Checks if a match is pending (i.e. locked or waiting).
*
* [Locked > Waiting] > Ready > Running > Completed > Archived
*
* @param match Partial match results.
*/
function isMatchPending(match) {
var _a, _b;
return !((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.id) || !((_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.id); // At least one BYE or TBD
}
exports.isMatchPending = isMatchPending;
/**
* Checks if a match is started.
*
* Note: this is score-based. A completed or archived match is seen as "started" as well.
*
* Locked > Waiting > Ready > [Running > Completed > Archived]
*
* @param match Partial match results.
*/
function isMatchStarted(match) {
var _a, _b;
return ((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.score) !== undefined || ((_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.score) !== undefined;
}
exports.isMatchStarted = isMatchStarted;
/**
* Checks if a match is completed (based on BYEs, forfeit or result).
*
* Note: archived matches are not seen as completed by this helper.
*
* Locked > Waiting > Ready > Running > [Completed] > Archived
*
* @param match Partial match results.
*/
function isMatchCompleted(match) {
return isMatchByeCompleted(match) || isMatchForfeitCompleted(match) || isMatchResultCompleted(match);
}
exports.isMatchCompleted = isMatchCompleted;
/**
* Checks if a match is ongoing (i.e. ready or running).
*
* Locked > Waiting > [Ready > Running] > Completed > Archived
*
* @param match Partial match results.
*/
function isMatchOngoing(match) {
return [brackets_model_1.Status.Ready, brackets_model_1.Status.Running].includes(match.status);
}
exports.isMatchOngoing = isMatchOngoing;
/**
* Checks if a match is stale (i.e. it should not change anymore).
*
* [Locked - BYE] > Waiting > Ready > Running > [Completed > Archived]
*
* @param match Partial match results.
*/
function isMatchStale(match) {
return match.status >= brackets_model_1.Status.Completed || isMatchByeCompleted(match);
}
exports.isMatchStale = isMatchStale;
/**
* Checks if a match is completed because of a forfeit.
*
* @param match Partial match results.
*/
function isMatchForfeitCompleted(match) {
var _a, _b;
return ((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.forfeit) !== undefined || ((_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.forfeit) !== undefined;
}
exports.isMatchForfeitCompleted = isMatchForfeitCompleted;
/**
* Checks if a match is completed because of a either a draw or a win.
*
* @param match Partial match results.
*/
function isMatchResultCompleted(match) {
return isMatchDrawCompleted(match) || isMatchWinCompleted(match);
}
exports.isMatchResultCompleted = isMatchResultCompleted;
/**
* Checks if a match is completed because of a draw.
*
* @param match Partial match results.
*/
function isMatchDrawCompleted(match) {
var _a, _b;
return ((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.result) === 'draw' && ((_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.result) === 'draw';
}
exports.isMatchDrawCompleted = isMatchDrawCompleted;
/**
* Checks if a match is completed because of a win.
*
* @param match Partial match results.
*/
function isMatchWinCompleted(match) {
var _a, _b, _c, _d;
return ((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.result) === 'win' || ((_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.result) === 'win'
|| ((_c = match.opponent1) === null || _c === void 0 ? void 0 : _c.result) === 'loss' || ((_d = match.opponent2) === null || _d === void 0 ? void 0 : _d.result) === 'loss';
}
exports.isMatchWinCompleted = isMatchWinCompleted;
/**
* Checks if a match is completed because of at least one BYE.
*
* A match "BYE vs. TBD" isn't considered completed yet.
*
* @param match Partial match results.
*/
function isMatchByeCompleted(match) {
var _a, _b;
return (match.opponent1 === null && ((_a = match.opponent2) === null || _a === void 0 ? void 0 : _a.id) !== null) // BYE vs. someone
|| (match.opponent2 === null && ((_b = match.opponent1) === null || _b === void 0 ? void 0 : _b.id) !== null) // someone vs. BYE
|| (match.opponent1 === null && match.opponent2 === null); // BYE vs. BYE
}
exports.isMatchByeCompleted = isMatchByeCompleted;
/**
* Checks if a match's results can't be updated.
*
* @param match The match to check.
*/
function isMatchUpdateLocked(match) {
return match.status === brackets_model_1.Status.Locked || match.status === brackets_model_1.Status.Waiting || match.status === brackets_model_1.Status.Archived || isMatchByeCompleted(match);
}
exports.isMatchUpdateLocked = isMatchUpdateLocked;
/**
* Checks if a match's participants can't be updated.
*
* @param match The match to check.
*/
function isMatchParticipantLocked(match) {
return match.status >= brackets_model_1.Status.Running;
}
exports.isMatchParticipantLocked = isMatchParticipantLocked;
/**
* Indicates whether a match has at least one BYE or not.
*
* @param match Partial match results.
*/
function hasBye(match) {
return match.opponent1 === null || match.opponent2 === null;
}
exports.hasBye = hasBye;
/**
* Returns the status of a match based on information about it.
*
* @param arg The opponents or partial results of the match.
*/
function getMatchStatus(arg) {
var _a, _b, _c, _d;
const match = Array.isArray(arg) ? {
opponent1: arg[0],
opponent2: arg[1],
} : arg;
if (hasBye(match)) // At least one BYE.
return brackets_model_1.Status.Locked;
if (((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.id) === null && ((_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.id) === null) // Two TBD opponents.
return brackets_model_1.Status.Locked;
if (((_c = match.opponent1) === null || _c === void 0 ? void 0 : _c.id) === null || ((_d = match.opponent2) === null || _d === void 0 ? void 0 : _d.id) === null) // One TBD opponent.
return brackets_model_1.Status.Waiting;
if (isMatchCompleted(match))
return brackets_model_1.Status.Completed;
if (isMatchStarted(match))
return brackets_model_1.Status.Running;
return brackets_model_1.Status.Ready;
}
exports.getMatchStatus = getMatchStatus;
/**
* Updates a match results based on an input.
*
* @param stored A reference to what will be updated in the storage.
* @param match Input of the update.
* @param inRoundRobin Indicates whether the match is in a round-robin stage.
*/
function setMatchResults(stored, match, inRoundRobin) {
var _a, _b;
handleGivenStatus(stored, match);
if (!inRoundRobin && (((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.result) === 'draw' || ((_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.result) === 'draw'))
throw Error('Having a draw is forbidden in an elimination tournament.');
const completed = isMatchCompleted(match);
const currentlyCompleted = isMatchCompleted(stored);
setExtraFields(stored, match);
handleOpponentsInversion(stored, match);
const statusChanged = setScores(stored, match);
if (completed && currentlyCompleted) {
// Ensure everything is good.
setCompleted(stored, match, inRoundRobin);
return { statusChanged: false, resultChanged: true };
}
if (completed && !currentlyCompleted) {
setCompleted(stored, match, inRoundRobin);
return { statusChanged: true, resultChanged: true };
}
if (!completed && currentlyCompleted) {
resetMatchResults(stored);
return { statusChanged: true, resultChanged: true };
}
return { statusChanged, resultChanged: false };
}
exports.setMatchResults = setMatchResults;
/**
* Resets the results of a match. (status, forfeit, result)
*
* @param stored A reference to what will be updated in the storage.
*/
function resetMatchResults(stored) {
if (stored.opponent1) {
stored.opponent1.forfeit = undefined;
stored.opponent1.result = undefined;
}
if (stored.opponent2) {
stored.opponent2.forfeit = undefined;
stored.opponent2.result = undefined;
}
stored.status = getMatchStatus(stored);
}
exports.resetMatchResults = resetMatchResults;
/**
* Passes user-defined extra fields to the stored match.
*
* @param stored A reference to what will be updated in the storage.
* @param match Input of the update.
*/
function setExtraFields(stored, match) {
const partialAssign = (target, update, ignoredKeys) => {
if (!target || !update)
return;
const retainedKeys = Object.keys(update).filter((key) => !ignoredKeys.includes(key));
retainedKeys.forEach(key => {
target[key] = update[key];
});
};
const ignoredKeys = [
'id',
'number',
'stage_id',
'group_id',
'round_id',
'status',
'opponent1',
'opponent2',
'child_count',
'parent_id',
];
const ignoredOpponentKeys = [
'id',
'score',
'position',
'forfeit',
'result',
];
partialAssign(stored, match, ignoredKeys);
partialAssign(stored.opponent1, match.opponent1, ignoredOpponentKeys);
partialAssign(stored.opponent2, match.opponent2, ignoredOpponentKeys);
}
exports.setExtraFields = setExtraFields;
/**
* Gets the id of the opponent at the given side of the given match.
*
* @param match The match to get the opponent from.
* @param side The side where to get the opponent from.
*/
function getOpponentId(match, side) {
const opponent = match[side];
return opponent && opponent.id;
}
exports.getOpponentId = getOpponentId;
/**
* Gets the origin position of a side of a match.
*
* @param match The match.
* @param side The side.
*/
function getOriginPosition(match, side) {
var _a;
const matchNumber = (_a = match[side]) === null || _a === void 0 ? void 0 : _a.position;
if (matchNumber === undefined)
throw Error('Position is undefined.');
return matchNumber;
}
exports.getOriginPosition = getOriginPosition;
/**
* Returns every loser in a list of matches.
*
* @param participants The list of participants.
* @param matches A list of matches to get losers of.
*/
function getLosers(participants, matches) {
const losers = [];
let currentRound = null;
let roundIndex = -1;
for (const match of matches) {
if (match.round_id !== currentRound) {
currentRound = match.round_id;
roundIndex++;
losers[roundIndex] = [];
}
const loser = getLoser(match);
if (loser === null)
continue;
losers[roundIndex].push(findParticipant(participants, loser));
}
return losers;
}
exports.getLosers = getLosers;
/**
* Makes final standings based on participants grouped by ranking.
*
* @param grouped A list of participants grouped by ranking.
*/
function makeFinalStandings(grouped) {
const standings = [];
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;
}
exports.makeFinalStandings = makeFinalStandings;
/**
* Returns the decisive match of a Grand Final.
*
* @param type The type of Grand Final.
* @param matches The matches in the Grand Final.
*/
function getGrandFinalDecisiveMatch(type, matches) {
if (type === 'simple')
return matches[0];
if (type === 'double') {
const result = getMatchResult(matches[0]);
if (result === 'opponent2')
return matches[1];
return matches[0];
}
throw Error('The Grand Final is disabled.');
}
exports.getGrandFinalDecisiveMatch = getGrandFinalDecisiveMatch;
/**
* Finds a participant in a list.
*
* @param participants The list of participants.
* @param slot The slot of the participant to find.
*/
function findParticipant(participants, slot) {
if (!slot)
throw Error('Cannot find a BYE participant.');
const participant = participants.find(participant => participant.id === (slot === null || slot === void 0 ? void 0 : slot.id));
if (!participant)
throw Error('Participant not found.');
return participant;
}
exports.findParticipant = findParticipant;
/**
* Gets the side the winner of the current match will go to in the next match.
*
* @param matchNumber Number of the current match.
* @param roundNumber Number of the current round.
* @param roundCount Count of rounds.
* @param matchLocation Location of the current match.
*/
function getNextSide(matchNumber, roundNumber, roundCount, matchLocation) {
// The nextSide comes from the same bracket.
if (matchLocation === 'loser_bracket' && roundNumber % 2 === 1)
return 'opponent2';
// The nextSide comes from the loser bracket to the final group.
if (matchLocation === 'loser_bracket' && roundNumber === roundCount)
return 'opponent2';
return getSide(matchNumber);
}
exports.getNextSide = getNextSide;
/**
* Gets the side the winner of the current match in loser bracket will go in the next match.
*
* @param matchNumber Number of the match.
* @param nextMatch The next match.
* @param roundNumber Number of the current round.
*/
function getNextSideLoserBracket(matchNumber, nextMatch, roundNumber) {
var _a;
// The nextSide comes from the WB.
if (roundNumber > 1)
return 'opponent1';
// The nextSide comes from the WB round 1.
if (((_a = nextMatch.opponent1) === null || _a === void 0 ? void 0 : _a.position) === matchNumber)
return 'opponent1';
return 'opponent2';
}
exports.getNextSideLoserBracket = getNextSideLoserBracket;
/**
* Gets the side the loser of the current match in loser bracket will go in the next match.
*
* @param roundNumber Number of the current round.
*/
function getNextSideConsolationFinalDoubleElimination(roundNumber) {
return isMajorRound(roundNumber) ? 'opponent1' : 'opponent2';
}
exports.getNextSideConsolationFinalDoubleElimination = getNextSideConsolationFinalDoubleElimination;
/**
* Sets an opponent in the next match he has to go.
*
* @param nextMatch A match which follows the current one.
* @param nextSide The side the opponent will be on in the next match.
* @param match The current match.
* @param currentSide The side the opponent is currently on.
*/
function setNextOpponent(nextMatch, nextSide, match, currentSide) {
var _a;
nextMatch[nextSide] = match[currentSide] && {
id: getOpponentId(match, currentSide),
position: (_a = nextMatch[nextSide]) === null || _a === void 0 ? void 0 : _a.position, // Keep position.
};
nextMatch.status = getMatchStatus(nextMatch);
}
exports.setNextOpponent = setNextOpponent;
/**
* Resets an opponent in the match following the current one.
*
* @param nextMatch A match which follows the current one.
* @param nextSide The side the opponent will be on in the next match.
*/
function resetNextOpponent(nextMatch, nextSide) {
var _a;
nextMatch[nextSide] = nextMatch[nextSide] && {
id: null,
position: (_a = nextMatch[nextSide]) === null || _a === void 0 ? void 0 : _a.position, // Keep position.
};
nextMatch.status = brackets_model_1.Status.Locked;
}
exports.resetNextOpponent = resetNextOpponent;
/**
* Inverts opponents if requested by the input.
*
* @param stored A reference to what will be updated in the storage.
* @param match Input of the update.
*/
function handleOpponentsInversion(stored, match) {
var _a, _b, _c, _d;
const id1 = (_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.id;
const id2 = (_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.id;
const storedId1 = (_c = stored.opponent1) === null || _c === void 0 ? void 0 : _c.id;
const storedId2 = (_d = stored.opponent2) === null || _d === void 0 ? void 0 : _d.id;
if (isDefined(id1) && id1 !== storedId1 && id1 !== storedId2)
throw Error('The given opponent1 ID does not exist in this match.');
if (isDefined(id2) && id2 !== storedId1 && id2 !== storedId2)
throw Error('The given opponent2 ID does not exist in this match.');
if (isDefined(id1) && id1 === storedId2 || isDefined(id2) && id2 === storedId1)
invertOpponents(match);
}
exports.handleOpponentsInversion = handleOpponentsInversion;
/**
* Sets the `result` of both opponents based on their scores.
*
* @param stored A reference to what will be updated in the storage.
* @param match Input of the update.
*/
function handleGivenStatus(stored, match) {
var _a, _b, _c, _d;
if (match.status === brackets_model_1.Status.Running) {
(_a = stored.opponent1) === null || _a === void 0 ? true : delete _a.result;
(_b = stored.opponent2) === null || _b === void 0 ? true : delete _b.result;
stored.status = brackets_model_1.Status.Running;
}
else if (match.status === brackets_model_1.Status.Completed) {
if (((_c = match.opponent1) === null || _c === void 0 ? void 0 : _c.score) === undefined || ((_d = match.opponent2) === null || _d === void 0 ? void 0 : _d.score) === undefined)
return;
if (match.opponent1.score > match.opponent2.score)
match.opponent1.result = 'win';
else if (match.opponent2.score > match.opponent1.score)
match.opponent2.result = 'win';
else {
// This will throw in an elimination stage.
match.opponent1.result = 'draw';
match.opponent2.result = 'draw';
}
stored.status = brackets_model_1.Status.Completed;
}
}
exports.handleGivenStatus = handleGivenStatus;
/**
* Inverts `opponent1` and `opponent2` in a match.
*
* @param match A match to update.
*/
function invertOpponents(match) {
[match.opponent1, match.opponent2] = [match.opponent2, match.opponent1];
}
exports.invertOpponents = invertOpponents;
/**
* Updates the scores of a match.
*
* @param stored A reference to what will be updated in the storage.
* @param match Input of the update.
* @returns `true` if the status of the match changed, `false` otherwise.
*/
function setScores(stored, match) {
var _a, _b, _c, _d;
// Skip if no score update.
if (((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.score) === ((_b = stored.opponent1) === null || _b === void 0 ? void 0 : _b.score) && ((_c = match.opponent2) === null || _c === void 0 ? void 0 : _c.score) === ((_d = stored.opponent2) === null || _d === void 0 ? void 0 : _d.score))
return false;
const oldStatus = stored.status;
stored.status = brackets_model_1.Status.Running;
if (match.opponent1 && stored.opponent1)
stored.opponent1.score = match.opponent1.score;
if (match.opponent2 && stored.opponent2)
stored.opponent2.score = match.opponent2.score;
return stored.status !== oldStatus;
}
exports.setScores = setScores;
/**
* Infers the win result based on BYEs.
*
* @param opponent1 Opponent 1.
* @param opponent2 Opponent 2.
*/
function getInferredResult(opponent1, opponent2) {
if (opponent1 && !opponent2) // someone vs. BYE
return { opponent1: { ...opponent1, result: 'win' }, opponent2: null };
if (!opponent1 && opponent2) // BYE vs. someone
return { opponent1: null, opponent2: { ...opponent2, result: 'win' } };
return { opponent1, opponent2 }; // Do nothing if both BYE or both someone
}
exports.getInferredResult = getInferredResult;
/**
* Completes a match and handles results and forfeits.
*
* @param stored A reference to what will be updated in the storage.
* @param match Input of the update.
* @param inRoundRobin Indicates whether the match is in a round-robin stage.
*/
function setCompleted(stored, match, inRoundRobin) {
stored.status = brackets_model_1.Status.Completed;
setResults(stored, match, 'win', 'loss', inRoundRobin);
setResults(stored, match, 'loss', 'win', inRoundRobin);
setResults(stored, match, 'draw', 'draw', inRoundRobin);
const { opponent1, opponent2 } = getInferredResult(stored.opponent1, stored.opponent2);
stored.opponent1 = opponent1;
stored.opponent2 = opponent2;
setForfeits(stored, match);
}
exports.setCompleted = setCompleted;
/**
* Enforces the symmetry between opponents.
*
* Sets an opponent's result to something, based on the result on the other opponent.
*
* @param stored A reference to what will be updated in the storage.
* @param match Input of the update.
* @param check A result to check in each opponent.
* @param change A result to set in each other opponent if `check` is correct.
* @param inRoundRobin Indicates whether the match is in a round-robin stage.
*/
function setResults(stored, match, check, change, inRoundRobin) {
var _a, _b;
if (match.opponent1 && match.opponent2) {
if (match.opponent1.result === 'win' && match.opponent2.result === 'win')
throw Error('There are two winners.');
if (match.opponent1.result === 'loss' && match.opponent2.result === 'loss')
throw Error('There are two losers.');
if (!inRoundRobin && match.opponent1.forfeit === true && match.opponent2.forfeit === true)
throw Error('There are two forfeits.');
}
if (((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.result) === check) {
if (stored.opponent1)
stored.opponent1.result = check;
else
stored.opponent1 = { id: null, result: check };
if (stored.opponent2)
stored.opponent2.result = change;
else
stored.opponent2 = { id: null, result: change };
}
if (((_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.result) === check) {
if (stored.opponent2)
stored.opponent2.result = check;
else
stored.opponent2 = { id: null, result: check };
if (stored.opponent1)
stored.opponent1.result = change;
else
stored.opponent1 = { id: null, result: change };
}
}
exports.setResults = setResults;
/**
* Sets forfeits for each opponent (if needed).
*
* @param stored A reference to what will be updated in the storage.
* @param match Input of the update.
*/
function setForfeits(stored, match) {
var _a, _b, _c, _d;
if (((_a = match.opponent1) === null || _a === void 0 ? void 0 : _a.forfeit) === true && ((_b = match.opponent2) === null || _b === void 0 ? void 0 : _b.forfeit) === true) {
if (stored.opponent1)
stored.opponent1.forfeit = true;
if (stored.opponent2)
stored.opponent2.forfeit = true;
// Don't set any result (win/draw/loss) with a double forfeit
// so that it doesn't count any point in the ranking.
return;
}
if (((_c = match.opponent1) === null || _c === void 0 ? void 0 : _c.forfeit) === true) {
if (stored.opponent1)
stored.opponent1.forfeit = true;
if (stored.opponent2)
stored.opponent2.result = 'win';
else
stored.opponent2 = { id: null, result: 'win' };
}
if (((_d = match.opponent2) === null || _d === void 0 ? void 0 : _d.forfeit) === true) {
if (stored.opponent2)
stored.opponent2.forfeit = true;
if (stored.opponent1)
stored.opponent1.result = 'win';
else
stored.opponent1 = { id: null, result: 'win' };
}
}
exports.setForfeits = setForfeits;
/**
* Indicates if a seeding is filled with participants' IDs.
*
* @param seeding The seeding.
*/
function isSeedingWithIds(seeding) {
return seeding.some(value => typeof value === 'number');
}
exports.isSeedingWithIds = isSeedingWithIds;
/**
* Extracts participants from a seeding, without the BYEs.
*
* @param tournamentId ID of the tournament.
* @param seeding The seeding (no IDs).
*/
function extractParticipantsFromSeeding(tournamentId, seeding) {
const withoutByes = seeding.filter((name) => name !== null);
const participants = withoutByes.map((item) => {
if (typeof item === 'string') {
return {
tournament_id: tournamentId,
name: item,
};
}
return {
...item,
tournament_id: tournamentId,
name: item.name,
};
});
return participants;
}
exports.extractParticipantsFromSeeding = extractParticipantsFromSeeding;
/**
* 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.
*/
function mapParticipantsNamesToDatabase(seeding, database, positions) {
return mapParticipantsToDatabase('name', seeding, database, positions);
}
exports.mapParticipantsNamesToDatabase = mapParticipantsNamesToDatabase;
/**
* 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.
*/
function mapParticipantsIdsToDatabase(seeding, database, positions) {
return mapParticipantsToDatabase('id', seeding, database, positions);
}
exports.mapParticipantsIdsToDatabase = mapParticipantsIdsToDatabase;
/**
* 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.
*/
function mapParticipantsToDatabase(prop, seeding, database, positions) {
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`.
}
exports.mapParticipantsToDatabase = mapParticipantsToDatabase;
/**
* Converts a list of matches to a seeding.
*
* @param matches The input matches.
*/
function convertMatchesToSeeding(matches) {
const flattened = [].concat(...matches.map(match => [match.opponent1, match.opponent2]));
return sortSeeding(flattened);
}
exports.convertMatchesToSeeding = convertMatchesToSeeding;
/**
* Converts a list of slots to an input seeding.
*
* @param slots The slots to convert.
*/
function convertSlotsToSeeding(slots) {
return slots.map(slot => {
if (slot === null || slot.id === null)
return null; // BYE or TBD.
return slot.id; // Let's return the ID instead of the name to be sure we keep the same reference.
});
}
exports.convertSlotsToSeeding = convertSlotsToSeeding;
/**
* Sorts the seeding with the BYEs in the correct position.
*
* @param slots A list of slots to sort.
*/
function sortSeeding(slots) {
const withoutByes = slots.filter(v => v !== null);
// a and b are not null because of the filter.
// The slots are from seeding slots, thus they have a position.
withoutByes.sort((a, b) => a.position - b.position);
if (withoutByes.length === slots.length)
return withoutByes;
// Same for v and position.
const placed = Object.fromEntries(withoutByes.map(v => [v.position - 1, v]));
const sortedWithByes = Array.from({ length: slots.length }, (_, i) => placed[i] || null);
return sortedWithByes;
}
exports.sortSeeding = sortSeeding;
/**
* Returns only the non null elements.
*
* @param array The array to process.
*/
function getNonNull(array) {
// Use a TS type guard to exclude null from the resulting type.
const nonNull = array.filter((element) => element !== null);
return nonNull;
}
exports.getNonNull = getNonNull;
/**
* Returns a list of objects which have unique values of a specific key.
*
* @param array The array to process.
* @param key The key to filter by.
*/
function uniqueBy(array, key) {
const seen = new Set();
return array.filter(item => {
const value = key(item);
if (!value)
return true;
if (seen.has(value))
return false;
seen.add(value);
return true;
});
}
exports.uniqueBy = uniqueBy;
/**
* Indicates whether the loser bracket round i