Skip to content

Commit 8daf7a2

Browse files
committed
Render consolation final in double elimination
1 parent e27bc50 commit 8daf7a2

File tree

9 files changed

+98
-47
lines changed

9 files changed

+98
-47
lines changed

demo/with-api.html

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,6 @@
5656
return `${t(`abbreviations.${info.groupType}`)} Semi Finals`
5757
}
5858
}
59-
60-
if (info.finalType === 'grand-final') {
61-
if (info.roundCount > 1) {
62-
return `${t(`abbreviations.${info.finalType}`)} Final Round ${info.roundNumber}`
63-
}
64-
65-
return `Grand Final`
66-
}
6759
},
6860
onMatchClick: match => console.log('A match was clicked', match),
6961
selector: '#example',

demo/with-local-storage.html

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,6 @@
6565
return `${t(`abbreviations.${info.groupType}`)} Semi Finals`
6666
}
6767
}
68-
69-
if (info.finalType === 'grand-final') {
70-
if (info.roundCount > 1) {
71-
return `${t(`abbreviations.${info.finalType}`)} Final Round ${info.roundNumber}`
72-
}
73-
74-
return `Grand Final`
75-
}
7668
},
7769
onMatchClick: match => console.log('A match was clicked', match),
7870
selector: '#example',

