mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-02 03:48:14 +03:00
Merge pull request #648 from TNG-dev:zkldi/issue-647
Migrate to using RG-Stats for RG Stats Library.
This commit is contained in:
@@ -79,6 +79,7 @@
|
||||
"prudence": "0.9.7",
|
||||
"rate-limit-redis": "2.1.0",
|
||||
"redis": "3.1.2",
|
||||
"rg-stats": "^0.2.0",
|
||||
"rimraf": "3.0.2",
|
||||
"safe-json-stringify": "1.2.0",
|
||||
"seq-logging": "1.1.1",
|
||||
|
||||
Generated
+6
@@ -59,6 +59,7 @@ specifiers:
|
||||
prudence: 0.9.7
|
||||
rate-limit-redis: 2.1.0
|
||||
redis: 3.1.2
|
||||
rg-stats: ^0.2.0
|
||||
rimraf: 3.0.2
|
||||
safe-json-stringify: 1.2.0
|
||||
seq-logging: 1.1.1
|
||||
@@ -104,6 +105,7 @@ dependencies:
|
||||
prudence: 0.9.7
|
||||
rate-limit-redis: 2.1.0
|
||||
redis: 3.1.2
|
||||
rg-stats: 0.2.0
|
||||
rimraf: 3.0.2
|
||||
safe-json-stringify: 1.2.0
|
||||
seq-logging: 1.1.1
|
||||
@@ -4976,6 +4978,10 @@ packages:
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/rg-stats/0.2.0:
|
||||
resolution: {integrity: sha512-d+IFYlryQMZjMYVJgL0vIZCdUpkYhUC9vzfMHOs/HNEeM5jjlVLDyuiru4UJlc67XoqGgby2Mp6IkcFoRXz4Gw==}
|
||||
dev: false
|
||||
|
||||
/rimraf/2.4.5:
|
||||
resolution: {integrity: sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=}
|
||||
hasBin: true
|
||||
|
||||
@@ -11,17 +11,20 @@ import {
|
||||
} from "tachi-common";
|
||||
import { DryScore } from "../common/types";
|
||||
import {
|
||||
CalculateBPI,
|
||||
CalculateCHUNITHMRating,
|
||||
CalculateGITADORASkill,
|
||||
CalculateJubility,
|
||||
CalculateKTLampRatingIIDX,
|
||||
CalculateKTRating,
|
||||
CalculateMFCP,
|
||||
CalculateSieglinde,
|
||||
CalculateVF6,
|
||||
CalculateWACCARate,
|
||||
} from "./stats";
|
||||
import {
|
||||
PoyashiBPI,
|
||||
Volforce,
|
||||
CHUNITHMRating,
|
||||
GITADORASkill,
|
||||
Jubility,
|
||||
PopnClassPoints,
|
||||
WACCARate,
|
||||
} from "rg-stats";
|
||||
|
||||
export async function CreateCalculatedData(
|
||||
dryScore: DryScore,
|
||||
@@ -53,8 +56,8 @@ type CalculatedDataFunctions = {
|
||||
};
|
||||
|
||||
const CalculatedDataFunctions: CalculatedDataFunctions = {
|
||||
"iidx:SP": CalculateDataIIDXSP,
|
||||
"iidx:DP": CalculateDataIIDXDP,
|
||||
"iidx:SP": CalculateDataIIDX,
|
||||
"iidx:DP": CalculateDataIIDX,
|
||||
"sdvx:Single": CalculateDataSDVXorUSC,
|
||||
"popn:9B": CalculateDataPopn,
|
||||
"museca:Single": CalculateDataMuseca,
|
||||
@@ -92,18 +95,18 @@ export async function CalculateDataForGamePT<G extends Game>(
|
||||
|
||||
type CalculatedData<I extends IDStrings> = Required<ScoreDocument<I>["calculatedData"]>;
|
||||
|
||||
function CalculateDataIIDXSP(
|
||||
function CalculateDataIIDX(
|
||||
dryScore: DryScore,
|
||||
chart: ChartDocument<"iidx:SP">,
|
||||
chart: ChartDocument<"iidx:SP" | "iidx:DP">,
|
||||
logger: KtLogger
|
||||
): CalculatedData<"iidx:SP"> {
|
||||
let bpi;
|
||||
|
||||
if (chart.data.kaidenAverage && chart.data.worldRecord) {
|
||||
bpi = CalculateBPI(
|
||||
bpi = PoyashiBPI.calculate(
|
||||
dryScore.scoreData.score,
|
||||
chart.data.kaidenAverage,
|
||||
chart.data.worldRecord,
|
||||
dryScore.scoreData.score,
|
||||
(chart as ChartDocument<"iidx:SP">).data.notecount * 2,
|
||||
chart.data.bpiCoefficient
|
||||
);
|
||||
@@ -115,34 +118,10 @@ function CalculateDataIIDXSP(
|
||||
|
||||
return {
|
||||
BPI: bpi,
|
||||
ktLampRating: CalculateKTLampRatingIIDX(dryScore, "SP", chart as ChartDocument<"iidx:SP">),
|
||||
};
|
||||
}
|
||||
|
||||
function CalculateDataIIDXDP(
|
||||
dryScore: DryScore,
|
||||
chart: ChartDocument<"iidx:SP" | "iidx:DP">,
|
||||
logger: KtLogger
|
||||
): CalculatedData<"iidx:DP"> {
|
||||
let bpi;
|
||||
|
||||
if (chart.data.kaidenAverage && chart.data.worldRecord) {
|
||||
bpi = CalculateBPI(
|
||||
chart.data.kaidenAverage,
|
||||
chart.data.worldRecord,
|
||||
dryScore.scoreData.score,
|
||||
(chart as ChartDocument<"iidx:DP">).data.notecount * 2,
|
||||
chart.data.bpiCoefficient
|
||||
);
|
||||
|
||||
// kesdc = esd === null ? null : CalculateKESDC(BPIData.kesd, esd); disabled
|
||||
} else {
|
||||
bpi = null;
|
||||
}
|
||||
|
||||
return {
|
||||
BPI: bpi,
|
||||
ktLampRating: CalculateKTLampRatingIIDX(dryScore, "DP", chart as ChartDocument<"iidx:DP">),
|
||||
ktLampRating: CalculateKTLampRatingIIDX(
|
||||
dryScore,
|
||||
chart as ChartDocument<"iidx:SP" | "iidx:DP">
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -159,17 +138,13 @@ function CalculateDataSDVXorUSC(
|
||||
return { VF6: null };
|
||||
}
|
||||
|
||||
const VF6 = CalculateVF6(
|
||||
dryScore.scoreData.grade as Grades["sdvx:Single"],
|
||||
const VF6 = Volforce.calculateVF6(
|
||||
dryScore.scoreData.score,
|
||||
dryScore.scoreData.lamp as Lamps["sdvx:Single"],
|
||||
dryScore.scoreData.percent,
|
||||
chart.levelNum,
|
||||
logger
|
||||
chart.levelNum
|
||||
);
|
||||
|
||||
return {
|
||||
VF6,
|
||||
};
|
||||
return { VF6 };
|
||||
}
|
||||
|
||||
function CalculateDataMuseca(
|
||||
@@ -188,7 +163,7 @@ function CalculateDataJubeat(
|
||||
logger: KtLogger
|
||||
): CalculatedData<"jubeat:Single"> {
|
||||
return {
|
||||
jubility: CalculateJubility(
|
||||
jubility: Jubility.calculate(
|
||||
dryScore.scoreData.score,
|
||||
dryScore.scoreData.percent,
|
||||
chart.levelNum
|
||||
@@ -201,7 +176,7 @@ function CalculateDataCHUNITHM(
|
||||
chart: ChartDocument
|
||||
): CalculatedData<"chunithm:Single"> {
|
||||
return {
|
||||
rating: CalculateCHUNITHMRating(dryScore, chart),
|
||||
rating: CHUNITHMRating.calculate(dryScore.scoreData.score, chart.levelNum),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,7 +196,7 @@ function CalculateDataGitadora(
|
||||
chart: ChartDocument
|
||||
): CalculatedData<"gitadora:Gita" | "gitadora:Dora"> {
|
||||
return {
|
||||
skill: CalculateGITADORASkill(dryScore, chart),
|
||||
skill: GITADORASkill.calculate(dryScore.scoreData.percent, chart.levelNum),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -255,32 +230,21 @@ function CalculateDataWACCA(
|
||||
chart: ChartDocument,
|
||||
logger: KtLogger
|
||||
): CalculatedData<"wacca:Single"> {
|
||||
const rate = CalculateWACCARate(dryScore.scoreData.score, chart.levelNum);
|
||||
const rate = WACCARate.calculate(dryScore.scoreData.score, chart.levelNum);
|
||||
|
||||
return { rate };
|
||||
}
|
||||
|
||||
function CalculateDataPopn(
|
||||
dryScore: DryScore,
|
||||
chart: ChartDocument,
|
||||
dryScore: DryScore<"popn:9B">,
|
||||
chart: ChartDocument<"popn:9B">,
|
||||
logger: KtLogger
|
||||
): CalculatedData<"popn:9B"> {
|
||||
const score = dryScore.scoreData.score;
|
||||
const lamp = dryScore.scoreData.lamp;
|
||||
|
||||
if (score <= 50000) {
|
||||
return { classPoints: 0 };
|
||||
}
|
||||
|
||||
let clearBonus = 0;
|
||||
|
||||
if (lamp === "CLEAR" || lamp === "EASY CLEAR") {
|
||||
clearBonus = 3000;
|
||||
} else if (lamp === "FULL COMBO" || lamp === "PERFECT") {
|
||||
clearBonus = 5000;
|
||||
}
|
||||
|
||||
return {
|
||||
classPoints: (10_000 * chart.levelNum + score - 50_000 + clearBonus) / 5440,
|
||||
classPoints: PopnClassPoints.calculate(
|
||||
dryScore.scoreData.score,
|
||||
dryScore.scoreData.lamp,
|
||||
chart.levelNum
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,212 +1,9 @@
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ChartDocument, Difficulties, Lamps, ScoreDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
import { isApproximately } from "test-utils/asserts";
|
||||
import { CHUNITHMBBKKChart, Testing511SPA, TestingIIDXSPDryScore } from "test-utils/test-data";
|
||||
import { Testing511SPA, TestingIIDXSPDryScore } from "test-utils/test-data";
|
||||
import { DryScore } from "../common/types";
|
||||
import {
|
||||
CalculateBPI,
|
||||
CalculateCHUNITHMRating,
|
||||
CalculateGITADORASkill,
|
||||
CalculateKTLampRatingIIDX,
|
||||
CalculateMFCP,
|
||||
CalculateVF6,
|
||||
CalculateWACCARate,
|
||||
} from "./stats";
|
||||
import deepmerge from "deepmerge";
|
||||
|
||||
t.test("#CalculateBPI", (t) => {
|
||||
t.test("AA BPI tests", (t) => {
|
||||
// data accurate as of 14/04/2021
|
||||
const WR_AA = 3650;
|
||||
const KAVG_AA = 3204;
|
||||
const MAX_AA = 1834 * 2;
|
||||
const COEF_AA = 1.25945;
|
||||
|
||||
// AA, the song.
|
||||
function AA_BPI(yourEx: number) {
|
||||
return CalculateBPI(KAVG_AA, WR_AA, yourEx, MAX_AA, COEF_AA);
|
||||
}
|
||||
|
||||
t.equal(AA_BPI(KAVG_AA), 0, "A score of KAVG should be exactly 0BPI");
|
||||
isApproximately(
|
||||
t,
|
||||
AA_BPI(3393),
|
||||
10.02,
|
||||
"A score with 3393 should be approximately 10.02BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AA_BPI(3481),
|
||||
20.09,
|
||||
"A score with 3481 should be approximately 20.09BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AA_BPI(3535),
|
||||
30.01,
|
||||
"A score with 3535 should be approximately 30.01BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AA_BPI(3572),
|
||||
40.18,
|
||||
"A score with 3572 should be approximately 40.18BPI"
|
||||
);
|
||||
isApproximately(t, AA_BPI(3597), 50.1, "A score with 3597 should be approximately 50.1BPI");
|
||||
isApproximately(
|
||||
t,
|
||||
AA_BPI(3615),
|
||||
60.12,
|
||||
"A score with 3615 should be approximately 60.12BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AA_BPI(3628),
|
||||
70.11,
|
||||
"A score with 3628 should be approximately 70.11BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AA_BPI(3638),
|
||||
80.62,
|
||||
"A score with 3638 should be approximately 80.62BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AA_BPI(3645),
|
||||
90.59,
|
||||
"A score with 3645 should be approximately 90.59BPI"
|
||||
);
|
||||
t.equal(AA_BPI(WR_AA), 100, "A score of WR should be exactly 100BPI");
|
||||
|
||||
isApproximately(t, AA_BPI(3041), -5, "A score with 3628 should be approximately -5BPI");
|
||||
isApproximately(
|
||||
t,
|
||||
AA_BPI(2886),
|
||||
-9.99,
|
||||
"A score with 3638 should be approximately -9.99BPI"
|
||||
);
|
||||
|
||||
t.equal(AA_BPI(0), -15, "Excessively bad (vs kavg) scores should cap at -15BPI");
|
||||
|
||||
isApproximately(
|
||||
t,
|
||||
AA_BPI(MAX_AA),
|
||||
244.56,
|
||||
"A score of MAX should be approximately 244.56BPI"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("COLOSSEUM BPI Tests", (t) => {
|
||||
const WR_AFT = 2312;
|
||||
const KAVG_CL = 2131;
|
||||
const MAX_CL = 2318;
|
||||
const COEF_CL = -1;
|
||||
|
||||
function CL_BPI(yourEx: number) {
|
||||
return CalculateBPI(KAVG_CL, WR_AFT, yourEx, MAX_CL, COEF_CL);
|
||||
}
|
||||
|
||||
isApproximately(t, CL_BPI(2307), 79.63);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// This song has both no co-efficient and is an sp11, so it serves
|
||||
// as another interesting test.
|
||||
t.test("Afterimage d'automne BPI tests", (t) => {
|
||||
// data accurate as of 14/04/2021
|
||||
const WR_AFT = 2891;
|
||||
const KAVG_AFT = 2497;
|
||||
const MAX_AFT = 1480 * 2;
|
||||
const COEF_AFT = null;
|
||||
|
||||
function AFT_BPI(yourEx: number) {
|
||||
return CalculateBPI(KAVG_AFT, WR_AFT, yourEx, MAX_AFT, COEF_AFT);
|
||||
}
|
||||
|
||||
t.equal(AFT_BPI(KAVG_AFT), 0, "A score of KAVG should be exactly 0BPI");
|
||||
isApproximately(t, AFT_BPI(2606), 10, "A score with 2606 should be approximately 10BPI");
|
||||
isApproximately(
|
||||
t,
|
||||
AFT_BPI(2675),
|
||||
20.07,
|
||||
"A score with 2675 should be approximately 20.07BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AFT_BPI(2727),
|
||||
30.18,
|
||||
"A score with 2727 should be approximately 30.18BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AFT_BPI(2767),
|
||||
40.12,
|
||||
"A score with 2767 should be approximately 40.12BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AFT_BPI(2799),
|
||||
50.06,
|
||||
"A score with 2799 should be approximately 50.06BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AFT_BPI(2826),
|
||||
60.43,
|
||||
"A score with 2826 should be approximately 60.43BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AFT_BPI(2847),
|
||||
70.3,
|
||||
"A score with 2847 should be approximately 70.3BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AFT_BPI(2865),
|
||||
80.57,
|
||||
"A score with 2865 should be approximately 80.57BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AFT_BPI(2879),
|
||||
90.18,
|
||||
"A score with 2879 should be approximately 90.18BPI"
|
||||
);
|
||||
t.equal(AFT_BPI(WR_AFT), 100, "A score of WR should be exactly 100BPI");
|
||||
|
||||
isApproximately(
|
||||
t,
|
||||
AFT_BPI(2423),
|
||||
-4.99,
|
||||
"A score with 2423 should be approximately -4.99BPI"
|
||||
);
|
||||
isApproximately(
|
||||
t,
|
||||
AFT_BPI(2354),
|
||||
-10.04,
|
||||
"A score with 2354 should be approximately -10.04BPI"
|
||||
);
|
||||
|
||||
t.equal(AFT_BPI(0), -15, "Excessively bad (vs kavg) scores should cap at -15BPI");
|
||||
|
||||
isApproximately(
|
||||
t,
|
||||
AFT_BPI(MAX_AFT),
|
||||
431.57,
|
||||
"A score of MAX should be approximately 431.57BPI"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
import { CalculateKTLampRatingIIDX, CalculateMFCP } from "./stats";
|
||||
|
||||
t.test("#CalculateKTLampRatingIIDX", (t) => {
|
||||
function c(
|
||||
@@ -238,61 +35,61 @@ t.test("#CalculateKTLampRatingIIDX", (t) => {
|
||||
}
|
||||
|
||||
t.equal(
|
||||
CalculateKTLampRatingIIDX(s("CLEAR"), "SP", c(10.5, 10.6, 10.7)),
|
||||
CalculateKTLampRatingIIDX(s("CLEAR"), c(10.5, 10.6, 10.7)),
|
||||
10.5,
|
||||
"Should return NC if the score was NC."
|
||||
);
|
||||
|
||||
t.equal(
|
||||
CalculateKTLampRatingIIDX(s("HARD CLEAR"), "SP", c(10.5, 10.6, 10.7)),
|
||||
CalculateKTLampRatingIIDX(s("HARD CLEAR"), c(10.5, 10.6, 10.7)),
|
||||
10.6,
|
||||
"Should return HC if the score was HC."
|
||||
);
|
||||
|
||||
t.equal(
|
||||
CalculateKTLampRatingIIDX(s("EX HARD CLEAR"), "SP", c(10.5, 10.6, 10.7)),
|
||||
CalculateKTLampRatingIIDX(s("EX HARD CLEAR"), c(10.5, 10.6, 10.7)),
|
||||
10.7,
|
||||
"Should return EXHC if the score was EXHC."
|
||||
);
|
||||
|
||||
t.equal(
|
||||
CalculateKTLampRatingIIDX(s("CLEAR"), "SP", c(null, 10.6, 10.7)),
|
||||
CalculateKTLampRatingIIDX(s("CLEAR"), c(null, 10.6, 10.7)),
|
||||
0,
|
||||
"Should return 0 if the score was NC but no NC was available."
|
||||
);
|
||||
|
||||
t.equal(
|
||||
CalculateKTLampRatingIIDX(s("HARD CLEAR"), "SP", c(null, null, 10.7)),
|
||||
CalculateKTLampRatingIIDX(s("HARD CLEAR"), c(null, null, 10.7)),
|
||||
0,
|
||||
"Should return 0 if the score was HC but no HC or NC was available."
|
||||
);
|
||||
|
||||
t.equal(
|
||||
CalculateKTLampRatingIIDX(s("HARD CLEAR"), "SP", c(10.5, null, 10.7)),
|
||||
CalculateKTLampRatingIIDX(s("HARD CLEAR"), c(10.5, null, 10.7)),
|
||||
10.5,
|
||||
"Should return NC if the score was HC but no HC was available."
|
||||
);
|
||||
|
||||
t.equal(
|
||||
CalculateKTLampRatingIIDX(s("HARD CLEAR"), "SP", c(10.9, 10.5, 10.7)),
|
||||
CalculateKTLampRatingIIDX(s("HARD CLEAR"), c(10.9, 10.5, 10.7)),
|
||||
10.9,
|
||||
"Should return NC if the score was HC but NC was worth more."
|
||||
);
|
||||
|
||||
t.equal(
|
||||
CalculateKTLampRatingIIDX(s("EX HARD CLEAR"), "SP", c(10.9, 10.5, 10.7)),
|
||||
CalculateKTLampRatingIIDX(s("EX HARD CLEAR"), c(10.9, 10.5, 10.7)),
|
||||
10.9,
|
||||
"Should return NC if the score was EXHC but NC was worth more."
|
||||
);
|
||||
|
||||
t.equal(
|
||||
CalculateKTLampRatingIIDX(s("EX HARD CLEAR"), "SP", c(10.4, 10.9, 10.7)),
|
||||
CalculateKTLampRatingIIDX(s("EX HARD CLEAR"), c(10.4, 10.9, 10.7)),
|
||||
10.9,
|
||||
"Should return HC if the score was EXHC but HC was worth more."
|
||||
);
|
||||
|
||||
t.equal(
|
||||
CalculateKTLampRatingIIDX(s("CLEAR"), "SP", c(null, null, null)),
|
||||
CalculateKTLampRatingIIDX(s("CLEAR"), c(null, null, null)),
|
||||
10,
|
||||
"Should return chart level if the chart has no tierlist data."
|
||||
);
|
||||
@@ -300,29 +97,6 @@ t.test("#CalculateKTLampRatingIIDX", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#CalculateGITADORARating", (t) => {
|
||||
function TestGitadoraRating(percent: number, levelNum: number) {
|
||||
return CalculateGITADORASkill(
|
||||
{ scoreData: { percent } } as DryScore,
|
||||
{ levelNum } as ChartDocument
|
||||
);
|
||||
}
|
||||
|
||||
t.equal(
|
||||
TestGitadoraRating(89.48, 3.4),
|
||||
60.84,
|
||||
"Test GITADORA Rating function aligns with game (1)"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
TestGitadoraRating(70.76, 5.8),
|
||||
82.08,
|
||||
"Test GITADORA Rating function aligns with game (2)"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#CalculateMFCP", (t) => {
|
||||
@@ -436,103 +210,6 @@ t.test("#CalculateMFCP", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
const bbkk = CHUNITHMBBKKChart;
|
||||
|
||||
// unit testing a mathematical function is a square-round-hole problem.
|
||||
// that said, we can blast some stuff at it we know to be correct,
|
||||
// and just hope we're right.
|
||||
t.test("#CalculateCHUNITHMRating", (t) => {
|
||||
const chart = deepmerge<ChartDocument>(bbkk, { levelNum: 12.5 });
|
||||
|
||||
// cutoffs
|
||||
t.equal(
|
||||
CalculateCHUNITHMRating({ scoreData: { score: 1_010_000 } } as DryScore, chart),
|
||||
12.5 + 2
|
||||
);
|
||||
t.equal(
|
||||
CalculateCHUNITHMRating({ scoreData: { score: 1_005_000 } } as DryScore, chart),
|
||||
12.5 + 1.5
|
||||
);
|
||||
t.equal(
|
||||
CalculateCHUNITHMRating({ scoreData: { score: 1_000_000 } } as DryScore, chart),
|
||||
12.5 + 1
|
||||
);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 975_000 } } as DryScore, chart), 12.5);
|
||||
t.equal(
|
||||
CalculateCHUNITHMRating({ scoreData: { score: 925_000 } } as DryScore, chart),
|
||||
12.5 - 3
|
||||
);
|
||||
t.equal(
|
||||
CalculateCHUNITHMRating({ scoreData: { score: 900_000 } } as DryScore, chart),
|
||||
12.5 - 5
|
||||
);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 800_000 } } as DryScore, chart), 3.75);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 0 } } as DryScore, chart), 0);
|
||||
|
||||
// inbetweens
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 987_000 } } as DryScore, chart), 12.98);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_008_000 } } as DryScore, chart), 14.5);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_003_000 } } as DryScore, chart), 13.8);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 999_000 } } as DryScore, chart), 13.46);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 980_000 } } as DryScore, chart), 12.7);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 950_000 } } as DryScore, chart), 11);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 920_000 } } as DryScore, chart), 9.1);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 810_000 } } as DryScore, chart), 4.12);
|
||||
t.equal(CalculateCHUNITHMRating({ scoreData: { score: 50_000 } } as DryScore, chart), 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Random assertions plucked from bemaniwiki.
|
||||
t.test("#CalculateVF6", (t) => {
|
||||
t.equal(CalculateVF6("S", "CLEAR", 99, 16, logger), 0.332);
|
||||
|
||||
t.equal(CalculateVF6("AAA+", "ULTIMATE CHAIN", 98, 17, logger), 0.356);
|
||||
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 16, logger), 0.369);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 17, logger), 0.392);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 18, logger), 0.415);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 19, logger), 0.438);
|
||||
t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 20, logger), 0.462);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#CalculateWACCARate", (t) => {
|
||||
// General Tests
|
||||
t.equal(CalculateWACCARate(1_000_000, 10.2), 40.8);
|
||||
t.equal(CalculateWACCARate(990_000, 10.2), 40.8, "990K and 1M should give the same rate.");
|
||||
t.equal(CalculateWACCARate(980_000, 10.7), 40.125);
|
||||
t.equal(
|
||||
CalculateWACCARate(1_000_000, 0),
|
||||
0,
|
||||
"A chart with level of 0 should always be worth 0."
|
||||
);
|
||||
t.equal(
|
||||
CalculateWACCARate(975_000, 10),
|
||||
CalculateWACCARate(970_000, 10),
|
||||
"Should be no difference between 975k and 970k."
|
||||
);
|
||||
t.equal(CalculateWACCARate(960_000, 9), 29.25);
|
||||
t.equal(CalculateWACCARate(950_000, 8), 24);
|
||||
t.equal(CalculateWACCARate(940_000, 8), 22);
|
||||
t.equal(CalculateWACCARate(920_000, 8), 20);
|
||||
t.equal(CalculateWACCARate(900_000, 8), 16);
|
||||
t.equal(CalculateWACCARate(850_000, 8), 12);
|
||||
t.equal(CalculateWACCARate(500, 10), 10);
|
||||
t.equal(CalculateWACCARate(0, 10), 10);
|
||||
t.equal(CalculateWACCARate(849_999, 10), 10);
|
||||
|
||||
// A bunch of assertions from cg505's account
|
||||
t.equal(CalculateWACCARate(990_084, 13.2), 52.8);
|
||||
t.equal(CalculateWACCARate(984_040, 12.9), 48.375);
|
||||
t.equal(CalculateWACCARate(950_326, 12.2), 36.6);
|
||||
t.equal(CalculateWACCARate(997_719, 4), 16);
|
||||
t.equal(CalculateWACCARate(906440, 13.8), 27.6);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.skip("#CalculateKTRating", (t) => {
|
||||
// t.test("Should call the success calculator if percent > pivotPercent", async (t) => {
|
||||
// const r = await CalculateKTRating(
|
||||
|
||||
@@ -12,144 +12,6 @@ import {
|
||||
import { RoundToNDecimalPlaces } from "utils/misc";
|
||||
import { DryScore } from "../common/types";
|
||||
|
||||
/**
|
||||
* Calculates the in-game CHUNITHM rating for a score.
|
||||
*/
|
||||
export function CalculateCHUNITHMRating(dryScore: DryScore, chartData: ChartDocument) {
|
||||
const score = dryScore.scoreData.score;
|
||||
const levelBase = chartData.levelNum * 100;
|
||||
|
||||
let val = 0;
|
||||
|
||||
if (score >= 1_007_500) {
|
||||
val = levelBase + 200;
|
||||
} else if (score >= 1_005_000) {
|
||||
val = levelBase + 150 + ((score - 1_005_000) * 10) / 500;
|
||||
} else if (score >= 1_000_000) {
|
||||
val = levelBase + 100 + ((score - 1_000_000) * 5) / 500;
|
||||
} else if (score >= 975_000) {
|
||||
val = levelBase + ((score - 975_000) * 2) / 500;
|
||||
} else if (score >= 925_000) {
|
||||
val = levelBase - 300 + ((score - 925_000) * 3) / 500;
|
||||
} else if (score >= 900_000) {
|
||||
val = levelBase - 500 + ((score - 900_000) * 4) / 500;
|
||||
} else if (score >= 800_000) {
|
||||
val = (levelBase - 500) / 2 + ((score - 800_000) * ((levelBase - 500) / 2)) / 100_000;
|
||||
}
|
||||
|
||||
return Math.max(Math.floor(val) / 100, 0);
|
||||
}
|
||||
|
||||
export function CalculateJubility(score: number, musicRate: number, level: number) {
|
||||
if (score < 700_000) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return level * 12.5 * (musicRate / 99);
|
||||
}
|
||||
|
||||
export function CalculateWACCARate(score: number, levelNum: number) {
|
||||
let scoreCoef = 1;
|
||||
|
||||
// See https://github.com/TNG-dev/tachi-server/issues/598.
|
||||
// This is genuinely how the game works.
|
||||
if (score >= 990_000) {
|
||||
scoreCoef = 4;
|
||||
} else if (score >= 980_000) {
|
||||
scoreCoef = 3.75;
|
||||
} else if (score >= 970_000) {
|
||||
scoreCoef = 3.5;
|
||||
} else if (score >= 960_000) {
|
||||
scoreCoef = 3.25;
|
||||
} else if (score >= 950_000) {
|
||||
scoreCoef = 3;
|
||||
} else if (score >= 940_000) {
|
||||
scoreCoef = 2.75;
|
||||
} else if (score >= 920_000) {
|
||||
scoreCoef = 2.5;
|
||||
} else if (score >= 900_000) {
|
||||
scoreCoef = 2;
|
||||
} else if (score >= 850_000) {
|
||||
scoreCoef = 1.5;
|
||||
}
|
||||
|
||||
// That's... it??
|
||||
// How discrete. How boring!
|
||||
return RoundToNDecimalPlaces(scoreCoef * levelNum, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the in-game GITADORA rating for a score.
|
||||
*/
|
||||
export function CalculateGITADORASkill(dryScore: DryScore, chartData: ChartDocument) {
|
||||
const trueRating = (dryScore.scoreData.percent / 100) * chartData.levelNum * 20;
|
||||
const flooredRating = Math.floor(trueRating * 100) / 100;
|
||||
return flooredRating;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the PikaGreatFunction, used in BPI. I have no idea what this does.
|
||||
* @returns
|
||||
*/
|
||||
function BPIPikaGreatFn(score: integer, max: integer) {
|
||||
return score === max ? max * 0.8 : 1 + (score / max - 0.5) / (1 - score / max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Oh boy.
|
||||
*
|
||||
* Calculates the "Beat Performance Index" of an IIDX score. This algorithm has many issues,
|
||||
* but is a direct port of Poyashi's implementation for consistencies sake.
|
||||
* https://github.com/potakusan/iidx_score_manager/blob/f21ba6b85fcc0bf8b7ca888fa2239a3951a9c9c2/src/components/bpi/index.tsx#L120
|
||||
*
|
||||
* @param kaidenEx The kaiden average EX score.
|
||||
* @param wrEx The world record's EX score.
|
||||
* @param yourEx Your EX score.
|
||||
* @param max The maximum amount of EX achievable on this chart.
|
||||
* @param powCoef What power the BPI should be raised to. This is arbitrary, and assigned on a per-song basis. Defaults to 1.175.
|
||||
* @returns A number between -15 and 100. Unless your score is better than the world record, in which case
|
||||
* returns can be above 100.
|
||||
*/
|
||||
export function CalculateBPI(
|
||||
kaidenEx: integer,
|
||||
wrEx: integer,
|
||||
yourEx: integer,
|
||||
max: integer,
|
||||
pc: number | null
|
||||
) {
|
||||
let powCoef = pc ?? 1.175;
|
||||
if (powCoef === -1) {
|
||||
powCoef = 1.175;
|
||||
}
|
||||
|
||||
const yourPGF = BPIPikaGreatFn(yourEx, max);
|
||||
const kaidenPGF = BPIPikaGreatFn(kaidenEx, max);
|
||||
const wrPGF = BPIPikaGreatFn(wrEx, max);
|
||||
|
||||
// no idea what these var names are
|
||||
const _s_ = yourPGF / kaidenPGF;
|
||||
const _z_ = wrPGF / kaidenPGF;
|
||||
|
||||
const isBetterThanKavg = yourEx >= kaidenEx;
|
||||
|
||||
// this line of code isn't mine, and that's why it's *really* bad here.
|
||||
const bpi =
|
||||
Math.round(
|
||||
(isBetterThanKavg ? 100 : -100) *
|
||||
Math.pow(
|
||||
(isBetterThanKavg ? Math.log(_s_) : -Math.log(_s_)) / Math.log(_z_),
|
||||
powCoef
|
||||
) *
|
||||
100
|
||||
) / 100;
|
||||
|
||||
if (bpi < -15) {
|
||||
return -15;
|
||||
}
|
||||
|
||||
return bpi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Marvelous Full Combo Points. This algorithm
|
||||
* is used in LIFE4, and described here:
|
||||
@@ -190,63 +52,6 @@ export function CalculateMFCP(dryScore: DryScore, chartData: ChartDocument, logg
|
||||
return null;
|
||||
}
|
||||
|
||||
const VF5GradeCoefficients = {
|
||||
PUC: 1.05,
|
||||
S: 1.05,
|
||||
"AAA+": 1.02,
|
||||
AAA: 1.0,
|
||||
"AA+": 0.97,
|
||||
AA: 0.94,
|
||||
"A+": 0.91, // everything below this point (incl. this) is marked with a (?) in bemaniwiki.
|
||||
A: 0.88,
|
||||
B: 0.85,
|
||||
C: 0.82,
|
||||
D: 0.8,
|
||||
};
|
||||
|
||||
const VF5LampCoefficients = {
|
||||
"PERFECT ULTIMATE CHAIN": 1.1,
|
||||
"ULTIMATE CHAIN": 1.05,
|
||||
"EXCESSIVE CLEAR": 1.02,
|
||||
CLEAR: 1.0,
|
||||
FAILED: 0.5,
|
||||
};
|
||||
|
||||
function FloorToNDP(number: number, dp: integer) {
|
||||
const mul = 10 ** dp;
|
||||
return Math.floor(number * mul) / mul;
|
||||
}
|
||||
|
||||
export function CalculateVF6(
|
||||
grade: Grades["sdvx:Single"],
|
||||
lamp: Lamps["sdvx:Single"],
|
||||
per: number,
|
||||
levelNum: number,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const gradeCoefficient = VF5GradeCoefficients[grade];
|
||||
const lampCoefficient = VF5LampCoefficients[lamp];
|
||||
|
||||
if (!lampCoefficient) {
|
||||
logger.warn(`Invalid lamp of ${lamp} passed to CalculateVF5. Returning null.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!gradeCoefficient) {
|
||||
logger.warn(`Invalid grade of ${grade} passed to CalculateVF5. Returning null.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const percent = per / 100;
|
||||
if (!levelNum || !percent) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const realVF6 = (levelNum * 2 * percent * gradeCoefficient * lampCoefficient) / 100;
|
||||
|
||||
return FloorToNDP(realVF6, 3);
|
||||
}
|
||||
|
||||
interface RatingParameters {
|
||||
failHarshnessMultiplier: number;
|
||||
pivotPercent: number;
|
||||
@@ -351,7 +156,6 @@ export function CalculateKTRating(
|
||||
|
||||
export function CalculateKTLampRatingIIDX(
|
||||
dryScore: DryScore,
|
||||
playtype: Playtypes[Game],
|
||||
chart: ChartDocument<"iidx:SP" | "iidx:DP">
|
||||
) {
|
||||
const ncValue = chart.tierlistInfo["kt-NC"]?.value ?? 0;
|
||||
@@ -359,12 +163,12 @@ export function CalculateKTLampRatingIIDX(
|
||||
const exhcValue = Math.max(chart.tierlistInfo["kt-EXHC"]?.value ?? 0, hcValue);
|
||||
|
||||
if (!exhcValue && !hcValue && !ncValue) {
|
||||
return LampRatingNoTierlistInfo(dryScore, "iidx", playtype, chart);
|
||||
return LampRatingNoTierlistInfo(dryScore, "iidx", chart.playtype, chart);
|
||||
}
|
||||
|
||||
const lamp = dryScore.scoreData.lamp;
|
||||
|
||||
const gptConfig = GetGamePTConfig("iidx", playtype);
|
||||
const gptConfig = GetGamePTConfig("iidx", chart.playtype);
|
||||
|
||||
const lampIndex = gptConfig.lamps.indexOf(lamp);
|
||||
|
||||
@@ -413,58 +217,3 @@ export function CalculateSieglinde(chart: ChartDocument, lampIndex: integer) {
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// deprecated calcs
|
||||
|
||||
// const VF4GradeCoefficients = {
|
||||
// S: 1.0,
|
||||
// "AAA+": 0.99,
|
||||
// AAA: 0.98,
|
||||
// "AA+": 0.97,
|
||||
// AA: 0.96,
|
||||
// "A+": 0.95,
|
||||
// A: 0.94,
|
||||
// B: 0.93,
|
||||
// C: 0.92,
|
||||
// D: 0.91,
|
||||
// };
|
||||
|
||||
// export function CalculateVF4(
|
||||
// grade: Grades["sdvx:Single"],
|
||||
// per: number,
|
||||
// levelNum: number,
|
||||
// logger: KtLogger
|
||||
// ) {
|
||||
// const multiplier = 25;
|
||||
|
||||
// const gradeCoefficient = VF4GradeCoefficients[grade];
|
||||
|
||||
// if (!gradeCoefficient) {
|
||||
// logger.warn(`Invalid grade of ${grade} passed to CalculateVF4. Returning null.`);
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// const percent = per / 100;
|
||||
// if (!levelNum || !percent) {
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
// return Math.floor(multiplier * (levelNum + 1) * percent * gradeCoefficient);
|
||||
// }
|
||||
|
||||
// // VF5 is just VF6 but to three decimal places instead of four.
|
||||
// export function CalculateVF5(
|
||||
// grade: Grades["sdvx:Single"],
|
||||
// lamp: Lamps["sdvx:Single"],
|
||||
// per: number,
|
||||
// levelNum: number,
|
||||
// logger: KtLogger
|
||||
// ) {
|
||||
// const vf6 = CalculateVF6(grade, lamp, per, levelNum, logger);
|
||||
|
||||
// if (vf6 === null) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// return FloorToNDP(vf6, 2);
|
||||
// }
|
||||
|
||||
@@ -2,8 +2,8 @@ import db from "external/mongo/db";
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { FindChartWithChartID } from "utils/queries/charts";
|
||||
import { CalculateVF6 } from "../calculated-data/stats";
|
||||
import { InternalFailure } from "../common/converter-failures";
|
||||
import { Volforce } from "rg-stats";
|
||||
|
||||
export async function IIDXMergeFn(
|
||||
pbDoc: PBScoreDocument<"iidx:SP" | "iidx:DP">,
|
||||
@@ -100,12 +100,10 @@ export async function USCMergeFn(
|
||||
throw new InternalFailure(`Chart ${pbDoc.chartID} disappeared underfoot?`);
|
||||
}
|
||||
|
||||
pbDoc.calculatedData.VF6 = CalculateVF6(
|
||||
pbDoc.scoreData.grade,
|
||||
pbDoc.calculatedData.VF6 = Volforce.calculateVF6(
|
||||
pbDoc.scoreData.score,
|
||||
pbDoc.scoreData.lamp,
|
||||
pbDoc.scoreData.percent,
|
||||
chart.levelNum,
|
||||
logger
|
||||
chart.levelNum
|
||||
);
|
||||
|
||||
return true;
|
||||
@@ -133,12 +131,10 @@ export async function SDVXMergeFn(
|
||||
throw new InternalFailure(`Chart ${pbDoc.chartID} disappeared underfoot?`);
|
||||
}
|
||||
|
||||
pbDoc.calculatedData.VF6 = CalculateVF6(
|
||||
pbDoc.scoreData.grade,
|
||||
pbDoc.calculatedData.VF6 = Volforce.calculateVF6(
|
||||
pbDoc.scoreData.score,
|
||||
pbDoc.scoreData.lamp,
|
||||
pbDoc.scoreData.percent,
|
||||
chart.levelNum,
|
||||
logger
|
||||
chart.levelNum
|
||||
);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -348,7 +348,7 @@ export const TestingSDVXSingleDryScore: DryScore<"sdvx:Single"> = {
|
||||
service: "e-amusement",
|
||||
game: "sdvx",
|
||||
scoreData: {
|
||||
score: 95000000,
|
||||
score: 9_500_000,
|
||||
lamp: "EXCESSIVE CLEAR",
|
||||
percent: 95,
|
||||
grade: "AA+",
|
||||
|
||||
Reference in New Issue
Block a user