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; +}