From c2d529f261b9e5cfd724defd82ec7f21b208b28f Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:32:06 +0300 Subject: [PATCH] Fix SE quarterfinal losers showing as losers while 3rd place match in progress --- .../core/Bracket/SingleEliminationBracket.ts | 47 ++++++++---- .../tournament/core/Standings.test.ts | 75 +++++++++++++++++++ .../2026-09-02-third-place-match-standings.md | 5 ++ 3 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 changelog/2026-09-02-third-place-match-standings.md diff --git a/app/features/tournament-bracket/core/Bracket/SingleEliminationBracket.ts b/app/features/tournament-bracket/core/Bracket/SingleEliminationBracket.ts index e71db5c2c..2e629b9b7 100644 --- a/app/features/tournament-bracket/core/Bracket/SingleEliminationBracket.ts +++ b/app/features/tournament-bracket/core/Bracket/SingleEliminationBracket.ts @@ -65,6 +65,24 @@ export class SingleEliminationBracket extends Bracket { return R.unique(this.data.match.map((m) => m.groupId)).length > 1; } + private thirdPlaceMatch() { + if (!this.hasThirdPlaceMatch()) return undefined; + + const thirdPlaceGroupId = Math.max( + ...this.data.group.map((group) => group.id), + ); + + return this.data.match.find((match) => match.groupId === thirdPlaceGroupId); + } + + private thirdPlaceMatchUndecided() { + const thirdPlaceMatch = this.thirdPlaceMatch(); + + return ( + Boolean(thirdPlaceMatch) && !winnerOfThirdPlaceMatch(thirdPlaceMatch) + ); + } + protected calculateStandings(): Standing[] { const teams: { id: number; lostAt: number }[] = []; @@ -140,20 +158,18 @@ export class SingleEliminationBracket extends Bracket { }); } - const thirdPlaceMatch = this.hasThirdPlaceMatch() - ? this.data.match.find((m) => m.groupId !== matches[0].groupId) - : undefined; - const thirdPlaceMatchWinner = winnerOfThirdPlaceMatch(thirdPlaceMatch); + const thirdPlaceMatchWinner = winnerOfThirdPlaceMatch( + this.thirdPlaceMatch(), + ); const resultWithThirdPlaceTiebroken = result - .flatMap((standing) => { - if (standing.placement !== 3 || !thirdPlaceMatch) return [standing]; - // semifinal losers have not finished their run before the third place match is played - if (!thirdPlaceMatchWinner) return []; - if (thirdPlaceMatchWinner.id !== standing.team.id) { - return [{ ...standing, placement: 4 }]; - } - return [standing]; + .map((standing) => { + // semifinal losers stay tied until the third place match decides between them + if (standing.placement !== 3 || !thirdPlaceMatchWinner) return standing; + + return thirdPlaceMatchWinner.id === standing.team.id + ? standing + : { ...standing, placement: 4 }; }) .sort((a, b) => a.placement - b.placement); @@ -169,7 +185,12 @@ export class SingleEliminationBracket extends Bracket { ); if (placements.every((placement) => placement > 0)) { - return this.sourceByStandings(placements, rest === true); + const source = this.sourceByStandings(placements, rest === true); + + // 3rd and 4th are only decided once the third place match is played + return this.thirdPlaceMatchUndecided() + ? { ...source, relevantMatchesFinished: false } + : source; } // third place match lives in a separate (higher) group, the lowest group id is the winners group diff --git a/app/features/tournament/core/Standings.test.ts b/app/features/tournament/core/Standings.test.ts index f8aa18ae2..898cb40d7 100644 --- a/app/features/tournament/core/Standings.test.ts +++ b/app/features/tournament/core/Standings.test.ts @@ -165,6 +165,33 @@ describe("tournamentStandings", () => { 1, 2, 3, 4, 5, 5, 5, 5, ]); }); + + test("keeps quarterfinal losers 5th while the third place match is still pending", () => { + const result = tournamentStandings( + singleEliminationWithPendingThirdPlaceMatch(), + ); + invariant(result.type === "single"); + + const placementOf = (teamId: number) => + result.standings.find((standing) => standing.team.id === teamId) + ?.placement; + + expect(placementOf(1)).toBe(1); + expect(placementOf(2)).toBe(2); + expect([5, 6, 7, 8].map(placementOf)).toEqual([5, 5, 5, 5]); + }); + + test("lists the semifinal losers while the third place match is still pending", () => { + const result = tournamentStandings( + singleEliminationWithPendingThirdPlaceMatch(), + ); + invariant(result.type === "single"); + + const teamIds = result.standings.map((standing) => standing.team.id); + + expect(teamIds).toContain(3); + expect(teamIds).toContain(4); + }); }); describe("reNumberPlacements", () => { @@ -517,6 +544,54 @@ function singleEliminationTournament() { }); } +/** Eight teams, every match reported except the third place match. */ +function singleEliminationWithPendingThirdPlaceMatch() { + let data = createResolved({ + type: "single_elimination", + seeding: [1, 2, 3, 4, 5, 6, 7, 8], + settings: { consolationFinal: true }, + }); + const thirdPlaceGroupId = Math.max(...data.group.map((group) => group.id)); + + while (true) { + const pending = data.match.find( + (match) => + match.groupId !== thirdPlaceGroupId && + typeof match.opponent1?.id === "number" && + typeof match.opponent2?.id === "number" && + !match.winnerSide, + ); + if (!pending) break; + + const winnerIsOpp1 = + (pending.opponent1!.id as number) < (pending.opponent2!.id as number); + data = Engine.reportResult(data, { + matchId: pending.id, + scores: [winnerIsOpp1 ? 2 : 0, winnerIsOpp1 ? 0 : 2], + winnerSide: winnerIsOpp1 ? "opponent1" : "opponent2", + }).data; + } + + return testTournament({ + ctx: { + settings: { + bracketProgression: [ + { + type: "single_elimination", + name: "Main Bracket", + requiresCheckIn: false, + settings: { thirdPlaceMatch: true }, + }, + ], + }, + teams: [1, 2, 3, 4, 5, 6, 7, 8].map((id) => + tournamentCtxTeam(id, { seed: id }), + ), + }, + data, + }); +} + function singleEliminationWithUndergroundTournament({ undergroundSeeding = [5, 6, 7, 8], undergroundConsolationFinal = undergroundSeeding.length > 2, diff --git a/changelog/2026-09-02-third-place-match-standings.md b/changelog/2026-09-02-third-place-match-standings.md new file mode 100644 index 000000000..f22205d8e --- /dev/null +++ b/changelog/2026-09-02-third-place-match-standings.md @@ -0,0 +1,5 @@ +--- +navItem: calendar +type: bug +--- +Tournament results no longer show the quarterfinal losers as 3rd while the third place match is still being played