From 73f929b3e4c059c0e8ad32f801f7d8697efc0237 Mon Sep 17 00:00:00 2001
From: zkldi <20380519+zkldi@users.noreply.github.com>
Date: Sun, 30 Oct 2022 19:24:17 +0000
Subject: [PATCH] feat: initial pb-union code
---
.../rivals/RivalCompareTop100Page.tsx | 30 +++++
.../_playtype/rivals/RivalsComparePage.tsx | 5 +-
.../games/_game/_playtype/pbs/router.ts | 124 +++++++++++++++++-
server/src/utils/queries/pbs.ts | 38 +++++-
4 files changed, 191 insertions(+), 6 deletions(-)
create mode 100644 client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx
diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx
new file mode 100644
index 000000000..72e05ca08
--- /dev/null
+++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx
@@ -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
You have no settings. How did you get here?
;
+ }
+
+ const { data, error } = useApiQuery(
+ `/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=nythil`
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx
index a8a70cba0..a013d6934 100644
--- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx
+++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx
@@ -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({
+ >
+
+
{
*
* @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.
diff --git a/server/src/utils/queries/pbs.ts b/server/src/utils/queries/pbs.ts
index fdc1a0bb6..4e4f0ddcb 100644
--- a/server/src/utils/queries/pbs.ts
+++ b/server/src/utils/queries/pbs.ts
@@ -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;
+
+/**
+ * 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,
+ 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;
+
+ 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;
+}