generate ladder matches

This commit is contained in:
Kalle (Sendou) 2021-01-25 21:00:18 +02:00
parent c5bd3b2702
commit c274aaea12
6 changed files with 318 additions and 3 deletions

149
lib/playFunctions.ts Normal file
View File

@ -0,0 +1,149 @@
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 bestPairs: TeamsWithRanking[][] | undefined;
// helper variable accessed from generatePairings
let bestAverageQuality = Infinity;
// first round matches actual
let firstRound: TeamsWithRanking[][] | undefined;
generatePairings(teamsRoundOne, 0);
firstRound = bestPairs;
bestAverageQuality = -Infinity;
generatePairings(teamsRoundTwo, 0);
if (!firstRound || !bestPairs || firstRound === bestPairs) {
throw Error("unexpected falsy firstROund or bestPairs");
}
return [firstRound, bestPairs];
// 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;
bestPairs = 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);
}
}
};

70
package-lock.json generated
View File

@ -3538,6 +3538,11 @@
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
},
"complex.js": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.11.tgz",
"integrity": "sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw=="
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@ -4262,6 +4267,11 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"decimal.js": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz",
"integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw=="
},
"decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
@ -4665,6 +4675,11 @@
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
},
"escape-latex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
"integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw=="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@ -5145,6 +5160,11 @@
"mime-types": "^2.1.12"
}
},
"fraction.js": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.13.tgz",
"integrity": "sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA=="
},
"fragment-cache": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@ -6145,6 +6165,11 @@
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"dev": true
},
"javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k="
},
"jest-get-type": {
"version": "26.3.0",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz",
@ -6853,6 +6878,21 @@
"resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz",
"integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg=="
},
"mathjs": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-8.1.1.tgz",
"integrity": "sha512-b3TX3EgiZObujjwb8lZnTDLUuivC2jar4ZBjmGJ4stFYCDXx/DNwx5yry5t/z65p9mvejyZel1qoeR05KtChcQ==",
"requires": {
"complex.js": "^2.0.11",
"decimal.js": "^10.2.1",
"escape-latex": "^1.2.0",
"fraction.js": "^4.0.13",
"javascript-natural-sort": "^0.7.1",
"seedrandom": "^3.0.5",
"tiny-emitter": "^2.1.0",
"typed-function": "^2.0.0"
}
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -9251,6 +9291,11 @@
"ajv-keywords": "^3.5.2"
}
},
"seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@ -10103,6 +10148,11 @@
"setimmediate": "^1.0.4"
}
},
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
@ -10217,6 +10267,11 @@
"resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
"integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA=="
},
"ts-gaussian": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ts-gaussian/-/ts-gaussian-2.0.2.tgz",
"integrity": "sha512-DdMwQc1bgzRwbKQ3VoiV3aK2isz7CJhOmtO2oA+mp9soGpbN/p2pkBaIrZnJBW3jB293CUARLFmNktcTSHjypQ=="
},
"ts-node": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz",
@ -10236,6 +10291,16 @@
"resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
"integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw=="
},
"ts-trueskill": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ts-trueskill/-/ts-trueskill-3.2.0.tgz",
"integrity": "sha512-bzTv5KAWHvplf1tMCoD7/v5z/jNLXHTLX9nF6RIM/LIO/SbPfhxMqXLofCrP+HlZ0MlE4Sjf4IIjxxSdpwAVLA==",
"requires": {
"mathjs": "^8.0.1",
"ts-gaussian": "^2.0.2",
"uuid": "^8.3.1"
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
@ -10271,6 +10336,11 @@
"integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
"dev": true
},
"typed-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-2.0.0.tgz",
"integrity": "sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA=="
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",

View File

@ -52,6 +52,7 @@
"react-string-replace": "^0.4.4",
"recharts": "^2.0.3",
"swr": "^0.4.0",
"ts-trueskill": "^3.2.0",
"uuid": "^8.3.2",
"zod": "^1.11.11"
},

View File

@ -0,0 +1,74 @@
import { getMySession } from "lib/getMySession";
import { getLadderRounds } from "lib/playFunctions";
import { NextApiRequest, NextApiResponse } from "next";
import { getAllLadderRegisteredTeamsForMatches } from "prisma/queries/getAllLadderRegisteredTeamsForMatches";
const matchesHandler = async (req: NextApiRequest, res: NextApiResponse) => {
switch (req.method) {
case "POST":
await postHandler(req, res);
break;
default:
res.status(405).end();
}
};
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getMySession();
// if (user?.discordId !== ADMIN_DISCORD_ID) {
// return res.status(401).end();
// }
const teams = (await getAllLadderRegisteredTeamsForMatches()).filter(
(team) => team.roster.length === 4
);
if (teams.length < 4) return res.status(400).end();
const matches = getLadderRounds(teams);
// await Promise.all(
// matches.flatMap((round, i) =>
// round.map((match) =>
// prisma.ladderMatch.create({
// data: {
// date: "",
// maplist: {},
// order: i + 1,
// players: {
// create: match.flatMap((team, teamI) =>
// team.roster.map((user) => ({
// userId: user.id,
// team: teamI === 0 ? "ALPHA" : "BRAVO",
// }))
// ),
// },
// },
// })
// )
// )
// );
res.status(200).json(
matches.flatMap((round, i) =>
round.map((match) => ({
data: {
date: "",
maplist: {},
order: i + 1,
players: {
create: match.flatMap((team, teamI) =>
team.roster.map((user) => ({
userId: user.id,
team: teamI === 0 ? "ALPHA" : "BRAVO",
}))
),
},
},
}))
)
);
}
export default matchesHandler;

View File

@ -45,7 +45,7 @@ const PlayPage = () => {
)
.find((tuple) => tuple[1] >= 3);
return (
<Box my={4}>
<Box key={team.id} my={4}>
{teamTuple ? (
<Flex fontWeight="bold" align="center">
{teamTuple[0].twitterName && (
@ -56,12 +56,14 @@ const PlayPage = () => {
/>
)}
{teamTuple[0].name}
{teamTuple[1] === 3 && <SubText ml={1}>+1</SubText>}
{teamTuple[1] === 3 && team.roster.length >= 4 && (
<SubText ml={1}>+1</SubText>
)}
</Flex>
) : (
<HStack>
{team.roster.map((member) => (
<UserAvatar user={member} size="sm" />
<UserAvatar key={member.id} user={member} size="sm" />
))}
</HStack>
)}

View File

@ -0,0 +1,19 @@
import { Prisma } from "@prisma/client";
import prisma from "prisma/client";
export type GetAllLadderRegisteredTeamsForMatchesData = Prisma.PromiseReturnType<
typeof getAllLadderRegisteredTeamsForMatches
>;
export const getAllLadderRegisteredTeamsForMatches = async () =>
prisma.ladderRegisteredTeam.findMany({
select: {
id: true,
roster: {
select: {
id: true,
trueSkill: true,
},
},
},
});