mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-06-02 22:26:57 -05:00
generate ladder matches
This commit is contained in:
parent
c5bd3b2702
commit
c274aaea12
149
lib/playFunctions.ts
Normal file
149
lib/playFunctions.ts
Normal 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
70
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
74
pages/api/play/teams/matches.ts
Normal file
74
pages/api/play/teams/matches.ts
Normal 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;
|
||||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
19
prisma/queries/getAllLadderRegisteredTeamsForMatches.ts
Normal file
19
prisma/queries/getAllLadderRegisteredTeamsForMatches.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user