From d0654a320154b7166fae96bae4f2b08da12743bc Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:46:04 +0300 Subject: [PATCH] Fix Swiss not counting every meeting between tied teams for the head-to-head tiebreaker --- .../tournament-bracket/core/Bracket.test.ts | 81 +++++++++++++++++++ .../core/Bracket/SwissBracket.ts | 25 +++--- 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/app/features/tournament-bracket/core/Bracket.test.ts b/app/features/tournament-bracket/core/Bracket.test.ts index d111a42d6..42fbefe56 100644 --- a/app/features/tournament-bracket/core/Bracket.test.ts +++ b/app/features/tournament-bracket/core/Bracket.test.ts @@ -215,6 +215,87 @@ describe("swiss standings - cross group ties", () => { }); }); +describe("swiss standings - rematches between tied teams", () => { + // 4-team Swiss with 5 rounds, so rounds 4 and 5 are forced rematches. Teams 1 + // and 2 finish 4-1, having met twice and split 1-1 (team 1 won the round 1 + // meeting, team 2 the round 4 one), so head-to-head is even and opponent set + // win % should decide: team 2's opponents won 12 of 25 sets (48%), team 1's + // 10 of 25 (40%). + const swissTournamentWithRematches = () => { + const data = Engine.create({ + type: "swiss", + seeding: [1, 2, 3, 4], + settings: { groupCount: 1, roundCount: 5 }, + }); + + const roundResults: Array> = [ + [ + [1, 2], + [3, 4], + ], + [ + [1, 3], + [2, 4], + ], + [ + [1, 4], + [2, 3], + ], + [ + [2, 1], + [3, 4], + ], + [ + [1, 4], + [2, 3], + ], + ]; + + data.match = roundResults.flatMap((results, roundIdx) => + results.map( + ([winnerId, loserId], matchIdx): MatchData => ({ + id: roundIdx * 2 + matchIdx, + stageId: data.stage[0].id, + groupId: data.group[0].id, + roundId: data.round[roundIdx].id, + number: matchIdx + 1, + opponent1: { id: winnerId, score: 2 }, + opponent2: { id: loserId, score: 0 }, + winnerSide: "opponent1", + }), + ), + ); + + return testTournament({ + ctx: { + settings: { + bracketProgression: [ + { + type: "swiss", + name: "Swiss", + requiresCheckIn: false, + settings: { groupCount: 1, roundCount: 5 }, + sources: [], + }, + ], + }, + }, + data, + }); + }; + + it("counts every meeting between tied teams for the head-to-head tiebreaker", () => { + const standings = swissTournamentWithRematches().bracketByIdx(0)!.standings; + + const team1 = standings.find((s) => s.team.id === 1)!; + const team2 = standings.find((s) => s.team.id === 2)!; + + expect(team2.placement).toBe(1); + expect(team1.stats?.lossesAgainstTied).toBe(1); + expect(team2.stats?.winsAgainstTied).toBe(1); + }); +}); + describe("round robin standings", () => { it("should sort teams primarily by set wins (per group) in paddling pool 255", () => { const tournamentPP255 = new Tournament(PADDLING_POOL_255()); diff --git a/app/features/tournament-bracket/core/Bracket/SwissBracket.ts b/app/features/tournament-bracket/core/Bracket/SwissBracket.ts index 4d52bce64..4892646b0 100644 --- a/app/features/tournament-bracket/core/Bracket/SwissBracket.ts +++ b/app/features/tournament-bracket/core/Bracket/SwissBracket.ts @@ -330,7 +330,7 @@ export class SwissBracket extends Bracket { // they are different teams and are tied, let's check who won - const finishedMatchBetweenTeams = matches.find((match) => { + const finishedMatchesBetweenTeams = matches.filter((match) => { const isBetweenTeams = (match.opponent1?.id === team.id && match.opponent2?.id === team2.id) || @@ -342,19 +342,18 @@ export class SwissBracket extends Bracket { return isBetweenTeams && isFinished; }); - // they did not play each other - if (!finishedMatchBetweenTeams) continue; + for (const finishedMatchBetweenTeams of finishedMatchesBetweenTeams) { + const wonTheirMatch = + (finishedMatchBetweenTeams.opponent1!.id === team.id && + finishedMatchBetweenTeams.winnerSide === "opponent1") || + (finishedMatchBetweenTeams.opponent2!.id === team.id && + finishedMatchBetweenTeams.winnerSide === "opponent2"); - const wonTheirMatch = - (finishedMatchBetweenTeams.opponent1!.id === team.id && - finishedMatchBetweenTeams.winnerSide === "opponent1") || - (finishedMatchBetweenTeams.opponent2!.id === team.id && - finishedMatchBetweenTeams.winnerSide === "opponent2"); - - if (wonTheirMatch) { - team.winsAgainstTied++; - } else { - team.lossesAgainstTied++; + if (wonTheirMatch) { + team.winsAgainstTied++; + } else { + team.lossesAgainstTied++; + } } } }