Skip to content

Commit b9d95b5

Browse files
authored
Support double forfeit in final of elimination for standings (#243)
* Support double forfeit in final of elimination for standings * Add test case for reset match
1 parent 580a650 commit b9d95b5

2 files changed

Lines changed: 189 additions & 15 deletions

File tree

src/get.ts

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -337,22 +337,27 @@ export class Get extends BaseGetter {
337337
const final = matches.filter(match => match.group_id === singleBracket.id).pop();
338338
if (!final) throw Error('Final not found.');
339339

340-
// 1st place: Final winner.
341-
grouped[0] = [helpers.findParticipant(participants, getFinalWinnerIfDefined(final))];
340+
const finalDoubleForfeitParticipants = getDoubleForfeitParticipants(participants, final);
341+
342+
// 1st place: Final winner, or both finalists when the final is a double forfeit.
343+
grouped[0] = finalDoubleForfeitParticipants || [helpers.findParticipant(participants, getFinalWinnerIfDefined(final))];
342344

343345
// Rest: every loser in reverse order.
344346
const losers = helpers.getLosers(participants, matches.filter(match => match.group_id === singleBracket.id));
345-
grouped.push(...losers.reverse());
347+
grouped.push(...losers.reverse().filter(group => group.length > 0));
346348

347349
if (stage.settings?.consolationFinal) {
348350
const consolationFinal = matches.filter(match => match.group_id === finalGroup.id).pop();
349351
if (!consolationFinal) throw Error('Consolation final not found.');
350352

351-
const consolationFinalWinner = helpers.findParticipant(participants, getFinalWinnerIfDefined(consolationFinal));
352-
const consolationFinalLoser = helpers.findParticipant(participants, helpers.getLoser(consolationFinal));
353+
const consolationFinalDoubleForfeitParticipants = getDoubleForfeitParticipants(participants, consolationFinal);
354+
const consolationFinalGroups = consolationFinalDoubleForfeitParticipants ? [consolationFinalDoubleForfeitParticipants] : [
355+
[helpers.findParticipant(participants, getFinalWinnerIfDefined(consolationFinal))],
356+
[helpers.findParticipant(participants, helpers.getLoser(consolationFinal))],
357+
];
353358

354359
// Overwrite semi-final losers with the consolation final results.
355-
grouped.splice(2, 1, [consolationFinalWinner], [consolationFinalLoser]);
360+
grouped.splice(finalDoubleForfeitParticipants ? 1 : 2, 1, ...consolationFinalGroups);
356361
}
357362

358363
return helpers.makeFinalStandings(grouped);
@@ -377,25 +382,35 @@ export class Get extends BaseGetter {
377382
const finalLB = matches.filter(match => match.group_id === loserBracket.id).pop();
378383
if (!finalLB) throw Error('LB final not found.');
379384

380-
// 1st place: WB Final winner.
381-
grouped[0] = [helpers.findParticipant(participants, getFinalWinnerIfDefined(finalWB))];
385+
const finalWBDoubleForfeitParticipants = getDoubleForfeitParticipants(participants, finalWB);
386+
387+
// 1st place: WB Final winner, or both finalists when the WB Final is a double forfeit.
388+
grouped[0] = finalWBDoubleForfeitParticipants || [helpers.findParticipant(participants, getFinalWinnerIfDefined(finalWB))];
382389

383-
// 2nd place: LB Final winner.
384-
grouped[1] = [helpers.findParticipant(participants, getFinalWinnerIfDefined(finalLB))];
390+
const finalLBDoubleForfeitParticipants = getDoubleForfeitParticipants(participants, finalLB);
391+
392+
// 2nd place: LB Final winner, or both finalists when the LB Final is a double forfeit.
393+
grouped[1] = finalLBDoubleForfeitParticipants || [helpers.findParticipant(participants, getFinalWinnerIfDefined(finalLB))];
385394
} else {
386395
const grandFinalMatches = matches.filter(match => match.group_id === finalGroup.id);
387396
const decisiveMatch = helpers.getGrandFinalDecisiveMatch(stage.settings?.grandFinal || 'none', grandFinalMatches);
397+
const grandFinalDoubleForfeitParticipants = getDoubleForfeitParticipants(participants, decisiveMatch);
388398

389-
// 1st place: Grand Final winner.
390-
grouped[0] = [helpers.findParticipant(participants, getFinalWinnerIfDefined(decisiveMatch))];
399+
if (grandFinalDoubleForfeitParticipants) {
400+
// 1st place: Both grand finalists; the final creates no loser.
401+
grouped[0] = grandFinalDoubleForfeitParticipants;
402+
} else {
403+
// 1st place: Grand Final winner.
404+
grouped[0] = [helpers.findParticipant(participants, getFinalWinnerIfDefined(decisiveMatch))];
391405

392-
// 2nd place: Grand Final loser.
393-
grouped[1] = [helpers.findParticipant(participants, helpers.getLoser(decisiveMatch))];
406+
// 2nd place: Grand Final loser.
407+
grouped[1] = [helpers.findParticipant(participants, helpers.getLoser(decisiveMatch))];
408+
}
394409
}
395410

396411
// Rest: every loser in reverse order.
397412
const losers = helpers.getLosers(participants, matches.filter(match => match.group_id === loserBracket.id));
398-
grouped.push(...losers.reverse());
413+
grouped.push(...losers.reverse().filter(group => group.length > 0));
399414

400415
return helpers.makeFinalStandings(grouped);
401416
}
@@ -436,3 +451,13 @@ const getFinalWinnerIfDefined = (match: Match): ParticipantSlot => {
436451
if (!winner) throw Error('The final match does not have a winner.');
437452
return winner;
438453
};
454+
455+
const getDoubleForfeitParticipants = (participants: Participant[], match: Match): Participant[] | null => {
456+
if (!helpers.isDoubleForfeitCompleted(match))
457+
return null;
458+
459+
return [
460+
helpers.findParticipant(participants, match.opponent1),
461+
helpers.findParticipant(participants, match.opponent2),
462+
];
463+
};

test/get.spec.js

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ const { JsonDatabase } = require('brackets-json-db');
88
const storage = new JsonDatabase();
99
const manager = new BracketsManager(storage);
1010

11+
const makeStandingsItem = (participants, slot, rank) => {
12+
const participant = participants.find(participant => participant.id === slot.id);
13+
return { id: participant.id, name: participant.name, rank };
14+
};
15+
1116
describe('Get child games', () => {
1217

1318
beforeEach(() => {
@@ -132,6 +137,72 @@ describe('Get final standings', () => {
132137
]);
133138
});
134139

140+
it('should get shared first place for a single elimination final double forfeit', async () => {
141+
await manager.create.stage({
142+
name: 'Example',
143+
tournamentId: 0,
144+
type: 'single_elimination',
145+
seeding: ['Team 1', 'Team 2', 'Team 3', 'Team 4'],
146+
});
147+
148+
await manager.update.match({ id: 0, opponent1: { result: 'win' } });
149+
await manager.update.match({ id: 1, opponent1: { result: 'win' } });
150+
151+
const semiFinals = await storage.select('match', { round_id: 0 });
152+
const final = await storage.select('match', 2);
153+
154+
await manager.update.match({
155+
id: final.id,
156+
opponent1: { forfeit: true },
157+
opponent2: { forfeit: true },
158+
});
159+
160+
const participants = await storage.select('participant', { tournament_id: 0 });
161+
const finalStandings = await manager.get.finalStandings(0);
162+
163+
assert.deepEqual(finalStandings, [
164+
makeStandingsItem(participants, final.opponent1, 1),
165+
makeStandingsItem(participants, final.opponent2, 1),
166+
makeStandingsItem(participants, semiFinals[0].opponent2, 2),
167+
makeStandingsItem(participants, semiFinals[1].opponent2, 2),
168+
]);
169+
});
170+
171+
it('should keep consolation final ranks after a single elimination final double forfeit', async () => {
172+
await manager.create.stage({
173+
name: 'Example',
174+
tournamentId: 0,
175+
type: 'single_elimination',
176+
seeding: ['Team 1', 'Team 2', 'Team 3', 'Team 4'],
177+
settings: { consolationFinal: true },
178+
});
179+
180+
await manager.update.match({ id: 0, opponent1: { result: 'win' } });
181+
await manager.update.match({ id: 1, opponent1: { result: 'win' } });
182+
183+
const groups = await storage.select('group', { stage_id: 0 });
184+
const final = (await storage.select('match', { group_id: groups[0].id })).pop();
185+
const consolationFinal = (await storage.select('match', { group_id: groups[1].id })).pop();
186+
187+
await manager.update.match({ id: consolationFinal.id, opponent1: { result: 'win' } });
188+
189+
await manager.update.match({
190+
id: final.id,
191+
opponent1: { forfeit: true },
192+
opponent2: { forfeit: true },
193+
});
194+
195+
const participants = await storage.select('participant', { tournament_id: 0 });
196+
const finalStandings = await manager.get.finalStandings(0);
197+
198+
assert.deepEqual(finalStandings, [
199+
makeStandingsItem(participants, final.opponent1, 1),
200+
makeStandingsItem(participants, final.opponent2, 1),
201+
makeStandingsItem(participants, consolationFinal.opponent1, 2),
202+
makeStandingsItem(participants, consolationFinal.opponent2, 3),
203+
]);
204+
});
205+
135206
it('should get the final standings for a double elimination stage with a grand final', async () => {
136207
await manager.create.stage({
137208
name: 'Example',
@@ -165,6 +236,84 @@ describe('Get final standings', () => {
165236
]);
166237
});
167238

239+
it('should get shared first place for a double elimination grand final double forfeit', async () => {
240+
await manager.create.stage({
241+
name: 'Example',
242+
tournamentId: 0,
243+
type: 'double_elimination',
244+
seeding: [
245+
'Team 1', 'Team 2', 'Team 3', 'Team 4',
246+
],
247+
settings: { grandFinal: 'simple' },
248+
});
249+
250+
const groups = await storage.select('group', { stage_id: 0 });
251+
const grandFinal = (await storage.select('match', { group_id: groups[2].id }))[0];
252+
const matches = (await storage.select('match', { stage_id: 0 }))
253+
.filter(match => match.id !== grandFinal.id)
254+
.sort((a, b) => a.id - b.id);
255+
256+
for (const match of matches)
257+
await manager.update.match({ id: match.id, opponent1: { result: 'win' } });
258+
259+
const final = await storage.select('match', grandFinal.id);
260+
261+
await manager.update.match({
262+
id: final.id,
263+
opponent1: { forfeit: true },
264+
opponent2: { forfeit: true },
265+
});
266+
267+
const participants = await storage.select('participant', { tournament_id: 0 });
268+
const finalStandings = await manager.get.finalStandings(0);
269+
270+
assert.deepEqual(finalStandings.slice(0, 2), [
271+
makeStandingsItem(participants, final.opponent1, 1),
272+
makeStandingsItem(participants, final.opponent2, 1),
273+
]);
274+
assert.strictEqual(finalStandings[2].rank, 2);
275+
});
276+
277+
it('should get shared first place for a double grand final reset match double forfeit', async () => {
278+
await manager.create.stage({
279+
name: 'Example',
280+
tournamentId: 0,
281+
type: 'double_elimination',
282+
seeding: [
283+
'Team 1', 'Team 2', 'Team 3', 'Team 4',
284+
],
285+
settings: { grandFinal: 'double' },
286+
});
287+
288+
const groups = await storage.select('group', { stage_id: 0 });
289+
const grandFinalMatches = await storage.select('match', { group_id: groups[2].id });
290+
const matches = (await storage.select('match', { stage_id: 0 }))
291+
.filter(match => match.group_id !== groups[2].id)
292+
.sort((a, b) => a.id - b.id);
293+
294+
for (const match of matches)
295+
await manager.update.match({ id: match.id, opponent1: { result: 'win' } });
296+
297+
await manager.update.match({ id: grandFinalMatches[0].id, opponent2: { result: 'win' } });
298+
299+
const resetMatch = await storage.select('match', grandFinalMatches[1].id);
300+
301+
await manager.update.match({
302+
id: resetMatch.id,
303+
opponent1: { forfeit: true },
304+
opponent2: { forfeit: true },
305+
});
306+
307+
const participants = await storage.select('participant', { tournament_id: 0 });
308+
const finalStandings = await manager.get.finalStandings(0);
309+
310+
assert.deepEqual(finalStandings.slice(0, 2), [
311+
makeStandingsItem(participants, resetMatch.opponent1, 1),
312+
makeStandingsItem(participants, resetMatch.opponent2, 1),
313+
]);
314+
assert.strictEqual(finalStandings[2].rank, 2);
315+
});
316+
168317
it('should get the final standings for a double elimination stage without a grand final', async () => {
169318
await manager.create.stage({
170319
name: 'Example',

0 commit comments

Comments
 (0)