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.`;
+ }
+ }
+ },
],
};