Fix SE quarterfinal losers showing as losers while 3rd place match in progress

This commit is contained in:
Kalle
2026-09-02 20:32:06 +03:00
parent 1908dae11c
commit c2d529f261
3 changed files with 114 additions and 13 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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