mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-04 21:08:09 +03:00
144 lines
5.4 KiB
JavaScript
144 lines
5.4 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.PercentCompare = exports.ESDCompare = exports.CalculateESD = void 0;
|
|
/**
|
|
* Cumulative Distribution Function
|
|
* https://en.wikipedia.org/wiki/Cumulative_distribution_function
|
|
*/
|
|
function cdf(x, mean, variance) {
|
|
return 0.5 * (1 + erf((x - mean) / Math.sqrt(2 * variance)));
|
|
}
|
|
/**
|
|
* Error Function
|
|
* https://en.wikipedia.org/wiki/Error_function
|
|
*/
|
|
function erf(x) {
|
|
// save the sign of x
|
|
const sign = x >= 0 ? 1 : -1;
|
|
x = Math.abs(x);
|
|
// constants
|
|
const a1 = 0.254829592;
|
|
const a2 = -0.284496736;
|
|
const a3 = 1.421413741;
|
|
const a4 = -1.453152027;
|
|
const a5 = 1.061405429;
|
|
const p = 0.3275911;
|
|
// A&S formula 7.1.26
|
|
const t = 1.0 / (1.0 + p * x);
|
|
const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
|
|
return sign * y; // erf(-x) = -erf(x);
|
|
}
|
|
function CDFBetween(lowBound, highBound, mean, variance) {
|
|
// Since the normal distribution is symmetrical, we want to double this, as the cut will only get one tail.
|
|
return 2 * (cdf(highBound, mean, variance) - cdf(lowBound, mean, variance));
|
|
}
|
|
// This is a direct port of ESD-JS to typescript. Maybe this should be released as its own module some day.
|
|
const MEAN = 0;
|
|
/**
|
|
* Gets the percent this score would roughly be, given a standard deviation.
|
|
* @param judgements - The judgements this game uses.
|
|
* @param stddev - The standard deviation to estimate the percent of.
|
|
* @param largestValue - The largest "value" of a judgement in this game. This lets us normalise results.
|
|
* @returns The percent this std. deviation would produce.
|
|
*/
|
|
function StdDeviationToPercent(judgements, stddev, largestValue) {
|
|
let lastJudgeMSBorder = 0;
|
|
let prbSum = 0;
|
|
for (let i = 0; i < judgements.length; i++) {
|
|
const judge = judgements[i];
|
|
const nVal = CDFBetween(lastJudgeMSBorder, judge.msBorder, MEAN, Math.pow(stddev, 2)) * judge.value;
|
|
lastJudgeMSBorder = judge.msBorder;
|
|
prbSum += nVal;
|
|
}
|
|
prbSum /= largestValue;
|
|
return prbSum;
|
|
}
|
|
const ACCEPTABLE_ERROR = 0.001;
|
|
const MAX_ITERATIONS = 50;
|
|
/**
|
|
* Given judgements and a percent, estimate the standard deviation needed to get a score
|
|
* with that percent.
|
|
* @param judgements - The judgements for this game.
|
|
* @param percent - The percent to estimate SD needed to get.
|
|
* @param errOnInaccuracy - Whether or whether not to throw if the estimate is not accurate enough.
|
|
* @returns
|
|
*/
|
|
function CalculateESD(judgements, percent, errOnInaccuracy = false) {
|
|
if (percent > 1 || percent < 0 || Number.isNaN(percent)) {
|
|
throw new Error("(ESD) Invalid percent. Percent must be between 0 and 1, and also a number.");
|
|
}
|
|
const largestValue = judgements.slice(0).sort((a, b) => b.value - a.value)[0].value;
|
|
// massive optimisation possible here by using better initial estimates with precalc'd table of values.
|
|
// until then, it's just kinda slow.
|
|
let minSD = 0;
|
|
let maxSD = 200;
|
|
let estSD = (minSD + maxSD) / 2;
|
|
// So, fundamentally the function that takes SD and returns estimated percent is NOT invertible.
|
|
// as in, it is very literally not invertible.
|
|
// if you figure it out, let me know.
|
|
// until then; we make MAX_ITERATIONS attempts at finding a value within the acceptable range of error.
|
|
// the defaults for these are 100 for iterations, and 0.001 for error.
|
|
// for most of what i've tested, this has been fine.
|
|
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
const estimatedPercent = StdDeviationToPercent(judgements, estSD, largestValue);
|
|
if (Math.abs(estimatedPercent - percent) < ACCEPTABLE_ERROR) {
|
|
return estSD;
|
|
}
|
|
if (estimatedPercent < percent) {
|
|
maxSD = estSD;
|
|
}
|
|
else {
|
|
minSD = estSD;
|
|
}
|
|
if (estSD === (minSD + maxSD) / 2) {
|
|
// if it isn't moving, just terminate
|
|
break;
|
|
}
|
|
estSD = (minSD + maxSD) / 2;
|
|
}
|
|
if (errOnInaccuracy) {
|
|
throw `(ESD-JS) Did not reach value within MAX_ITERATIONS (${MAX_ITERATIONS})`;
|
|
}
|
|
return estSD;
|
|
}
|
|
exports.CalculateESD = CalculateESD;
|
|
/**
|
|
* Compares two ESD values such that 1->2 produces a larger value than 101->102.
|
|
* @param baseESD - The first ESD to compare.
|
|
* @param compareESD - The second ESD to compare.
|
|
* @param cdeg - The degrees of confidence to use. This should be 1.
|
|
* @returns A number between -100 and 100.
|
|
*/
|
|
function ESDCompare(baseESD, compareESD, cdeg = 1) {
|
|
const CONFIDENCE_DEGREE = cdeg;
|
|
const BASE_CASE = CDFBetween(-1 * CONFIDENCE_DEGREE, CONFIDENCE_DEGREE, 0, 1) / 2;
|
|
let inv = false;
|
|
let variance;
|
|
let bound;
|
|
if (compareESD > baseESD) {
|
|
inv = true;
|
|
variance = Math.pow(compareESD, 2);
|
|
bound = CONFIDENCE_DEGREE * baseESD;
|
|
}
|
|
else {
|
|
variance = Math.pow(baseESD, 2);
|
|
bound = CONFIDENCE_DEGREE * compareESD;
|
|
}
|
|
const esdc = CDFBetween(-1 * bound, bound, 0, variance) / 2;
|
|
let besdc = BASE_CASE - esdc;
|
|
if (inv) {
|
|
besdc *= -1;
|
|
}
|
|
return besdc * 100;
|
|
}
|
|
exports.ESDCompare = ESDCompare;
|
|
/**
|
|
* Converts two percents to ESD, then runs ESDCompare.
|
|
*/
|
|
function PercentCompare(judgements, baseP, compareP, cdeg = 1) {
|
|
const e1 = CalculateESD(judgements, baseP);
|
|
const e2 = CalculateESD(judgements, compareP);
|
|
return ESDCompare(e1, e2, cdeg);
|
|
}
|
|
exports.PercentCompare = PercentCompare;
|
|
//# sourceMappingURL=esd.js.map
|