Fix teams with big score difference matched up against each other in swiss Closes #2496
Some checks are pending
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

This commit is contained in:
Kalle
2025-08-27 21:05:08 +03:00
parent c4fd9332a7
commit 17250ba177
3 changed files with 198 additions and 3 deletions

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
import { Tournament } from "~/features/tournament-bracket/core/Tournament";
import {
LOW_INK_AUGUST_2025,
RUSH_WEEKEND_3,
} from "~/features/tournament-bracket/core/tests/mocks-swiss";
import { ZONES_WEEKLY_38 } from "~/features/tournament-bracket/core/tests/mocks-zones-weekly";
import invariant from "~/utils/invariant";
import * as Swiss from "./Swiss";
@@ -162,4 +166,100 @@ describe("Swiss", () => {
});
});
});
const PAIR_UP_TEST_CASES = [RUSH_WEEKEND_3, LOW_INK_AUGUST_2025];
describe("pairUp()", () => {
it.for(PAIR_UP_TEST_CASES)(
"all teams have matches (pair up test cases idx %#)",
(testCase) => {
const result = Swiss.pairUp(testCase);
const inputTeams = testCase
.map((team) => team.id)
.sort((a, b) => a - b);
const resultTeams = result
.flatMap((match) => [match.opponentOne, match.opponentTwo])
.filter((val) => val !== null)
.sort((a, b) => a - b);
expect(inputTeams).toEqual(resultTeams);
},
);
it.for(PAIR_UP_TEST_CASES)(
"every pair is max one set win from each other (pair up test cases idx %#)",
(testCase) => {
const result = Swiss.pairUp(testCase);
for (const match of result) {
if (match.opponentOne === null || match.opponentTwo === null)
continue;
const opponentOneScore = testCase.find(
(t) => t.id === match.opponentOne,
)!.score;
const opponentTwoScore = testCase.find(
(t) => t.id === match.opponentTwo,
)!.score;
expect(
Math.abs(opponentOneScore - opponentTwoScore),
`Teams ${match.opponentOne} and ${match.opponentTwo} have too large score difference (${opponentOneScore} vs ${opponentTwoScore})`,
).toBeLessThanOrEqual(1);
}
},
);
it.for(PAIR_UP_TEST_CASES)(
"should match perfect records against each other as much as possible (pair up test cases idx %#)",
(testCase) => {
const result = Swiss.pairUp(testCase);
const maxScore = testCase.reduce(
(max, team) => Math.max(max, team.score),
0,
);
const perfectRecordsCount = testCase.filter(
(team) => team.score === maxScore,
).length;
let perfectRecordsPlayingEachOtherCount = 0;
for (const match of result) {
if (match.opponentOne === null || match.opponentTwo === null)
continue;
const oneIsPerfectScore = testCase.some(
(team) => team.id === match.opponentOne && team.score === maxScore,
);
const twoIsPerfectScore = testCase.some(
(team) => team.id === match.opponentTwo && team.score === maxScore,
);
if (oneIsPerfectScore && twoIsPerfectScore) {
perfectRecordsPlayingEachOtherCount++;
}
}
expect(perfectRecordsPlayingEachOtherCount).toBe(
Math.floor(perfectRecordsCount / 2),
);
},
);
it.for(PAIR_UP_TEST_CASES)(
"generates max one bye (pair up test cases idx %#)",
(testCase) => {
const result = Swiss.pairUp(testCase);
let byes = 0;
for (const match of result) {
if (match.opponentOne === null || match.opponentTwo === null) byes++;
}
expect(byes).toBeLessThanOrEqual(1);
},
);
});
});

View File

