mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 07:12:36 -05:00
Fix teams with big score difference matched up against each other in swiss Closes #2496
This commit is contained in:
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
69
app/features/tournament-bracket/core/tests/mocks-swiss.ts
Normal file
69
app/features/tournament-bracket/core/tests/mocks-swiss.ts
Normal 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] },
|
||||
];
|
||||
Reference in New Issue
Block a user