UNPKG

rg-stats

Version:

A library for calculating various rhythm game stats.

172 lines 7.92 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.inverse = exports.calculate = void 0; const math_1 = require("../util/math"); const throw_if_1 = require("../util/throw-if"); /** * Calculates the "Beat Performance Index" of an IIDX score. * this is a port of Poyashi's implementation. * https://github.com/potakusan/iidx_score_manager/blob/f21ba6b85fcc0bf8b7ca888fa2239a3951a9c9c2/src/components/bpi/index.tsx#L120 * * @param yourEx Your EX score. * @param kaidenAverage The kaiden average EX score. * @param worldRecord The world record's 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. * If powCoef is set to -1, it will be overrode 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. * * @edgecase Even though any score better than the world record *is* now the world record, * it takes around 6 months for the official data on the world record to be updated. * Since changing the worldRecord affects all scores on the chart, poyashi's * implementation just lets BPI exceed 100 until patched. * * @edgecase BPI is undefined for the case where the world record is equal to the * kaiden average. */ function calculate(yourEx, kaidenAverage, worldRecord, max, powCoef) { let powCoefficient = powCoef ?? 1.175; if (powCoefficient === -1) { powCoefficient = 1.175; } AssertProvidedEXScores(kaidenAverage, worldRecord, max, powCoefficient); throw_if_1.ThrowIf.not(Number.isSafeInteger(yourEx), "Provided EX was not an integer.", { yourEx }); (0, throw_if_1.ThrowIf)(yourEx > max, "Provided EX was greater than MAX.", { yourEx, max }); throw_if_1.ThrowIf.negative(yourEx, "Provided EX was negative.", { yourEx }); (0, throw_if_1.ThrowIf)(worldRecord === kaidenAverage, "BPI is undefined for the case where Kaiden Average equals the World Record", { kaidenAverage, worldRecord }); const yourPGF = PikaGreatFunction(yourEx, max); const kaidenPGF = PikaGreatFunction(kaidenAverage, max); const wrPGF = PikaGreatFunction(worldRecord, max); // Work out the ratio of your PGF versus the kaiden PGF. const yourScorePrime = yourPGF / kaidenPGF; const wrScorePrime = wrPGF / kaidenPGF; const isWorseThanKavg = yourEx < kaidenAverage; const logExPrime = (0, math_1.LogToBase)(yourScorePrime, wrScorePrime); if (isWorseThanKavg) { // since logExPrime is negative (because yourScorePrime is between 0 and 1) // We can't raise it to a power natively, as -0.5 to the power of something // like 1.175 is imaginary. // To fix this, the poyashi implementation negates logExPrime // and raises it to the powCoef. const negativeRaisedValue = (-1 * logExPrime) ** powCoefficient; // and then negates it again when multiplying by 100. const bpi = 100 * -1 * negativeRaisedValue; if (bpi < -15) { return -15; } return bpi; } else { return 100 * logExPrime ** powCoefficient; } } exports.calculate = calculate; /** * Calculates the "PGF" of an IIDX score. This returns a number that indicates how * many pgreats you are expected to get for every great. */ function PikaGreatFunction(score, max) { // if score === max, then the subsequent lines will involve a divide by zero // so we have to avoid that somehow. if (score === max) { // The poyashi implementation multiplies by 0.8 here for an unknown reason. // // This multiplication essentially asserts that on a perfect score on a chart with // 1000 notes, it expects every 1600(!!) notes you make a mistake. return max * 0.8; } const scorePercent = score / max; return 0.5 / (1 - scorePercent); } /** * Returns the EX Score necessary to achieve the provided BPI. * * @param bpi - The BPI you wish to find out how much ex is needed for. * @param kaidenAverage The kaiden average EX score. * @param worldRecord The world record's EX score. * @param powCoef What power the BPI should be raised to. This is arbitrary, and assigned on a per-song basis. Defaults to 1.175. * If powCoef is set to -1, it will be overrode to 1.175. * * @returns A number between 0 and `max`. * * @edgecase -15BPI is achievable by many possible EXs, since it is the lower bound * for any given chart. Passing -15 into the function will return the largest * possible exScore that results in -15BPI. */ function inverse(bpi, kaidenAverage, worldRecord, max, powCoef) { let powCoefficient = powCoef ?? 1.175; if (powCoefficient === -1) { powCoefficient = 1.175; } AssertProvidedEXScores(kaidenAverage, worldRecord, max, powCoefficient); (0, throw_if_1.ThrowIf)(bpi < -15, "BPI must be greater than or equal to -15.", { bpi }); const isWorseThanKavg = bpi < 0; let logExPrime; if (isWorseThanKavg) { logExPrime = -1 * (bpi / -100) ** (1 / powCoefficient); } else { logExPrime = (bpi / 100) ** (1 / powCoefficient); } const kaidenPGF = PikaGreatFunction(kaidenAverage, max); const wrPGF = PikaGreatFunction(worldRecord, max); const wrScorePrime = wrPGF / kaidenPGF; const exScorePrime = wrScorePrime ** logExPrime; const exScorePGF = exScorePrime * kaidenPGF; const exScore = InversePikaGreatFunction(exScorePGF, max); // This is because InversePGF can't correctly apply the 0.8x multiplier. // If your exScore ceil's to MAX, then the ex score needed to get this score is max. // note that this only applies to the case where bpi > 100. if (bpi > 100 && Math.ceil(exScore) === max) { return max; } return Math.round(exScore); } exports.inverse = inverse; /** * Inverts the PGF by returning the EXScore necessary to get a given PGF. * * @see {PikaGreatFunction} */ function InversePikaGreatFunction(pgf, max) { return (pgf * max - 0.5 * max) / pgf; } /** * Apply assertions and constraints into values that BPI recieves. */ function AssertProvidedEXScores(kaidenAverage, worldRecord, max, powCoef) { // Is integer? throw_if_1.ThrowIf.not(Number.isSafeInteger(kaidenAverage), "Kaiden Average was not an integer.", { kaidenAverage, }); throw_if_1.ThrowIf.not(Number.isSafeInteger(worldRecord), "World Record was not an integer.", { worldRecord, }); throw_if_1.ThrowIf.not(Number.isSafeInteger(max), "MAX was not an integer.", { max }); // Is bounded correctly? (0, throw_if_1.ThrowIf)(kaidenAverage > max, "Kaiden Average was greater than MAX.", { kaidenAverage, max, }); (0, throw_if_1.ThrowIf)(worldRecord > max, "World Record was greater than MAX.", { worldRecord, max, }); (0, throw_if_1.ThrowIf)(kaidenAverage > worldRecord, "Kaiden Average was greater than WR.", { kaidenAverage, worldRecord, }); throw_if_1.ThrowIf.negativeOrZero(kaidenAverage, "Kaiden Average was negative or zero.", { kaidenAverage, }); throw_if_1.ThrowIf.negativeOrZero(max, "MAX was negative or zero.", { max }); throw_if_1.ThrowIf.negativeOrZero(powCoef, "Power Coefficient was not positive.", { powCoef }); // This assertion never triggers, because WR has to be greater than KAVG, and KAVG // is non-negative. // ThrowIf.negativeOrZero(worldRecord, "World Record was negative or zero.", { worldRecord }); // Other edge case -- WR == KAVG breaks BPI immediately. (0, throw_if_1.ThrowIf)(worldRecord === kaidenAverage, "BPI is undefined for the case where Kaiden Average equals the World Record", { kaidenAverage, worldRecord }); } //# sourceMappingURL=poyashi-bpi.js.map