mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-11 00:38:28 +03:00
More test setup
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import Pr from "prudence";
|
||||
import t from "tap";
|
||||
import { CloseConnection } from "../../../../db/db";
|
||||
import CreateLogCtx from "../../../../logger";
|
||||
import prAssert from "../../../../test-utils/prassert";
|
||||
import {
|
||||
Testing511SPA,
|
||||
TestingDoraChart,
|
||||
TestingGITADORADoraDryScore,
|
||||
TestingIIDXSPDryScore,
|
||||
} from "../../../../test-utils/test-data";
|
||||
import { CreateCalculatedData } from "./calculated-data";
|
||||
|
||||
const mockLogger = CreateLogCtx("calculated-data.test.ts");
|
||||
|
||||
t.test("#CreateCalculatedData", async (t) => {
|
||||
let res = await CreateCalculatedData(TestingIIDXSPDryScore, Testing511SPA, mockLogger);
|
||||
|
||||
prAssert(
|
||||
res,
|
||||
// mock return, we're just checking it returns the right structure, really.
|
||||
{
|
||||
rating: Pr.equalTo(0),
|
||||
lampRating: Pr.equalTo(0),
|
||||
ranking: "null",
|
||||
outOf: "null",
|
||||
gameSpecific: {
|
||||
BPI: "?number",
|
||||
KESDC: "?number",
|
||||
"K%": "?number",
|
||||
},
|
||||
},
|
||||
"Should correctly produce calculatedData"
|
||||
);
|
||||
|
||||
let gitadoraRes = await CreateCalculatedData(
|
||||
TestingGITADORADoraDryScore,
|
||||
TestingDoraChart,
|
||||
mockLogger
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
gitadoraRes,
|
||||
{
|
||||
rating: Pr.isPositiveNonZero,
|
||||
lampRating: Pr.equalTo(0),
|
||||
ranking: "null",
|
||||
outOf: "null",
|
||||
gameSpecific: {},
|
||||
},
|
||||
"Should correctly call rating function overrides for different games"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseConnection);
|
||||
@@ -14,7 +14,6 @@ import { CalculateCHUNITHMRating, CalculateGITADORARating } from "./game-specifi
|
||||
export async function CreateCalculatedData(
|
||||
dryScore: DryScore,
|
||||
chart: ChartDocument,
|
||||
song: SongDocument,
|
||||
logger: Logger
|
||||
): Promise<ScoreDocument["calculatedData"]> {
|
||||
const game = dryScore.game;
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import t from "tap";
|
||||
import { CloseConnection } from "../../../../db/db";
|
||||
import { DryScore } from "../../../../types";
|
||||
import { ChartDocument, Difficulties, Lamps, ScoreDocument } from "kamaitachi-common";
|
||||
import {
|
||||
CalculateBPI,
|
||||
CalculateCHUNITHMRating,
|
||||
CalculateGITADORARating,
|
||||
CalculateKESDC,
|
||||
CalculateMFCP,
|
||||
CalculateVF4,
|
||||
CalculateVF5,
|
||||
KaidenPercentile,
|
||||
} from "./game-specific-stats";
|
||||
import CreateLogCtx from "../../../../logger";
|
||||
|
||||
function isApproximately(number: number, target: number, message: string, lenience = 0.01) {
|
||||
let result = t.ok(Math.abs(number - target) < lenience, message);
|
||||
|
||||
if (!result) {
|
||||
throw new Error(`${number} was not close enough to ${target}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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(AA_BPI(3393), 10.02, "A score with 3393 should be approximately 10.02BPI");
|
||||
isApproximately(AA_BPI(3481), 20.09, "A score with 3481 should be approximately 20.09BPI");
|
||||
isApproximately(AA_BPI(3535), 30.01, "A score with 3535 should be approximately 30.01BPI");
|
||||
isApproximately(AA_BPI(3572), 40.18, "A score with 3572 should be approximately 40.18BPI");
|
||||
isApproximately(AA_BPI(3597), 50.1, "A score with 3597 should be approximately 50.1BPI");
|
||||
isApproximately(AA_BPI(3615), 60.12, "A score with 3615 should be approximately 60.12BPI");
|
||||
isApproximately(AA_BPI(3628), 70.11, "A score with 3628 should be approximately 70.11BPI");
|
||||
isApproximately(AA_BPI(3638), 80.62, "A score with 3638 should be approximately 80.62BPI");
|
||||
isApproximately(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(AA_BPI(3041), -5, "A score with 3628 should be approximately -5BPI");
|
||||
isApproximately(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(AA_BPI(MAX_AA), 244.56, "A score of MAX should be approximately 244.56BPI");
|
||||
|
||||
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(AFT_BPI(2606), 10, "A score with 2606 should be approximately 10BPI");
|
||||
isApproximately(AFT_BPI(2675), 20.07, "A score with 2675 should be approximately 20.07BPI");
|
||||
isApproximately(AFT_BPI(2727), 30.18, "A score with 2727 should be approximately 30.18BPI");
|
||||
isApproximately(AFT_BPI(2767), 40.12, "A score with 2767 should be approximately 40.12BPI");
|
||||
isApproximately(AFT_BPI(2799), 50.06, "A score with 2799 should be approximately 50.06BPI");
|
||||
isApproximately(AFT_BPI(2826), 60.43, "A score with 2826 should be approximately 60.43BPI");
|
||||
isApproximately(AFT_BPI(2847), 70.3, "A score with 2847 should be approximately 70.3BPI");
|
||||
isApproximately(AFT_BPI(2865), 80.57, "A score with 2865 should be approximately 80.57BPI");
|
||||
isApproximately(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(AFT_BPI(2423), -4.99, "A score with 2423 should be approximately -4.99BPI");
|
||||
isApproximately(
|
||||
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(
|
||||
AFT_BPI(MAX_AFT),
|
||||
431.57,
|
||||
"A score of MAX should be approximately 431.57BPI"
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#CalculateGITADORARating", (t) => {
|
||||
function TestGitadoraRating(percent: number, levelNum: number) {
|
||||
return CalculateGITADORARating(
|
||||
{ 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();
|
||||
});
|
||||
|
||||
// this metric is ground to be removed, possibly.
|
||||
t.test("#CalculateKESDC", (t) => {
|
||||
t.equal(CalculateKESDC(10, 4), 37.18460876390943, "Should perform ESDC on two numbers.");
|
||||
t.equal(CalculateKESDC(null, 4), null, "Should return null if kaidenESD is null.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
const mockLogger = CreateLogCtx("game-specific-stats.test.ts");
|
||||
|
||||
t.test("#CalculateMFCP", (t) => {
|
||||
function TestMFCP(
|
||||
lamp: Lamps["ddr:SP" | "ddr:DP"],
|
||||
levelNum: number,
|
||||
difficulty: Difficulties["ddr:DP" | "ddr:SP"]
|
||||
) {
|
||||
return CalculateMFCP(
|
||||
{
|
||||
scoreData: {
|
||||
lamp,
|
||||
},
|
||||
} as ScoreDocument,
|
||||
{
|
||||
difficulty,
|
||||
levelNum,
|
||||
} as ChartDocument,
|
||||
mockLogger
|
||||
);
|
||||
}
|
||||
|
||||
t.equal(TestMFCP("FAILED", 10, "EXPERT"), null, "Should return null for non-mfcs");
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 10, "BASIC"),
|
||||
null,
|
||||
"Should reject charts on BASIC difficulty."
|
||||
);
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 10, "BEGINNER"),
|
||||
null,
|
||||
"Should reject charts on BEGINNER difficulty."
|
||||
);
|
||||
|
||||
t.test("Should return null for charts with level less than 8", (t) => {
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", i, "EXPERT"),
|
||||
null,
|
||||
`Should return null for charts with level ${i}`
|
||||
);
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 8, "EXPERT"),
|
||||
1,
|
||||
"Should return 1 for charts with level 8"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 9, "EXPERT"),
|
||||
1,
|
||||
"Should return 1 for charts with level 9"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 10, "EXPERT"),
|
||||
1,
|
||||
"Should return 1 for charts with level 10"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 11, "EXPERT"),
|
||||
2,
|
||||
"Should return 2 for charts with level 11"
|
||||
);
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 12, "EXPERT"),
|
||||
2,
|
||||
"Should return 2 for charts with level 12"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 13, "EXPERT"),
|
||||
4,
|
||||
"Should return 4 for charts with level 13"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 14, "EXPERT"),
|
||||
8,
|
||||
"Should return 8 for charts with level 14"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", 15, "EXPERT"),
|
||||
15,
|
||||
"Should return 15 for charts with level 15"
|
||||
);
|
||||
|
||||
t.test("Should return 25 for charts with level 16-20", (t) => {
|
||||
for (let i = 16; i <= 20; i++) {
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", i, "EXPERT"),
|
||||
25,
|
||||
`Should return 25 for charts with level ${i}`
|
||||
);
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.equal(
|
||||
TestMFCP("MARVELOUS FULL COMBO", NaN, "EXPERT"),
|
||||
null,
|
||||
"Invalid level triggers failsafe."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseConnection);
|
||||
@@ -133,7 +133,7 @@ export function CalculateKESDC(kaidenESD: number | null, yourESD: number) {
|
||||
* https://life4ddr.com/requirements/#mfcpoints
|
||||
* @returns Null if this score was not eligible, a number otherwise.
|
||||
*/
|
||||
export function CalculateMFCP(dryScore: DryScore, chartData: ChartDocument) {
|
||||
export function CalculateMFCP(dryScore: DryScore, chartData: ChartDocument, logger: Logger) {
|
||||
if (dryScore.scoreData.lamp !== "MARVELOUS FULL COMBO") {
|
||||
return null;
|
||||
}
|
||||
@@ -145,9 +145,9 @@ export function CalculateMFCP(dryScore: DryScore, chartData: ChartDocument) {
|
||||
|
||||
if (chartData.levelNum < 8) {
|
||||
return null;
|
||||
} else if (chartData.levelNum < 10) {
|
||||
} else if (chartData.levelNum <= 10) {
|
||||
return 1;
|
||||
} else if (chartData.levelNum < 12) {
|
||||
} else if (chartData.levelNum <= 12) {
|
||||
return 2;
|
||||
} else if (chartData.levelNum === 13) {
|
||||
return 4;
|
||||
@@ -159,7 +159,11 @@ export function CalculateMFCP(dryScore: DryScore, chartData: ChartDocument) {
|
||||
return 25;
|
||||
}
|
||||
|
||||
// cannot be hit?
|
||||
logger.warn(
|
||||
`Invalid levelNum passed to MFCP ${chartData.levelNum}. ChartID ${chartData.chartID}.`
|
||||
);
|
||||
|
||||
// failsafe
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Pr from "prudence";
|
||||
import t from "tap";
|
||||
import { CloseConnection, ReOpenConnection } from "../../../../db/db";
|
||||
import { CloseConnection } from "../../../../db/db";
|
||||
import CreateLogCtx from "../../../../logger";
|
||||
import prAssert from "../../../../test-utils/prassert";
|
||||
import {
|
||||
@@ -27,11 +27,11 @@ t.test("#CreateGameSpecific", (t) => {
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
BPI: "null",
|
||||
"K%": "null",
|
||||
KESDC: "null",
|
||||
BPI: "?number",
|
||||
"K%": "?number",
|
||||
KESDC: "?number",
|
||||
},
|
||||
"Response should contain nulled keys for IIDX:SP GameSpecifics"
|
||||
"Response should contain keys for IIDX:SP GameSpecifics"
|
||||
);
|
||||
|
||||
t.end();
|
||||
@@ -49,10 +49,10 @@ t.test("#CreateGameSpecific", (t) => {
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
BPI: "null",
|
||||
KESDC: "null",
|
||||
BPI: "?null",
|
||||
KESDC: "?null",
|
||||
},
|
||||
"Response should contain nulled keys for IIDX:DP GameSpecifics"
|
||||
"Response should contain keys for IIDX:DP GameSpecifics"
|
||||
);
|
||||
|
||||
t.end();
|
||||
@@ -73,7 +73,7 @@ t.test("#CreateGameSpecific", (t) => {
|
||||
VF4: Pr.nullable(Pr.isPositiveInteger),
|
||||
VF5: Pr.nullable(Pr.isPositive),
|
||||
},
|
||||
"Response should contain nulled keys for SDVX:Single GameSpecifics"
|
||||
"Response should contain keys for SDVX:Single GameSpecifics"
|
||||
);
|
||||
|
||||
t.end();
|
||||
@@ -91,9 +91,9 @@ t.test("#CreateGameSpecific", (t) => {
|
||||
prAssert(
|
||||
res,
|
||||
{
|
||||
MFCP: "null",
|
||||
MFCP: Pr.nullable(Pr.isPositiveInteger),
|
||||
},
|
||||
"Response should contain nulled keys for DDR:SP GameSpecifics"
|
||||
"Response should contain keys for DDR:SP GameSpecifics"
|
||||
);
|
||||
|
||||
t.end();
|
||||
|
||||
@@ -60,7 +60,7 @@ export async function CreateGameSpecific<G extends Game>(
|
||||
);
|
||||
} else if (game === "ddr") {
|
||||
// either playtype
|
||||
gameSpecific.MFCP = CalculateMFCP(dryScore, chart);
|
||||
gameSpecific.MFCP = CalculateMFCP(dryScore, chart, logger);
|
||||
}
|
||||
|
||||
return gameSpecific;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[{
|
||||
"coef": null,
|
||||
"kavg": 1300,
|
||||
"wr": 1400,
|
||||
"chartID": "c2311194e3897ddb5745b1760d2c0141f933e683",
|
||||
"kesd": 13
|
||||
}]
|
||||
@@ -17,6 +17,14 @@ export const Testing511SPA = JSONFile("./test-data/kamaitachi/iidx-511spa.json")
|
||||
"iidx:SP"
|
||||
>;
|
||||
|
||||
export const TestingDoraChart = JSONFile(
|
||||
"./test-data/kamaitachi/gitadora-ithinkabtyou.json"
|
||||
) as ChartDocument<"gitadora", "Dora", "gitadora:Dora">;
|
||||
|
||||
export const TestingSDVXSingleDryScore = JSONFile(
|
||||
"./test-data/kamaitachi/sdvx-dryscore.json"
|
||||
) as DryScore<"sdvx", "Single", "sdvx:Single">;
|
||||
|
||||
export const TestingGITADORADoraDryScore = JSONFile(
|
||||
"./test-data/kamaitachi/gitadora-dryscore.json"
|
||||
) as DryScore<"gitadora", "Dora", "gitadora:Dora">;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"service": "fake-service",
|
||||
"game": "gitadora",
|
||||
"scoreData": {
|
||||
"score": 70.76,
|
||||
"lamp": "CLEAR",
|
||||
"percent": 70.76,
|
||||
"grade": "B",
|
||||
"esd": 30,
|
||||
"hitData": {},
|
||||
"hitMeta": {}
|
||||
},
|
||||
"scoreMeta": {},
|
||||
"timeAchieved": null,
|
||||
"comment": null,
|
||||
"importType": "any:batch-manual"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": 0,
|
||||
"chartID": "29f0bfab357ba54e3fd0176fb3cbc578c9ec8df5",
|
||||
"difficulty": "BASIC",
|
||||
"playtype": "Dora",
|
||||
"levelNum": 1.6,
|
||||
"level": "1.60",
|
||||
"notedata": {
|
||||
"notecount": 169,
|
||||
"objects": {
|
||||
"LEFT CYMBAL": 54,
|
||||
"HI HAT": 78,
|
||||
"LEFT PEDAL": 24,
|
||||
"SNARE": 1,
|
||||
"HI TOM": 1,
|
||||
"RIGHT PEDAL": 2,
|
||||
"LOW TOM": 4,
|
||||
"FLOOR TOM": 5,
|
||||
"RIGHT CYMBAL": 0
|
||||
}
|
||||
},
|
||||
"length": null,
|
||||
"bpmMin": 117,
|
||||
"bpmMax": 117,
|
||||
"monoBPM": true,
|
||||
"indicators": {},
|
||||
"flags": {
|
||||
"IN BASE GAME": true
|
||||
},
|
||||
"internals": {
|
||||
"inGameID": "0",
|
||||
"inGameINTID": 0
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
{
|
||||
"service": "e-amusement",
|
||||
"game": "sdvx",
|
||||
"game": "iidx",
|
||||
"scoreData": {
|
||||
"score": 95000000,
|
||||
"lamp": "EXCESSIVE CLEAR",
|
||||
"percent": 95,
|
||||
"grade": "AA+",
|
||||
"esd": null,
|
||||
"hitData": {},
|
||||
"score": 786,
|
||||
"lamp": "HARD CLEAR",
|
||||
"percent": 50,
|
||||
"grade": "C",
|
||||
"esd": 30,
|
||||
"hitData": {
|
||||
"pgreat": 50,
|
||||
"great": 10
|
||||
},
|
||||
"hitMeta": {}
|
||||
},
|
||||
"scoreMeta": {},
|
||||
"timeAchieved": null,
|
||||
"comment": null,
|
||||
"importType": "iidx:sdvx-csv"
|
||||
"importType": "iidx:eamusement-csv"
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
{
|
||||
"service": "e-amusement",
|
||||
"game": "iidx",
|
||||
"game": "sdvx",
|
||||
"scoreData": {
|
||||
"score": 786,
|
||||
"lamp": "HARD CLEAR",
|
||||
"percent": 50,
|
||||
"grade": "C",
|
||||
"esd": 30,
|
||||
"hitData": {
|
||||
"pgreat": 50,
|
||||
"great": 10
|
||||
},
|
||||
"score": 95000000,
|
||||
"lamp": "EXCESSIVE CLEAR",
|
||||
"percent": 95,
|
||||
"grade": "AA+",
|
||||
"esd": null,
|
||||
"hitData": {},
|
||||
"hitMeta": {}
|
||||
},
|
||||
"scoreMeta": {},
|
||||
"timeAchieved": null,
|
||||
"comment": null,
|
||||
"importType": "iidx:eamusement-csv"
|
||||
"importType": "sdvx:eamusement-csv"
|
||||
}
|
||||
Reference in New Issue
Block a user