From e8b460b9eec00e4540a8525275fef7d474c99af9 Mon Sep 17 00:00:00 2001 From: nairobi <61069237+nyairobi@users.noreply.github.com> Date: Tue, 26 May 2026 19:25:16 +0200 Subject: [PATCH] feat: better arcaea judgement cell + ptt calculation fixes (#1589) * feat: PM judgements + correct rating calc * apparently, I forgot SQL syntax * rg-stats test case --- .../20260525194600_recalc_arcaea.sql | 4 + .../tables/cells/ArcaeaJudgementCell.tsx | 36 --------- typescript/client/src/lib/games/arcaea.tsx | 74 +++++++++++++++++-- .../common/src/config/game-support/arcaea.ts | 3 +- .../rg-stats/src/algorithms/potential.test.ts | 1 + .../rg-stats/src/algorithms/potential.ts | 9 +-- .../game-implementations/games/arcaea.test.ts | 24 +++++- .../src/game-implementations/games/arcaea.ts | 14 ++++ 8 files changed, 117 insertions(+), 48 deletions(-) create mode 100644 db/migrations/20260525194600_recalc_arcaea.sql delete mode 100644 typescript/client/src/components/tables/cells/ArcaeaJudgementCell.tsx diff --git a/db/migrations/20260525194600_recalc_arcaea.sql b/db/migrations/20260525194600_recalc_arcaea.sql new file mode 100644 index 000000000..094185cad --- /dev/null +++ b/db/migrations/20260525194600_recalc_arcaea.sql @@ -0,0 +1,4 @@ +INSERT INTO score_rederive (chart_id) +SELECT id as chart_id +FROM chart +WHERE chart.game = 'arcaea'; diff --git a/typescript/client/src/components/tables/cells/ArcaeaJudgementCell.tsx b/typescript/client/src/components/tables/cells/ArcaeaJudgementCell.tsx deleted file mode 100644 index 4180c1c40..000000000 --- a/typescript/client/src/components/tables/cells/ArcaeaJudgementCell.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { IsNullish } from "#util/misc"; -import React from "react"; -import { COLOUR_SET, type PBScoreDocument, type ScoreDocument } from "tachi-common"; - -export default function ArcaeaJudgementCell({ - score, -}: { - score: PBScoreDocument<"arcaea"> | ScoreDocument<"arcaea">; -}) { - // even if we dont have judgement data, we know what they got. - if (score.scoreData.lamp === "PURE MEMORY") { - return ( - - - 0- - 0 - - - ); - } - - const judgements = score.scoreData.judgements; - - if (IsNullish(judgements.far) || IsNullish(judgements.lost)) { - return No Data.; - } - - return ( - - - {judgements.far}- - {judgements.lost} - - - ); -} diff --git a/typescript/client/src/lib/games/arcaea.tsx b/typescript/client/src/lib/games/arcaea.tsx index 90f6c44cd..dc4cfbd8b 100644 --- a/typescript/client/src/lib/games/arcaea.tsx +++ b/typescript/client/src/lib/games/arcaea.tsx @@ -1,12 +1,17 @@ -import ArcaeaJudgementCell from "#components/tables/cells/ArcaeaJudgementCell"; +import QuickTooltip from "#components/layout/misc/QuickTooltip"; import LampCell from "#components/tables/cells/LampCell"; import MillionsScoreCell from "#components/tables/cells/MillionsScoreCell"; -import RatingCell from "#components/tables/cells/RatingCell"; import { GetEnumColour } from "#lib/game-implementations"; import { type GPTClientImplementation } from "#lib/types"; +import { FormatScoreRating, IsNullish } from "#util/misc"; import { NumericSOV } from "#util/sorts"; import React from "react"; -import { COLOUR_SET, type GamesForGroup } from "tachi-common"; +import { + COLOUR_SET, + type GamesForGroup, + type PBScoreDocument, + type ScoreDocument, +} from "tachi-common"; import { bgc } from "./_util"; @@ -67,12 +72,62 @@ const ARCAEA_COLORS: GPTClientImplementation["classColo }, }; +// Returns 0 for MAX PM, -10 for MAX-10, and -10k if undefined/not applicable +const MaxPureDelta = (sc: PBScoreDocument<"arcaea"> | ScoreDocument<"arcaea">) => + IsNullish(sc.scoreData.judgements.pure) || sc.scoreData.lamp !== "PURE MEMORY" + ? -10000 + : sc.scoreData.score - sc.scoreData.judgements.pure - 10_000_000; + const ARCAEA_SCORE_HEADERS: GPTClientImplementation["scoreHeaders"] = [ ["Score", "Score", NumericSOV((x) => x.scoreData.score)], - ["Far - Lost", "Far - Lost", NumericSOV((x) => x.scoreData.score)], + [ + "Judgements", + "Judgements", + NumericSOV((sc) => + sc.scoreData.lamp !== "PURE MEMORY" + ? sc.scoreData.score - 20_000_000 + : MaxPureDelta(sc), + ), + ], ["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)], ]; +const ArcaeaJudgementCell = ({ + score, +}: { + score: PBScoreDocument<"arcaea"> | ScoreDocument<"arcaea">; +}) => { + const judgements = score.scoreData.judgements; + + if (IsNullish(judgements.far) || IsNullish(judgements.lost)) { + return No Data.; + } + + if (score.scoreData.lamp === "PURE MEMORY") { + const delta = MaxPureDelta(score); + return ( + + + {delta < 0 ? ( + MAX{delta} + ) : ( + MAX + )} + + + ); + } + + return ( + + + {judgements.far}- + {judgements.lost} + + + ); +}; + const ArcaeaCoreCells: GPTClientImplementation["scoreCoreCells"] = ({ sc, }) => ( @@ -90,7 +145,16 @@ const ArcaeaCoreCells: GPTClientImplementation["scoreCo const ArcaeaRatingCell: GPTClientImplementation["ratingCell"] = ({ sc, rating, -}) => ; +}) => { + const ptt = sc.calculatedData.potential ?? 0; + return ( + {ptt.toFixed(6)}}> + +
{FormatScoreRating("arcaea", rating, ptt)}
+ +
+ ); +}; export const ARCAEA_TOUCH_IMPL: GPTClientImplementation<"arcaea"> = { sessionImportantScoreCount: 30, diff --git a/typescript/common/src/config/game-support/arcaea.ts b/typescript/common/src/config/game-support/arcaea.ts index 9018b7bd8..f3fc003f3 100644 --- a/typescript/common/src/config/game-support/arcaea.ts +++ b/typescript/common/src/config/game-support/arcaea.ts @@ -90,7 +90,7 @@ export const GAME_ARCAEA_CONF = { naivePotential: { description: "The average of your best 30 Potential values. This is different to the in-game algorithm, as it does not take your recent scores into account in any way.", - formatter: ToDecimalPlaces(2), + formatter: (v) => (Math.round(v * 100.0) / 100.0).toFixed(2), associatedScoreAlgs: ["potential"], }, }, @@ -117,6 +117,7 @@ export const GAME_ARCAEA_CONF = { badge: { type: "DERIVED", values: ArcaeaBadges, + minimumRelevantValue: "RED", }, courseBanner: { type: "PROVIDED", diff --git a/typescript/rg-stats/src/algorithms/potential.test.ts b/typescript/rg-stats/src/algorithms/potential.test.ts index 2b13e64a0..b80dd839c 100644 --- a/typescript/rg-stats/src/algorithms/potential.test.ts +++ b/typescript/rg-stats/src/algorithms/potential.test.ts @@ -17,6 +17,7 @@ test("Arcaea Potential Tests", () => { MakeTestCase(9_977_755, 11.3, 13.19), MakeTestCase(9_934_498, 11.1, 12.77), MakeTestCase(9_932_746, 10.9, 12.56), + MakeTestCase(9_805_015, 11.6, 12.63), MakeTestCase(10_000_000, 7.0, 9.0), MakeTestCase(9_900_000, 8.0, 9.5), diff --git a/typescript/rg-stats/src/algorithms/potential.ts b/typescript/rg-stats/src/algorithms/potential.ts index 5d5324a60..43d41d6c7 100644 --- a/typescript/rg-stats/src/algorithms/potential.ts +++ b/typescript/rg-stats/src/algorithms/potential.ts @@ -12,16 +12,15 @@ export function calculate(score: number, internalChartLevel: number) { level: internalChartLevel, }); - const iclInt = Math.round(internalChartLevel * 100); let potential = 0; if (score >= 10_000_000) { - potential = iclInt + 200; + potential = internalChartLevel + 2; } else if (score >= 9_800_000) { - potential = iclInt + 100 + Math.floor((score - 9_800_000) / 2_000); + potential = internalChartLevel + 1 + (score - 9_800_000) / 200_000; } else { - potential = iclInt + Math.floor((score - 9_500_000) / 3_000); + potential = internalChartLevel + (score - 9_500_000) / 300_000; } - return Math.max(potential / 100, 0); + return Math.max(potential, 0); } diff --git a/typescript/server/src/game-implementations/games/arcaea.test.ts b/typescript/server/src/game-implementations/games/arcaea.test.ts index 96b414264..913e574e9 100644 --- a/typescript/server/src/game-implementations/games/arcaea.test.ts +++ b/typescript/server/src/game-implementations/games/arcaea.test.ts @@ -131,7 +131,7 @@ describe("ARCAEA_IMPL", () => { expect( ARCAEA_IMPL.scoreCalcs(scoreData, ARCAEA_IMPL.scoreDeriver(scoreData, chart), chart) .potential, - ).toBe(11.99); + ).toBe(11.99683); }); describe("classDerivers (naivePotential → badge)", () => { @@ -330,5 +330,27 @@ describe("ARCAEA_IMPL", () => { ARCAEA_IMPL.chartSpecificValidators.score(10_001_152, chartWithoutNotecount), ).toBe(true); }); + + it("rejects inconsistent pure counts", () => { + expect( + runVal({ + scoreData: { + lamp: "PURE MEMORY", + score: 10_001_151, + judgements: { pure: 1150, far: 0, lost: 0 }, + }, + }), + ).toEqual([`Impossible PURE MEMORY. Got ${10_001_151} with ${1150} pures.`]); + + expect( + runVal({ + scoreData: { + lamp: "PURE MEMORY", + score: 10_001_151, + judgements: { pure: 1151, far: 0, lost: 0 }, + }, + }), + ).toBeUndefined(); + }); }); }); diff --git a/typescript/server/src/game-implementations/games/arcaea.ts b/typescript/server/src/game-implementations/games/arcaea.ts index 7e13791e7..f044924df 100644 --- a/typescript/server/src/game-implementations/games/arcaea.ts +++ b/typescript/server/src/game-implementations/games/arcaea.ts @@ -125,5 +125,19 @@ export const ARCAEA_IMPL: GameImplementation<"arcaea"> = { return "Cannot have a FULL RECALL with non-zero lost count."; } }, + (s) => { + const { pure, far, lost } = s.scoreData.judgements; + + if ( + !IsNullish(pure) && + (far ?? 0) + (lost ?? 0) === 0 && + s.scoreData.lamp === "PURE MEMORY" + ) { + const shinyPure = s.scoreData.score - 10_000_000; + if (shinyPure > pure) { + return `Impossible PURE MEMORY. Got ${s.scoreData.score} with ${pure} pures.`; + } + } + }, ], };