Merge pull request #648 from TNG-dev:zkldi/issue-647

Migrate to using RG-Stats for RG Stats Library.
This commit is contained in:
zkldi
2022-01-31 17:34:01 +00:00
committed by GitHub
7 changed files with 63 additions and 670 deletions
+1
View File
@@ -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",
+6
View File
@@ -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;
+1 -1
View File
@@ -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+",