More test setup

This commit is contained in:
zkldi
2021-04-14 21:09:42 +01:00
parent 68abbb6ce8
commit 44767c9fc7
12 changed files with 414 additions and 36 deletions
@@ -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
}]
+8
View File
@@ -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"
}