@@ -262,7 +262,10 @@ interface SwissPairingTeam {
}
// adapted from https://github.com/slashinfty/tournament-pairings
function pairUp(players: SwissPairingTeam[]) {
export function pairUp(players: SwissPairingTeam[]) {
// uncomment to add a new test case to PAIR_UP_TEST_CASES
// console.log(players);
const matches = [];
const playerArray = R.shuffle(players).map((p, i) => ({ ...p, index: i }));
const scoreGroups = [...new Set(playerArray.map((p) => p.score))].sort(
@@ -295,12 +298,35 @@ function pairUp(players: SwissPairingTeam[]) {
2);
wt +=
5 - 5 / (scoreSums.findIndex((s) => s === curr.score + opp.score) + 1);
let scoreGroupDiff = Math.abs(
const scoreGroupDiff = Math.abs(
scoreGroups.findIndex((s) => s === curr.score) -
scoreGroups.findIndex((s) => s === opp.score),
);
scoreGroupDiff += 0.2;
// TODO: consider "pairedUpDown"
// if (
// scoreGroupDiff === 1 &&
// curr.hasOwnProperty("pairedUpDown") &&
// curr.pairedUpDown === false &&
// opp.hasOwnProperty("pairedUpDown") &&
// opp.pairedUpDown === false
// ) {
// scoreGroupDiff -= 0.65;
// } else if (
// scoreGroupDiff > 0 &&
// ((curr.hasOwnProperty("pairedUpDown") && curr.pairedUpDown === true) ||
// (opp.hasOwnProperty("pairedUpDown") && opp.pairedUpDown === true))
// ) {
// scoreGroupDiff += 0.2;
// }
wt += 23 / (2 * (scoreGroupDiff + 2));
// Lower weight for larger score differences, we really want to avoid 2-0 playing 0-2 etc.
if (scoreGroupDiff >= 2) {
wt -= 10;
}
if (
(Object.hasOwn(curr, "receivedBye") && curr.receivedBye) ||
(Object.hasOwn(opp, "receivedBye") && opp.receivedBye)

View File

@@ -0,0 +1,69 @@
export const RUSH_WEEKEND_3 = [
{ id: 39795, score: 2, receivedBye: false, avoid: [39950, 39745] },
{ id: 39066, score: 2, receivedBye: false, avoid: [40168, 38337] },
{ id: 38951, score: 2, receivedBye: false, avoid: [39454, 39451] },
{ id: 37953, score: 2, receivedBye: false, avoid: [38435, 39861] },
{ id: 38193, score: 2, receivedBye: false, avoid: [39451, 39659] },
{ id: 39659, score: 1, receivedBye: false, avoid: [39850, 38193] },
{ id: 39861, score: 1, receivedBye: false, avoid: [39322, 37953] },
{ id: 39745, score: 1, receivedBye: false, avoid: [39951, 39795] },
{ id: 38337, score: 1, receivedBye: false, avoid: [39814, 39066] },
{ id: 39454, score: 1, receivedBye: false, avoid: [38951, 38435] },
{ id: 39950, score: 1, receivedBye: false, avoid: [39795, 40168] },
{ id: 39850, score: 1, receivedBye: false, avoid: [39659, 39814] },
{ id: 39322, score: 1, receivedBye: false, avoid: [39861, 39951] },
{ id: 39451, score: 0, receivedBye: false, avoid: [38193, 38951] },
{ id: 40168, score: 0, receivedBye: false, avoid: [39066, 39950] },
{ id: 38435, score: 0, receivedBye: false, avoid: [37953, 39454] },
{ id: 39951, score: 0, receivedBye: false, avoid: [39745, 39322] },
{ id: 39814, score: 0, receivedBye: false, avoid: [38337, 39850] },
];
export const LOW_INK_AUGUST_2025 = [
{ id: 40878, score: 2, receivedBye: false, avoid: [39949, 41092] },
{ id: 40986, score: 2, receivedBye: false, avoid: [41308, 40796] },
{ id: 39811, score: 2, receivedBye: false, avoid: [41157, 41026] },
{ id: 41280, score: 2, receivedBye: false, avoid: [41035, 41117] },
{ id: 40981, score: 2, receivedBye: false, avoid: [40769, 40582] },
{ id: 40236, score: 2, receivedBye: false, avoid: [40709, 41290] },
{ id: 40318, score: 2, receivedBye: false, avoid: [39790, 40646] },
{ id: 39812, score: 2, receivedBye: false, avoid: [41089, 41085] },
{ id: 41218, score: 2, receivedBye: false, avoid: [40494, 40900] },
{ id: 40772, score: 2, receivedBye: false, avoid: [41041, 41014] },
{ id: 40881, score: 2, receivedBye: false, avoid: [41161, 40767] },
{ id: 40651, score: 2, receivedBye: false, avoid: [41262, 40842] },
{ id: 40693, score: 2, receivedBye: false, avoid: [39834, 41210] },
{ id: 41308, score: 1, receivedBye: true, avoid: [40986] },
{ id: 41117, score: 1, receivedBye: false, avoid: [40993, 41280] },
{ id: 40767, score: 1, receivedBye: false, avoid: [40850, 40881] },
{ id: 40842, score: 1, receivedBye: false, avoid: [41068, 40651] },
{ id: 40796, score: 1, receivedBye: false, avoid: [41184, 40986] },
{ id: 41014, score: 1, receivedBye: false, avoid: [41263, 40772] },
{ id: 40646, score: 1, receivedBye: false, avoid: [41207, 40318] },
{ id: 41085, score: 1, receivedBye: false, avoid: [40490, 39812] },
{ id: 41026, score: 1, receivedBye: false, avoid: [39806, 39811] },
{ id: 40582, score: 1, receivedBye: false, avoid: [41186, 40981] },
{ id: 41035, score: 1, receivedBye: false, avoid: [41280, 41257] },
{ id: 40769, score: 1, receivedBye: false, avoid: [40981, 41262] },
{ id: 41290, score: 1, receivedBye: false, avoid: [39827, 40236] },
{ id: 40900, score: 1, receivedBye: false, avoid: [41257, 41218] },
{ id: 39949, score: 1, receivedBye: false, avoid: [40878, 39806] },
{ id: 41092, score: 1, receivedBye: false, avoid: [41210, 40878] },
{ id: 40490, score: 1, receivedBye: false, avoid: [41085, 41161] },
{ id: 40993, score: 1, receivedBye: false, avoid: [41117, 39834] },
{ id: 41263, score: 1, receivedBye: false, avoid: [41014, 40494] },
{ id: 40850, score: 1, receivedBye: false, avoid: [40767, 39827] },
{ id: 41207, score: 1, receivedBye: false, avoid: [40646, 41041] },
{ id: 41184, score: 1, receivedBye: false, avoid: [40796, 41089] },
{ id: 41041, score: 0, receivedBye: false, avoid: [40772, 41207] },
{ id: 39790, score: 0, receivedBye: false, avoid: [40318, 41157] },
{ id: 41089, score: 0, receivedBye: false, avoid: [39812, 41184] },
{ id: 41161, score: 0, receivedBye: false, avoid: [40881, 40490] },
{ id: 39834, score: 0, receivedBye: false, avoid: [40693, 40993] },
{ id: 40494, score: 0, receivedBye: false, avoid: [41218, 41263] },
{ id: 41262, score: 0, receivedBye: false, avoid: [40651, 40769] },
{ id: 39827, score: 0, receivedBye: false, avoid: [41290, 40850] },
{ id: 41186, score: 0, receivedBye: false, avoid: [40582, 41068] },
{ id: 39806, score: 0, receivedBye: false, avoid: [41026, 39949] },
{ id: 41257, score: 0, receivedBye: false, avoid: [40900, 41035] },
];