skillsToLeaderboard logic

This commit is contained in:
Kalle
2022-03-26 15:27:04 +02:00
parent 6064ede66a
commit a2aa773487
5 changed files with 224 additions and 1 deletions

View File

@@ -0,0 +1,126 @@
import { suite } from "uvu";
import * as assert from "uvu/assert";
import { UserLean } from "~/utils";
import { skillsToLeaderboard } from "./leaderboards";
import { muSigmaToSP } from "./utils";
const SkillsToLeaderboard = suite("skillsToLeaderboard()");
const USER: UserLean = {
discordAvatar: "",
discordDiscriminator: "1234",
discordId: "12341234",
discordName: "Sendou",
id: "1",
};
const USER_2: UserLean = {
discordAvatar: "",
discordDiscriminator: "12342",
discordId: "123412342",
discordName: "Sendou2",
id: "2",
};
SkillsToLeaderboard("Works with empty array", () => {
const players = skillsToLeaderboard([]);
assert.not.ok(players.length);
});
SkillsToLeaderboard("Ignores skills if below amount required", () => {
const players = skillsToLeaderboard([
{
match: { createdAt: new Date() },
mu: 10,
sigma: 2,
userId: "1",
user: USER,
},
]);
assert.not.ok(players.length);
});
SkillsToLeaderboard("Gets peak", () => {
const players = skillsToLeaderboard(
new Array(10).fill(null).map((_) => ({
match: { createdAt: new Date() },
mu: 10,
sigma: 2,
userId: "1",
user: USER,
}))
);
assert.equal(players[0].MMR, muSigmaToSP({ sigma: 2, mu: 10 }));
});
SkillsToLeaderboard("Ignores peaks at the start", () => {
const players = skillsToLeaderboard(
new Array(10).fill(null).map((_, i) => ({
match: { createdAt: new Date() },
mu: i === 0 ? 30 : 10,
sigma: 2,
userId: "1",
user: USER,
}))
);
assert.equal(players[0].MMR, muSigmaToSP({ sigma: 2, mu: 10 }));
});
SkillsToLeaderboard("Gets peak from in between", () => {
const players = skillsToLeaderboard(
new Array(10).fill(null).map((_, i) => ({
match: { createdAt: new Date() },
mu: i === 8 ? 30 : 10,
sigma: 2,
userId: "1",
user: USER,
}))
);
assert.equal(players[0].MMR, muSigmaToSP({ sigma: 2, mu: 30 }));
});
SkillsToLeaderboard("Calculates entries", () => {
const players = skillsToLeaderboard(
new Array(10).fill(null).map((_, i) => ({
match: { createdAt: new Date() },
mu: i === 8 ? 30 : 10,
sigma: 2,
userId: "1",
user: USER,
}))
);
assert.equal(players[0].entries, 10);
});
SkillsToLeaderboard("Orders by MMR", () => {
const players = skillsToLeaderboard(
new Array(10)
.fill(null)
.map((_, i) => ({
match: { createdAt: new Date() },
mu: i === 8 ? 30 : 10,
sigma: 2,
userId: "1",
user: USER,
}))
.concat(
new Array(10).fill(null).map((_) => ({
match: { createdAt: new Date() },
mu: 40,
sigma: 2,
userId: "2",
user: USER_2,
}))
)
);
assert.equal(players[0].user.id, USER_2.id);
});
SkillsToLeaderboard.run();

View File

@@ -0,0 +1,64 @@
import { Skill } from "@prisma/client";
import { AMOUNT_OF_ENTRIES_REQUIRED_FOR_LEADERBOARD } from "~/constants";
import { UserLean } from "~/utils";
import { muSigmaToSP } from "./utils";
export interface LeaderboardEntry {
MMR: number;
user: UserLean;
createdAtTimestamp: number;
entries: number;
}
type SkillInput = Pick<Skill, "mu" | "sigma" | "userId"> & {
match: { createdAt: Date } | null;
user: UserLean;
};
type UserId = string;
export function skillsToLeaderboard(skills: SkillInput[]): LeaderboardEntry[] {
const counts: Record<UserId, number> = {};
const peakMMR: Record<UserId, LeaderboardEntry> = {};
for (const skill of skills.sort(sortSkillsByCreatedAt)) {
if (!counts[skill.userId]) {
counts[skill.userId] = 1;
} else {
counts[skill.userId]++;
}
if (counts[skill.userId] < AMOUNT_OF_ENTRIES_REQUIRED_FOR_LEADERBOARD) {
continue;
}
const MMR = muSigmaToSP(skill);
if (!peakMMR[skill.userId] || peakMMR[skill.userId].MMR < MMR) {
peakMMR[skill.userId] = {
MMR,
createdAtTimestamp: skill.match?.createdAt.getTime() ?? 0,
user: {
discordAvatar: skill.user.discordAvatar,
discordDiscriminator: skill.user.discordDiscriminator,
discordId: skill.user.discordId,
discordName: skill.user.discordName,
id: skill.user.id,
},
// we set this below
entries: 0,
};
}
}
for (const [userId, count] of Object.entries(counts)) {
if (!peakMMR[userId]) continue;
peakMMR[userId].entries = count;
}
return Object.values(peakMMR).sort((a, b) => b.MMR - a.MMR);
}
function sortSkillsByCreatedAt(a: SkillInput, b: SkillInput) {
return (
(a.match?.createdAt.getTime() ?? 0) - (b.match?.createdAt.getTime() ?? 0)
);
}

View File

@@ -12,3 +12,7 @@ export function findAllMostRecent() {
distinct: "userId",
});
}
export function findAll() {
return db.skill.findMany({ include: { match: true, user: true } });
}

View File

@@ -0,0 +1,29 @@
import { json, LoaderFunction, MetaFunction, useLoaderData } from "remix";
import { makeTitle } from "~/utils";
import * as Skill from "~/models/Skill.server";
import { LeaderboardEntry, skillsToLeaderboard } from "~/core/mmr/leaderboards";
export const meta: MetaFunction = () => {
return {
title: makeTitle("Leaderboards"),
};
};
export type LookingLoaderDataGroup = {
players: LeaderboardEntry[];
};
export const loader: LoaderFunction = async () => {
// TODO: only get skills of March
const skills = await Skill.findAll();
return json<LookingLoaderDataGroup>({ players: skillsToLeaderboard(skills) });
};
export default function LeaderboardsPage() {
const data = useLoaderData<LookingLoaderDataGroup>();
console.log({ data });
return <div>boards lol</div>;
}

View File

@@ -53,7 +53,7 @@ export const meta: MetaFunction = ({
return {
title: makeTitle([
data
? `(${data.likedGroups.length}/${data.neutralGroups.length}/${data.likerGroups.length})`
? `(${data.likedGroups?.length}/${data.neutralGroups?.length}/${data.likerGroups?.length})`
: "(???)",
"Looking",
]),