dist/brackets-viewer.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/stage-form-creator.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/en/translation.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"winner-bracket-semi-final": "Loser of $t(abbreviations.winner-bracket) Semi {{position}}",
66
"winner-bracket-final": "Loser of $t(abbreviations.winner-bracket) Final",
77
"consolation-final": "Loser of Semi {{position}}",
8-
"grand-final": "Winner of $t(abbreviations.loser-bracket) Final"
8+
"grand-final": "Winner of $t(abbreviations.loser-bracket) Final",
9+
"double-elimination-consolation-final-opponent-1": "Loser of $t(abbreviations.loser-bracket) Semi 1",
10+
"double-elimination-consolation-final-opponent-2": "Loser of $t(abbreviations.loser-bracket) Final"
911
},
1012
"match-label": {
1113
"default": "Match {{matchNumber}}",

src/i18n/fr/translation.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"winner-bracket-semi-final": "Perdant $t(abbreviations.winner-bracket) Semi {{position}}",
66
"winner-bracket-final": "Perdant Finale $t(abbreviations.winner-bracket)",
77
"consolation-final": "Perdant Semi {{position}}",
8-
"grand-final": "Gagnant Finale $t(abbreviations.loser-bracket)"
8+
"grand-final": "Gagnant Finale $t(abbreviations.loser-bracket)",
9+
"double-elimination-consolation-final-opponent-1": "Perdant $t(abbreviations.loser-bracket) Semi 1",
10+
"double-elimination-consolation-final-opponent-2": "Perdant $t(abbreviations.loser-bracket) Final"
911
},
1012
"match-label": {
1113
"default": "Match {{matchNumber}}",

src/lang.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import i18next, { StringMap, TOptions } from 'i18next';
22
import LanguageDetector from 'i18next-browser-languagedetector';
33

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

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

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

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

126131
/**

src/main.ts

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export class BracketsViewer {
114114
.map(match => ({
115115
...match,
116116
metadata: {
117+
stageType: stage.type,
117118
games: data.matchGames.filter(game => game.parent_id === match.id),
118119
},
119120
})),
@@ -194,7 +195,7 @@ export class BracketsViewer {
194195
throw Error(`Unknown bracket type: ${stage.type as string}`);
195196
}
196197

197-
this.renderConsolationMatches(root, matchesByGroup);
198+
this.renderConsolationMatches(root, stage, matchesByGroup);
198199
}
199200

200201
/**
@@ -265,9 +266,10 @@ export class BracketsViewer {
265266
* Renders a list of consolation matches.
266267
*
267268
* @param root The root element.
269+
* @param stage The stage to render.
268270
* @param matchesByGroup A list of matches for each group.
269271
*/
270-
private renderConsolationMatches(root: DocumentFragment, matchesByGroup: MatchWithMetadata[][]): void {
272+
private renderConsolationMatches(root: DocumentFragment, stage: Stage, matchesByGroup: MatchWithMetadata[][]): void {
271273
const consolationMatches = matchesByGroup[-1];
272274
if (!consolationMatches?.length)
273275
return;
@@ -281,6 +283,7 @@ export class BracketsViewer {
281283
...match,
282284
metadata: {
283285
label: lang.t('match-label.default', { matchNumber: ++matchNumber }),
286+
stageType: stage.type,
284287
games: [],
285288
},
286289
}, true));
@@ -297,15 +300,13 @@ export class BracketsViewer {
297300
* @param matchesByGroup A list of matches for each group.
298301
*/
299302
private renderSingleElimination(container: HTMLElement, matchesByGroup: MatchWithMetadata[][]): void {
300-
const hasFinal = matchesByGroup[1] !== undefined;
301303
const bracketMatches = splitBy(matchesByGroup[0], 'round_id').map(matches => sortBy(matches, 'number'));
304+
const { hasFinal, connectFinal, finalMatches } = this.getFinalInfoSingleElimination(matchesByGroup);
302305

303-
this.renderBracket(container, bracketMatches, lang.getRoundName, 'single_bracket');
306+
this.renderBracket(container, bracketMatches, lang.getRoundName, 'single_bracket', connectFinal);
304307

305-
if (hasFinal) {
306-
const finalMatches = sortBy(matchesByGroup[1], 'number');
308+
if (hasFinal)
307309
this.renderFinal(container, 'consolation_final', finalMatches);
308-
}
309310
}
310311

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

322-
this.renderBracket(container, winnerBracketMatches, lang.getWinnerBracketRoundName, 'winner_bracket', hasFinal);
323+
this.renderBracket(container, winnerBracketMatches, lang.getWinnerBracketRoundName, 'winner_bracket', connectFinal);
323324

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

329330
if (hasFinal) {
330-
const finalMatches = sortBy(matchesByGroup[2], 'number');
331-
this.renderFinal(container, 'grand_final', finalMatches);
331+
this.renderFinal(container, 'grand_final', grandFinalMatches);
332+
this.renderFinal(container, 'consolation_final', consolationFinalMatches);
332333
}
333334
}
334335

336+
/**
337+
* Returns information about the final group in single elimination.
338+
*
339+
* @param matchesByGroup A list of matches for each group.
340+
*/
341+
private getFinalInfoSingleElimination(matchesByGroup: MatchWithMetadata[][]): {
342+
hasFinal: boolean,
343+
connectFinal: boolean,
344+
finalMatches: MatchWithMetadata[]
345+
} {
346+
const hasFinal = matchesByGroup[1] !== undefined;
347+
const finalMatches = sortBy(matchesByGroup[1] ?? [], 'number');
348+
349+
// In single elimination, the only possible type of final is a consolation final,
350+
// and it has to be disconnected from the bracket because it doesn't directly follows its last match.
351+
const connectFinal = false;
352+
353+
return { hasFinal, connectFinal, finalMatches };
354+
}
355+
356+
/**
357+
* Returns information about the final group in double elimination.
358+
*
359+
* @param matchesByGroup A list of matches for each group.
360+
*/
361+
private getFinalInfoDoubleElimination(matchesByGroup: MatchWithMetadata[][]): {
362+
hasFinal: boolean,
363+
connectFinal: boolean,
364+
grandFinalMatches: MatchWithMetadata[]
365+
consolationFinalMatches: MatchWithMetadata[]
366+
} {
367+
const hasFinal = matchesByGroup[2] !== undefined;
368+
const finalMatches = sortBy(matchesByGroup[2] ?? [], 'number');
369+
370+
// All grand final matches have a `number: 1` property. We can have 0, 1 or 2 of them.
371+
const grandFinalMatches = finalMatches.filter(match => match.number === 1);
372+
// All consolation matches have a `number: 2` property (set by the manager). We can only have 0 or 1 of them.
373+
const consolationFinalMatches = finalMatches.filter(match => match.number === 2);
374+
375+
// In double elimination, we can have a grand final, a consolation final, or both.
376+
// We only want to connect the upper bracket with the final group when we have at least one grand final match.
377+
// The grand final will always be placed directly next to the bracket.
378+
const connectFinal = grandFinalMatches.length > 0;
379+
380+
return { hasFinal, connectFinal, grandFinalMatches, consolationFinalMatches };
381+
}
382+
335383
/**
336384
* Renders a bracket.
337385
*
338386
* @param container The container to render into.
339387
* @param matchesByRound A list of matches for each round.
340388
* @param getRoundName A function giving a round's name based on its number.
341389
* @param bracketType Type of the bracket.
342-
* @param connectFinal Whether to connect the last match of the bracket to the final.
390+
* @param connectFinal Whether to connect the last match of the bracket to the first match of the final group.
343391
*/
344392
private renderBracket(container: HTMLElement, matchesByRound: MatchWithMetadata[][], getRoundName: RoundNameGetter, bracketType: GroupType, connectFinal?: boolean): void {
345393
const groupId = matchesByRound[0][0].group_id;
@@ -392,6 +440,10 @@ export class BracketsViewer {
392440
* @param matches Matches of the final.
393441
*/
394442
private renderFinal(container: HTMLElement, finalType: FinalType, matches: MatchWithMetadata[]): void {
443+
// Double elimination stages can have a grand final, or a consolation final, or both.
444+
if (matches.length === 0)
445+
return;
446+
395447
const upperBracket = container.querySelector('.bracket .rounds');
396448
if (!upperBracket) throw Error('Upper bracket not found.');
397449

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

455+
const defaultFinalRoundNameGetter: RoundNameGetter = ({ roundNumber, roundCount }) => lang.getFinalMatchLabel(finalType, roundNumber, roundCount);
456+
403457
for (let roundIndex = 0; roundIndex < finalMatches.length; roundIndex++) {
404458
const roundNumber = roundIndex + 1;
405459
const roundName = this.getRoundName({
406460
roundNumber,
407461
roundCount,
408462
groupType: lang.toI18nKey('final_group'),
409463
finalType: lang.toI18nKey(finalType),
410-
}, lang.getRoundName);
464+
}, defaultFinalRoundNameGetter);
411465

412466
const finalMatch: MatchWithMetadata = {
413467
...finalMatches[roundIndex],
@@ -502,18 +556,18 @@ export class BracketsViewer {
502556
/**
503557
* Creates a match in a final.
504558
*
505-
* @param type Type of the final.
559+
* @param finalType Type of the final.
506560
* @param match Information about the match.
507561
*/
508-
private createFinalMatch(type: FinalType, match: MatchWithMetadata): HTMLElement {
562+
private createFinalMatch(finalType: FinalType, match: MatchWithMetadata): HTMLElement {
509563
const { roundNumber, roundCount } = match.metadata;
510564

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

514-
const connection = dom.getFinalConnection(type, roundNumber, roundCount);
515-
const matchLabel = lang.getFinalMatchLabel(type, roundNumber, roundCount);
516-
const originHint = lang.getFinalOriginHint(type, roundNumber);
568+
const connection = dom.getFinalConnection(finalType, roundNumber, roundCount);
569+
const matchLabel = lang.getFinalMatchLabel(finalType, roundNumber, roundCount);
570+
const originHint = lang.getFinalOriginHint(match.metadata.stageType, finalType, roundNumber);
517571

518572
match.metadata.connection = connection;
519573
match.metadata.label = matchLabel;

src/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Stage, Match, MatchGame, Participant, GroupType, FinalType, Id } from 'brackets-model';
1+
import { Stage, Match, MatchGame, Participant, GroupType, FinalType, Id, StageType } from 'brackets-model';
22
import { CallbackFunction, FormConfiguration } from './form';
33
import { InMemoryDatabase } from 'brackets-memory-db';
44
import { BracketsViewer } from './main';
@@ -30,6 +30,10 @@ declare global {
3030
*/
3131
export interface MatchWithMetadata extends Match {
3232
metadata: {
33+
// Information known since the beginning
34+
35+
/** Type of the stage this match is in. */
36+
stageType: StageType
3337
/** The list of child games of this match. */
3438
games: MatchGame[]
3539

0 commit comments

Comments
 (0)