diff --git a/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts b/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts index 108a76ed9..ffd792f4b 100644 --- a/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts +++ b/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts @@ -18,6 +18,7 @@ t.test("#CreateCalculatedData", async (t) => { const res = await CreateCalculatedData(TestingIIDXSPDryScore, Testing511SPA, 30, logger); prAssert( + t, res, { ktLampRating: p.equalTo(10), @@ -34,6 +35,7 @@ t.test("#CreateCalculatedData", async (t) => { ); prAssert( + t, gitadoraRes, { skill: p.isPositiveNonZero, @@ -82,6 +84,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { ktLampRating: "?number", @@ -105,6 +108,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { ktLampRating: "?number", @@ -127,6 +131,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { VF6: p.nullable(p.isPositive), @@ -148,6 +153,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { MFCP: p.nullable(p.isPositiveInteger), @@ -170,6 +176,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { MFCP: "null", @@ -192,6 +199,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { rating: "?number", @@ -213,6 +221,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { ktRating: "?number", @@ -234,6 +243,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { ktRating: "?number", @@ -255,6 +265,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { sieglinde: "?number", @@ -276,6 +287,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { sieglinde: "?number", @@ -297,6 +309,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { skill: "?number", @@ -318,6 +331,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { skill: "?number", @@ -339,6 +353,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { VF6: "?number", @@ -360,6 +375,7 @@ t.test("#CalculateDataForGamePT", (t) => { ); prAssert( + t, res, { VF6: "?number", @@ -380,7 +396,7 @@ t.test("#CalculateDataForGamePT", (t) => { // logger // ); - // prAssert( + // prAssert(t , // res, // { // jubility: "?number", @@ -402,7 +418,7 @@ t.test("#CalculateDataForGamePT", (t) => { // logger // ); - // prAssert(res, {}, "Response should contain nulled keys for popn:9B"); + // prAssert(t ,res, {}, "Response should contain nulled keys for popn:9B"); // t.end(); // }); diff --git a/server/src/lib/score-import/framework/calculated-data/stats.test.ts b/server/src/lib/score-import/framework/calculated-data/stats.test.ts index f7e6760ed..34af47d4f 100644 --- a/server/src/lib/score-import/framework/calculated-data/stats.test.ts +++ b/server/src/lib/score-import/framework/calculated-data/stats.test.ts @@ -29,23 +29,73 @@ t.test("#CalculateBPI", (t) => { } 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"); + 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(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"); + 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(AA_BPI(MAX_AA), 244.56, "A score of MAX should be approximately 244.56BPI"); + isApproximately( + t, + AA_BPI(MAX_AA), + 244.56, + "A score of MAX should be approximately 244.56BPI" + ); t.end(); }); @@ -60,7 +110,7 @@ t.test("#CalculateBPI", (t) => { return CalculateBPI(KAVG_CL, WR_AFT, yourEx, MAX_CL, COEF_CL); } - isApproximately(CL_BPI(2307), 79.63); + isApproximately(t, CL_BPI(2307), 79.63); t.end(); }); @@ -79,19 +129,65 @@ t.test("#CalculateBPI", (t) => { } 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"); + 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(AFT_BPI(2423), -4.99, "A score with 2423 should be approximately -4.99BPI"); 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" @@ -100,6 +196,7 @@ t.test("#CalculateBPI", (t) => { 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" diff --git a/server/src/lib/score-import/framework/common/score-utils.test.ts b/server/src/lib/score-import/framework/common/score-utils.test.ts index dab0021ea..f20d51306 100644 --- a/server/src/lib/score-import/framework/common/score-utils.test.ts +++ b/server/src/lib/score-import/framework/common/score-utils.test.ts @@ -1,5 +1,8 @@ +import { ChartDocument, Game } from "tachi-common"; import t from "tap"; -import { GetGradeFromPercent } from "./score-utils"; +import { isApproximately } from "test-utils/asserts"; +import { Testing511SPA } from "test-utils/test-data"; +import { GenericCalculatePercent, GetGradeFromPercent, ValidatePercent } from "./score-utils"; t.test("#GetGradeForPercent", (t) => { t.equal( @@ -31,3 +34,68 @@ t.test("#GetGradeForPercent", (t) => { t.end(); }); + +t.test("#GenericCalculatePercent", (t) => { + const f = (game: Game, score: number, equal: number) => + isApproximately(t, GenericCalculatePercent(game, score), equal); + + for (const game of ["iidx", "bms", "pms"] as const) { + isApproximately(t, GenericCalculatePercent(game, 1240, Testing511SPA), 78.88); + isApproximately(t, GenericCalculatePercent(game, 0, Testing511SPA), 0); + } + + f("popn", 100_000, 100); + f("popn", 50_000, 50); + f("popn", 0, 0); + + f("maimai", 10, 10); + f("maimai", 101.12, 101.12); + + for (const game of ["usc", "sdvx"] as const) { + f(game, 10_000_000, 100); + f(game, 5_000_000, 50); + f(game, 9_950_000, 99.5); + f(game, 1_370_000, 13.7); + f(game, 0, 0); + } + + for (const game of ["ddr", "museca", "chunithm", "wacca"] as const) { + f(game, 1_000_000, 100); + f(game, 500_000, 50); + f(game, 993_121, 99.3121); + f(game, 1_000, 0.1); + } + + t.throws(() => GenericCalculatePercent("unknown_game" as Game, 100), { + message: /Invalid game/u, + }); + t.throws(() => GenericCalculatePercent("iidx", 100), { + message: /No Chart passed/u, + }); + + t.end(); +}); + +t.test("#ValidatePercent", (t) => { + for (const playtype of ["SP", "DP"] as const) { + t.doesNotThrow(() => ValidatePercent("iidx", playtype, 90, Testing511SPA)); + t.doesNotThrow(() => ValidatePercent("iidx", playtype, 0, Testing511SPA)); + t.doesNotThrow(() => ValidatePercent("iidx", playtype, 100, Testing511SPA)); + + t.throws(() => ValidatePercent("iidx", playtype, -50, Testing511SPA)); + t.throws(() => ValidatePercent("iidx", playtype, 101, Testing511SPA)); + } + + const m = (maxPercent: number) => ({ data: { maxPercent } } as ChartDocument); + + t.doesNotThrow(() => ValidatePercent("maimai", "Single", 90, m(100))); + t.doesNotThrow(() => ValidatePercent("maimai", "Single", 100, m(100))); + t.doesNotThrow(() => ValidatePercent("maimai", "Single", 0, m(100))); + t.doesNotThrow(() => ValidatePercent("maimai", "Single", 110, m(120))); + t.doesNotThrow(() => ValidatePercent("maimai", "Single", 120, m(120))); + t.throws(() => ValidatePercent("maimai", "Single", 130, m(120)), { + message: /expected a number less than 120/u, + }); + + t.end(); +}); diff --git a/server/src/lib/score-import/framework/common/score-utils.ts b/server/src/lib/score-import/framework/common/score-utils.ts index 7a2315421..9a86b3d0e 100644 --- a/server/src/lib/score-import/framework/common/score-utils.ts +++ b/server/src/lib/score-import/framework/common/score-utils.ts @@ -58,23 +58,33 @@ export function GenericCalculatePercent(game: Game, score: number, chart?: Chart return (score / 10_000_000) * 100; case "popn": return (score / 100_000) * 100; - case "gitadora": case "maimai": + // score in maimai is actually just your percent, since nobody cares about + // money score. return score; case "bms": + case "pms": case "iidx": if (!chart) { - logger.severe("No Chart passed to GenericCalcPercent but game was iidx/bms."); + logger.severe("No Chart passed to GenericCalcPercent but game was iidx/bms/pms."); throw new InternalFailure( - "No Chart passed to GenericCalcPercent but game was iidx/bms." + "No Chart passed to GenericCalcPercent but game was iidx/bms/pms." ); } // Yeah, we declare it like this so the below return is actually clear. // eslint-disable-next-line no-case-declarations const MAX = - (chart as ChartDocument<"iidx:SP" | "bms:7K" | "bms:14K" | "iidx:DP">).data - .notecount * 2; + ( + chart as ChartDocument< + | "iidx:SP" + | "bms:7K" + | "bms:14K" + | "pms:Controller" + | "pms:Keyboard" + | "iidx:DP" + > + ).data.notecount * 2; return (100 * score) / MAX; default: @@ -100,16 +110,22 @@ export function ValidatePercent( const mmChart = chart as ChartDocument<"maimai:Single">; if (percent > mmChart.data.maxPercent) { throw new InvalidScoreFailure( - `Invalid percent - expected less than ${mmChart.data.maxPercent}.` + `Invalid percent - expected a number less than ${mmChart.data.maxPercent}.` + ); + } + } else { + const gptConfig = GetGamePTConfig(game, playtype); + + if (percent > gptConfig.percentMax) { + throw new InvalidScoreFailure( + `Invalid percent of ${percent} - expected a value less than ${gptConfig.percentMax}% (${chart.songID} ${chart.playtype} ${chart.difficulty}).` ); } } - const gptConfig = GetGamePTConfig(game, playtype); - - if (percent > gptConfig.percentMax) { + if (percent < 0) { throw new InvalidScoreFailure( - `Invalid percent of ${percent} - expected a value less than ${gptConfig.percentMax}% (${chart.songID} ${chart.playtype} ${chart.difficulty}).` + `Invalid percent of ${percent} - Expected a positive number? (${chart.songID} ${chart.playtype} ${chart.difficulty})` ); } } diff --git a/server/src/test-utils/asserts.ts b/server/src/test-utils/asserts.ts index 3fe680801..492ae9998 100644 --- a/server/src/test-utils/asserts.ts +++ b/server/src/test-utils/asserts.ts @@ -1,18 +1,26 @@ -import { rootLogger } from "lib/logger/logger"; import Prudence, { PrudenceSchema } from "prudence"; -import t from "tap"; - -export function isApproximately(number: number, target: number, message?: string, lenience = 0.01) { - const result = t.ok(Math.abs(number - target) < lenience, message); +export function isApproximately( + t: Tap.Test, + number: number, + target: number, + message?: string, + lenience = 0.01 +) { + const result = Math.abs(number - target) < lenience; if (!result) { - rootLogger.error(`${number} was not close enough to ${target}`); + return t.fail(`${number} was not close enough to ${target}`, { + errmsg: `${number} was not close enough to ${target}`, + }); } + t.pass(message); + // return true; } export function prAssert( + t: Tap.Test, obj: Record | unknown, schema: PrudenceSchema, message = "Unnamed Prudence Assertion"