mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-07 06:18:03 +03:00
feat: and the last bit, score calcs over
This commit is contained in:
@@ -62,7 +62,10 @@ function JsonCell({ label, value }: { label: string; value: unknown }) {
|
||||
return (
|
||||
<details>
|
||||
<summary className="small text-body-secondary">{label}</summary>
|
||||
<pre className="small mb-0 mt-1 p-2 bg-body-secondary rounded" style={{ maxWidth: "20rem" }}>
|
||||
<pre
|
||||
className="small mb-0 mt-1 p-2 bg-body-secondary rounded"
|
||||
style={{ maxWidth: "20rem" }}
|
||||
>
|
||||
{json}
|
||||
</pre>
|
||||
</details>
|
||||
@@ -178,11 +181,15 @@ export default function AdminActionsPage() {
|
||||
</td>
|
||||
<td className="small">{action.kind}</td>
|
||||
<td>
|
||||
<Badge bg={resultVariant(action.result)}>{action.result}</Badge>
|
||||
<Badge bg={resultVariant(action.result)}>
|
||||
{action.result}
|
||||
</Badge>
|
||||
</td>
|
||||
<td>
|
||||
{action.username ? (
|
||||
<Link to={`/u/${action.username}`}>{action.username}</Link>
|
||||
<Link to={`/u/${action.username}`}>
|
||||
{action.username}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-body-secondary">—</span>
|
||||
)}
|
||||
@@ -204,7 +211,10 @@ export default function AdminActionsPage() {
|
||||
{totalPages > 1 && (
|
||||
<div className="d-flex align-items-center gap-3">
|
||||
{currentPage > 0 && (
|
||||
<Link className="btn btn-sm btn-outline-primary" to={buildPageUrl(currentPage - 1)}>
|
||||
<Link
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
to={buildPageUrl(currentPage - 1)}
|
||||
>
|
||||
← Prev
|
||||
</Link>
|
||||
)}
|
||||
@@ -212,7 +222,10 @@ export default function AdminActionsPage() {
|
||||
Page {currentPage + 1} of {totalPages}
|
||||
</span>
|
||||
{currentPage < totalPages - 1 && (
|
||||
<Link className="btn btn-sm btn-outline-primary" to={buildPageUrl(currentPage + 1)}>
|
||||
<Link
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
to={buildPageUrl(currentPage + 1)}
|
||||
>
|
||||
Next →
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -97,7 +97,9 @@ export default function AdminCronJobsPage() {
|
||||
<div className="mb-2">
|
||||
<code className="me-2">{task.id}</code>
|
||||
<code className="text-body-secondary">{task.schedule}</code>
|
||||
{task.description && <p className="small mb-1 mt-1">{task.description}</p>}
|
||||
{task.description && (
|
||||
<p className="small mb-1 mt-1">{task.description}</p>
|
||||
)}
|
||||
<div className="small text-body-secondary d-flex flex-wrap gap-3">
|
||||
<span>
|
||||
Last scheduled:{" "}
|
||||
@@ -110,7 +112,8 @@ export default function AdminCronJobsPage() {
|
||||
</div>
|
||||
|
||||
<h3 className="h6">
|
||||
Recent executions <span className="badge bg-secondary">{execs.length}</span>
|
||||
Recent executions{" "}
|
||||
<span className="badge bg-secondary">{execs.length}</span>
|
||||
</h3>
|
||||
{execs.length === 0 ? (
|
||||
<p className="small text-body-secondary">No executions yet.</p>
|
||||
@@ -140,19 +143,25 @@ export default function AdminCronJobsPage() {
|
||||
{exec.completed_at ? (
|
||||
MillisToSince(Date.parse(exec.completed_at))
|
||||
) : (
|
||||
<span className="text-warning">In progress</span>
|
||||
<span className="text-warning">
|
||||
In progress
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="font-monospace small">
|
||||
{durationMs(exec.started_at, exec.completed_at)}
|
||||
</td>
|
||||
<td>
|
||||
<Badge bg={execStatusVariant(exec.status)}>{exec.status}</Badge>
|
||||
<Badge bg={execStatusVariant(exec.status)}>
|
||||
{exec.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="small">
|
||||
{exec.error ? (
|
||||
<details>
|
||||
<summary className="text-danger">Error</summary>
|
||||
<summary className="text-danger">
|
||||
Error
|
||||
</summary>
|
||||
<pre className="small mb-0 mt-1 p-2 bg-danger bg-opacity-10 rounded">
|
||||
{exec.error}
|
||||
</pre>
|
||||
@@ -165,7 +174,9 @@ export default function AdminCronJobsPage() {
|
||||
</pre>
|
||||
</details>
|
||||
) : (
|
||||
<span className="text-body-secondary">—</span>
|
||||
<span className="text-body-secondary">
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -14,8 +14,8 @@ export default function AdminDestructivePage() {
|
||||
const [ugptUserId, setUgptUserId] = useState("");
|
||||
const [ugptGame, setUgptGame] = useState<GameGroup>(TachiConfig.GAMES[0]);
|
||||
const ugptGameConfig = useMemo(() => GetGameGroupConfig(ugptGame), [ugptGame]);
|
||||
const [ugptPlaytype, setUgptPlaytype] = useState<string>(() =>
|
||||
GetGameGroupConfig(TachiConfig.GAMES[0]).playtypes[0],
|
||||
const [ugptPlaytype, setUgptPlaytype] = useState<string>(
|
||||
() => GetGameGroupConfig(TachiConfig.GAMES[0]).playtypes[0],
|
||||
);
|
||||
|
||||
const [destroyChartId, setDestroyChartId] = useState("");
|
||||
@@ -29,7 +29,9 @@ export default function AdminDestructivePage() {
|
||||
<Row className="g-4">
|
||||
<Col lg={6}>
|
||||
<Card className="border-danger">
|
||||
<Card.Header className="bg-danger bg-opacity-10 text-danger">Delete score</Card.Header>
|
||||
<Card.Header className="bg-danger bg-opacity-10 text-danger">
|
||||
Delete score
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Form.Group className="mb-3" controlId="delete-score">
|
||||
<Form.Label>Score ID</Form.Label>
|
||||
@@ -70,7 +72,9 @@ export default function AdminDestructivePage() {
|
||||
|
||||
<Col lg={6}>
|
||||
<Card className="border-danger">
|
||||
<Card.Header className="bg-danger bg-opacity-10 text-danger">Delete session</Card.Header>
|
||||
<Card.Header className="bg-danger bg-opacity-10 text-danger">
|
||||
Delete session
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Form.Group className="mb-3" controlId="delete-session">
|
||||
<Form.Label>Session ID</Form.Label>
|
||||
@@ -194,7 +198,9 @@ export default function AdminDestructivePage() {
|
||||
|
||||
<Col lg={6}>
|
||||
<Card className="border-danger">
|
||||
<Card.Header className="bg-danger bg-opacity-10 text-danger">Destroy chart</Card.Header>
|
||||
<Card.Header className="bg-danger bg-opacity-10 text-danger">
|
||||
Destroy chart
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Form.Group className="mb-3" controlId="destroy-chart-game">
|
||||
<Form.Label>Game</Form.Label>
|
||||
|
||||
@@ -38,7 +38,10 @@ function PayloadCell({ payload }: { payload: unknown }) {
|
||||
return (
|
||||
<details>
|
||||
<summary className="small text-body-secondary">View payload</summary>
|
||||
<pre className="small mb-0 mt-1 p-2 bg-body-secondary rounded" style={{ maxWidth: "28rem" }}>
|
||||
<pre
|
||||
className="small mb-0 mt-1 p-2 bg-body-secondary rounded"
|
||||
style={{ maxWidth: "28rem" }}
|
||||
>
|
||||
{json}
|
||||
</pre>
|
||||
</details>
|
||||
@@ -115,8 +118,7 @@ export default function AdminJobQueuePage() {
|
||||
{activeJobs.length > 0 && (
|
||||
<section>
|
||||
<h2 className="h5">
|
||||
Active jobs{" "}
|
||||
<span className="badge bg-primary">{activeJobs.length}</span>
|
||||
Active jobs <span className="badge bg-primary">{activeJobs.length}</span>
|
||||
</h2>
|
||||
<div className="table-responsive">
|
||||
<Table hover size="sm" striped>
|
||||
@@ -144,10 +146,14 @@ export default function AdminJobQueuePage() {
|
||||
|
||||
<section>
|
||||
<h2 className="h5">
|
||||
All jobs <span className="badge bg-secondary">{jobQueue.total.toLocaleString()}</span>
|
||||
All jobs{" "}
|
||||
<span className="badge bg-secondary">{jobQueue.total.toLocaleString()}</span>
|
||||
</h2>
|
||||
|
||||
<Form className="d-flex flex-wrap align-items-end gap-3 mb-3" onSubmit={onFilterSubmit}>
|
||||
<Form
|
||||
className="d-flex flex-wrap align-items-end gap-3 mb-3"
|
||||
onSubmit={onFilterSubmit}
|
||||
>
|
||||
<Form.Group>
|
||||
<Form.Label className="small mb-0">Status</Form.Label>
|
||||
<Form.Select
|
||||
@@ -223,7 +229,10 @@ export default function AdminJobQueuePage() {
|
||||
{totalPages > 1 && (
|
||||
<div className="d-flex align-items-center gap-3 mt-2">
|
||||
{currentPage > 0 && (
|
||||
<Link className="btn btn-sm btn-outline-primary" to={buildPageUrl(currentPage - 1)}>
|
||||
<Link
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
to={buildPageUrl(currentPage - 1)}
|
||||
>
|
||||
← Prev
|
||||
</Link>
|
||||
)}
|
||||
@@ -231,7 +240,10 @@ export default function AdminJobQueuePage() {
|
||||
Page {currentPage + 1} of {totalPages}
|
||||
</span>
|
||||
{currentPage < totalPages - 1 && (
|
||||
<Link className="btn btn-sm btn-outline-primary" to={buildPageUrl(currentPage + 1)}>
|
||||
<Link
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
to={buildPageUrl(currentPage + 1)}
|
||||
>
|
||||
Next →
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -19,9 +19,7 @@ export default function AdminOperationsPage() {
|
||||
|
||||
const [supporterUser, setSupporterUser] = useState("");
|
||||
|
||||
const announcementGameConfig = announcementGame
|
||||
? GetGameGroupConfig(announcementGame)
|
||||
: null;
|
||||
const announcementGameConfig = announcementGame ? GetGameGroupConfig(announcementGame) : null;
|
||||
|
||||
return (
|
||||
<Row className="g-4">
|
||||
@@ -74,7 +72,9 @@ export default function AdminOperationsPage() {
|
||||
<Button
|
||||
disabled={!announcementTitle.trim()}
|
||||
onClick={() => {
|
||||
const body: Record<string, unknown> = { title: announcementTitle.trim() };
|
||||
const body: Record<string, unknown> = {
|
||||
title: announcementTitle.trim(),
|
||||
};
|
||||
if (announcementGame) {
|
||||
body.game = announcementGame;
|
||||
}
|
||||
@@ -190,7 +190,8 @@ export default function AdminOperationsPage() {
|
||||
<Card.Header>Reprocess all goals</Card.Header>
|
||||
<Card.Body>
|
||||
<p className="text-body-secondary small">
|
||||
Re-runs goal and quest processing for every user game profile. Heavy operation.
|
||||
Re-runs goal and quest processing for every user game profile. Heavy
|
||||
operation.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() =>
|
||||
|
||||
@@ -4,15 +4,12 @@ import type {
|
||||
GPTClassDerivers,
|
||||
GPTGoalFormatters,
|
||||
GPTGoalProgressFormatters,
|
||||
GPTNewCalcs,
|
||||
GPTNewProfileCalcs,
|
||||
GPTProfileCalculators,
|
||||
GPTScoreCalculators,
|
||||
GPTProfileCalcs,
|
||||
GPTScoreCalcs,
|
||||
GPTScoreDeriver,
|
||||
GPTSessionCalcs,
|
||||
PBMergeFunction,
|
||||
PBRankingValuesFunction,
|
||||
ScoreCalculator,
|
||||
ScoreValidator,
|
||||
} from "#game-implementations/types";
|
||||
|
||||
@@ -94,9 +91,6 @@ export const IIDXLIKE_PB_RANKING_VALUES: PBRankingValuesFunction<IIDXLikes> = (p
|
||||
tb5: null,
|
||||
});
|
||||
|
||||
export const VF6Calc: ScoreCalculator<GPTStrings["sdvx" | "usc"]> = (scoreData, chart) =>
|
||||
Volforce.calculateVF6(scoreData.score, scoreData.lamp, chart.levelNum);
|
||||
|
||||
export function VF6ToClass(
|
||||
vf: number,
|
||||
): MONGO_SpecificUserGameStats<"sdvx:Single">["classes"]["vfClass"] {
|
||||
@@ -184,23 +178,17 @@ export function VF6ToClass(
|
||||
return "SIENNA_I";
|
||||
}
|
||||
|
||||
export const SDVXLIKE_SCORE_CALCS: GPTScoreCalculators<SDVXLikes> = { VF6: VF6Calc };
|
||||
|
||||
export const SDVXLIKE_NEW_CALCS: GPTNewCalcs<SDVXLikes> = (scoreData, _derivedData, chart) => ({
|
||||
export const SDVXLIKE_SCORE_CALCS: GPTScoreCalcs<SDVXLikes> = (scoreData, _derivedData, chart) => ({
|
||||
VF6: Volforce.calculateVF6(scoreData.score, scoreData.lamp, chart.levelNum),
|
||||
});
|
||||
|
||||
export const SDVXLIKE_PROFILE_CALCS: GPTProfileCalculators<SDVXLikes> = {
|
||||
VF6: ProfileSumBestN("VF6", 50),
|
||||
};
|
||||
|
||||
export const SDVXLIKE_SESSION_CALCS: GPTSessionCalcs<SDVXLikes> = (arr) => {
|
||||
const v = SessionAvgBest10For("VF6")(arr);
|
||||
|
||||
return { ProfileVF6: v !== null ? v * 50 : null };
|
||||
};
|
||||
|
||||
export const SDVXLIKE_NEW_PROFILE_CALCS: GPTNewProfileCalcs<SDVXLikes> = async (
|
||||
export const SDVXLIKE_PROFILE_CALCS: GPTProfileCalcs<SDVXLikes> = async (
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
@@ -262,28 +250,11 @@ export const SDVXLIKE_SCORE_VALIDATORS: Array<ScoreValidator<SDVXLikes>> = [
|
||||
},
|
||||
];
|
||||
|
||||
export const SGLCalc: ScoreCalculator<GPTStrings["bms" | "pms"]> = (scoreData, chart) => {
|
||||
const ecValue = chart.data.sglEC ?? 0;
|
||||
const hcValue = chart.data.sglHC ?? 0;
|
||||
|
||||
switch (scoreData.lamp) {
|
||||
case "FULL COMBO":
|
||||
case "EX HARD CLEAR":
|
||||
case "HARD CLEAR":
|
||||
return Math.max(hcValue, ecValue);
|
||||
case "CLEAR":
|
||||
case "EASY CLEAR":
|
||||
return ecValue;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const SGL_SESSION_CALCS: GPTSessionCalcs<GPTStrings["bms" | "pms"]> = (arr) => ({
|
||||
sieglinde: SessionAvgBest10For("sieglinde")(arr),
|
||||
});
|
||||
|
||||
export const SGL_NEW_PROFILE_CALCS: GPTNewProfileCalcs<GPTStrings["bms" | "pms"]> = async (
|
||||
export const SGL_PROFILE_CALCS: GPTProfileCalcs<GPTStrings["bms" | "pms"]> = async (
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
@@ -291,7 +262,7 @@ export const SGL_NEW_PROFILE_CALCS: GPTNewProfileCalcs<GPTStrings["bms" | "pms"]
|
||||
sieglinde: await ProfileAvgBestN("sieglinde", 20)(game, playtype, userID),
|
||||
});
|
||||
|
||||
export const SGL_NEW_CALCS: GPTNewCalcs<GPTStrings["bms" | "pms"]> = (
|
||||
export const SGL_SCORE_CALCS: GPTScoreCalcs<GPTStrings["bms" | "pms"]> = (
|
||||
scoreData,
|
||||
_derivedData,
|
||||
chart,
|
||||
|
||||
@@ -48,8 +48,10 @@ t.test("Arcaea Implementation", (t) => {
|
||||
t.test("Grade Deriver", (t) => {
|
||||
const f = (score: number, expected: string) =>
|
||||
t.equal(
|
||||
ARCAEA_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingArcaeaSheriruthFTR)
|
||||
.grade,
|
||||
ARCAEA_IMPL.scoreDeriver(
|
||||
dmf(baseMetrics, { score }) as any,
|
||||
TestingArcaeaSheriruthFTR,
|
||||
).grade,
|
||||
expected,
|
||||
`A score of ${score.toLocaleString()} should result in grade=${expected}.`,
|
||||
);
|
||||
@@ -67,7 +69,11 @@ t.test("Arcaea Implementation", (t) => {
|
||||
|
||||
t.test("Rating Calc", (t) => {
|
||||
t.equal(
|
||||
ARCAEA_IMPL.scoreCalcs.potential(scoreData, TestingArcaeaSheriruthFTR),
|
||||
ARCAEA_IMPL.scoreCalcs(
|
||||
scoreData,
|
||||
ARCAEA_IMPL.scoreDeriver(scoreData, TestingArcaeaSheriruthFTR),
|
||||
TestingArcaeaSheriruthFTR,
|
||||
).potential,
|
||||
11.99,
|
||||
"Basic potential check",
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ARCAEA_IMPL: GPTServerImplementation<GPTStrings["arcaea"]> = {
|
||||
scoreDeriver: (scoreData, _chart) => ({
|
||||
grade: GetGrade(ARCAEA_GBOUNDARIES, scoreData.score),
|
||||
}),
|
||||
newCalcs: (scoreData, _derivedData, chart) => ({
|
||||
scoreCalcs: (scoreData, _derivedData, chart) => ({
|
||||
potential: Potential.calculate(scoreData.score, chart.levelNum),
|
||||
}),
|
||||
pbRankingValues: (pb) => ({
|
||||
@@ -37,13 +37,10 @@ export const ARCAEA_IMPL: GPTServerImplementation<GPTStrings["arcaea"]> = {
|
||||
tb4: null,
|
||||
tb5: null,
|
||||
}),
|
||||
scoreCalcs: {
|
||||
potential: (scoreData, chart) => Potential.calculate(scoreData.score, chart.levelNum),
|
||||
},
|
||||
sessionCalcs: (arr) => ({
|
||||
naivePotential: SessionAvgBest10For("potential")(arr),
|
||||
}),
|
||||
newProfileCalcs: async (game, playtype, userID) => ({
|
||||
profileCalcs: async (game, playtype, userID) => ({
|
||||
naivePotential: await ProfileAvgBestN("potential", 30)(game, playtype, userID),
|
||||
}),
|
||||
classDerivers: (ratings) => {
|
||||
@@ -71,9 +68,6 @@ export const ARCAEA_IMPL: GPTServerImplementation<GPTStrings["arcaea"]> = {
|
||||
|
||||
return { badge: "BLUE" };
|
||||
},
|
||||
profileCalcs: {
|
||||
naivePotential: ProfileAvgBestN("potential", 30),
|
||||
},
|
||||
goalCriteriaFormatters: {
|
||||
score: GoalFmtScore,
|
||||
},
|
||||
|
||||
@@ -169,16 +169,16 @@ for (const [game, playtype, impl] of [
|
||||
chartData: Partial<ChartDocumentData[BMSPMS]>,
|
||||
expected: any,
|
||||
msg: string,
|
||||
) =>
|
||||
t.equal(
|
||||
impl.scoreCalcs.sieglinde(
|
||||
dmf(mockScore.scoreData, scoreData),
|
||||
// @ts-expect-error zzz
|
||||
dmf(chart, { data: chartData as any }),
|
||||
),
|
||||
) => {
|
||||
const sd = dmf(mockScore.scoreData, scoreData);
|
||||
const ch = dmf(chart, { data: chartData as any });
|
||||
|
||||
return t.equal(
|
||||
impl.scoreCalcs(sd, impl.scoreDeriver(sd, ch), ch).sieglinde,
|
||||
expected,
|
||||
msg,
|
||||
);
|
||||
};
|
||||
|
||||
t.test("null sieglinde", (t) => {
|
||||
f({ lamp: "FAILED" }, {}, 0, "fails should be worth 0");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { GPTServerImplementation, PBMergeFunction } from "#game-implementations/types";
|
||||
|
||||
import { CreatePBMergeFor } from "#game-implementations/utils/pb-merge";
|
||||
import { ProfileAvgBestN } from "#game-implementations/utils/profile-calc";
|
||||
import { type GPTStrings, IIDXLIKE_GBOUNDARIES } from "tachi-common";
|
||||
|
||||
import {
|
||||
@@ -12,10 +11,9 @@ import {
|
||||
IIDXLIKE_SCORE_DERIVER,
|
||||
IIDXLIKE_SCORE_VALIDATORS,
|
||||
IIDXLIKE_VALIDATORS,
|
||||
SGL_NEW_CALCS,
|
||||
SGL_NEW_PROFILE_CALCS,
|
||||
SGL_PROFILE_CALCS,
|
||||
SGL_SCORE_CALCS,
|
||||
SGL_SESSION_CALCS,
|
||||
SGLCalc,
|
||||
} from "./_common";
|
||||
|
||||
const BMS_PMS_MERGERS: Array<PBMergeFunction<GPTStrings["bms" | "pms"]>> = [
|
||||
@@ -40,14 +38,12 @@ const BMS_PMS_MERGERS: Array<PBMergeFunction<GPTStrings["bms" | "pms"]>> = [
|
||||
|
||||
const BMS_IMPL: GPTServerImplementation<GPTStrings["bms" | "pms"]> = {
|
||||
scoreDeriver: IIDXLIKE_SCORE_DERIVER,
|
||||
newCalcs: SGL_NEW_CALCS,
|
||||
scoreCalcs: SGL_SCORE_CALCS,
|
||||
sessionCalcs: SGL_SESSION_CALCS,
|
||||
newProfileCalcs: SGL_NEW_PROFILE_CALCS,
|
||||
profileCalcs: SGL_PROFILE_CALCS,
|
||||
classDerivers: (_ratings) => ({}),
|
||||
chartSpecificValidators: IIDXLIKE_VALIDATORS,
|
||||
pbRankingValues: IIDXLIKE_PB_RANKING_VALUES,
|
||||
scoreCalcs: { sieglinde: SGLCalc },
|
||||
profileCalcs: { sieglinde: ProfileAvgBestN("sieglinde", 20) },
|
||||
goalCriteriaFormatters: {
|
||||
percent: GoalFmtPercent,
|
||||
score: (v) => `Get a score of ${v} on`,
|
||||
|
||||
@@ -42,7 +42,8 @@ t.test("CHUNITHM Implementation", (t) => {
|
||||
t.test("Grade Deriver", (t) => {
|
||||
const f = (score: number, expected: any) =>
|
||||
t.equal(
|
||||
CHUNITHM_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, CHUNITHMBBKKChart).grade,
|
||||
CHUNITHM_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, CHUNITHMBBKKChart)
|
||||
.grade,
|
||||
expected,
|
||||
`A score of ${score.toLocaleString()} should result in grade=${expected}.`,
|
||||
);
|
||||
@@ -67,7 +68,11 @@ t.test("CHUNITHM Implementation", (t) => {
|
||||
|
||||
t.test("Rating Calc", (t) => {
|
||||
t.equal(
|
||||
CHUNITHM_IMPL.scoreCalcs.rating(scoreData, CHUNITHMBBKKChart),
|
||||
CHUNITHM_IMPL.scoreCalcs(
|
||||
scoreData,
|
||||
CHUNITHM_IMPL.scoreDeriver(scoreData, CHUNITHMBBKKChart),
|
||||
CHUNITHMBBKKChart,
|
||||
).rating,
|
||||
4.3,
|
||||
"Basic rating check",
|
||||
);
|
||||
@@ -98,7 +103,7 @@ t.test("CHUNITHM Implementation", (t) => {
|
||||
t.test("Floating-point edge case", async (t) => {
|
||||
await mockPBs(Array(50).fill(17.15));
|
||||
|
||||
t.equal(await CHUNITHM_IMPL.profileCalcs.naiveRating("chunithm", "Single", 1), 17.15);
|
||||
t.equal((await CHUNITHM_IMPL.profileCalcs("chunithm", "Single", 1)).naiveRating, 17.15);
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -106,7 +111,7 @@ t.test("CHUNITHM Implementation", (t) => {
|
||||
t.test("Profile with fewer than 50 scores", async (t) => {
|
||||
await mockPBs([16, 16, 16, 16]);
|
||||
|
||||
t.equal(await CHUNITHM_IMPL.profileCalcs.naiveRating("chunithm", "Single", 1), 1.28);
|
||||
t.equal((await CHUNITHM_IMPL.profileCalcs("chunithm", "Single", 1)).naiveRating, 1.28);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ export const CHUNITHM_IMPL: GPTServerImplementation<"chunithm:Single"> = {
|
||||
scoreDeriver: (scoreData, _chart) => ({
|
||||
grade: GetGrade(CHUNITHM_GBOUNDARIES, scoreData.score),
|
||||
}),
|
||||
newCalcs: (scoreData, _derivedData, chart) => ({
|
||||
scoreCalcs: (scoreData, _derivedData, chart) => ({
|
||||
rating: CHUNITHMRating.calculate(scoreData.score, chart.levelNum),
|
||||
}),
|
||||
pbRankingValues: (pb) => ({
|
||||
@@ -25,13 +25,10 @@ export const CHUNITHM_IMPL: GPTServerImplementation<"chunithm:Single"> = {
|
||||
tb4: null,
|
||||
tb5: null,
|
||||
}),
|
||||
scoreCalcs: {
|
||||
rating: (scoreData, chart) => CHUNITHMRating.calculate(scoreData.score, chart.levelNum),
|
||||
},
|
||||
sessionCalcs: (arr) => ({
|
||||
naiveRating: SessionAvgBest10For("rating")(arr),
|
||||
}),
|
||||
newProfileCalcs: async (game, playtype, userID) => ({
|
||||
profileCalcs: async (game, playtype, userID) => ({
|
||||
naiveRating: await ProfileAvgBestN("rating", 50, false, 100)(game, playtype, userID),
|
||||
}),
|
||||
classDerivers: (ratings) => {
|
||||
@@ -75,7 +72,6 @@ export const CHUNITHM_IMPL: GPTServerImplementation<"chunithm:Single"> = {
|
||||
|
||||
return { colour: "BLUE" };
|
||||
},
|
||||
profileCalcs: { naiveRating: ProfileAvgBestN("rating", 50, false, 100) },
|
||||
goalCriteriaFormatters: {
|
||||
score: GoalFmtScore,
|
||||
},
|
||||
|
||||
@@ -25,7 +25,8 @@ t.test("DDR Implementation", (t) => {
|
||||
t.test("Grade", (t) => {
|
||||
const f = (score: number, expected: any) =>
|
||||
t.equal(
|
||||
impl.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingDDRSP as any).grade,
|
||||
impl.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingDDRSP as any)
|
||||
.grade,
|
||||
expected,
|
||||
`A score of ${score} should result in grade=${expected}.`,
|
||||
);
|
||||
@@ -119,15 +120,16 @@ t.test("DDR Implementation", (t) => {
|
||||
chartData: Partial<ChartDocumentData["ddr:DP" | "ddr:SP"]>,
|
||||
expected: any,
|
||||
msg: string,
|
||||
) =>
|
||||
t.equal(
|
||||
impl.scoreCalcs.flareSkill(
|
||||
dmf(TestingDDRSPScore.scoreData, scoreData),
|
||||
dmf(TestingDDRSP, { data: chartData as any }) as any,
|
||||
),
|
||||
) => {
|
||||
const sd = dmf(TestingDDRSPScore.scoreData, scoreData);
|
||||
const ch = dmf(TestingDDRSP, { data: chartData as any }) as any;
|
||||
|
||||
return t.equal(
|
||||
impl.scoreCalcs(sd, impl.scoreDeriver(sd, ch), ch).flareSkill,
|
||||
expected,
|
||||
msg,
|
||||
);
|
||||
};
|
||||
|
||||
f(
|
||||
{ grade: "AA", lamp: "CLEAR", optional: { flare: "II", enumIndexes: {} } },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
GPTGoalFormatters,
|
||||
GPTGoalProgressFormatters,
|
||||
GPTNewProfileCalcs,
|
||||
GPTProfileCalcs,
|
||||
GPTServerImplementation,
|
||||
ScoreValidator,
|
||||
} from "#game-implementations/types";
|
||||
@@ -11,15 +11,12 @@ import { DDRFlare } from "rg-stats";
|
||||
import {
|
||||
DDR_GBOUNDARIES,
|
||||
FmtNum,
|
||||
type GameGroup,
|
||||
GetGrade,
|
||||
GetSpecificGPTConfig,
|
||||
type integer,
|
||||
type MONGO_ChartDocument,
|
||||
type MONGO_PBScoreDocument,
|
||||
type MONGO_ScoreDocument,
|
||||
type MONGO_SongDocument,
|
||||
type Playtype,
|
||||
} from "tachi-common";
|
||||
|
||||
import { IsNullish } from "../../utils/misc";
|
||||
@@ -155,11 +152,7 @@ export const DDR_SCORE_VALIDATORS: Array<ScoreValidator<"ddr:DP" | "ddr:SP">> =
|
||||
},
|
||||
];
|
||||
|
||||
const DDR_CALCULATE_FLARE_SKILL: GPTNewProfileCalcs<"ddr:DP" | "ddr:SP"> = async (
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
) => {
|
||||
const DDR_PROFILE_CALCS: GPTProfileCalcs<"ddr:DP" | "ddr:SP"> = async (game, playtype, userID) => {
|
||||
const sc: Array<PBScoreDocumentWithSong> = await MONGODB_KILL["personal-bests"].aggregate([
|
||||
{
|
||||
$match: {
|
||||
@@ -315,7 +308,7 @@ export const DDR_IMPL: GPTServerImplementation<"ddr:DP" | "ddr:SP"> = {
|
||||
scoreDeriver: (scoreData, _chart) => ({
|
||||
grade: scoreData.lamp === "FAILED" ? "E" : GetGrade(DDR_GBOUNDARIES, scoreData.score),
|
||||
}),
|
||||
newCalcs: (scoreData, _derivedData, chart) => {
|
||||
scoreCalcs: (scoreData, _derivedData, chart) => {
|
||||
if (scoreData.lamp === "FAILED") {
|
||||
return { flareSkill: 0 };
|
||||
}
|
||||
@@ -354,27 +347,6 @@ export const DDR_IMPL: GPTServerImplementation<"ddr:DP" | "ddr:SP"> = {
|
||||
sessionCalcs: (arr) => ({
|
||||
flareSkill: SessionAvgBest10For("flareSkill")(arr),
|
||||
}),
|
||||
newProfileCalcs: DDR_CALCULATE_FLARE_SKILL,
|
||||
profileCalcs: {
|
||||
flareSkill: async (game: GameGroup, playtype: Playtype, userID: integer) =>
|
||||
(await DDR_CALCULATE_FLARE_SKILL(game as "ddr", playtype as "DP" | "SP", userID))
|
||||
.flareSkill,
|
||||
},
|
||||
scoreCalcs: {
|
||||
flareSkill: (scoreData, chart) => {
|
||||
// No flare if the song is failed
|
||||
if (scoreData.lamp === "FAILED") {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const flareLevel = scoreData.optional.flare
|
||||
? GetSpecificGPTConfig("ddr:SP").optionalMetrics.flare.values.indexOf(
|
||||
scoreData.optional.flare,
|
||||
)
|
||||
: 0;
|
||||
|
||||
return DDRFlare.calculate(chart.levelNum, flareLevel);
|
||||
},
|
||||
},
|
||||
profileCalcs: DDR_PROFILE_CALCS,
|
||||
scoreValidators: DDR_SCORE_VALIDATORS,
|
||||
};
|
||||
|
||||
@@ -46,8 +46,10 @@ for (const [playtype, impl] of [
|
||||
t.test("Grade Deriver", (t) => {
|
||||
const f = (percent: number, expected: any) =>
|
||||
t.equal(
|
||||
impl.scoreDeriver(dmf(baseMetrics, { percent }) as any, TestingGitadoraChart as any)
|
||||
.grade,
|
||||
impl.scoreDeriver(
|
||||
dmf(baseMetrics, { percent }) as any,
|
||||
TestingGitadoraChart as any,
|
||||
).grade,
|
||||
expected,
|
||||
`A percent of ${percent}% should result in grade=${expected}.`,
|
||||
);
|
||||
@@ -76,12 +78,16 @@ for (const [playtype, impl] of [
|
||||
modifant: Partial<ScoreData<GPTStrings["gitadora"]>>,
|
||||
expected: any,
|
||||
msg: string,
|
||||
) =>
|
||||
t.equal(
|
||||
impl.scoreCalcs.skill(dmf(scoreData, modifant), TestingGitadoraChart as any),
|
||||
) => {
|
||||
const sd = dmf(scoreData, modifant);
|
||||
const ch = TestingGitadoraChart as any;
|
||||
|
||||
return t.equal(
|
||||
impl.scoreCalcs(sd, impl.scoreDeriver(sd, ch), ch).skill,
|
||||
expected,
|
||||
msg,
|
||||
);
|
||||
};
|
||||
|
||||
f({ percent: 76.57 }, 72.74, "Basic Skill Check");
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const GITADORA_IMPL: GPTServerImplementation<"gitadora:Dora" | "gitadora:Gita">
|
||||
scoreDeriver: (scoreData, _chart) => ({
|
||||
grade: GetGrade(GITADORA_GBOUNDARIES, scoreData.percent),
|
||||
}),
|
||||
newCalcs: (scoreData, _derivedData, chart) => ({
|
||||
scoreCalcs: (scoreData, _derivedData, chart) => ({
|
||||
skill: GITADORASkill.calculate(scoreData.percent, chart.levelNum),
|
||||
}),
|
||||
pbRankingValues: (pb) => ({
|
||||
@@ -25,13 +25,10 @@ const GITADORA_IMPL: GPTServerImplementation<"gitadora:Dora" | "gitadora:Gita">
|
||||
tb4: null,
|
||||
tb5: null,
|
||||
}),
|
||||
scoreCalcs: {
|
||||
skill: (scoreData, chart) => GITADORASkill.calculate(scoreData.percent, chart.levelNum),
|
||||
},
|
||||
sessionCalcs: (arr) => ({
|
||||
skill: SessionAvgBest10For("skill")(arr),
|
||||
}),
|
||||
newProfileCalcs: async (game, playtype, userID) => ({
|
||||
profileCalcs: async (game, playtype, userID) => ({
|
||||
naiveSkill: await ProfileSumBestN("skill", 50)(game, playtype, userID),
|
||||
}),
|
||||
classDerivers: (ratings) => {
|
||||
@@ -77,7 +74,6 @@ const GITADORA_IMPL: GPTServerImplementation<"gitadora:Dora" | "gitadora:Gita">
|
||||
|
||||
return { colour: "WHITE" };
|
||||
},
|
||||
profileCalcs: { naiveSkill: ProfileSumBestN("skill", 50) },
|
||||
goalCriteriaFormatters: {
|
||||
percent: GoalFmtPercent,
|
||||
},
|
||||
|
||||
@@ -39,7 +39,8 @@ t.test("IIDX Implementation", (t) => {
|
||||
t.test("Percent", (t) => {
|
||||
const f = (modifant: Partial<typeof baseMetrics>, expected: any, msg: string) =>
|
||||
t.equal(
|
||||
impl.scoreDeriver(dmf(baseMetrics, modifant) as any, Testing511SPA as any).percent,
|
||||
impl.scoreDeriver(dmf(baseMetrics, modifant) as any, Testing511SPA as any)
|
||||
.percent,
|
||||
expected,
|
||||
msg,
|
||||
);
|
||||
@@ -144,15 +145,16 @@ t.test("IIDX Implementation", (t) => {
|
||||
chartData: Partial<ChartDocumentData["iidx:DP" | "iidx:DP"]>,
|
||||
expected: any,
|
||||
msg: string,
|
||||
) =>
|
||||
t.equal(
|
||||
impl.scoreCalcs.BPI(
|
||||
dmf(TestingIIDXSPScore.scoreData, scoreData),
|
||||
dmf(Testing511SPA, { data: chartData as any }) as any,
|
||||
),
|
||||
) => {
|
||||
const sd = dmf(TestingIIDXSPScore.scoreData, scoreData);
|
||||
const ch = dmf(Testing511SPA, { data: chartData as any }) as any;
|
||||
|
||||
return t.equal(
|
||||
impl.scoreCalcs(sd, impl.scoreDeriver(sd, ch), ch).BPI,
|
||||
expected,
|
||||
msg,
|
||||
);
|
||||
};
|
||||
|
||||
f(
|
||||
{ score: 123 },
|
||||
@@ -190,15 +192,16 @@ t.test("IIDX Implementation", (t) => {
|
||||
chartData: Partial<ChartDocumentData["iidx:SP"]>,
|
||||
expected: any,
|
||||
msg: string,
|
||||
) =>
|
||||
t.equal(
|
||||
IIDX_SP_IMPL.scoreCalcs.ktLampRating(
|
||||
dmf(TestingIIDXSPScore.scoreData, scoreData),
|
||||
dmf(Testing511SPA, { data: chartData as any }),
|
||||
),
|
||||
) => {
|
||||
const sd = dmf(TestingIIDXSPScore.scoreData, scoreData);
|
||||
const ch = dmf(Testing511SPA, { data: chartData as any });
|
||||
|
||||
return t.equal(
|
||||
IIDX_SP_IMPL.scoreCalcs(sd, IIDX_SP_IMPL.scoreDeriver(sd, ch), ch).ktLampRating,
|
||||
expected,
|
||||
msg,
|
||||
);
|
||||
};
|
||||
|
||||
f({ lamp: "FAILED" }, {}, 0, "fails should be worth 0");
|
||||
f({ lamp: "ASSIST CLEAR" }, {}, 0, "ac should be worth 0");
|
||||
@@ -260,15 +263,16 @@ t.test("IIDX Implementation", (t) => {
|
||||
chartData: Partial<ChartDocumentData["iidx:DP"]>,
|
||||
expected: any,
|
||||
msg: string,
|
||||
) =>
|
||||
t.equal(
|
||||
IIDX_DP_IMPL.scoreCalcs.ktLampRating(
|
||||
dmf(TestingIIDXSPScore.scoreData, scoreData),
|
||||
dmf(Testing511SPA as any, { data: chartData as any }),
|
||||
),
|
||||
) => {
|
||||
const sd = dmf(TestingIIDXSPScore.scoreData, scoreData);
|
||||
const ch = dmf(Testing511SPA as any, { data: chartData as any });
|
||||
|
||||
return t.equal(
|
||||
IIDX_DP_IMPL.scoreCalcs(sd, IIDX_DP_IMPL.scoreDeriver(sd, ch), ch).ktLampRating,
|
||||
expected,
|
||||
msg,
|
||||
);
|
||||
};
|
||||
|
||||
f({ lamp: "FAILED" }, {}, 0, "fails should be worth 0");
|
||||
f({ lamp: "ASSIST CLEAR" }, {}, 0, "ac should be worth 0");
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type {
|
||||
GPTGoalFormatters,
|
||||
GPTGoalProgressFormatters,
|
||||
GPTNewProfileCalcs,
|
||||
GPTProfileCalcs,
|
||||
GPTServerImplementation,
|
||||
GPTSessionCalcs,
|
||||
PBMergeFunction,
|
||||
ScoreCalculator,
|
||||
ScoreValidator,
|
||||
} from "#game-implementations/types";
|
||||
|
||||
@@ -26,31 +25,12 @@ import {
|
||||
IIDXLIKE_VALIDATORS,
|
||||
} from "./_common";
|
||||
|
||||
const BPICalc: ScoreCalculator<GPTStrings["iidx"]> = (scoreData, chart) => {
|
||||
if (chart.data.kaidenAverage === null || chart.data.worldRecord === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return PoyashiBPI.calculate(
|
||||
scoreData.score,
|
||||
chart.data.kaidenAverage,
|
||||
chart.data.worldRecord,
|
||||
chart.data.notecount * 2,
|
||||
chart.data.bpiCoefficient,
|
||||
);
|
||||
};
|
||||
|
||||
const IIDX_SESSION_CALCS: GPTSessionCalcs<"iidx:DP" | "iidx:SP"> = (arr) => ({
|
||||
BPI: SessionAvgBest10For("BPI")(arr),
|
||||
ktLampRating: SessionAvgBest10For("ktLampRating")(arr),
|
||||
});
|
||||
|
||||
const IIDX_PROFILE_CALCS: GPTServerImplementation<"iidx:DP" | "iidx:SP">["profileCalcs"] = {
|
||||
BPI: ProfileAvgBestN("BPI", 20, true),
|
||||
ktLampRating: ProfileAvgBestN("ktLampRating", 20),
|
||||
};
|
||||
|
||||
const IIDX_NEW_PROFILE_CALCS: GPTNewProfileCalcs<"iidx:DP" | "iidx:SP"> = async (
|
||||
const IIDX_PROFILE_CALCS: GPTProfileCalcs<"iidx:DP" | "iidx:SP"> = async (
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
@@ -132,7 +112,7 @@ export const IIDX_SP_IMPL: GPTServerImplementation<"iidx:SP"> = {
|
||||
scoreDeriver: IIDXLIKE_SCORE_DERIVER,
|
||||
chartSpecificValidators: IIDXLIKE_VALIDATORS,
|
||||
pbRankingValues: IIDXLIKE_PB_RANKING_VALUES,
|
||||
newCalcs: (scoreData, _derivedData, chart) => {
|
||||
scoreCalcs: (scoreData, _derivedData, chart) => {
|
||||
const bpi =
|
||||
chart.data.kaidenAverage === null || chart.data.worldRecord === null
|
||||
? null
|
||||
@@ -173,35 +153,9 @@ export const IIDX_SP_IMPL: GPTServerImplementation<"iidx:SP"> = {
|
||||
|
||||
return { BPI: bpi, ktLampRating };
|
||||
},
|
||||
scoreCalcs: {
|
||||
BPI: BPICalc,
|
||||
ktLampRating: (scoreData, chart) => {
|
||||
// if chart has no ncValue, use the rating of the chart instead.
|
||||
const ncValue = chart.data.ncTier?.value ?? chart.levelNum;
|
||||
|
||||
// if hc < nc, hcValue = ncValue
|
||||
// this means you can't lose score from getting a better clear.
|
||||
// same for exhc < hc.
|
||||
const hcValue = Math.max(chart.data.hcTier?.value ?? 0, ncValue);
|
||||
const exhcValue = Math.max(chart.data.exhcTier?.value ?? 0, hcValue);
|
||||
|
||||
switch (scoreData.lamp) {
|
||||
case "FULL COMBO":
|
||||
case "EX HARD CLEAR":
|
||||
return exhcValue;
|
||||
case "HARD CLEAR":
|
||||
return hcValue;
|
||||
case "CLEAR":
|
||||
return ncValue;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
sessionCalcs: IIDX_SESSION_CALCS,
|
||||
newProfileCalcs: IIDX_NEW_PROFILE_CALCS,
|
||||
classDerivers: (_ratings) => ({}),
|
||||
profileCalcs: IIDX_PROFILE_CALCS,
|
||||
classDerivers: (_ratings) => ({}),
|
||||
goalCriteriaFormatters: IIDX_GOAL_FMT,
|
||||
goalProgressFormatters: IIDX_GOAL_PG_FMT,
|
||||
goalOutOfFormatters: IIDX_GOAL_OO_FMT,
|
||||
@@ -214,7 +168,7 @@ export const IIDX_DP_IMPL: GPTServerImplementation<"iidx:DP"> = {
|
||||
scoreDeriver: IIDXLIKE_SCORE_DERIVER,
|
||||
chartSpecificValidators: IIDXLIKE_VALIDATORS,
|
||||
pbRankingValues: IIDXLIKE_PB_RANKING_VALUES,
|
||||
newCalcs: (scoreData, _derivedData, chart) => {
|
||||
scoreCalcs: (scoreData, _derivedData, chart) => {
|
||||
const bpi =
|
||||
chart.data.kaidenAverage === null || chart.data.worldRecord === null
|
||||
? null
|
||||
@@ -246,28 +200,9 @@ export const IIDX_DP_IMPL: GPTServerImplementation<"iidx:DP"> = {
|
||||
|
||||
return { BPI: bpi, ktLampRating };
|
||||
},
|
||||
scoreCalcs: {
|
||||
BPI: BPICalc,
|
||||
ktLampRating: (scoreData, chart) => {
|
||||
// if chart has no tier, use the rating of the chart instead.
|
||||
const ecValue = chart.data.dpTier?.value ?? chart.levelNum;
|
||||
|
||||
switch (scoreData.lamp) {
|
||||
case "FULL COMBO":
|
||||
case "EX HARD CLEAR":
|
||||
case "HARD CLEAR":
|
||||
case "CLEAR":
|
||||
case "EASY CLEAR":
|
||||
return ecValue;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
sessionCalcs: IIDX_SESSION_CALCS,
|
||||
newProfileCalcs: IIDX_NEW_PROFILE_CALCS,
|
||||
classDerivers: (_ratings) => ({}),
|
||||
profileCalcs: IIDX_PROFILE_CALCS,
|
||||
classDerivers: (_ratings) => ({}),
|
||||
goalCriteriaFormatters: IIDX_GOAL_FMT,
|
||||
goalProgressFormatters: IIDX_GOAL_PG_FMT,
|
||||
goalOutOfFormatters: IIDX_GOAL_OO_FMT,
|
||||
|
||||
@@ -19,7 +19,7 @@ export const ITG_STAMINA_IMPL: GPTServerImplementation<"itg:Stamina"> = {
|
||||
grade:
|
||||
scoreData.lamp === "FAILED" ? "F" : GetGrade(ITG_GBOUNDARIES, scoreData.scorePercent),
|
||||
}),
|
||||
newCalcs: (scoreData, _derivedData, chart) => {
|
||||
scoreCalcs: (scoreData, _derivedData, chart) => {
|
||||
const blockRating = scoreData.lamp === "FAILED" ? null : chart.data.rankedLevel;
|
||||
|
||||
const diedAtMeasure =
|
||||
@@ -46,47 +46,10 @@ export const ITG_STAMINA_IMPL: GPTServerImplementation<"itg:Stamina"> = {
|
||||
tb4: null,
|
||||
tb5: null,
|
||||
}),
|
||||
scoreCalcs: {
|
||||
blockRating: (scoreData, chart) => {
|
||||
if (scoreData.lamp === "FAILED") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return chart.data.rankedLevel;
|
||||
},
|
||||
fastest32: (scoreData, chart) => {
|
||||
const diedAtMeasure =
|
||||
scoreData.lamp === "FAILED"
|
||||
? (scoreData.survivedPercent / 100) * chart.data.notesPerMeasure.length
|
||||
: null;
|
||||
|
||||
const fastest32 = ITGHighestUnbroken.calculateFromNPSPerMeasure(
|
||||
chart.data.npsPerMeasure,
|
||||
chart.data.notesPerMeasure,
|
||||
diedAtMeasure,
|
||||
32, // 32 measures is generally peoples go-to.
|
||||
);
|
||||
|
||||
if (fastest32 === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// To avoid confusing players, we reject highest 32s less than
|
||||
// 100bpm. Due to how highest32 is calculated, it correctly comes
|
||||
// to the confusing conclusion that sometimes you technically just hit
|
||||
// 32 unbroken measures at like 14 BPM. This is confusing to end users,
|
||||
// so we should hide it.
|
||||
if (fastest32 < 100) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fastest32;
|
||||
},
|
||||
},
|
||||
sessionCalcs: (arr) => ({
|
||||
blockRating: SessionAvgBestNFor("blockRating", 5)(arr),
|
||||
}),
|
||||
newProfileCalcs: async (game, playtype, userID) => {
|
||||
profileCalcs: async (game, playtype, userID) => {
|
||||
const [highestBlock, fastest32] = await Promise.all([
|
||||
ProfileSumBestN("blockRating", 1, true)(game, playtype, userID),
|
||||
ProfileSumBestN("fastest32", 1, true)(game, playtype, userID),
|
||||
@@ -95,12 +58,6 @@ export const ITG_STAMINA_IMPL: GPTServerImplementation<"itg:Stamina"> = {
|
||||
return { highestBlock, fastest32 };
|
||||
},
|
||||
classDerivers: (_ratings) => ({}),
|
||||
profileCalcs: {
|
||||
// the sum of your 1 best blockrating/fastest32 is basically just
|
||||
// picking your best blockrating/fastest32. neat.
|
||||
highestBlock: ProfileSumBestN("blockRating", 1, true),
|
||||
fastest32: ProfileSumBestN("fastest32", 1, true),
|
||||
},
|
||||
goalCriteriaFormatters: {
|
||||
survivedPercent: (val) => `Survive ${NumToDP(val)}% through`,
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ t.test("Jubeat Implementation", (t) => {
|
||||
t.test("Grade Deriver", (t) => {
|
||||
const f = (score: number, expected: any) =>
|
||||
t.equal(
|
||||
JUBEAT_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingJubeatChart).grade,
|
||||
JUBEAT_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingJubeatChart)
|
||||
.grade,
|
||||
expected,
|
||||
`A score of ${score.toLocaleString()} should result in grade=${expected}.`,
|
||||
);
|
||||
@@ -116,7 +117,11 @@ t.test("Jubeat Implementation", (t) => {
|
||||
|
||||
t.test("Jubility Calc", (t) => {
|
||||
t.equal(
|
||||
JUBEAT_IMPL.scoreCalcs.jubility(scoreData, TestingJubeatChart),
|
||||
JUBEAT_IMPL.scoreCalcs(
|
||||
scoreData,
|
||||
JUBEAT_IMPL.scoreDeriver(scoreData, TestingJubeatChart),
|
||||
TestingJubeatChart,
|
||||
).jubility,
|
||||
63.7,
|
||||
"Basic Jubility Test",
|
||||
);
|
||||
|
||||
@@ -165,7 +165,7 @@ export const JUBEAT_IMPL: GPTServerImplementation<"jubeat:Single"> = {
|
||||
scoreDeriver: (scoreData, _chart) => ({
|
||||
grade: GetGrade(JUBEAT_GBOUNDARIES, scoreData.score),
|
||||
}),
|
||||
newCalcs: (scoreData, _derivedData, chart) => ({
|
||||
scoreCalcs: (scoreData, _derivedData, chart) => ({
|
||||
jubility: Jubility.calculate(scoreData.score, scoreData.musicRate, chart.levelNum),
|
||||
}),
|
||||
pbRankingValues: (pb) => ({
|
||||
@@ -176,14 +176,10 @@ export const JUBEAT_IMPL: GPTServerImplementation<"jubeat:Single"> = {
|
||||
tb4: null,
|
||||
tb5: null,
|
||||
}),
|
||||
scoreCalcs: {
|
||||
jubility: (scoreData, chart) =>
|
||||
Jubility.calculate(scoreData.score, scoreData.musicRate, chart.levelNum),
|
||||
},
|
||||
sessionCalcs: (arr) => ({
|
||||
jubility: SessionAvgBest10For("jubility")(arr),
|
||||
}),
|
||||
newProfileCalcs: async (game, playtype, userID) => {
|
||||
profileCalcs: async (game, playtype, userID) => {
|
||||
const [jubility, naiveJubility] = await Promise.all([
|
||||
CalculateJubility(game, playtype, userID),
|
||||
ProfileSumBestN("jubility", 60)(game, playtype, userID),
|
||||
@@ -220,10 +216,6 @@ export const JUBEAT_IMPL: GPTServerImplementation<"jubeat:Single"> = {
|
||||
|
||||
return { colour: "BLACK" };
|
||||
},
|
||||
profileCalcs: {
|
||||
jubility: CalculateJubility,
|
||||
naiveJubility: ProfileSumBestN("jubility", 60),
|
||||
},
|
||||
goalCriteriaFormatters: {
|
||||
musicRate: (v) => `Get a music rate of ${v.toFixed(1)}% on`,
|
||||
score: GoalFmtScore,
|
||||
|
||||
@@ -58,7 +58,8 @@ t.test("maimai Implementation", (t) => {
|
||||
t.test("Grade Deriver", (t) => {
|
||||
const f = (percent: number, expected: string) =>
|
||||
t.equal(
|
||||
MAIMAI_IMPL.scoreDeriver(dmf(baseMetrics, { percent }) as any, TestingMaimaiChart).grade,
|
||||
MAIMAI_IMPL.scoreDeriver(dmf(baseMetrics, { percent }) as any, TestingMaimaiChart)
|
||||
.grade,
|
||||
expected,
|
||||
`A percent of ${percent} should result in grade=${expected}.`,
|
||||
);
|
||||
@@ -83,7 +84,11 @@ t.test("maimai Implementation", (t) => {
|
||||
|
||||
t.test("Score Calcs", (t) => {
|
||||
t.equal(
|
||||
MAIMAI_IMPL.scoreCalcs.rate(scoreData, TestingMaimaiChart),
|
||||
MAIMAI_IMPL.scoreCalcs(
|
||||
scoreData,
|
||||
MAIMAI_IMPL.scoreDeriver(scoreData, TestingMaimaiChart),
|
||||
TestingMaimaiChart,
|
||||
).rate,
|
||||
14.85,
|
||||
"Basic rate check",
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ export const MAIMAI_IMPL: GPTServerImplementation<"maimai:Single"> = {
|
||||
? "SSS+"
|
||||
: GetGrade(MAIMAI_GBOUNDARIES, scoreData.percent),
|
||||
}),
|
||||
newCalcs: (scoreData, _derivedData, chart) => ({
|
||||
scoreCalcs: (scoreData, _derivedData, chart) => ({
|
||||
rate: MaimaiRate.calculate(scoreData.percent, chart.data.maxPercent, chart.levelNum),
|
||||
}),
|
||||
pbRankingValues: (pb) => ({
|
||||
@@ -40,14 +40,10 @@ export const MAIMAI_IMPL: GPTServerImplementation<"maimai:Single"> = {
|
||||
tb4: null,
|
||||
tb5: null,
|
||||
}),
|
||||
scoreCalcs: {
|
||||
rate: (scoreData, chart) =>
|
||||
MaimaiRate.calculate(scoreData.percent, chart.data.maxPercent, chart.levelNum),
|
||||
},
|
||||
sessionCalcs: (arr) => ({
|
||||
rate: SessionAvgBest10For("rate")(arr),
|
||||
}),
|
||||
newProfileCalcs: async (game, playtype, userID) => ({
|
||||
profileCalcs: async (game, playtype, userID) => ({
|
||||
naiveRate: await ProfileAvgBestN("rate", 30)(game, playtype, userID),
|
||||
}),
|
||||
classDerivers: (ratings) => {
|
||||
@@ -79,9 +75,6 @@ export const MAIMAI_IMPL: GPTServerImplementation<"maimai:Single"> = {
|
||||
|
||||
return { colour: "WHITE" };
|
||||
},
|
||||
profileCalcs: {
|
||||
naiveRate: ProfileAvgBestN("rate", 30),
|
||||
},
|
||||
goalCriteriaFormatters: {
|
||||
percent: GoalFmtPercent,
|
||||
},
|
||||
|
||||
@@ -38,8 +38,10 @@ t.test("Maimai DX Implementation", (t) => {
|
||||
t.test("Grade Deriver", (t) => {
|
||||
const f = (percent: number, expected: any) =>
|
||||
t.equal(
|
||||
MAIMAIDX_IMPL.scoreDeriver(dmf(baseMetrics, { percent }) as any, TestingMaimaiDXChart)
|
||||
.grade,
|
||||
MAIMAIDX_IMPL.scoreDeriver(
|
||||
dmf(baseMetrics, { percent }) as any,
|
||||
TestingMaimaiDXChart,
|
||||
).grade,
|
||||
expected,
|
||||
`A percent of ${percent} should result in grade=${expected}.`,
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export const MAIMAIDX_IMPL: GPTServerImplementation<"maimaidx:Single"> = {
|
||||
scoreDeriver: (scoreData, _chart) => ({
|
||||
grade: GetGrade(MAIMAIDX_GBOUNDARIES, scoreData.percent),
|
||||
}),
|
||||
newCalcs: (scoreData, _derivedData, chart) => ({
|
||||
scoreCalcs: (scoreData, _derivedData, chart) => ({
|
||||
rate: MaimaiDXRate.calculate(scoreData.percent, chart.levelNum, scoreData.lamp),
|
||||
}),
|
||||
pbRankingValues: (pb) => ({
|
||||
@@ -25,14 +25,10 @@ export const MAIMAIDX_IMPL: GPTServerImplementation<"maimaidx:Single"> = {
|
||||
tb4: null,
|
||||
tb5: null,
|
||||
}),
|
||||
scoreCalcs: {
|
||||
rate: (scoreData, chart) =>
|
||||
MaimaiDXRate.calculate(scoreData.percent, chart.levelNum, scoreData.lamp),
|
||||
},
|
||||
sessionCalcs: (arr) => ({
|
||||
rate: SessionAvgBest10For("rate")(arr),
|
||||
}),
|
||||
newProfileCalcs: async (game, playtype, userID) => ({
|
||||
profileCalcs: async (game, playtype, userID) => ({
|
||||
naiveRate: await ProfileSumBestN("rate", 50)(game, playtype, userID),
|
||||
}),
|
||||
classDerivers: (ratings) => {
|
||||
@@ -66,9 +62,6 @@ export const MAIMAIDX_IMPL: GPTServerImplementation<"maimaidx:Single"> = {
|
||||
|
||||
return { colour: "WHITE" };
|
||||
},
|
||||
profileCalcs: {
|
||||
naiveRate: ProfileSumBestN("rate", 50),
|
||||
},
|
||||
goalCriteriaFormatters: {
|
||||
percent: (v) => GoalFmtPercent(v, 4),
|
||||
},
|
||||
|
||||
@@ -33,7 +33,8 @@ t.test("MUSECA Implementation", (t) => {
|
||||
t.test("Grade Deriver", (t) => {
|
||||
const f = (score: number, expected: any) =>
|
||||
t.equal(
|
||||
MUSECA_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingMusecaChart).grade,
|
||||
MUSECA_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingMusecaChart)
|
||||
.grade,
|
||||
expected,
|
||||
`A score of ${score.toLocaleString()} should result in grade=${expected}.`,
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ export const MUSECA_IMPL: GPTServerImplementation<"museca:Single"> = {
|
||||
scoreDeriver: (scoreData, _chart) => ({
|
||||
grade: GetGrade(MUSECA_GBOUNDARIES, scoreData.score),
|
||||
}),
|
||||
newCalcs: (scoreData, _derivedData, chart) => ({
|
||||
scoreCalcs: (scoreData, _derivedData, chart) => ({
|
||||
curatorSkill: CuratorSkill.calculate(scoreData.score, chart.levelNum),
|
||||
}),
|
||||
pbRankingValues: (pb) => ({
|
||||
@@ -24,19 +24,13 @@ export const MUSECA_IMPL: GPTServerImplementation<"museca:Single"> = {
|
||||
tb4: null,
|
||||
tb5: null,
|
||||
}),
|
||||
scoreCalcs: {
|
||||
curatorSkill: (scoreData, chart) => CuratorSkill.calculate(scoreData.score, chart.levelNum),
|
||||
},
|
||||
sessionCalcs: (arr) => ({
|
||||
curatorSkill: SessionAvgBest10For("curatorSkill")(arr),
|
||||
}),
|
||||
newProfileCalcs: async (game, playtype, userID) => ({
|
||||
profileCalcs: async (game, playtype, userID) => ({
|
||||
curatorSkill: await ProfileSumBestN("curatorSkill", 20)(game, playtype, userID),
|
||||
}),
|
||||
classDerivers: (_ratings) => ({}),
|
||||
profileCalcs: {
|
||||
curatorSkill: ProfileSumBestN("curatorSkill", 20),
|
||||
},
|
||||
goalCriteriaFormatters: {
|
||||
score: GoalFmtScore,
|
||||
},
|
||||
|
||||
@@ -48,7 +48,8 @@ t.test("ONGEKI Implementation", (t: any) => {
|
||||
t.test("Grade Deriver", (t: any) => {
|
||||
const f = (score: number, expected: string) =>
|
||||
t.equal(
|
||||
ONGEKI_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingOngekiChart).grade,
|
||||
ONGEKI_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingOngekiChart)
|
||||
.grade,
|
||||
expected,
|
||||
`A score of ${score.toLocaleString()} should result in grade=${expected}.`,
|
||||
);
|
||||
@@ -94,20 +95,15 @@ t.test("ONGEKI Implementation", (t: any) => {
|
||||
});
|
||||
|
||||
t.test("Rating Calc", (t: any) => {
|
||||
t.equal(
|
||||
ONGEKI_IMPL.scoreCalcs.rating(scoreData, TestingOngekiChart),
|
||||
12.1,
|
||||
"Basic classic rating check",
|
||||
);
|
||||
const derived = ONGEKI_IMPL.scoreDeriver(scoreData, TestingOngekiChart);
|
||||
const calcs = ONGEKI_IMPL.scoreCalcs(scoreData, derived, TestingOngekiChart);
|
||||
|
||||
t.equal(calcs.rating, 12.1, "Basic classic rating check");
|
||||
|
||||
t.equal(calcs.scoreRating, 12.1, "Basic score rating check");
|
||||
|
||||
t.equal(
|
||||
ONGEKI_IMPL.scoreCalcs.scoreRating(scoreData, TestingOngekiChart),
|
||||
12.1,
|
||||
"Basic score rating check",
|
||||
);
|
||||
|
||||
t.equal(
|
||||
ONGEKI_IMPL.scoreCalcs.starRating(scoreData, TestingOngekiChart),
|
||||
calcs.starRating,
|
||||
Math.floor(scoreData.platinumStars * (TestingOngekiChart.levelNum * 10) ** 2) /
|
||||
100000.0,
|
||||
"Basic star rating check",
|
||||
@@ -152,16 +148,16 @@ t.test("ONGEKI Implementation", (t: any) => {
|
||||
await mockPBs(Array(45).fill(16.27), "rating");
|
||||
await mockPBs(Array(60).fill(16.271), "scoreRating");
|
||||
|
||||
t.equal(await ONGEKI_IMPL.profileCalcs.naiveRating("ongeki", "Single", 1), 16.27);
|
||||
t.equal((await ONGEKI_IMPL.profileCalcs("ongeki", "Single", 1)).naiveRating, 16.27);
|
||||
t.equal(
|
||||
await ONGEKI_IMPL.profileCalcs.naiveRatingRefresh("ongeki", "Single", 1),
|
||||
(await ONGEKI_IMPL.profileCalcs("ongeki", "Single", 1)).naiveRatingRefresh,
|
||||
19.525,
|
||||
);
|
||||
|
||||
await mockPBs([1, 1, 0, 0], "starRating");
|
||||
|
||||
t.equal(
|
||||
await ONGEKI_IMPL.profileCalcs.naiveRatingRefresh("ongeki", "Single", 1),
|
||||
(await ONGEKI_IMPL.profileCalcs("ongeki", "Single", 1)).naiveRatingRefresh,
|
||||
19.565,
|
||||
);
|
||||
|
||||
@@ -172,16 +168,16 @@ t.test("ONGEKI Implementation", (t: any) => {
|
||||
await mockPBs([16, 16, 16, 16], "rating");
|
||||
await mockPBs([16, 16, 16, 16], "scoreRating");
|
||||
|
||||
t.equal(await ONGEKI_IMPL.profileCalcs.naiveRating("ongeki", "Single", 1), 1.42);
|
||||
t.equal((await ONGEKI_IMPL.profileCalcs("ongeki", "Single", 1)).naiveRating, 1.42);
|
||||
t.equal(
|
||||
await ONGEKI_IMPL.profileCalcs.naiveRatingRefresh("ongeki", "Single", 1),
|
||||
(await ONGEKI_IMPL.profileCalcs("ongeki", "Single", 1)).naiveRatingRefresh,
|
||||
1.279,
|
||||
);
|
||||
|
||||
await mockPBs([1, 1, 0, 0], "starRating");
|
||||
|
||||
t.equal(
|
||||
await ONGEKI_IMPL.profileCalcs.naiveRatingRefresh("ongeki", "Single", 1),
|
||||
(await ONGEKI_IMPL.profileCalcs("ongeki", "Single", 1)).naiveRatingRefresh,
|
||||
1.319,
|
||||
);
|
||||
|
||||
@@ -191,7 +187,10 @@ t.test("ONGEKI Implementation", (t: any) => {
|
||||
t.test("Profile with few scores #2", async (t: any) => {
|
||||
await mockPBs([1, 1, 0, 0], "starRating");
|
||||
|
||||
t.equal(await ONGEKI_IMPL.profileCalcs.naiveRatingRefresh("ongeki", "Single", 1), 0.04);
|
||||
t.equal(
|
||||
(await ONGEKI_IMPL.profileCalcs("ongeki", "Single", 1)).naiveRatingRefresh,
|
||||
0.04,
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -9,12 +9,9 @@ import {
|
||||
FmtNum,
|
||||
FmtStars,
|
||||
FmtStarsCompact,
|
||||
type GameGroup,
|
||||
GetGrade,
|
||||
type integer,
|
||||
type MONGO_ChartDocument,
|
||||
ONGEKI_GBOUNDARIES,
|
||||
type Playtype,
|
||||
} from "tachi-common";
|
||||
|
||||
import { GoalFmtScore, GoalOutOfFmtScore, GradeGoalFormatter } from "./_common";
|
||||
@@ -67,7 +64,7 @@ export const ONGEKI_IMPL: GPTServerImplementation<"ongeki:Single"> = {
|
||||
grade: GetGrade(ONGEKI_GBOUNDARIES, scoreData.score),
|
||||
platinumStars: starCount(scoreData.platinumScore, chart.data.maxPlatScore),
|
||||
}),
|
||||
newCalcs: (scoreData, derivedData, chart) => {
|
||||
scoreCalcs: (scoreData, derivedData, chart) => {
|
||||
if (isUnranked(chart)) {
|
||||
return { rating: 0, scoreRating: 0, starRating: 0 };
|
||||
}
|
||||
@@ -91,32 +88,12 @@ export const ONGEKI_IMPL: GPTServerImplementation<"ongeki:Single"> = {
|
||||
tb4: null,
|
||||
tb5: null,
|
||||
}),
|
||||
scoreCalcs: {
|
||||
rating: (scoreData, chart) =>
|
||||
isUnranked(chart) ? 0 : ONGEKIRating.calculate(scoreData.score, chart.levelNum),
|
||||
scoreRating: (scoreData, chart) =>
|
||||
isUnranked(chart)
|
||||
? 0
|
||||
: ONGEKIRating.calculateRefresh(
|
||||
chart.levelNum,
|
||||
scoreData.score,
|
||||
scoreData.score === 1010000 ? "ALL BREAK+" : scoreData.noteLamp,
|
||||
scoreData.bellLamp === "FULL BELL",
|
||||
),
|
||||
starRating: (scoreData, chart) =>
|
||||
isUnranked(chart)
|
||||
? 0
|
||||
: ONGEKIRating.calculatePlatinum(
|
||||
chart.levelNum,
|
||||
starCount(scoreData.platinumScore, chart.data.maxPlatScore),
|
||||
),
|
||||
},
|
||||
sessionCalcs: (arr) => ({
|
||||
naiveRating: SessionAvgBest10For("rating")(arr),
|
||||
naiveScoreRating: SessionAvgBest10For("scoreRating")(arr),
|
||||
starRating: SessionAvgBest10For("starRating")(arr),
|
||||
}),
|
||||
newProfileCalcs: async (game, playtype, userID) => {
|
||||
profileCalcs: async (game, playtype, userID) => {
|
||||
const [naiveRating, score, star] = await Promise.all([
|
||||
ProfileAvgBestN("rating", 45, false, 100)(game, playtype, userID),
|
||||
ProfileAvgBestN("scoreRating", 60, false, 1000)(game, playtype, userID),
|
||||
@@ -162,20 +139,6 @@ export const ONGEKI_IMPL: GPTServerImplementation<"ongeki:Single"> = {
|
||||
|
||||
return { colour: "BLUE" };
|
||||
},
|
||||
profileCalcs: {
|
||||
naiveRating: ProfileAvgBestN("rating", 45, false, 100),
|
||||
naiveRatingRefresh: async (game: GameGroup, playtype: Playtype, userID: integer) => {
|
||||
const [score, star] = await Promise.all([
|
||||
ProfileAvgBestN("scoreRating", 60, false, 1000)(game, playtype, userID),
|
||||
ProfileAvgBestN("starRating", 50, false, 1000)(game, playtype, userID),
|
||||
]);
|
||||
|
||||
const score1k = Math.round((score ?? 0) * 1000);
|
||||
const star1k = Math.round((star ?? 0) * 1000);
|
||||
|
||||
return (Math.floor(score1k * 1.2) + star1k) / 1000.0;
|
||||
},
|
||||
},
|
||||
goalCriteriaFormatters: {
|
||||
score: GoalFmtScore,
|
||||
platinumScore: (val: number) => `Get ${val.toLocaleString("en-GB")} Platinum Score on`,
|
||||
|
||||
@@ -41,7 +41,8 @@ t.test("Pop'n Implementation", (t) => {
|
||||
t.test("Grade Deriver", (t) => {
|
||||
const f = (score: number, expected: any) =>
|
||||
t.equal(
|
||||
POPN_9B_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingPopnChart).grade,
|
||||
POPN_9B_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingPopnChart)
|
||||
.grade,
|
||||
expected,
|
||||
`A score of ${score.toLocaleString()} should result in grade=${expected}.`,
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ export const POPN_9B_IMPL: GPTServerImplementation<"popn:9B"> = {
|
||||
: GetGrade(POPN_GBOUNDARIES, scoreData.score),
|
||||
};
|
||||
},
|
||||
newCalcs: (scoreData, derivedData, chart) => ({
|
||||
scoreCalcs: (scoreData, derivedData, chart) => ({
|
||||
classPoints: PopnClassPoints.calculate(scoreData.score, derivedData.lamp, chart.levelNum),
|
||||
}),
|
||||
pbRankingValues: (pb) => ({
|
||||
@@ -57,18 +57,10 @@ export const POPN_9B_IMPL: GPTServerImplementation<"popn:9B"> = {
|
||||
tb4: null,
|
||||
tb5: null,
|
||||
}),
|
||||
scoreCalcs: {
|
||||
classPoints: (scoreData, chart) =>
|
||||
PopnClassPoints.calculate(
|
||||
scoreData.score,
|
||||
PopnClearMedalToLamp(scoreData.clearMedal),
|
||||
chart.levelNum,
|
||||
),
|
||||
},
|
||||
sessionCalcs: (arr) => ({
|
||||
classPoints: SessionAvgBest10For("classPoints")(arr),
|
||||
}),
|
||||
newProfileCalcs: async (game, playtype, userID) => ({
|
||||
profileCalcs: async (game, playtype, userID) => ({
|
||||
naiveClassPoints: await ProfileAvgBestN("classPoints", 20)(game, playtype, userID),
|
||||
}),
|
||||
classDerivers: (ratings) => {
|
||||
@@ -96,9 +88,6 @@ export const POPN_9B_IMPL: GPTServerImplementation<"popn:9B"> = {
|
||||
|
||||
return { class: "GOD" };
|
||||
},
|
||||
profileCalcs: {
|
||||
naiveClassPoints: ProfileAvgBestN("classPoints", 20),
|
||||
},
|
||||
goalCriteriaFormatters: {
|
||||
score: GoalFmtScore,
|
||||
},
|
||||
|
||||
@@ -49,7 +49,8 @@ t.test("SDVX Implementation", (t) => {
|
||||
t.test("Grade Deriver", (t) => {
|
||||
const f = (score: number, expected: any) =>
|
||||
t.equal(
|
||||
SDVX_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingSDVXAlbidaChart).grade,
|
||||
SDVX_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingSDVXAlbidaChart)
|
||||
.grade,
|
||||
expected,
|
||||
`A score of ${score.toLocaleString()} should result in grade=${expected}.`,
|
||||
);
|
||||
|
||||
@@ -8,20 +8,18 @@ import {
|
||||
SDVXLIKE_GOAL_FMT,
|
||||
SDVXLIKE_GOAL_OO_FMT,
|
||||
SDVXLIKE_GOAL_PG_FMT,
|
||||
SDVXLIKE_NEW_CALCS,
|
||||
SDVXLIKE_SCORE_DERIVER,
|
||||
SDVXLIKE_NEW_PROFILE_CALCS,
|
||||
SDVXLIKE_PROFILE_CALCS,
|
||||
SDVXLIKE_SCORE_CALCS,
|
||||
SDVXLIKE_SCORE_DERIVER,
|
||||
SDVXLIKE_SCORE_VALIDATORS,
|
||||
SDVXLIKE_SESSION_CALCS,
|
||||
} from "./_common";
|
||||
|
||||
export const SDVX_IMPL: GPTServerImplementation<"sdvx:Single"> = {
|
||||
scoreDeriver: SDVXLIKE_SCORE_DERIVER,
|
||||
newCalcs: SDVXLIKE_NEW_CALCS,
|
||||
scoreCalcs: SDVXLIKE_SCORE_CALCS,
|
||||
sessionCalcs: SDVXLIKE_SESSION_CALCS,
|
||||
newProfileCalcs: SDVXLIKE_NEW_PROFILE_CALCS,
|
||||
profileCalcs: SDVXLIKE_PROFILE_CALCS,
|
||||
classDerivers: SDVXLIKE_CLASS_DERIVERS,
|
||||
pbRankingValues: (pb) => ({
|
||||
ranking: pb.scoreData.score,
|
||||
@@ -45,8 +43,6 @@ export const SDVX_IMPL: GPTServerImplementation<"sdvx:Single"> = {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
scoreCalcs: SDVXLIKE_SCORE_CALCS,
|
||||
profileCalcs: SDVXLIKE_PROFILE_CALCS,
|
||||
goalCriteriaFormatters: SDVXLIKE_GOAL_FMT,
|
||||
goalProgressFormatters: SDVXLIKE_GOAL_PG_FMT,
|
||||
goalOutOfFormatters: SDVXLIKE_GOAL_OO_FMT,
|
||||
|
||||
@@ -44,7 +44,8 @@ for (const [playtype, impl] of [
|
||||
t.test("Grade Deriver", (t) => {
|
||||
const f = (score: number, expected: any) =>
|
||||
t.equal(
|
||||
impl.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingUSCChart as any).grade,
|
||||
impl.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingUSCChart as any)
|
||||
.grade,
|
||||
expected,
|
||||
`A score of ${score.toLocaleString()} should result in grade=${expected}.`,
|
||||
);
|
||||
|
||||
@@ -7,21 +7,19 @@ import {
|
||||
SDVXLIKE_GOAL_FMT,
|
||||
SDVXLIKE_GOAL_OO_FMT,
|
||||
SDVXLIKE_GOAL_PG_FMT,
|
||||
SDVXLIKE_NEW_CALCS,
|
||||
SDVXLIKE_SCORE_DERIVER,
|
||||
SDVXLIKE_NEW_PROFILE_CALCS,
|
||||
SDVXLIKE_PB_MERGERS,
|
||||
SDVXLIKE_PROFILE_CALCS,
|
||||
SDVXLIKE_SCORE_CALCS,
|
||||
SDVXLIKE_SCORE_DERIVER,
|
||||
SDVXLIKE_SCORE_VALIDATORS,
|
||||
SDVXLIKE_SESSION_CALCS,
|
||||
} from "./_common";
|
||||
|
||||
const USC_IMPL: GPTServerImplementation<GPTStrings["usc"]> = {
|
||||
scoreDeriver: SDVXLIKE_SCORE_DERIVER,
|
||||
newCalcs: SDVXLIKE_NEW_CALCS,
|
||||
scoreCalcs: SDVXLIKE_SCORE_CALCS,
|
||||
sessionCalcs: SDVXLIKE_SESSION_CALCS,
|
||||
newProfileCalcs: SDVXLIKE_NEW_PROFILE_CALCS,
|
||||
profileCalcs: SDVXLIKE_PROFILE_CALCS,
|
||||
classDerivers: SDVXLIKE_CLASS_DERIVERS,
|
||||
pbRankingValues: (pb) => ({
|
||||
ranking: pb.scoreData.score,
|
||||
@@ -32,8 +30,6 @@ const USC_IMPL: GPTServerImplementation<GPTStrings["usc"]> = {
|
||||
tb5: null,
|
||||
}),
|
||||
chartSpecificValidators: {},
|
||||
scoreCalcs: SDVXLIKE_SCORE_CALCS,
|
||||
profileCalcs: SDVXLIKE_PROFILE_CALCS,
|
||||
goalCriteriaFormatters: SDVXLIKE_GOAL_FMT,
|
||||
goalProgressFormatters: SDVXLIKE_GOAL_PG_FMT,
|
||||
goalOutOfFormatters: SDVXLIKE_GOAL_OO_FMT,
|
||||
|
||||
@@ -41,7 +41,8 @@ t.test("WACCA Implementation", (t) => {
|
||||
t.test("Grade Deriver", (t) => {
|
||||
const f = (score: number, expected: any) =>
|
||||
t.equal(
|
||||
WACCA_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingWaccaPupaExp).grade,
|
||||
WACCA_IMPL.scoreDeriver(dmf(baseMetrics, { score }) as any, TestingWaccaPupaExp)
|
||||
.grade,
|
||||
expected,
|
||||
`A score of ${score.toLocaleString()} should result in grade=${expected}.`,
|
||||
);
|
||||
@@ -65,7 +66,11 @@ t.test("WACCA Implementation", (t) => {
|
||||
|
||||
t.test("Rating Calc", (t) => {
|
||||
t.equal(
|
||||
WACCA_IMPL.scoreCalcs.rate(scoreData, TestingWaccaPupaExp),
|
||||
WACCA_IMPL.scoreCalcs(
|
||||
scoreData,
|
||||
WACCA_IMPL.scoreDeriver(scoreData, TestingWaccaPupaExp),
|
||||
TestingWaccaPupaExp,
|
||||
).rate,
|
||||
41.1,
|
||||
"Basic rating check",
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export const WACCA_IMPL: GPTServerImplementation<"wacca:Single"> = {
|
||||
scoreDeriver: (scoreData, _chart) => ({
|
||||
grade: GetGrade(WACCA_GBOUNDARIES, scoreData.score),
|
||||
}),
|
||||
newCalcs: (scoreData, _derivedData, chart) => ({
|
||||
scoreCalcs: (scoreData, _derivedData, chart) => ({
|
||||
rate: WACCARate.calculate(scoreData.score, chart.levelNum),
|
||||
}),
|
||||
pbRankingValues: (pb) => ({
|
||||
@@ -25,13 +25,10 @@ export const WACCA_IMPL: GPTServerImplementation<"wacca:Single"> = {
|
||||
tb4: null,
|
||||
tb5: null,
|
||||
}),
|
||||
scoreCalcs: {
|
||||
rate: (scoreData, chart) => WACCARate.calculate(scoreData.score, chart.levelNum),
|
||||
},
|
||||
sessionCalcs: (arr) => ({
|
||||
rate: SessionAvgBest10For("rate")(arr),
|
||||
}),
|
||||
newProfileCalcs: async (game, playtype, userID) => ({
|
||||
profileCalcs: async (game, playtype, userID) => ({
|
||||
naiveRate: await ProfileSumBestN("rate", 50)(game, playtype, userID),
|
||||
}),
|
||||
classDerivers: (ratings) => {
|
||||
@@ -61,9 +58,6 @@ export const WACCA_IMPL: GPTServerImplementation<"wacca:Single"> = {
|
||||
|
||||
return { colour: "ASH" };
|
||||
},
|
||||
profileCalcs: {
|
||||
naiveRate: ProfileSumBestN("rate", 50),
|
||||
},
|
||||
goalCriteriaFormatters: {
|
||||
score: GoalFmtScore,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { DryScoreData } from "#lib/score-import/framework/common/types";
|
||||
import type { MONGO_PBScoreDocumentNoRank } from "#lib/score-import/framework/pb/create-pb-doc";
|
||||
import type {
|
||||
ClassConfigs,
|
||||
@@ -34,32 +33,10 @@ interface ChartDependentMax {
|
||||
chartDependentMax: true;
|
||||
}
|
||||
|
||||
export type ScoreCalculator<GPT extends GPTString> = (
|
||||
scoreData: DryScoreData<GPT>,
|
||||
chart: MONGO_ChartDocument<GPT>,
|
||||
) => number | null;
|
||||
|
||||
export type SessionCalculator<GPT extends GPTString> = (
|
||||
scoreCalcData: Array<MONGO_ScoreDocument<GPT>["calculatedData"]>,
|
||||
) => number | null;
|
||||
|
||||
/**
|
||||
* Return a number/null from this players UGPT info. This will involve database queries.
|
||||
*/
|
||||
export type ProfileCalculator<GPT extends GPTString> = (
|
||||
game: GPTStringToGame[GPT],
|
||||
playtype: GPTStringToPlaytype[GPT],
|
||||
userID: integer,
|
||||
) => Promise<number | null>;
|
||||
|
||||
export type GPTScoreCalculators<GPT extends GPTString> = {
|
||||
[S in ScoreRatingAlgorithms[GPT]]: ScoreCalculator<GPT>;
|
||||
};
|
||||
|
||||
export type GPTProfileCalculators<GPT extends GPTString> = {
|
||||
[S in ProfileRatingAlgorithms[GPT]]: ProfileCalculator<GPT>;
|
||||
};
|
||||
|
||||
export type ClassDeriver<GPT extends GPTString, V extends string> = (
|
||||
profileRatings: MONGO_SpecificUserGameStats<GPT>["ratings"],
|
||||
) => V | null | undefined;
|
||||
@@ -114,9 +91,7 @@ export type GPTScoreDeriver<GPT extends GPTString> = (
|
||||
chart: MONGO_ChartDocument<GPT>,
|
||||
) => MongoDerivedMetrics[GPT];
|
||||
|
||||
// New-style score calc; just f(scoreData, derivedData, chart) -> calculatedData
|
||||
// instead of the per-algorithm record above.
|
||||
export type GPTNewCalcs<GPT extends GPTString> = (
|
||||
export type GPTScoreCalcs<GPT extends GPTString> = (
|
||||
scoreData: MONGO_ScoreData<GPT>,
|
||||
derivedData: MongoDerivedMetrics[GPT],
|
||||
chart: MONGO_ChartDocument<GPT>,
|
||||
@@ -127,9 +102,8 @@ export type GPTSessionCalcs<GPT extends GPTString> = (
|
||||
scoreCalcData: Array<MONGO_ScoreDocument<GPT>["calculatedData"]>,
|
||||
) => Record<SessionRatingAlgorithms[GPT], number | null>;
|
||||
|
||||
// New-style profile calc; just f(game, playtype, userID) -> profileCalcData
|
||||
// instead of the per-algorithm record above.
|
||||
export type GPTNewProfileCalcs<GPT extends GPTString> = (
|
||||
/** Profile ratings from UGPT: async f(game, playtype, userID) -> profile ratings record. */
|
||||
export type GPTProfileCalcs<GPT extends GPTString> = (
|
||||
game: GPTStringToGame[GPT],
|
||||
playtype: GPTStringToPlaytype[GPT],
|
||||
userID: integer,
|
||||
@@ -221,13 +195,8 @@ export interface GPTServerImplementation<GPT extends GPTString> {
|
||||
|
||||
/**
|
||||
* How should we compute the score rating algorithms for this game?
|
||||
*
|
||||
* New style, simpler function.
|
||||
*/
|
||||
newCalcs: GPTNewCalcs<GPT>;
|
||||
|
||||
// assorted calculator functions
|
||||
scoreCalcs: GPTScoreCalculators<GPT>;
|
||||
scoreCalcs: GPTScoreCalcs<GPT>;
|
||||
|
||||
/**
|
||||
* How should we compute session ratings for this game?
|
||||
@@ -236,11 +205,8 @@ export interface GPTServerImplementation<GPT extends GPTString> {
|
||||
|
||||
/**
|
||||
* How should we compute profile ratings for this game?
|
||||
*
|
||||
* New style, simpler function.
|
||||
*/
|
||||
newProfileCalcs: GPTNewProfileCalcs<GPT>;
|
||||
profileCalcs: GPTProfileCalculators<GPT>;
|
||||
profileCalcs: GPTProfileCalcs<GPT>;
|
||||
|
||||
/**
|
||||
* For any "derived" classes for this game (i.e. classes that are the function
|
||||
|
||||
@@ -46,10 +46,7 @@ describe("cdn (S3 integration)", () => {
|
||||
it("handles concurrent writes to different keys", async () => {
|
||||
const a = testKey("concurrent-a.txt");
|
||||
const b = testKey("concurrent-b.txt");
|
||||
await Promise.all([
|
||||
CDNStoreOrOverwrite(a, "alpha"),
|
||||
CDNStoreOrOverwrite(b, "beta"),
|
||||
]);
|
||||
await Promise.all([CDNStoreOrOverwrite(a, "alpha"), CDNStoreOrOverwrite(b, "beta")]);
|
||||
expect((await CDNRetrieve(a)).toString("utf8")).toBe("alpha");
|
||||
expect((await CDNRetrieve(b)).toString("utf8")).toBe("beta");
|
||||
await CDNDelete(a);
|
||||
|
||||
@@ -2,7 +2,12 @@ import type { Readable } from "node:stream";
|
||||
|
||||
import { log } from "#lib/log/log";
|
||||
import { ServerConfig } from "#lib/setup/config";
|
||||
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
GetObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { buffer as streamToBuffer } from "node:stream/consumers";
|
||||
|
||||
const saveLoc = ServerConfig.CDN_CONFIG.SAVE_LOCATION;
|
||||
|
||||
@@ -10,23 +10,19 @@ import {
|
||||
/**
|
||||
* Calculate profile ratings for this UGPT.
|
||||
*/
|
||||
export async function CalculateProfileRatings(
|
||||
export function CalculateProfileRatings(
|
||||
game: GameGroup,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
) {
|
||||
const calculatedData: Record<string, number | null> = {};
|
||||
|
||||
): Promise<MONGO_UserGameStats["ratings"]> {
|
||||
const gptString = GetGPTString(game, playtype);
|
||||
|
||||
// Do this in parallel for performance.
|
||||
await Promise.all(
|
||||
Object.entries(GPT_SERVER_IMPLEMENTATIONS[gptString].profileCalcs).map(
|
||||
async ([key, fn]) => {
|
||||
calculatedData[key] = await fn(game, playtype, userID);
|
||||
},
|
||||
),
|
||||
);
|
||||
// Per-GPT `profileCalcs` take `GPTStringToGame<GPT>`; at runtime `game` is correct for `gptString`.
|
||||
const profileCalcs = GPT_SERVER_IMPLEMENTATIONS[gptString].profileCalcs as (
|
||||
game: GameGroup,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
) => Promise<MONGO_UserGameStats["ratings"]>;
|
||||
|
||||
return calculatedData as MONGO_UserGameStats["ratings"];
|
||||
return profileCalcs(game, playtype, userID);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
GetGPTString,
|
||||
type GPTString,
|
||||
type MONGO_ChartDocument,
|
||||
type MONGO_ScoreData,
|
||||
type MongoDerivedMetrics,
|
||||
} from "tachi-common";
|
||||
|
||||
import type { DryScoreData } from "../common/types";
|
||||
@@ -14,16 +16,25 @@ import type { DryScoreData } from "../common/types";
|
||||
*/
|
||||
export function CreateScoreCalcData<GPT extends GPTString>(
|
||||
game: GameGroup,
|
||||
dryScoreData: DryScoreData<GPT>,
|
||||
scoreData: DryScoreData<GPT> | MONGO_ScoreData<GPT>,
|
||||
chart: MONGO_ChartDocument<GPT>,
|
||||
) {
|
||||
const gptString = GetGPTString(game, chart.playtype);
|
||||
const impl = GPT_SERVER_IMPLEMENTATIONS[gptString];
|
||||
|
||||
const calcData: Record<string, number | null> = {};
|
||||
// Union of per-GPT `scoreDeriver` signatures is not callable with generic `GPT`.
|
||||
|
||||
for (const [key, fn] of Object.entries(GPT_SERVER_IMPLEMENTATIONS[gptString].scoreCalcs)) {
|
||||
calcData[key] = fn(dryScoreData, chart);
|
||||
}
|
||||
const derivedData = impl.scoreDeriver(
|
||||
scoreData as any,
|
||||
chart as any,
|
||||
) as MongoDerivedMetrics[GPT];
|
||||
|
||||
return calcData;
|
||||
// Per-GPT `scoreCalcs` take game-specific score/derived types; at runtime inputs match `gptString`.
|
||||
const scoreCalcs = impl.scoreCalcs as unknown as (
|
||||
scoreData: MONGO_ScoreData<GPT>,
|
||||
derivedData: MongoDerivedMetrics[GPT],
|
||||
chart: MONGO_ChartDocument<GPT>,
|
||||
) => Record<string, number | null>;
|
||||
|
||||
return scoreCalcs(scoreData as MONGO_ScoreData<GPT>, derivedData, chart);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { KtLogger } from "#lib/log/log";
|
||||
|
||||
import { GPT_SERVER_IMPLEMENTATIONS } from "#game-implementations/game-implementations";
|
||||
import {
|
||||
GetGPTConfig,
|
||||
|
||||
@@ -31,7 +31,7 @@ export function HydrateScore(
|
||||
|
||||
const scoreData = CreateFullScoreData(gpt, dryScore.scoreData, chart, log);
|
||||
|
||||
const calculatedData = CreateScoreCalcData(dryScore.game, dryScore.scoreData, chart);
|
||||
const calculatedData = CreateScoreCalcData(dryScore.game, scoreData, chart);
|
||||
|
||||
const score: MONGO_ScoreDocument = {
|
||||
...dryScore,
|
||||
|
||||
@@ -48,9 +48,7 @@ export async function ensureTestCdnBucket() {
|
||||
}
|
||||
|
||||
if (bucket !== "tachi-public") {
|
||||
throw new Error(
|
||||
`TACHI_CDN_SAVE_LOCATION_BUCKET must be "tachi-public"; got "${bucket}".`,
|
||||
);
|
||||
throw new Error(`TACHI_CDN_SAVE_LOCATION_BUCKET must be "tachi-public"; got "${bucket}".`);
|
||||
}
|
||||
|
||||
const client = new S3Client({
|
||||
|
||||
Reference in New Issue
Block a user