UNPKG

brackets-manager

Version:

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

1,307 lines 68.7 kB
"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