mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-27 21:55:15 -05:00
Adjust Round Robin & Swiss standings logic (#2036)
* Test fails * Add & fix tests
This commit is contained in:
@@ -116,6 +116,18 @@ export function PlacementsTable({
|
||||
<abbr title="Losses against tied opponents">TB</abbr>
|
||||
</th>
|
||||
) : null}
|
||||
{bracket.type === "swiss" ? (
|
||||
<>
|
||||
<th>
|
||||
<abbr title="Opponents' set win percentage average">OW%</abbr>
|
||||
</th>
|
||||
<th>
|
||||
<abbr title="Opponents' map win percentage average">
|
||||
OW% (M)
|
||||
</abbr>
|
||||
</th>
|
||||
</>
|
||||
) : null}
|
||||
<th>
|
||||
<abbr title="Map wins and losses">W/L (M)</abbr>
|
||||
</th>
|
||||
@@ -124,20 +136,6 @@ export function PlacementsTable({
|
||||
<abbr title="Score summed up">Scr</abbr>
|
||||
</th>
|
||||
) : null}
|
||||
{bracket.type === "swiss" ? (
|
||||
<>
|
||||
<th>
|
||||
<abbr title="Buchholz (summed set wins of opponents)">
|
||||
Buch.
|
||||
</abbr>
|
||||
</th>
|
||||
<th>
|
||||
<abbr title="Buchholz (summed map wins of opponents)">
|
||||
Buch. (M)
|
||||
</abbr>
|
||||
</th>
|
||||
</>
|
||||
) : null}
|
||||
<th>Seed</th>
|
||||
<th />
|
||||
{canEditDestination ? <th /> : null}
|
||||
@@ -199,6 +197,16 @@ export function PlacementsTable({
|
||||
<span>{(stats.lossesAgainstTied ?? 0) * -1}</span>
|
||||
</td>
|
||||
) : null}
|
||||
{bracket.type === "swiss" ? (
|
||||
<>
|
||||
<td>
|
||||
<span>{stats.opponentSetWinPercentage?.toFixed(2)}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{stats.opponentMapWinPercentage?.toFixed(2)}</span>
|
||||
</td>
|
||||
</>
|
||||
) : null}
|
||||
<td>
|
||||
<span>
|
||||
{stats.mapWins}/{stats.mapLosses}
|
||||
@@ -209,16 +217,6 @@ export function PlacementsTable({
|
||||
<span>{stats.points}</span>
|
||||
</td>
|
||||
) : null}
|
||||
{bracket.type === "swiss" ? (
|
||||
<>
|
||||
<td>
|
||||
<span>{stats.buchholzSets}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{stats.buchholzMaps}</span>
|
||||
</td>
|
||||
</>
|
||||
) : null}
|
||||
<td>{team?.seed}</td>
|
||||
<EditableDestination
|
||||
key={overridenDestinationBracket?.idx}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { removeDuplicates } from "../../../utils/arrays";
|
||||
import invariant from "../../../utils/invariant";
|
||||
import { Tournament } from "./Tournament";
|
||||
import { PADDLING_POOL_255 } from "./tests/mocks";
|
||||
import { LOW_INK_DECEMBER_2024 } from "./tests/mocks-li";
|
||||
|
||||
const TEAM_ERROR_404_ID = 17354;
|
||||
@@ -38,3 +40,69 @@ describe("swiss standings", () => {
|
||||
expect(standing.stats?.lossesAgainstTied).toBe(0); // they lost against "Tidy Tidings" but that team dropped out before final round
|
||||
});
|
||||
});
|
||||
|
||||
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());
|
||||
|
||||
const standings = tournamentPP255.bracketByIdx(0)!.standings;
|
||||
|
||||
const groupIds = removeDuplicates(
|
||||
standings.map((standing) => standing.groupId),
|
||||
);
|
||||
expect(
|
||||
groupIds.length,
|
||||
"Paddling Pool 255 should have groups from Group A to Group I",
|
||||
).toBe(9);
|
||||
|
||||
for (const groupId of groupIds) {
|
||||
const groupStandings = standings.filter(
|
||||
(standing) => standing.groupId === groupId,
|
||||
);
|
||||
|
||||
for (let i = 0; i < groupStandings.length; i++) {
|
||||
const current = groupStandings[i];
|
||||
const next = groupStandings[i + 1];
|
||||
|
||||
if (!next) {
|
||||
break;
|
||||
}
|
||||
|
||||
expect(
|
||||
current.stats!.setWins,
|
||||
`Team with ID ${current.team.id} in wrong spot relative to ${next.team.id}`,
|
||||
).toBeGreaterThanOrEqual(next.stats!.setWins);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("has ascending order from lower group id to higher group id for same placements", () => {
|
||||
const tournamentPP255 = new Tournament(PADDLING_POOL_255());
|
||||
|
||||
const standings = tournamentPP255.bracketByIdx(0)!.standings;
|
||||
|
||||
const placements = removeDuplicates(
|
||||
standings.map((standing) => standing.placement),
|
||||
).sort((a, b) => a - b);
|
||||
|
||||
for (const placement of placements) {
|
||||
const placementStandings = standings.filter(
|
||||
(standing) => standing.placement === placement,
|
||||
);
|
||||
|
||||
for (let i = 0; i < placementStandings.length; i++) {
|
||||
const current = placementStandings[i];
|
||||
const next = placementStandings[i + 1];
|
||||
|
||||
if (!next) {
|
||||
break;
|
||||
}
|
||||
|
||||
expect(
|
||||
current.groupId,
|
||||
`Team with ID ${current.team.id} in wrong spot relative to ${next.team.id}`,
|
||||
).toBeLessThan(next.groupId!);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { removeDuplicates } from "~/utils/arrays";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { logger } from "~/utils/logger";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { cutToNDecimalPlaces } from "../../../utils/number";
|
||||
import { fillWithNullTillPowerOfTwo } from "../tournament-bracket-utils";
|
||||
import * as Progression from "./Progression";
|
||||
import type { OptionalIdObject, Tournament } from "./Tournament";
|
||||
@@ -50,11 +51,16 @@ export interface Standing {
|
||||
winsAgainstTied: number;
|
||||
// first tiebreaker in swiss
|
||||
lossesAgainstTied?: number;
|
||||
buchholzSets?: number;
|
||||
buchholzMaps?: number;
|
||||
opponentSetWinPercentage?: number;
|
||||
opponentMapWinPercentage?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface TeamTrackRecord {
|
||||
wins: number;
|
||||
losses: number;
|
||||
}
|
||||
|
||||
export abstract class Bracket {
|
||||
id;
|
||||
idx;
|
||||
@@ -1059,6 +1065,9 @@ class RoundRobinBracket extends Bracket {
|
||||
if (a.mapWins > b.mapWins) return -1;
|
||||
if (a.mapWins < b.mapWins) return 1;
|
||||
|
||||
if (a.mapLosses < b.mapLosses) return -1;
|
||||
if (a.mapLosses > b.mapLosses) return 1;
|
||||
|
||||
if (a.points > b.points) return -1;
|
||||
if (a.points < b.points) return 1;
|
||||
|
||||
@@ -1223,8 +1232,8 @@ class SwissBracket extends Bracket {
|
||||
mapLosses: number;
|
||||
winsAgainstTied: number;
|
||||
lossesAgainstTied: number;
|
||||
buchholzSets: number;
|
||||
buchholzMaps: number;
|
||||
opponentSets: TeamTrackRecord;
|
||||
opponentMaps: TeamTrackRecord;
|
||||
}[] = [];
|
||||
|
||||
const updateTeam = ({
|
||||
@@ -1233,16 +1242,16 @@ class SwissBracket extends Bracket {
|
||||
setLosses = 0,
|
||||
mapWins = 0,
|
||||
mapLosses = 0,
|
||||
buchholzSets = 0,
|
||||
buchholzMaps = 0,
|
||||
opponentSets = { wins: 0, losses: 0 },
|
||||
opponentMaps = { wins: 0, losses: 0 },
|
||||
}: {
|
||||
teamId: number;
|
||||
setWins?: number;
|
||||
setLosses?: number;
|
||||
mapWins?: number;
|
||||
mapLosses?: number;
|
||||
buchholzSets?: number;
|
||||
buchholzMaps?: number;
|
||||
opponentSets?: TeamTrackRecord;
|
||||
opponentMaps?: TeamTrackRecord;
|
||||
}) => {
|
||||
const team = teams.find((team) => team.id === teamId);
|
||||
if (team) {
|
||||
@@ -1250,8 +1259,11 @@ class SwissBracket extends Bracket {
|
||||
team.setLosses += setLosses;
|
||||
team.mapWins += mapWins;
|
||||
team.mapLosses += mapLosses;
|
||||
team.buchholzSets += buchholzSets;
|
||||
team.buchholzMaps += buchholzMaps;
|
||||
|
||||
team.opponentSets.wins += opponentSets.wins;
|
||||
team.opponentSets.losses += opponentSets.losses;
|
||||
team.opponentMaps.wins += opponentMaps.wins;
|
||||
team.opponentMaps.losses += opponentMaps.losses;
|
||||
} else {
|
||||
teams.push({
|
||||
id: teamId,
|
||||
@@ -1261,8 +1273,8 @@ class SwissBracket extends Bracket {
|
||||
mapLosses,
|
||||
winsAgainstTied: 0,
|
||||
lossesAgainstTied: 0,
|
||||
buchholzMaps,
|
||||
buchholzSets,
|
||||
opponentMaps,
|
||||
opponentSets,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1357,12 +1369,18 @@ class SwissBracket extends Bracket {
|
||||
});
|
||||
}
|
||||
|
||||
// buchholz
|
||||
// opponent win %
|
||||
for (const team of teams) {
|
||||
const teamsWhoPlayedAgainst = matchUps.get(team.id) ?? [];
|
||||
|
||||
let buchholzSets = 0;
|
||||
let buchholzMaps = 0;
|
||||
const opponentSets = {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
};
|
||||
const opponentMaps = {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
};
|
||||
|
||||
for (const teamId of teamsWhoPlayedAgainst) {
|
||||
const opponent = teams.find((t) => t.id === teamId);
|
||||
@@ -1373,14 +1391,17 @@ class SwissBracket extends Bracket {
|
||||
continue;
|
||||
}
|
||||
|
||||
buchholzSets += opponent.setWins;
|
||||
buchholzMaps += opponent.mapWins;
|
||||
opponentSets.wins += opponent.setWins;
|
||||
opponentSets.losses += opponent.setLosses;
|
||||
|
||||
opponentMaps.wins += opponent.mapWins;
|
||||
opponentMaps.losses += opponent.mapLosses;
|
||||
}
|
||||
|
||||
updateTeam({
|
||||
teamId: team.id,
|
||||
buchholzSets,
|
||||
buchholzMaps,
|
||||
opponentSets,
|
||||
opponentMaps,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1443,15 +1464,33 @@ class SwissBracket extends Bracket {
|
||||
if (a.lossesAgainstTied > b.lossesAgainstTied) return 1;
|
||||
if (a.lossesAgainstTied < b.lossesAgainstTied) return -1;
|
||||
|
||||
const aOpponentSetWinPercentage = this.trackRecordToWinPercentage(
|
||||
a.opponentSets,
|
||||
);
|
||||
const bOpponentSetWinPercentage = this.trackRecordToWinPercentage(
|
||||
b.opponentSets,
|
||||
);
|
||||
|
||||
if (aOpponentSetWinPercentage > bOpponentSetWinPercentage) {
|
||||
return -1;
|
||||
}
|
||||
if (aOpponentSetWinPercentage < bOpponentSetWinPercentage) return 1;
|
||||
|
||||
const aOpponentMapWinPercentage = this.trackRecordToWinPercentage(
|
||||
a.opponentMaps,
|
||||
);
|
||||
const bOpponentMapWinPercentage = this.trackRecordToWinPercentage(
|
||||
b.opponentMaps,
|
||||
);
|
||||
|
||||
if (aOpponentMapWinPercentage > bOpponentMapWinPercentage) {
|
||||
return -1;
|
||||
}
|
||||
if (aOpponentMapWinPercentage < bOpponentMapWinPercentage) return 1;
|
||||
|
||||
if (a.mapWins > b.mapWins) return -1;
|
||||
if (a.mapWins < b.mapWins) return 1;
|
||||
|
||||
if (a.buchholzSets > b.buchholzSets) return -1;
|
||||
if (a.buchholzSets < b.buchholzSets) return 1;
|
||||
|
||||
if (a.buchholzMaps > b.buchholzMaps) return -1;
|
||||
if (a.buchholzMaps < b.buchholzMaps) return 1;
|
||||
|
||||
const aSeed = Number(this.tournament.teamById(a.id)?.seed);
|
||||
const bSeed = Number(this.tournament.teamById(b.id)?.seed);
|
||||
|
||||
@@ -1472,8 +1511,12 @@ class SwissBracket extends Bracket {
|
||||
mapLosses: team.mapLosses,
|
||||
winsAgainstTied: team.winsAgainstTied,
|
||||
lossesAgainstTied: team.lossesAgainstTied,
|
||||
buchholzSets: team.buchholzSets,
|
||||
buchholzMaps: team.buchholzMaps,
|
||||
opponentSetWinPercentage: this.trackRecordToWinPercentage(
|
||||
team.opponentSets,
|
||||
),
|
||||
opponentMapWinPercentage: this.trackRecordToWinPercentage(
|
||||
team.opponentMaps,
|
||||
),
|
||||
points: 0,
|
||||
},
|
||||
};
|
||||
@@ -1510,6 +1553,13 @@ class SwissBracket extends Bracket {
|
||||
);
|
||||
}
|
||||
|
||||
private trackRecordToWinPercentage(trackRecord: TeamTrackRecord) {
|
||||
return cutToNDecimalPlaces(
|
||||
(trackRecord.wins / (trackRecord.wins + trackRecord.losses)) * 100,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
get type(): Tables["TournamentStage"]["type"] {
|
||||
return "swiss";
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user