mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-28 21:34:41 -05:00
151 lines
4.3 KiB
TypeScript
151 lines
4.3 KiB
TypeScript
import { GetAllLadderRegisteredTeamsForMatchesData } from "prisma/queries/getAllLadderRegisteredTeamsForMatches";
|
|
import { quality, Rating } from "ts-trueskill";
|
|
|
|
type TeamsWithRanking = {
|
|
id: number;
|
|
roster: {
|
|
id: number;
|
|
rating: Rating;
|
|
}[];
|
|
};
|
|
|
|
export const getLadderRounds = (
|
|
registeredTeams: GetAllLadderRegisteredTeamsForMatchesData
|
|
) => {
|
|
if (registeredTeams.length < 4) {
|
|
throw Error("registeredTeams length less than 4");
|
|
}
|
|
const teamsWithRanking: TeamsWithRanking[] = registeredTeams.map(
|
|
(registeredTeam) => ({
|
|
id: registeredTeam.id,
|
|
roster: registeredTeam.roster.map((user) => ({
|
|
id: user.id,
|
|
rating: user.trueSkill
|
|
? new Rating(user.trueSkill.mu, user.trueSkill.sigma)
|
|
: new Rating(),
|
|
})),
|
|
})
|
|
);
|
|
|
|
// this chooses the teams to sit out each round if uneven number of teams
|
|
// if even it just returns `teamsWithRanking` in both 0 index and 1 index
|
|
const [teamsRoundOne, teamsRoundTwo] = getTeamsForRounds();
|
|
|
|
// helper variable accessed from generatePairings
|
|
let bestRound: TeamsWithRanking[][] | undefined;
|
|
// helper variable accessed from generatePairings
|
|
let bestAverageQuality = -Infinity;
|
|
|
|
// first round matches actual
|
|
let firstRound: TeamsWithRanking[][] | undefined;
|
|
|
|
generatePairings(teamsRoundOne, 0);
|
|
|
|
firstRound = bestRound;
|
|
bestAverageQuality = -Infinity;
|
|
|
|
generatePairings(teamsRoundTwo, 0);
|
|
|
|
if (!firstRound || !bestRound || firstRound === bestRound) {
|
|
throw Error("unexpected falsy firstRound or bestPairs");
|
|
}
|
|
|
|
return [firstRound, bestRound];
|
|
|
|
// https://stackoverflow.com/a/37449857
|
|
// start is the current position in the list, advancing by 2 each time
|
|
// pass 0 as start when calling at the top level
|
|
function generatePairings(items: TeamsWithRanking[], start: number) {
|
|
if (items.length % 2 !== 0) {
|
|
throw Error("uneven number of teams in generatePairings");
|
|
}
|
|
|
|
// is this a complete pairing?
|
|
if (start === items.length) {
|
|
if (hasDuplicatePairing()) {
|
|
return;
|
|
}
|
|
|
|
let qualitySum = 0;
|
|
for (let i = 0; i < items.length; i += 2) {
|
|
const teamAlpha = items[i].roster.map((user) => user.rating);
|
|
const teamBravo = items[i + 1].roster.map((user) => user.rating);
|
|
|
|
qualitySum += quality([teamAlpha, teamBravo]);
|
|
}
|
|
|
|
qualitySum /= items.length / 2;
|
|
if (qualitySum > bestAverageQuality) {
|
|
bestAverageQuality = qualitySum;
|
|
bestRound = items
|
|
.map((team, i) => (i % 2 !== 0 ? null : [team, items[i + 1]]))
|
|
.filter((team) => team) as TeamsWithRanking[][];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// for the next pair, choose the first element in the list for the
|
|
// first item in the pair (meaning we don't have to do anything
|
|
// but leave it in place), and each of the remaining elements for
|
|
// the second item:
|
|
for (let j = start + 1; j < items.length; j++) {
|
|
// swap start+1 and j:
|
|
let temp = items[start + 1];
|
|
items[start + 1] = items[j];
|
|
items[j] = temp;
|
|
|
|
// recurse:
|
|
generatePairings(items, start + 2);
|
|
|
|
// swap them back:
|
|
temp = items[start + 1];
|
|
items[start + 1] = items[j];
|
|
items[j] = temp;
|
|
}
|
|
|
|
function hasDuplicatePairing() {
|
|
if (!firstRound) return false;
|
|
|
|
for (let i = 0; i < items.length; i += 2) {
|
|
const teamAlpha = items[i];
|
|
const teamBravo = items[i + 1];
|
|
|
|
if (
|
|
firstRound.some(
|
|
([pairsAlpha, pairsBravo]) =>
|
|
(pairsAlpha.id === teamAlpha.id &&
|
|
pairsBravo.id === teamBravo.id) ||
|
|
(pairsAlpha.id === teamBravo.id && pairsBravo.id === teamAlpha.id)
|
|
)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getTeamsForRounds() {
|
|
if (teamsWithRanking.length % 2 === 0) {
|
|
return [teamsWithRanking, teamsWithRanking];
|
|
}
|
|
|
|
const firstTeamToSitOut = randomChoiceIndex();
|
|
let secondTeamToSitOut = randomChoiceIndex();
|
|
while (secondTeamToSitOut === firstTeamToSitOut) {
|
|
secondTeamToSitOut = randomChoiceIndex();
|
|
}
|
|
|
|
return [
|
|
teamsWithRanking.filter((_, i) => i !== firstTeamToSitOut),
|
|
teamsWithRanking.filter((_, i) => i !== secondTeamToSitOut),
|
|
];
|
|
|
|
function randomChoiceIndex() {
|
|
return Math.floor(Math.random() * teamsWithRanking.length);
|
|
}
|
|
}
|
|
};
|