sendou.ink/utils/playFunctions.ts
2021-03-08 17:48:05 +02:00

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);
}
}
};