mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-22 19:16:09 -05:00
Fix Swiss algorithm assigning bye to top team
This commit is contained in:
@@ -3,6 +3,7 @@ import type { TournamentStageSettings } from "~/db/tables-json";
|
||||
import { Tournament } from "~/features/tournament-bracket/core/Tournament";
|
||||
import {
|
||||
LOW_INK_AUGUST_2025,
|
||||
LOW_INK_AUGUST_2026_ROUND_3,
|
||||
RUSH_WEEKEND_3,
|
||||
} from "~/features/tournament-bracket/core/tests/mocks-swiss";
|
||||
import { ZONES_WEEKLY_38 } from "~/features/tournament-bracket/core/tests/mocks-zones-weekly";
|
||||
@@ -232,7 +233,11 @@ describe("Swiss", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const PAIR_UP_TEST_CASES = [RUSH_WEEKEND_3, LOW_INK_AUGUST_2025];
|
||||
const PAIR_UP_TEST_CASES = [
|
||||
RUSH_WEEKEND_3,
|
||||
LOW_INK_AUGUST_2025,
|
||||
LOW_INK_AUGUST_2026_ROUND_3,
|
||||
];
|
||||
|
||||
describe("pairUp()", () => {
|
||||
test.for(PAIR_UP_TEST_CASES)(
|
||||
@@ -376,6 +381,23 @@ describe("Swiss", () => {
|
||||
|
||||
expect(bye?.opponentOne).toBe(2);
|
||||
});
|
||||
|
||||
test("gives the bye to the lowest score group even when byeing a top group team would keep every pairing within its score group", () => {
|
||||
// real state where the top score group had an odd size (7) while the
|
||||
// lower groups were even (16 and 6): byeing an undefeated team lets
|
||||
// every match stay within its score group, which the weights preferred:
|
||||
// live this gave the free win to the 2-0 top seed instead of a 0-2 team
|
||||
const result = Swiss.pairUp(LOW_INK_AUGUST_2026_ROUND_3);
|
||||
|
||||
const bye = result.find((match) => match.opponentTwo === null);
|
||||
invariant(bye, "bye not found");
|
||||
|
||||
const byeTeam = LOW_INK_AUGUST_2026_ROUND_3.find(
|
||||
(team) => team.id === bye.opponentOne,
|
||||
);
|
||||
|
||||
expect(byeTeam?.score).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateTeamStatus()", () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { calculateTeamStatus } from "./team-status";
|
||||
* to dwarf every other weight of the whole round, meaning rematches only happen when
|
||||
* unavoidable and then as few of them as possible.
|
||||
*/
|
||||
const REMATCH_PENALTY = 1_000_000;
|
||||
const REMATCH_PENALTY = 1_000_000_000;
|
||||
|
||||
/**
|
||||
* Weight added for each team of a pair that has already received a bye, making
|
||||
@@ -24,9 +24,21 @@ const REMATCH_PENALTY = 1_000_000;
|
||||
* so that a matching leaving a bye-less team unpaired always beats one giving a team
|
||||
* a second bye: with an odd team count exactly one team is left unpaired, so the
|
||||
* matchings differ by exactly one bonus, which is set to dwarf every score-based
|
||||
* weight while staying far below REMATCH_PENALTY.
|
||||
* weight (including HIGH_SCORE_PAIRING_BONUS) while staying far below
|
||||
* REMATCH_PENALTY.
|
||||
*/
|
||||
const PRIOR_BYE_PAIRING_BONUS = 10_000;
|
||||
const PRIOR_BYE_PAIRING_BONUS = 10_000_000;
|
||||
|
||||
/**
|
||||
* Weight added per score point of both teams of a pair. Summed over a full matching
|
||||
* this contributes the bonus times the total score of all paired teams, so with an
|
||||
* odd team count the matching maximizes it by leaving a lowest-score team unpaired —
|
||||
* that team receives the bye. Without this term the bye can drift to a top score
|
||||
* group team whenever the score group size parities make all-within-group pairings
|
||||
* possible only that way. Sized to dwarf the pairing quality weights (< ~100 per
|
||||
* pair) while a full round's worth stays far below PRIOR_BYE_PAIRING_BONUS.
|
||||
*/
|
||||
const HIGH_SCORE_PAIRING_BONUS = 10_000;
|
||||
|
||||
interface GroupArgs {
|
||||
groupId: number;
|
||||
@@ -295,6 +307,8 @@ function generateWeightedPairs({
|
||||
wt -= 10;
|
||||
}
|
||||
|
||||
wt += (curr.score + opp.score) * HIGH_SCORE_PAIRING_BONUS;
|
||||
|
||||
if (curr.receivedBye) {
|
||||
wt += PRIOR_BYE_PAIRING_BONUS;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,39 @@ export const RUSH_WEEKEND_3 = [
|
||||
{ id: 39814, score: 0, receivedBye: false, avoid: [38337, 39850] },
|
||||
];
|
||||
|
||||
// tournament 4224 group A entering round 3: 7 teams at score 2, 16 at score 1, 6 at score 0
|
||||
export const LOW_INK_AUGUST_2026_ROUND_3 = [
|
||||
{ id: 83693, score: 2, receivedBye: false, avoid: [85574, 84491] },
|
||||
{ id: 84224, score: 2, receivedBye: false, avoid: [86019, 85541] },
|
||||
{ id: 84735, score: 2, receivedBye: false, avoid: [84239, 83733] },
|
||||
{ id: 85791, score: 2, receivedBye: false, avoid: [85868, 85393] },
|
||||
{ id: 85832, score: 2, receivedBye: false, avoid: [83873, 84857] },
|
||||
{ id: 85909, score: 2, receivedBye: false, avoid: [86038, 86046] },
|
||||
{ id: 85970, score: 2, receivedBye: false, avoid: [85408, 85347] },
|
||||
{ id: 83619, score: 1, receivedBye: false, avoid: [83733, 85775] },
|
||||
{ id: 83733, score: 1, receivedBye: false, avoid: [83619, 84735] },
|
||||
{ id: 84491, score: 1, receivedBye: false, avoid: [84553, 83693] },
|
||||
{ id: 84553, score: 1, receivedBye: false, avoid: [84491, 86019] },
|
||||
{ id: 84857, score: 1, receivedBye: false, avoid: [86067, 85832] },
|
||||
{ id: 85347, score: 1, receivedBye: false, avoid: [85504, 85970] },
|
||||
{ id: 85393, score: 1, receivedBye: false, avoid: [85860, 85791] },
|
||||
{ id: 85408, score: 1, receivedBye: false, avoid: [85970, 85697] },
|
||||
{ id: 85536, score: 1, receivedBye: true, avoid: [86067] },
|
||||
{ id: 85541, score: 1, receivedBye: false, avoid: [85697, 84224] },
|
||||
{ id: 85574, score: 1, receivedBye: false, avoid: [83693, 84239] },
|
||||
{ id: 85860, score: 1, receivedBye: true, avoid: [85393] },
|
||||
{ id: 85868, score: 1, receivedBye: false, avoid: [85791, 85504] },
|
||||
{ id: 86038, score: 1, receivedBye: false, avoid: [85909, 83873] },
|
||||
{ id: 86046, score: 1, receivedBye: false, avoid: [85775, 85909] },
|
||||
{ id: 86067, score: 1, receivedBye: false, avoid: [84857, 85536] },
|
||||
{ id: 83873, score: 0, receivedBye: false, avoid: [85832, 86038] },
|
||||
{ id: 84239, score: 0, receivedBye: false, avoid: [84735, 85574] },
|
||||
{ id: 85504, score: 0, receivedBye: false, avoid: [85347, 85868] },
|
||||
{ id: 85697, score: 0, receivedBye: false, avoid: [85541, 85408] },
|
||||
{ id: 85775, score: 0, receivedBye: false, avoid: [86046, 83619] },
|
||||
{ id: 86019, score: 0, receivedBye: false, avoid: [84224, 84553] },
|
||||
];
|
||||
|
||||
export const LOW_INK_AUGUST_2025 = [
|
||||
{ id: 40878, score: 2, receivedBye: false, avoid: [39949, 41092] },
|
||||
{ id: 40986, score: 2, receivedBye: false, avoid: [41308, 40796] },
|
||||
|
||||
Reference in New Issue
Block a user