Skip to content

Commit

Permalink
Render consolation final in double elimination
Browse files Browse the repository at this point in the history
  • Loading branch information
Drarig29 committed Aug 10, 2023
1 parent e27bc50 commit 8daf7a2
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 47 deletions.
8 changes: 0 additions & 8 deletions demo/with-api.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,6 @@
return `${t(`abbreviations.${info.groupType}`)} Semi Finals`
}
}

if (info.finalType === 'grand-final') {
if (info.roundCount > 1) {
return `${t(`abbreviations.${info.finalType}`)} Final Round ${info.roundNumber}`
}

return `Grand Final`
}
},
onMatchClick: match => console.log('A match was clicked', match),
selector: '#example',
Expand Down
8 changes: 0 additions & 8 deletions demo/with-local-storage.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,6 @@
return `${t(`abbreviations.${info.groupType}`)} Semi Finals`
}
}

if (info.finalType === 'grand-final') {
if (info.roundCount > 1) {
return `${t(`abbreviations.${info.finalType}`)} Final Round ${info.roundNumber}`
}

return `Grand Final`
}
},
onMatchClick: match => console.log('A match was clicked', match),
selector: '#example',
Expand Down
2 changes: 1 addition & 1 deletion dist/brackets-viewer.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/stage-form-creator.min.js

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"winner-bracket-semi-final": "Loser of $t(abbreviations.winner-bracket) Semi {{position}}",
"winner-bracket-final": "Loser of $t(abbreviations.winner-bracket) Final",
"consolation-final": "Loser of Semi {{position}}",
"grand-final": "Winner of $t(abbreviations.loser-bracket) Final"
"grand-final": "Winner of $t(abbreviations.loser-bracket) Final",
"double-elimination-consolation-final-opponent-1": "Loser of $t(abbreviations.loser-bracket) Semi 1",
"double-elimination-consolation-final-opponent-2": "Loser of $t(abbreviations.loser-bracket) Final"
},
"match-label": {
"default": "Match {{matchNumber}}",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"winner-bracket-semi-final": "Perdant $t(abbreviations.winner-bracket) Semi {{position}}",
"winner-bracket-final": "Perdant Finale $t(abbreviations.winner-bracket)",
"consolation-final": "Perdant Semi {{position}}",
"grand-final": "Gagnant Finale $t(abbreviations.loser-bracket)"
"grand-final": "Gagnant Finale $t(abbreviations.loser-bracket)",
"double-elimination-consolation-final-opponent-1": "Perdant $t(abbreviations.loser-bracket) Semi 1",
"double-elimination-consolation-final-opponent-2": "Perdant $t(abbreviations.loser-bracket) Final"
},
"match-label": {
"default": "Match {{matchNumber}}",
Expand Down
21 changes: 13 additions & 8 deletions src/lang.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import i18next, { StringMap, TOptions } from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

import { Stage, Status, FinalType, GroupType } from 'brackets-model';
import { Stage, Status, FinalType, GroupType, StageType } from 'brackets-model';
import { isMajorRound } from './helpers';
import { OriginHint, RoundNameInfo } from './types';

Expand Down Expand Up @@ -107,20 +107,25 @@ export function getOriginHint(roundNumber: number, roundCount: number, skipFirst
/**
* Returns an origin hint function for a match in final.
*
* @param stageType Type of the stage.
* @param finalType Type of the final.
* @param roundNumber Number of the round.
*/
export function getFinalOriginHint(finalType: FinalType, roundNumber: number): OriginHint | undefined {
// Single elimination.
if (finalType === 'consolation_final')
export function getFinalOriginHint(stageType: StageType, finalType: FinalType, roundNumber: number): OriginHint | undefined {
if (stageType === 'single_elimination')
return (position: number): string => t('origin-hint.consolation-final', { position });

// Double elimination.
if (roundNumber === 1) // Grand Final round 1
return (): string => t('origin-hint.grand-final');
if (finalType === 'grand_final') {
return roundNumber === 1
? (): string => t('origin-hint.grand-final') // Grand Final round 1
: undefined; // Grand Final round 2 (no hint because it's obvious both participants come from the previous round)
}

// Grand Final round 2 (no hint because it's obvious both participants come from the previous round)
return undefined;
// Consolation final in double elimination.
return (position: number): string => position === 1
? t('origin-hint.double-elimination-consolation-final-opponent-1')
: t('origin-hint.double-elimination-consolation-final-opponent-2');
}

/**
Expand Down
90 changes: 72 additions & 18 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export class BracketsViewer {
.map(match => ({
...match,
metadata: {
stageType: stage.type,
games: data.matchGames.filter(game => game.parent_id === match.id),
},
})),
Expand Down Expand Up @@ -194,7 +195,7 @@ export class BracketsViewer {
throw Error(`Unknown bracket type: ${stage.type as string}`);
}

this.renderConsolationMatches(root, matchesByGroup);
this.renderConsolationMatches(root, stage, matchesByGroup);
}

/**
Expand Down Expand Up @@ -265,9 +266,10 @@ export class BracketsViewer {
* Renders a list of consolation matches.
*
* @param root The root element.
* @param stage The stage to render.
* @param matchesByGroup A list of matches for each group.
*/
private renderConsolationMatches(root: DocumentFragment, matchesByGroup: MatchWithMetadata[][]): void {
private renderConsolationMatches(root: DocumentFragment, stage: Stage, matchesByGroup: MatchWithMetadata[][]): void {
const consolationMatches = matchesByGroup[-1];
if (!consolationMatches?.length)
return;
Expand All @@ -281,6 +283,7 @@ export class BracketsViewer {
...match,
metadata: {
label: lang.t('match-label.default', { matchNumber: ++matchNumber }),
stageType: stage.type,
games: [],
},
}, true));
Expand All @@ -297,15 +300,13 @@ export class BracketsViewer {
* @param matchesByGroup A list of matches for each group.
*/
private renderSingleElimination(container: HTMLElement, matchesByGroup: MatchWithMetadata[][]): void {
const hasFinal = matchesByGroup[1] !== undefined;
const bracketMatches = splitBy(matchesByGroup[0], 'round_id').map(matches => sortBy(matches, 'number'));
const { hasFinal, connectFinal, finalMatches } = this.getFinalInfoSingleElimination(matchesByGroup);

this.renderBracket(container, bracketMatches, lang.getRoundName, 'single_bracket');
this.renderBracket(container, bracketMatches, lang.getRoundName, 'single_bracket', connectFinal);

if (hasFinal) {
const finalMatches = sortBy(matchesByGroup[1], 'number');
if (hasFinal)
this.renderFinal(container, 'consolation_final', finalMatches);
}
}

/**
Expand All @@ -316,30 +317,77 @@ export class BracketsViewer {
*/
private renderDoubleElimination(container: HTMLElement, matchesByGroup: MatchWithMetadata[][]): void {
const hasLoserBracket = matchesByGroup[1] !== undefined;
const hasFinal = matchesByGroup[2] !== undefined;
const winnerBracketMatches = splitBy(matchesByGroup[0], 'round_id').map(matches => sortBy(matches, 'number'));
const { hasFinal, connectFinal, grandFinalMatches, consolationFinalMatches } = this.getFinalInfoDoubleElimination(matchesByGroup);

this.renderBracket(container, winnerBracketMatches, lang.getWinnerBracketRoundName, 'winner_bracket', hasFinal);
this.renderBracket(container, winnerBracketMatches, lang.getWinnerBracketRoundName, 'winner_bracket', connectFinal);

if (hasLoserBracket) {
const loserBracketMatches = splitBy(matchesByGroup[1], 'round_id').map(matches => sortBy(matches, 'number'));
this.renderBracket(container, loserBracketMatches, lang.getLoserBracketRoundName, 'loser_bracket');
}

if (hasFinal) {
const finalMatches = sortBy(matchesByGroup[2], 'number');
this.renderFinal(container, 'grand_final', finalMatches);
this.renderFinal(container, 'grand_final', grandFinalMatches);
this.renderFinal(container, 'consolation_final', consolationFinalMatches);
}
}

/**
* Returns information about the final group in single elimination.
*
* @param matchesByGroup A list of matches for each group.
*/
private getFinalInfoSingleElimination(matchesByGroup: MatchWithMetadata[][]): {
hasFinal: boolean,
connectFinal: boolean,
finalMatches: MatchWithMetadata[]
} {
const hasFinal = matchesByGroup[1] !== undefined;
const finalMatches = sortBy(matchesByGroup[1] ?? [], 'number');

// In single elimination, the only possible type of final is a consolation final,
// and it has to be disconnected from the bracket because it doesn't directly follows its last match.
const connectFinal = false;

return { hasFinal, connectFinal, finalMatches };
}

/**
* Returns information about the final group in double elimination.
*
* @param matchesByGroup A list of matches for each group.
*/
private getFinalInfoDoubleElimination(matchesByGroup: MatchWithMetadata[][]): {
hasFinal: boolean,
connectFinal: boolean,
grandFinalMatches: MatchWithMetadata[]
consolationFinalMatches: MatchWithMetadata[]
} {
const hasFinal = matchesByGroup[2] !== undefined;
const finalMatches = sortBy(matchesByGroup[2] ?? [], 'number');

// All grand final matches have a `number: 1` property. We can have 0, 1 or 2 of them.
const grandFinalMatches = finalMatches.filter(match => match.number === 1);
// All consolation matches have a `number: 2` property (set by the manager). We can only have 0 or 1 of them.
const consolationFinalMatches = finalMatches.filter(match => match.number === 2);

// In double elimination, we can have a grand final, a consolation final, or both.
// We only want to connect the upper bracket with the final group when we have at least one grand final match.
// The grand final will always be placed directly next to the bracket.
const connectFinal = grandFinalMatches.length > 0;

return { hasFinal, connectFinal, grandFinalMatches, consolationFinalMatches };
}

/**
* Renders a bracket.
*
* @param container The container to render into.
* @param matchesByRound A list of matches for each round.
* @param getRoundName A function giving a round's name based on its number.
* @param bracketType Type of the bracket.
* @param connectFinal Whether to connect the last match of the bracket to the final.
* @param connectFinal Whether to connect the last match of the bracket to the first match of the final group.
*/
private renderBracket(container: HTMLElement, matchesByRound: MatchWithMetadata[][], getRoundName: RoundNameGetter, bracketType: GroupType, connectFinal?: boolean): void {
const groupId = matchesByRound[0][0].group_id;
Expand Down Expand Up @@ -392,6 +440,10 @@ export class BracketsViewer {
* @param matches Matches of the final.
*/
private renderFinal(container: HTMLElement, finalType: FinalType, matches: MatchWithMetadata[]): void {
// Double elimination stages can have a grand final, or a consolation final, or both.
if (matches.length === 0)
return;

const upperBracket = container.querySelector('.bracket .rounds');
if (!upperBracket) throw Error('Upper bracket not found.');

Expand All @@ -400,14 +452,16 @@ export class BracketsViewer {
const finalMatches = matches.slice(0, displayCount);
const roundCount = finalMatches.length;

const defaultFinalRoundNameGetter: RoundNameGetter = ({ roundNumber, roundCount }) => lang.getFinalMatchLabel(finalType, roundNumber, roundCount);

for (let roundIndex = 0; roundIndex < finalMatches.length; roundIndex++) {
const roundNumber = roundIndex + 1;
const roundName = this.getRoundName({
roundNumber,
roundCount,
groupType: lang.toI18nKey('final_group'),
finalType: lang.toI18nKey(finalType),
}, lang.getRoundName);
}, defaultFinalRoundNameGetter);

const finalMatch: MatchWithMetadata = {
...finalMatches[roundIndex],
Expand Down Expand Up @@ -502,18 +556,18 @@ export class BracketsViewer {
/**
* Creates a match in a final.
*
* @param type Type of the final.
* @param finalType Type of the final.
* @param match Information about the match.
*/
private createFinalMatch(type: FinalType, match: MatchWithMetadata): HTMLElement {
private createFinalMatch(finalType: FinalType, match: MatchWithMetadata): HTMLElement {
const { roundNumber, roundCount } = match.metadata;

if (roundNumber === undefined || roundCount === undefined)
throw Error(`The match's internal data is missing roundNumber or roundCount: ${JSON.stringify(match)}`);

const connection = dom.getFinalConnection(type, roundNumber, roundCount);
const matchLabel = lang.getFinalMatchLabel(type, roundNumber, roundCount);
const originHint = lang.getFinalOriginHint(type, roundNumber);
const connection = dom.getFinalConnection(finalType, roundNumber, roundCount);
const matchLabel = lang.getFinalMatchLabel(finalType, roundNumber, roundCount);
const originHint = lang.getFinalOriginHint(match.metadata.stageType, finalType, roundNumber);

match.metadata.connection = connection;
match.metadata.label = matchLabel;
Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Stage, Match, MatchGame, Participant, GroupType, FinalType, Id } from 'brackets-model';
import { Stage, Match, MatchGame, Participant, GroupType, FinalType, Id, StageType } from 'brackets-model';
import { CallbackFunction, FormConfiguration } from './form';
import { InMemoryDatabase } from 'brackets-memory-db';
import { BracketsViewer } from './main';
Expand Down Expand Up @@ -30,6 +30,10 @@ declare global {
*/
export interface MatchWithMetadata extends Match {
metadata: {
// Information known since the beginning

/** Type of the stage this match is in. */
stageType: StageType
/** The list of child games of this match. */
games: MatchGame[]

Expand Down

0 comments on commit 8daf7a2

Please sign in to comment.