feat: initial pb-union code

This commit is contained in:
zkldi
2022-10-30 19:24:17 +00:00
parent b318fa44dc
commit 73f929b3e4
4 changed files with 191 additions and 6 deletions
@@ -0,0 +1,30 @@
import DebugContent from "components/util/DebugContent";
import useApiQuery from "components/util/query/useApiQuery";
import useLUGPTSettings from "components/util/useLUGPTSettings";
import React from "react";
import { PublicUserDocument } from "tachi-common";
import { GamePT } from "types/react";
export default function RivalCompareTop100Page({
reqUser,
game,
playtype,
}: GamePT & {
reqUser: PublicUserDocument;
}) {
const { settings } = useLUGPTSettings();
if (!settings) {
return <div>You have no settings. How did you get here?</div>;
}
const { data, error } = useApiQuery(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=nythil`
);
return (
<div>
<DebugContent data={data} />
</div>
);
}
@@ -9,6 +9,7 @@ import React from "react";
import { Col, Row } from "react-bootstrap";
import { Route, Switch } from "react-router-dom";
import { FormatGame, Game, GetGameConfig, Playtype, PublicUserDocument } from "tachi-common";
import RivalCompareTop100Page from "./RivalCompareTop100Page";
export default function RivalsComparePage({
reqUser,
@@ -73,7 +74,9 @@ export default function RivalsComparePage({
<Route
exact
path="/dashboard/users/:userID/games/:game/:playtype/rivals/compare"
></Route>
>
<RivalCompareTop100Page reqUser={reqUser} game={game} playtype={playtype} />
</Route>
<Route
exact
@@ -2,14 +2,15 @@ import { Router } from "express";
import db from "external/mongo/db";
import { GetRivalUsers } from "lib/rivals/rivals";
import { SearchSpecificGameSongsAndCharts } from "lib/search/search";
import prValidate from "server/middleware/prudence-validate";
import { AggressiveRateLimitMiddleware } from "server/middleware/rate-limiter";
import { GetGamePTConfig } from "tachi-common";
import { FormatGame, GetGamePTConfig } from "tachi-common";
import { GetRelevantSongsAndCharts } from "utils/db";
import { IsValidScoreAlg } from "utils/misc";
import { GetAdjacentAbove, GetAdjacentBelow } from "utils/queries/pbs";
import { GetAdjacentAbove, GetAdjacentBelow, GetPBsWithUserRankings } from "utils/queries/pbs";
import { GetUGPT } from "utils/req-tachi-data";
import { FilterChartsAndSongs, GetPBOnChart, GetScoreIDsFromComposed } from "utils/scores";
import { GetUsersWithIDs } from "utils/user";
import { GetUsersWithIDs, ResolveUser } from "utils/user";
const router: Router = Router({ mergeParams: true });
@@ -97,7 +98,7 @@ router.get("/all", AggressiveRateLimitMiddleware, async (req, res) => {
*
* @name GET /api/v1/users/:userID/games/:game/:playtype/pbs/best
*/
router.get("/best", async (req, res) => {
router.get("/best", prValidate({ alg: "*string" }), async (req, res) => {
const { user, game, playtype } = GetUGPT(req);
const gptConfig = GetGamePTConfig(game, playtype);
@@ -141,6 +142,121 @@ router.get("/best", async (req, res) => {
});
});
/**
* Return this user's best 100 personal-bests, unioned with the best 100 of another
* player.
*
* This is used for comparing best-100s with a player, so for example, if player B
* has charts in their top 100 that player A doesn't, it will still fetch the PB
* of player A for comparison reasons.
*
* @param alg - Specifies an override for the default algorithm
* to sort on.
* @param withUser - The userID to union personal bests with.
*/
router.get("/best-union", prValidate({ alg: "*string", withUser: "string" }), async (req, res) => {
const { user, game, playtype } = GetUGPT(req);
const gptConfig = GetGamePTConfig(game, playtype);
const query = req.query as {
alg?: string;
withUser: string;
};
const otherUser = await ResolveUser(query.withUser);
if (!otherUser) {
return res.status(400).json({
success: false,
description: `The user '${query.withUser}' does not exist.`,
});
}
const hasPlayed = await db["game-stats"].findOne({
game,
playtype,
userID: otherUser.id,
});
if (!hasPlayed) {
return res.status(400).json({
success: false,
description: `The user '${otherUser.username}' has not played ${FormatGame(
game,
playtype
)}.`,
});
}
if (req.query.alg !== undefined && !IsValidScoreAlg(gptConfig, req.query.alg)) {
return res.status(400).json({
success: false,
description: `Invalid score algorithm. Expected any of ${gptConfig.scoreRatingAlgs.join(
", "
)}`,
});
}
const alg = req.query.alg ?? gptConfig.defaultScoreRatingAlg;
const options = {
projection: {
chartID: 1,
},
limit: 100,
sort: {
[`calculatedData.${alg}`]: -1,
},
};
// lets fetch the set of chartIDs in each users best 100s
const baseUserBestChartIDs = await db["personal-bests"].find(
{
userID: user.id,
game,
playtype,
isPrimary: true,
},
options
);
const withUserBestChartIDs = await db["personal-bests"].find(
{
userID: otherUser.id,
game,
playtype,
isPrimary: true,
},
options
);
const chartIDs = [
...baseUserBestChartIDs.map((e) => e.chartID),
...withUserBestChartIDs.map((e) => e.chartID),
];
// then fetch both of their scores on each.
const baseUserPBs = await GetPBsWithUserRankings(user.id, chartIDs, alg);
const withUserPBs = await GetPBsWithUserRankings(user.id, chartIDs, alg);
const { songs, charts } = await GetRelevantSongsAndCharts(
[...baseUserPBs, ...withUserPBs],
game
);
return res.status(200).json({
success: true,
description: `Retrieved the union best 100 of ${user.username} and ${otherUser.username}.`,
body: {
baseUserPBs,
withUserPBs,
songs,
charts,
},
});
});
/**
* Returns a user's PB on the given chart. If the user has not played this chart, 404 is
* returned.
+37 -1
View File
@@ -1,5 +1,5 @@
import db from "external/mongo/db";
import type { PBScoreDocument } from "tachi-common";
import type { IDStrings, integer, PBScoreDocument, ScoreCalculatedDataLookup } from "tachi-common";
export async function GetAdjacentAbove(userPB: PBScoreDocument, size = 5) {
const adjAbove = (await db["personal-bests"].find(
@@ -30,3 +30,39 @@ export async function GetAdjacentBelow(userPB: PBScoreDocument, size = 5) {
return adjAbove;
}
export type PBsWithPersonalRanking = Array<PBScoreDocument & { __personalRanking: integer }>;
/**
* Given a user, an algorithm, and an array of chartIDs, return the user's PBs
* on those charts with where they rank in their personal PBs.
*/
export async function GetPBsWithUserRankings(
userID: integer,
chartIDs: Array<string>,
alg: ScoreCalculatedDataLookup[IDStrings]
) {
// i was going to do this with a fancy aggregate pipeline
// but it's slower.
// ah well
const pbs = (await db["personal-bests"].find({
userID,
chartID: { $in: chartIDs },
isPrimary: true,
})) as Array<PBScoreDocument & { __personalRanking?: integer }>;
await Promise.all(
pbs.map(async (pb) => {
const personalRanking = await db["personal-bests"].count({
userID,
[`calculatedData.${alg}`]: { $gt: pb.calculatedData[alg] },
});
// eslint-disable-next-line require-atomic-updates
pb.__personalRanking = personalRanking + 1;
})
);
// __personalRanking guaranteed to be assigned
return pbs as PBsWithPersonalRanking;
}