Complete overhaul

This commit is contained in:
zkldi
2021-06-20 03:00:07 +01:00
parent 150f91b1df
commit 31077824c5
14 changed files with 2801 additions and 2739 deletions
+2 -1
View File
@@ -20,7 +20,8 @@
"singleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"endOfLine": "auto"
"endOfLine": "auto",
"useTabs": true
}],
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-unused-expressions": "error",
+2 -1
View File
@@ -1,4 +1,5 @@
/node_modules
/.vscode
.nyc_output
.nyc_output
coverage
+40 -26
View File
@@ -1,27 +1,41 @@
{
"name": "tachi-common",
"version": "0.1.0",
"description": "Common Types & Stuff for Tachi",
"main": "js/index.js",
"scripts": {
"test": "tap",
"build": "tsc"
},
"author": "zkldi",
"license": "ISC",
"devDependencies": {
"@types/tap": "^15.0.3",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"eslint": "^7.22.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^3.3.1",
"prettier": "^2.2.1",
"ts-node": "^10.0.0",
"typescript": "^4.3.4"
},
"dependencies": {
"monk": "^7.3.3",
"tap": "^15.0.9"
}
}
"name": "tachi-common",
"version": "0.1.0",
"description": "Common Types & Stuff for Tachi",
"main": "js/index.js",
"scripts": {
"test": "tap",
"build": "tsc"
},
"author": "zkldi",
"license": "ISC",
"devDependencies": {
"@types/tap": "^15.0.3",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"eslint": "^7.22.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^3.3.1",
"prettier": "^2.2.1",
"ts-node": "^10.0.0",
"typescript": "^4.3.4"
},
"dependencies": {
"monk": "^7.3.3",
"tap": "^15.0.9"
},
"nyc": {
"reporter": [
"html",
"lcov"
],
"all": true,
"include": [
"src/*/!(test-utils)/**/*.ts"
],
"exclude": [
"src/**/*.test.ts",
"src/external/mongo/schemas.ts"
]
}
}
+11 -11
View File
@@ -3,23 +3,23 @@ import { GetGameConfig, GetGamePTConfig } from "./config";
import { allSupportedGames } from "./static-config";
t.test("#GetGameConfig", (t) => {
t.plan(allSupportedGames.length);
t.plan(allSupportedGames.length);
for (const game of allSupportedGames) {
t.equal(GetGameConfig(game).internalName, game);
}
for (const game of allSupportedGames) {
t.equal(GetGameConfig(game).internalName, game);
}
});
// i have no idea why this one doesn't need t.end()
// and this one does
t.test("#GetGamePTConfig", (t) => {
for (const game of allSupportedGames) {
const gameConfig = GetGameConfig(game);
for (const game of allSupportedGames) {
const gameConfig = GetGameConfig(game);
for (const playtype of gameConfig.validPlaytypes) {
t.equal(GetGamePTConfig(game, playtype).idString, `${game}:${playtype}`);
}
}
for (const playtype of gameConfig.validPlaytypes) {
t.equal(GetGamePTConfig(game, playtype).idString, `${game}:${playtype}`);
}
}
t.end();
t.end();
});
File diff suppressed because it is too large Load Diff
+61 -61
View File
@@ -1,89 +1,89 @@
import { FileUploadImportTypes, IRImportTypes, APIImportTypes, ImportTypes, Game } from "..";
export const fileImportTypes: FileUploadImportTypes[] = [
"file/eamusement-iidx-csv",
"file/batch-manual",
"file/solid-state-squad",
"file/mer-iidx",
"file/pli-iidx-csv",
"file/eamusement-iidx-csv",
"file/batch-manual",
"file/solid-state-squad",
"file/mer-iidx",
"file/pli-iidx-csv",
];
export const irImportTypes: IRImportTypes[] = [
"ir/direct-manual",
"ir/barbatos",
"ir/fervidex",
"ir/fervidex-static",
"ir/beatoraja",
"ir/chunitachi",
"ir/usc",
"ir/direct-manual",
"ir/barbatos",
"ir/fervidex",
"ir/fervidex-static",
"ir/beatoraja",
"ir/chunitachi",
"ir/usc",
];
export const apiImportTypes: APIImportTypes[] = [
"api/arc-iidx",
"api/arc-sdvx",
"api/arc-ddr",
"api/eag-iidx",
"api/eag-sdvx",
"api/flo-iidx",
"api/flo-sdvx",
"api/arc-iidx",
"api/arc-sdvx",
"api/arc-ddr",
"api/eag-iidx",
"api/eag-sdvx",
"api/flo-iidx",
"api/flo-sdvx",
];
export const allImportTypes: ImportTypes[] = [
...fileImportTypes,
...irImportTypes,
...apiImportTypes,
...fileImportTypes,
...irImportTypes,
...apiImportTypes,
];
export const allSupportedGames: Game[] = [
"iidx",
"museca",
"maimai",
// "jubeat",
// "popn",
"sdvx",
"ddr",
"bms",
"chunithm",
"gitadora",
"usc",
"iidx",
"museca",
"maimai",
// "jubeat",
// "popn",
"sdvx",
"ddr",
"bms",
"chunithm",
"gitadora",
"usc",
];
export interface ServerConfig {
name: string;
supportedGames: Game[];
supportedImportTypes: ImportTypes[];
name: string;
supportedGames: Game[];
supportedImportTypes: ImportTypes[];
}
export const KTCHI_CONFIG: ServerConfig = {
name: "Kamaitachi",
supportedGames: ["iidx", "gitadora", "chunithm", "maimai", "museca", "sdvx"],
supportedImportTypes: [
"api/arc-iidx",
"api/arc-sdvx",
"api/arc-ddr",
"api/eag-iidx",
"api/eag-sdvx",
"api/flo-iidx",
"api/flo-sdvx",
"ir/direct-manual",
"ir/fervidex",
"ir/fervidex-static",
"ir/chunitachi",
"file/eamusement-iidx-csv",
"file/solid-state-squad",
"file/mer-iidx",
"file/pli-iidx-csv",
],
name: "Kamaitachi",
supportedGames: ["iidx", "gitadora", "chunithm", "maimai", "museca", "sdvx"],
supportedImportTypes: [
"api/arc-iidx",
"api/arc-sdvx",
"api/arc-ddr",
"api/eag-iidx",
"api/eag-sdvx",
"api/flo-iidx",
"api/flo-sdvx",
"ir/direct-manual",
"ir/fervidex",
"ir/fervidex-static",
"ir/chunitachi",
"file/eamusement-iidx-csv",
"file/solid-state-squad",
"file/mer-iidx",
"file/pli-iidx-csv",
],
};
export const BTCHI_CONFIG: ServerConfig = {
name: "Bokutachi",
supportedGames: ["usc", "bms"],
supportedImportTypes: ["ir/beatoraja", "ir/usc", "ir/direct-manual", "file/batch-manual"],
name: "Bokutachi",
supportedGames: ["usc", "bms"],
supportedImportTypes: ["ir/beatoraja", "ir/usc", "ir/direct-manual", "file/batch-manual"],
};
export const OMNI_CONFIG: ServerConfig = {
name: "Omnitachi",
supportedGames: allSupportedGames,
supportedImportTypes: allImportTypes,
name: "Omnitachi",
supportedGames: allSupportedGames,
supportedImportTypes: allImportTypes,
};
+155 -155
View File
@@ -4,191 +4,191 @@
import { IDStrings, integer } from "./types";
export interface ClassInfo {
display: string;
mouseover: string | null;
display: string;
mouseover: string | null;
}
function c(d: string, m: string) {
return { display: d, mouseover: m };
return { display: d, mouseover: m };
}
function n(d: string) {
return { display: d, mouseover: null };
return { display: d, mouseover: null };
}
export const IIDXDans: ClassInfo[] = [
c("七級", "7th Kyu"),
c("六級", "6th Kyu"),
c("五級", "5th Kyu"),
c("四級", "4th Kyu"),
c("三級", "3rd Kyu"),
c("二級", "2nd Kyu"),
c("一級", "1st Kyu"),
c("初段", "1st Dan"),
c("二段", "2nd Dan"),
c("三段", "3rd Dan"),
c("四段", "4th Dan"),
c("五段", "5th Dan"),
c("六段", "6th Dan"),
c("七段", "7th Dan"),
c("八段", "8th Dan"),
c("九段", "9th Dan"),
c("十段", "10th Dan"),
c("中伝", "Chuuden"),
c("皆伝", "Kaiden"),
c("七級", "7th Kyu"),
c("六級", "6th Kyu"),
c("五級", "5th Kyu"),
c("四級", "4th Kyu"),
c("三級", "3rd Kyu"),
c("二級", "2nd Kyu"),
c("一級", "1st Kyu"),
c("初段", "1st Dan"),
c("二段", "2nd Dan"),
c("三段", "3rd Dan"),
c("四段", "4th Dan"),
c("五段", "5th Dan"),
c("六段", "6th Dan"),
c("七段", "7th Dan"),
c("八段", "8th Dan"),
c("九段", "9th Dan"),
c("十段", "10th Dan"),
c("中伝", "Chuuden"),
c("皆伝", "Kaiden"),
];
export const GitadoraColours: ClassInfo[] = [
c("白", "White"),
c("橙", "Orange"),
c("橙グラ", "Orange Gradient"),
c("黄", "Yellow"),
c("黄グラ", "Yellow Gradient"),
c("緑", "Green"),
c("緑グラ", "Green Gradient"),
c("青", "Blue"),
c("青グラ", "Blue Gradient"),
c("紫", "Purple"),
c("紫グラ", "Purple Gradient"),
c("赤", "Red"),
c("赤グラ", "Red Gradient"),
c("銅", "Bronze"),
c("銀", "Silver"),
c("金", "Gold"),
c("虹", "Rainbow"),
c("白", "White"),
c("橙", "Orange"),
c("橙グラ", "Orange Gradient"),
c("黄", "Yellow"),
c("黄グラ", "Yellow Gradient"),
c("緑", "Green"),
c("緑グラ", "Green Gradient"),
c("青", "Blue"),
c("青グラ", "Blue Gradient"),
c("紫", "Purple"),
c("紫グラ", "Purple Gradient"),
c("赤", "Red"),
c("赤グラ", "Red Gradient"),
c("銅", "Bronze"),
c("銀", "Silver"),
c("金", "Gold"),
c("虹", "Rainbow"),
];
export const BMSGenocideDans: ClassInfo[] = [
c("☆1", "Normal 1st Dan"),
c("☆2", "Normal 2nd Dan"),
c("☆3", "Normal 3rd Dan"),
c("☆4", "Normal 4th Dan"),
c("☆5", "Normal 5th Dan"),
c("☆6", "Normal 6th Dan"),
c("☆7", "Normal 7th Dan"),
c("☆8", "Normal 8th Dan"),
c("☆9", "Normal 9th Dan"),
c("☆10", "Normal 10th Dan"),
c("★1", "Insane 1st Dan"),
c("★2", "Insane 2nd Dan"),
c("★3", "Insane 3rd Dan"),
c("★4", "Insane 4th Dan"),
c("★5", "Insane 5th Dan"),
c("★6", "Insane 6th Dan"),
c("★7", "Insane 7th Dan"),
c("★8", "Insane 8th Dan"),
c("★9", "Insane 9th Dan"),
c("★10", "Insane 10th Dan"),
c("★★", "Insane Kaiden"),
c("(^^)", "Overjoy"),
c("☆1", "Normal 1st Dan"),
c("☆2", "Normal 2nd Dan"),
c("☆3", "Normal 3rd Dan"),
c("☆4", "Normal 4th Dan"),
c("☆5", "Normal 5th Dan"),
c("☆6", "Normal 6th Dan"),
c("☆7", "Normal 7th Dan"),
c("☆8", "Normal 8th Dan"),
c("☆9", "Normal 9th Dan"),
c("☆10", "Normal 10th Dan"),
c("★1", "Insane 1st Dan"),
c("★2", "Insane 2nd Dan"),
c("★3", "Insane 3rd Dan"),
c("★4", "Insane 4th Dan"),
c("★5", "Insane 5th Dan"),
c("★6", "Insane 6th Dan"),
c("★7", "Insane 7th Dan"),
c("★8", "Insane 8th Dan"),
c("★9", "Insane 9th Dan"),
c("★10", "Insane 10th Dan"),
c("★★", "Insane Kaiden"),
c("(^^)", "Overjoy"),
];
export const BMSStSlDans: ClassInfo[] = [
c("sl0", "Satellite 0"),
c("sl1", "Satellite 1"),
c("sl2", "Satellite 2"),
c("sl3", "Satellite 3"),
c("sl4", "Satellite 4"),
c("sl5", "Satellite 5"),
c("sl6", "Satellite 6"),
c("sl7", "Satellite 7"),
c("sl8", "Satellite 8"),
c("sl9", "Satellite 9"),
c("sl10", "Satellite 10"),
c("sl11", "Satellite 11"),
c("sl12", "Satellite 12"),
c("st0", "Stella 0"),
c("st1", "Stella 1"),
c("st2", "Stella 2"),
c("st3", "Stella 3"),
c("st4", "Stella 4"),
c("st5", "Stella 5"),
c("st6", "Stella 6"),
c("st7", "Stella 7"),
c("st8", "Stella 8"),
c("st9", "Stella 9"),
c("st10", "Stella 10"),
c("st11", "Stella 11"),
c("sl0", "Satellite 0"),
c("sl1", "Satellite 1"),
c("sl2", "Satellite 2"),
c("sl3", "Satellite 3"),
c("sl4", "Satellite 4"),
c("sl5", "Satellite 5"),
c("sl6", "Satellite 6"),
c("sl7", "Satellite 7"),
c("sl8", "Satellite 8"),
c("sl9", "Satellite 9"),
c("sl10", "Satellite 10"),
c("sl11", "Satellite 11"),
c("sl12", "Satellite 12"),
c("st0", "Stella 0"),
c("st1", "Stella 1"),
c("st2", "Stella 2"),
c("st3", "Stella 3"),
c("st4", "Stella 4"),
c("st5", "Stella 5"),
c("st6", "Stella 6"),
c("st7", "Stella 7"),
c("st8", "Stella 8"),
c("st9", "Stella 9"),
c("st10", "Stella 10"),
c("st11", "Stella 11"),
];
export const SDVXDans: ClassInfo[] = [
c("LV.01", "1st Dan"),
c("LV.02", "2nd Dan"),
c("LV.03", "3rd Dan"),
c("LV.04", "4th Dan"),
c("LV.05", "5th Dan"),
c("LV.06", "6th Dan"),
c("LV.07", "7th Dan"),
c("LV.08", "8th Dan"),
c("LV.09", "9th Dan"),
c("LV.10", "10th Dan"),
c("LV.11", "11th Dan"),
c("LV.INF", "Inf. Dan"),
c("LV.01", "1st Dan"),
c("LV.02", "2nd Dan"),
c("LV.03", "3rd Dan"),
c("LV.04", "4th Dan"),
c("LV.05", "5th Dan"),
c("LV.06", "6th Dan"),
c("LV.07", "7th Dan"),
c("LV.08", "8th Dan"),
c("LV.09", "9th Dan"),
c("LV.10", "10th Dan"),
c("LV.11", "11th Dan"),
c("LV.INF", "Inf. Dan"),
];
export const SDVXVFClasses: ClassInfo[] = [
n("Sienna I"),
n("Sienna II"),
n("Sienna III"),
n("Sienna IV"),
n("Cobalt I"),
n("Cobalt II"),
n("Cobalt III"),
n("Cobalt IV"),
n("Dandelion I"),
n("Dandelion II"),
n("Dandelion III"),
n("Dandelion IV"),
n("Cyan I"),
n("Cyan II"),
n("Cyan III"),
n("Cyan IV"),
n("Scarlet I"),
n("Scarlet II"),
n("Scarlet III"),
n("Scarlet IV"),
n("Coral I"),
n("Coral II"),
n("Coral III"),
n("Coral IV"),
n("Argento I"),
n("Argento II"),
n("Argento III"),
n("Argento IV"),
n("Eldora I"),
n("Eldora II"),
n("Eldora III"),
n("Eldora IV"),
n("Crimson I"),
n("Crimson II"),
n("Crimson III"),
n("Crimson IV"),
n("Imperial I"),
n("Imperial II"),
n("Imperial III"),
n("Imperial IV"),
n("Sienna I"),
n("Sienna II"),
n("Sienna III"),
n("Sienna IV"),
n("Cobalt I"),
n("Cobalt II"),
n("Cobalt III"),
n("Cobalt IV"),
n("Dandelion I"),
n("Dandelion II"),
n("Dandelion III"),
n("Dandelion IV"),
n("Cyan I"),
n("Cyan II"),
n("Cyan III"),
n("Cyan IV"),
n("Scarlet I"),
n("Scarlet II"),
n("Scarlet III"),
n("Scarlet IV"),
n("Coral I"),
n("Coral II"),
n("Coral III"),
n("Coral IV"),
n("Argento I"),
n("Argento II"),
n("Argento III"),
n("Argento IV"),
n("Eldora I"),
n("Eldora II"),
n("Eldora III"),
n("Eldora IV"),
n("Crimson I"),
n("Crimson II"),
n("Crimson III"),
n("Crimson IV"),
n("Imperial I"),
n("Imperial II"),
n("Imperial III"),
n("Imperial IV"),
];
export interface GameClassSets {
"iidx:SP": "dan";
"iidx:DP": "dan";
"popn:9B": never;
"sdvx:Single": "dan" | "vfClass";
"usc:Single": never;
"ddr:SP": "dan";
"ddr:DP": "dan";
"maimai:Single": never;
"jubeat:Single": "colour";
"museca:Single": never;
"bms:7K": "genocideDan" | "stslDan";
"bms:14K": "genocideDan";
"chunithm:Single": never;
"gitadora:Gita": "colour";
"gitadora:Dora": "colour";
"iidx:SP": "dan";
"iidx:DP": "dan";
"popn:9B": never;
"sdvx:Single": "dan" | "vfClass";
"usc:Single": never;
"ddr:SP": "dan";
"ddr:DP": "dan";
"maimai:Single": never;
"jubeat:Single": "colour";
"museca:Single": never;
"bms:7K": "genocideDan" | "stslDan";
"bms:14K": "genocideDan";
"chunithm:Single": never;
"gitadora:Gita": "colour";
"gitadora:Dora": "colour";
}
export type AllClassSets = GameClassSets[IDStrings];
export type GameClasses<I extends IDStrings> = {
[K in GameClassSets[I]]: integer;
[K in GameClassSets[I]]: integer;
};
+13
View File
@@ -0,0 +1,13 @@
import t from "tap";
import * as tachiCommon from "./index";
// we just check that we're exporting stuff properly
t.test("Property Checks", (t) => {
t.type(tachiCommon.GetGameConfig, "function");
t.type(tachiCommon.GetGamePTConfig, "function");
t.type(tachiCommon.gameClasses, "object");
t.type(tachiCommon.COLOUR_SET, "object");
t.end();
});
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
import t from "tap";
import { GetGamePTConfig } from "../config/config";
import { CalculateESD, ESDCompare, PercentCompare } from "./esd";
function approx(t: Tap.Test, given: number, recieved: number, acc = 0.01) {
if (Math.abs(given - recieved) < acc) {
return t.pass();
} else {
return t.fail(`Expected ${given} to be within ${acc} of ${recieved}.`);
}
}
t.test("#CalculateESD", (t) => {
const jdg = GetGamePTConfig("iidx", "SP").judgementWindows!;
approx(t, CalculateESD(jdg, 0.8), 17.58);
approx(t, CalculateESD(jdg, 0), 200);
t.throws(() => CalculateESD(jdg, 0, true));
t.end();
});
t.test("#ESDCompare", (t) => {
approx(t, ESDCompare(16, 14), 6.42);
approx(t, ESDCompare(16, 14, 1), ESDCompare(16, 14));
approx(t, ESDCompare(0, 200, 1), -68.27);
t.end();
});
t.test("#PercentCompare", (t) => {
const jdg = GetGamePTConfig("iidx", "SP").judgementWindows!;
approx(t, PercentCompare(jdg, 0.99, 0.98), -5.53);
approx(t, PercentCompare(jdg, 0.99, 0.98), PercentCompare(jdg, 0.99, 0.98, 1));
t.end();
});
+100 -103
View File
@@ -3,7 +3,7 @@
* https://en.wikipedia.org/wiki/Cumulative_distribution_function
*/
function cdf(x: number, mean: number, variance: number) {
return 0.5 * (1 + erf((x - mean) / Math.sqrt(2 * variance)));
return 0.5 * (1 + erf((x - mean) / Math.sqrt(2 * variance)));
}
/**
@@ -11,27 +11,27 @@ function cdf(x: number, mean: number, variance: number) {
* https://en.wikipedia.org/wiki/Error_function
*/
function erf(x: number) {
// save the sign of x
const sign = x >= 0 ? 1 : -1;
x = Math.abs(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;
// 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);
// 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: number, highBound: number, mean: number, variance: number) {
// 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));
// 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.
@@ -46,32 +46,32 @@ const MEAN = 0;
* @returns The percent this std. deviation would produce.
*/
function StdDeviationToPercent(
judgements: ESDJudgementFormat[],
stddev: number,
largestValue: number
judgements: ESDJudgementFormat[],
stddev: number,
largestValue: number
) {
let lastJudgeMSBorder = 0;
let prbSum = 0;
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, stddev ** 2) * judge.value;
lastJudgeMSBorder = judge.msBorder;
prbSum += nVal;
}
for (let i = 0; i < judgements.length; i++) {
const judge = judgements[i];
const nVal = CDFBetween(lastJudgeMSBorder, judge.msBorder, MEAN, stddev ** 2) * judge.value;
lastJudgeMSBorder = judge.msBorder;
prbSum += nVal;
}
prbSum /= largestValue;
prbSum /= largestValue;
return prbSum;
return prbSum;
}
const ACCEPTABLE_ERROR = 0.001;
const MAX_ITERATIONS = 50;
export interface ESDJudgementFormat {
name: string;
msBorder: number;
value: number;
name: string;
msBorder: number;
value: number;
}
/**
@@ -83,58 +83,55 @@ export interface ESDJudgementFormat {
* @returns
*/
export function CalculateESD(
judgements: ESDJudgementFormat[],
percent: number,
errOnInaccuracy = false
judgements: ESDJudgementFormat[],
percent: number,
errOnInaccuracy = false
): number {
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;
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;
// 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);
// 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 (Math.abs(estimatedPercent - percent) < ACCEPTABLE_ERROR) {
return estSD;
}
if (estimatedPercent < percent) {
maxSD = estSD;
} else {
minSD = estSD;
}
if (estimatedPercent < percent) {
maxSD = estSD;
} else {
minSD = estSD;
}
if (estSD === (minSD + maxSD) / 2) {
// if it isn't moving, just terminate
break;
}
if (estSD === (minSD + maxSD) / 2) {
// if it isn't moving, just terminate
break;
}
estSD = (minSD + maxSD) / 2;
}
estSD = (minSD + maxSD) / 2;
}
if (errOnInaccuracy) {
throw `(ESD-JS) Did not reach value within MAX_ITERATIONS (${MAX_ITERATIONS})`;
} else {
console.warn(
`(ESD-JS) Did not reach value within MAX_ITERATIONS (${MAX_ITERATIONS}), returning last estimate (${estSD}), which was generated from ${percent}.`
);
return estSD;
}
if (errOnInaccuracy) {
throw `(ESD-JS) Did not reach value within MAX_ITERATIONS (${MAX_ITERATIONS})`;
}
return estSD;
}
/**
@@ -145,42 +142,42 @@ export function CalculateESD(
* @returns A number between -100 and 100.
*/
export function ESDCompare(baseESD: number, compareESD: number, cdeg = 1): number {
const CONFIDENCE_DEGREE = cdeg;
const BASE_CASE = CDFBetween(-1 * CONFIDENCE_DEGREE, CONFIDENCE_DEGREE, 0, 1) / 2;
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 = compareESD ** 2;
bound = CONFIDENCE_DEGREE * baseESD;
} else {
variance = baseESD ** 2;
bound = CONFIDENCE_DEGREE * compareESD;
}
let inv = false;
let variance;
let bound;
if (compareESD > baseESD) {
inv = true;
variance = compareESD ** 2;
bound = CONFIDENCE_DEGREE * baseESD;
} else {
variance = baseESD ** 2;
bound = CONFIDENCE_DEGREE * compareESD;
}
const esdc = CDFBetween(-1 * bound, bound, 0, variance) / 2;
const esdc = CDFBetween(-1 * bound, bound, 0, variance) / 2;
let besdc = BASE_CASE - esdc;
let besdc = BASE_CASE - esdc;
if (inv) {
besdc *= -1;
}
if (inv) {
besdc *= -1;
}
return besdc * 100;
return besdc * 100;
}
/**
* Converts two percents to ESD, then runs ESDCompare.
*/
export function PercentCompare(
judgements: ESDJudgementFormat[],
baseP: number,
compareP: number,
cdeg = 1
judgements: ESDJudgementFormat[],
baseP: number,
compareP: number,
cdeg = 1
): number {
const e1 = CalculateESD(judgements, baseP);
const e2 = CalculateESD(judgements, compareP);
return ESDCompare(e1, e2, cdeg);
const e1 = CalculateESD(judgements, baseP);
const e2 = CalculateESD(judgements, compareP);
return ESDCompare(e1, e2, cdeg);
}
+4 -4
View File
@@ -2,9 +2,9 @@ import t from "tap";
import { CalculateGradeDeltaIIDX } from "./util";
t.test("#CalculateGradeDeltaIIDX", (t) => {
t.equal(CalculateGradeDeltaIIDX("A", 1000, 2000), "A-334");
t.equal(CalculateGradeDeltaIIDX("AAA", 1778, 2000), "AAA+0");
t.equal(CalculateGradeDeltaIIDX("MAX", 1900, 2000), "MAX-100");
t.equal(CalculateGradeDeltaIIDX("A", 1000, 2000), "A-334");
t.equal(CalculateGradeDeltaIIDX("AAA", 1778, 2000), "AAA+0");
t.equal(CalculateGradeDeltaIIDX("MAX", 1900, 2000), "MAX-100");
t.end();
t.end();
});
+13 -13
View File
@@ -4,22 +4,22 @@ import { Grades, integer } from "../types";
const IIDXConfig = GetGamePTConfig("iidx", "SP");
export function CalculateGradeDeltaIIDX(
cmpGrade: Grades["iidx:SP" | "iidx:DP"],
score: integer,
notecount: integer
cmpGrade: Grades["iidx:SP" | "iidx:DP"],
score: integer,
notecount: integer
): string {
const cmpGradeIDX = IIDXConfig.grades.indexOf(cmpGrade);
const cmpGradeIDX = IIDXConfig.grades.indexOf(cmpGrade);
const cmpGradePercent = IIDXConfig.gradeBoundaries[cmpGradeIDX];
const cmpGradePercent = IIDXConfig.gradeBoundaries[cmpGradeIDX];
const cmpScore = Math.ceil(notecount * (cmpGradePercent / 100));
const cmpScore = Math.ceil(notecount * (cmpGradePercent / 100));
const delta = score - cmpScore;
const delta = score - cmpScore;
if (delta >= 0) {
return `${cmpGrade}+${delta}`;
} else {
// negative numbers implicitly stringify with the - prefix
return `${cmpGrade}${delta}`;
}
if (delta >= 0) {
return `${cmpGrade}+${delta}`;
} else {
// negative numbers implicitly stringify with the - prefix
return `${cmpGrade}${delta}`;
}
}
+952 -952
View File
File diff suppressed because it is too large Load Diff