diff --git a/app/modules/brackets-manager/base/getter.ts b/app/modules/brackets-manager/base/getter.ts index a5264238b..4e1c9dda5 100644 --- a/app/modules/brackets-manager/base/getter.ts +++ b/app/modules/brackets-manager/base/getter.ts @@ -108,7 +108,7 @@ export class BaseGetter { return this.getPreviousMatchesLB(match, stage, roundNumber); if (matchLocation === "final_group") - return this.getPreviousMatchesFinal(match, roundNumber); + return this.getPreviousMatchesFinal(match, stage, roundNumber); if (roundNumber === 1) return []; // The match is in the first round of an upper bracket. @@ -119,23 +119,89 @@ export class BaseGetter { * Gets the matches leading to the given match, which is in a final group (consolation final or grand final). * * @param match The current match. + * @param stage The parent stage. * @param roundNumber Number of the current round. */ - private getPreviousMatchesFinal(match: Match, roundNumber: number): Match[] { + private getPreviousMatchesFinal( + match: Match, + stage: Stage, + roundNumber: number + ): Match[] { + if (stage.type === "single_elimination") + return this.getPreviousMatchesFinalSingleElimination(match, stage); + + return this.getPreviousMatchesFinalDoubleElimination(match, roundNumber); + } + + /** + * Gets the matches leading to the given match, which is in a final group (consolation final). + * + * @param match The current match. + * @param stage The parent stage. + */ + private getPreviousMatchesFinalSingleElimination( + match: Match, + stage: Stage + ): Match[] { + const upperBracket = this.getUpperBracket(match.stage_id); + const upperBracketRoundCount = helpers.getUpperBracketRoundCount( + stage.settings.size! + ); + + const semiFinalsRound = this.storage.selectFirst("round", { + group_id: upperBracket.id, + number: upperBracketRoundCount - 1, // Second to last round + }); + + if (!semiFinalsRound) throw Error("Semi finals round not found."); + + const semiFinalMatches = this.storage.select("match", { + round_id: semiFinalsRound.id, + }); + + if (!semiFinalMatches) throw Error("Error getting semi final matches."); + + // In single elimination, both the final and consolation final have the same previous matches. + return semiFinalMatches; + } + + /** + * Gets the matches leading to the given match, which is in a final group (grand final). + * + * @param match The current match. + * @param stage The parent stage. + * @param roundNumber Number of the current round. + */ + private getPreviousMatchesFinalDoubleElimination( + match: Match, + roundNumber: number + ): Match[] { if (roundNumber > 1) + // Double grand final return [this.findMatch(match.group_id, roundNumber - 1, 1)]; - const upperBracket = this.getUpperBracket(match.stage_id); - const lastRound = this.getLastRound(upperBracket.id); + const winnerBracket = this.getUpperBracket(match.stage_id); + const lastRoundWB = this.getLastRound(winnerBracket.id); - const upperBracketFinalMatch = this.storage.selectFirst("match", { - round_id: lastRound.id, + const winnerBracketFinalMatch = this.storage.selectFirst("match", { + round_id: lastRoundWB.id, number: 1, }); - if (upperBracketFinalMatch === null) throw Error("Match not found."); + if (!winnerBracketFinalMatch) throw Error("Match not found."); - return [upperBracketFinalMatch]; + const loserBracket = this.getLoserBracket(match.stage_id); + if (!loserBracket) throw Error("Loser bracket not found."); + + const lastRoundLB = this.getLastRound(loserBracket.id); + const loserBracketFinalMatch = this.storage.selectFirst("match", { + round_id: lastRoundLB.id, + number: 1, + }); + + if (!loserBracketFinalMatch) throw Error("Match not found."); + + return [winnerBracketFinalMatch, loserBracketFinalMatch]; } /** diff --git a/app/modules/brackets-manager/base/updater.ts b/app/modules/brackets-manager/base/updater.ts index e224f136a..9371cc4f0 100644 --- a/app/modules/brackets-manager/base/updater.ts +++ b/app/modules/brackets-manager/base/updater.ts @@ -284,6 +284,8 @@ export class BaseUpdater extends BaseGetter { */ protected archiveMatches(matches: Match[]): void { for (const match of matches) { + if (match.status === Status.Archived) continue; + match.status = Status.Archived; this.applyMatchUpdate(match); } @@ -324,7 +326,13 @@ export class BaseUpdater extends BaseGetter { roundNumber, roundCount ); - if (nextMatches.length === 0) return; + if (nextMatches.length === 0) { + // Archive match if it doesn't have following matches and is completed. + // When the stage is fully complete, all matches should be archived. + if (match.status === Status.Completed) this.archiveMatches([match]); + + return; + } const winnerSide = helpers.getMatchResult(match); const actualRoundNumber = diff --git a/app/modules/brackets-manager/test/custom.test.ts b/app/modules/brackets-manager/test/custom.test.ts index 9cd15c33e..590890afd 100644 --- a/app/modules/brackets-manager/test/custom.test.ts +++ b/app/modules/brackets-manager/test/custom.test.ts @@ -160,18 +160,6 @@ ExtraFields("Extra fields when updating a match game", () => { // @ts-expect-error incomplete types foo: 42, // Extra field. }, - opponent2: { - score: 1, - result: "loss", - }, - }); - - manager.update.matchGame({ - id: 2, - opponent1: { - score: 3, - result: "win", - }, opponent2: { score: 1, result: "loss", @@ -182,7 +170,7 @@ ExtraFields("Extra fields when updating a match game", () => { assert.equal(storage.select("match_game", 0).weather, "rainy"); assert.equal(storage.select("match_game", 1).opponent1.foo, 42); - assert.equal(storage.select("match_game", 2).opponent2.info, { + assert.equal(storage.select("match_game", 1).opponent2.info, { replacements: [1, 2], }); }); diff --git a/app/modules/brackets-manager/test/double-elimination.test.ts b/app/modules/brackets-manager/test/double-elimination.test.ts index 174fed9fb..90ee1ca7c 100644 --- a/app/modules/brackets-manager/test/double-elimination.test.ts +++ b/app/modules/brackets-manager/test/double-elimination.test.ts @@ -288,6 +288,21 @@ MatchUpdateDoubleElimination("should determine matches in grand final", () => { storage.select("match", 6).opponent2.id, // Determined opponent for the grand final (round 2) storage.select("match", 1).opponent2.id // Winner of LB Final ); + + assert.equal(storage.select("match", 2).status, Status.Archived); + assert.equal(storage.select("match", 4).status, Status.Archived); + + assert.equal(storage.select("match", 5).status, Status.Completed); // Grand final (round 1) + assert.equal(storage.select("match", 6).status, Status.Ready); // Grand final (round 2) + + manager.update.match({ + id: 6, // Grand Final round 2 + opponent1: { score: 16, result: "win" }, + opponent2: { score: 10 }, + }); + + assert.equal(storage.select("match", 5).status, Status.Archived); // Grand final (round 1) + assert.equal(storage.select("match", 6).status, Status.Archived); // Grand final (round 2) }); MatchUpdateDoubleElimination( diff --git a/app/modules/brackets-manager/test/single-elimination.test.ts b/app/modules/brackets-manager/test/single-elimination.test.ts index 96c9b83a4..5acb70ec4 100644 --- a/app/modules/brackets-manager/test/single-elimination.test.ts +++ b/app/modules/brackets-manager/test/single-elimination.test.ts @@ -45,9 +45,9 @@ CreateSingleEliminationStage("should create a single elimination stage", () => { assert.equal(stage.name, example.name); assert.equal(stage.type, example.type); - assert.equal(storage.select("group")!.length, 1); - assert.equal(storage.select("round")!.length, 4); - assert.equal(storage.select("match")!.length, 15); + assert.equal(storage.select("group")!.length, 1); + assert.equal(storage.select("round")!.length, 4); + assert.equal(storage.select("match")!.length, 15); }); CreateSingleEliminationStage( @@ -97,9 +97,9 @@ CreateSingleEliminationStage( settings: { consolationFinal: true, seedOrdering: ["natural"] }, }); - assert.equal(storage.select("group")!.length, 2); - assert.equal(storage.select("round")!.length, 4); - assert.equal(storage.select("match")!.length, 8); + assert.equal(storage.select("group")!.length, 2); + assert.equal(storage.select("round")!.length, 4); + assert.equal(storage.select("match")!.length, 8); } ); @@ -152,10 +152,10 @@ CreateSingleEliminationStage( settings: { seedOrdering: ["natural"], matchesChildCount: 3 }, }); - assert.equal(storage.select("group")!.length, 1); - assert.equal(storage.select("round")!.length, 3); - assert.equal(storage.select("match")!.length, 7); - assert.equal(storage.select("match_game")!.length, 7 * 3); + assert.equal(storage.select("group")!.length, 1); + assert.equal(storage.select("round")!.length, 3); + assert.equal(storage.select("match")!.length, 7); + assert.equal(storage.select("match_game")!.length, 7 * 3); } ); @@ -320,12 +320,6 @@ PreviousAndNextMatchUpdate( opponent2: { score: 16, result: "win" }, }); - manager.update.match({ - id: 2, // Final - opponent1: { score: 16, result: "win" }, - opponent2: { score: 9 }, - }); - assert.equal( storage.select("match", 3).opponent1.id, // Determined opponent for the consolation final storage.select("match", 0).opponent2.id // Loser of Semi 1 @@ -335,6 +329,70 @@ PreviousAndNextMatchUpdate( storage.select("match", 3).opponent2.id, // Determined opponent for the consolation final storage.select("match", 1).opponent1.id // Loser of Semi 2 ); + + assert.equal(storage.select("match", 2).status, Status.Ready); + assert.equal(storage.select("match", 3).status, Status.Ready); + } +); + +PreviousAndNextMatchUpdate( + "should play both the final and consolation final in parallel", + () => { + manager.create({ + name: "Example", + tournamentId: 0, + type: "single_elimination", + seeding: ["Team 1", "Team 2", "Team 3", "Team 4"], + settings: { consolationFinal: true }, + }); + + manager.update.match({ + id: 0, // First match of round 1 + opponent1: { score: 16, result: "win" }, + opponent2: { score: 12 }, + }); + + manager.update.match({ + id: 1, // Second match of round 1 + opponent1: { score: 13 }, + opponent2: { score: 16, result: "win" }, + }); + + manager.update.match({ + id: 2, // Final + opponent1: { score: 12 }, + opponent2: { score: 9 }, + }); + + assert.equal(storage.select("match", 2).status, Status.Running); + assert.equal(storage.select("match", 3).status, Status.Ready); + + manager.update.match({ + id: 3, // Consolation final + opponent1: { score: 12 }, + opponent2: { score: 9 }, + }); + + assert.equal(storage.select("match", 2).status, Status.Running); + assert.equal(storage.select("match", 3).status, Status.Running); + + manager.update.match({ + id: 3, // Consolation final + opponent1: { score: 16, result: "win" }, + opponent2: { score: 9 }, + }); + + assert.equal(storage.select("match", 2).status, Status.Running); + assert.equal(storage.select("match", 3).status, Status.Archived); + + manager.update.match({ + id: 2, // Final + opponent1: { score: 16, result: "win" }, + opponent2: { score: 9 }, + }); + + assert.equal(storage.select("match", 2).status, Status.Archived); + assert.equal(storage.select("match", 3).status, Status.Archived); } ); @@ -374,7 +432,8 @@ PreviousAndNextMatchUpdate("should archive previous matches", () => { opponent2: { score: 9 }, }); - assert.equal(storage.select("match", 2).status, Status.Archived); + assert.equal(storage.select("match", 2).status, Status.Archived); // Final + assert.equal(storage.select("match", 3).status, Status.Archived); // Consolation final }); CreateSingleEliminationStage.run(); diff --git a/app/modules/brackets-manager/test/update.test.ts b/app/modules/brackets-manager/test/update.test.ts index e620dba6c..74e717571 100644 --- a/app/modules/brackets-manager/test/update.test.ts +++ b/app/modules/brackets-manager/test/update.test.ts @@ -489,7 +489,7 @@ UpdateMatchGames( }); finalMatchStatus = storage.select("match", 2).status; - assert.equal(finalMatchStatus, Status.Completed); + assert.equal(finalMatchStatus, Status.Archived); assert.equal(finalMatchStatus, storage.select("match_game", 4).status); const semi1Status = storage.select("match", 0).status; @@ -742,7 +742,7 @@ UpdateMatchGames( manager.update.matchGame({ id: 0, opponent1: { result: "win" } }); manager.update.matchGame({ id: 1, opponent1: { result: "win" } }); - assert.equal(storage.select("match", 0).status, Status.Completed); + assert.equal(storage.select("match", 0).status, Status.Archived); // Completed, but single match in the stage. manager.reset.matchGameResults(0); assert.equal(storage.select("match", 0).status, Status.Running);