feat: better arcaea judgement cell + ptt calculation fixes (#1589)

* feat: PM judgements + correct rating calc

* apparently, I forgot SQL syntax

* rg-stats test case
This commit is contained in:
nairobi
2026-05-26 18:25:16 +01:00
committed by GitHub
parent da9c6acd0a
commit e8b460b9ee
8 changed files with 117 additions and 48 deletions
@@ -0,0 +1,4 @@
INSERT INTO score_rederive (chart_id)
SELECT id as chart_id
FROM chart
WHERE chart.game = 'arcaea';
@@ -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 (
<td>
<strong>
<span style={{ color: COLOUR_SET.vibrantYellow }}>0</span>-
<span style={{ color: COLOUR_SET.red }}>0</span>
</strong>
</td>
);
}
const judgements = score.scoreData.judgements;
if (IsNullish(judgements.far) || IsNullish(judgements.lost)) {
return <td>No Data.</td>;
}
return (
<td>
<strong>
<span style={{ color: COLOUR_SET.vibrantYellow }}>{judgements.far}</span>-
<span style={{ color: COLOUR_SET.red }}>{judgements.lost}</span>
</strong>
</td>
);
}
+69 -5
View File
@@ -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<GamesForGroup["arcaea"]>["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<GamesForGroup["arcaea"]>["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 <td>No Data.</td>;
}
if (score.scoreData.lamp === "PURE MEMORY") {
const delta = MaxPureDelta(score);
return (
<td>
<strong>
{delta < 0 ? (
<span style={{ color: COLOUR_SET.blue }}>MAX{delta}</span>
) : (
<span style={{ color: COLOUR_SET.vibrantBlue }}>MAX</span>
)}
</strong>
</td>
);
}
return (
<td>
<strong>
<span style={{ color: COLOUR_SET.vibrantYellow }}>{judgements.far}</span>-
<span style={{ color: COLOUR_SET.red }}>{judgements.lost}</span>
</strong>
</td>
);
};
const ArcaeaCoreCells: GPTClientImplementation<GamesForGroup["arcaea"]>["scoreCoreCells"] = ({
sc,
}) => (
@@ -90,7 +145,16 @@ const ArcaeaCoreCells: GPTClientImplementation<GamesForGroup["arcaea"]>["scoreCo
const ArcaeaRatingCell: GPTClientImplementation<GamesForGroup["arcaea"]>["ratingCell"] = ({
sc,
rating,
}) => <RatingCell rating={rating} score={sc} />;
}) => {
const ptt = sc.calculatedData.potential ?? 0;
return (
<QuickTooltip tooltipContent={<div className="fs-5">{ptt.toFixed(6)}</div>}>
<td>
<div className="underline-on-hover">{FormatScoreRating("arcaea", rating, ptt)}</div>
</td>
</QuickTooltip>
);
};
export const ARCAEA_TOUCH_IMPL: GPTClientImplementation<"arcaea"> = {
sessionImportantScoreCount: 30,
@@ -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",
@@ -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),
@@ -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);
}
@@ -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();
});
});
});
@@ -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.`;
}
}
},
],
};