Sieglinde Initial Draft

This commit is contained in:
zkldi
2021-11-28 15:27:40 +00:00
commit 6f975ea320
13 changed files with 3520 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules
js
+92
View File
@@ -0,0 +1,92 @@
{
"root": true,
"env": {
"node": true,
"es6": true
},
"plugins": [
"@typescript-eslint",
"import"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2020
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"plugin:import/typescript",
"plugin:import/warnings",
"plugin:import/errors"
],
"rules": {
"prettier/prettier": [
"error",
{
"printWidth": 100,
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"endOfLine": "auto",
"useTabs": true
}
],
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "logger"
}
],
"require-await": "warn",
"no-await-in-loop": "warn",
"no-unused-labels": "error",
"prefer-const": "error",
"no-console": "error",
"eqeqeq": "error",
"no-eval": "error",
"no-var": "error",
"no-new-object": "error",
"quote-props": [
"error",
"as-needed"
],
"no-array-constructor": "error",
"prefer-template": "error",
"prefer-promise-reject-errors": "error",
"no-useless-escape": "error",
"no-loop-func": "error",
"prefer-rest-params": "error",
"default-param-last": "error",
"no-new-func": "error",
"arrow-body-style": "error",
"no-useless-constructor": "error",
"no-duplicate-imports": "error",
"dot-notation": "error",
"space-before-blocks": "error",
"no-prototype-builtins": "off",
"prefer-arrow-callback": "error",
"curly": "error",
"no-template-curly-in-string": "error",
"no-implied-eval": "error",
"yoda": "error",
"no-labels": "error",
"no-useless-concat": "error",
"radix": "error",
"no-octal-escape": "error",
"no-param-reassign": "error",
"no-proto": "error",
"no-return-await": "error",
"no-throw-literal": "error",
"no-unmodified-loop-condition": "error",
"no-unused-expressions": "error",
"no-useless-call": "error",
"no-useless-return": "error",
"require-unicode-regexp": "error"
}
}
+9
View File
@@ -0,0 +1,9 @@
/node_modules
.nyc_output
coverage
/js
.pnpm-debug
logs
+28
View File
@@ -0,0 +1,28 @@
{
"name": "tachi-sieglinde",
"version": "0.1.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "zkldi",
"license": "MIT",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"eslint": "^8.3.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.5.0",
"typescript": "^4.5.2"
},
"dependencies": {
"@types/node-fetch": "2",
"commander": "^8.3.0",
"fast-xml-parser": "^4.0.0-beta.2",
"mei-logger": "^1.0.1",
"node-fetch": "2"
}
}
+2981
View File
File diff suppressed because it is too large Load Diff
+87
View File
@@ -0,0 +1,87 @@
/* eslint-disable no-await-in-loop */
import { TableRes } from "../fetch-tables";
import logger from "../logger";
import { CalcReturns } from "../types";
import { GetBaseline, GetFString, GetScoresForMD5, GetSigmoidalValue, Mean } from "../util";
/**
* Sieglinde V0 calc. This is an incredibly naive implementation of a difficulty
* engine.
*
* The idea is simple - given a base level, check how much the clear rate deviates
* for the norm for that base level, and adjust the level accordingly. There isn't
* much complex going on. Just basic maths.
*/
export default async function SieglindeV0Calc(tableInfo: TableRes): Promise<CalcReturns[]> {
const dataset = [];
for (const chart of tableInfo.charts) {
const scores = await GetScoresForMD5(chart.md5);
const ecRate = 0;
const hcRate = 0;
const baseLine = GetBaseline(tableInfo.table, chart.level);
if (baseLine === null) {
logger.info(`Skipped level ${tableInfo.table.name} ${chart.level}`);
continue;
}
let nextBaseLine = GetBaseline(tableInfo.table, `${Number(chart.level) + 1}`);
if (nextBaseLine === null) {
logger.info(`No next baseline -- assuming +1.`);
nextBaseLine = baseLine + 1;
}
const str = GetFString(tableInfo.table, chart);
dataset.push({
title: chart.title,
md5: chart.md5,
ecRate,
hcRate,
scoreCount: scores.length,
baseLine,
nextBaseLine,
str,
});
}
const groupAverageECRate = Mean(dataset.map((e) => e.ecRate));
const groupAverageHCRate = Mean(dataset.map((e) => e.hcRate));
const returns = [];
for (const data of dataset) {
const confidence = GetSigmoidalValue(data.scoreCount / 100);
const ecDiff = groupAverageECRate - data.ecRate;
const hcDiff = groupAverageHCRate - data.hcRate;
const ecValue = Math.max(0, data.baseLine - ecDiff * confidence);
const hcValue = Math.max(0, data.baseLine - hcDiff * confidence);
const gutter = data.baseLine + data.nextBaseLine;
const ecLerp = gutter * 0.5 - (ecDiff * confidence) / gutter;
const hcLerp = gutter * 0.5 - (hcDiff * confidence) / gutter;
const ecStr = data.str + ecLerp.toFixed(2);
const hcStr = data.str + hcLerp.toFixed(2);
logger.verbose(`${data.title} | EC: ${ecStr}, HC: ${hcStr}`);
returns.push({
md5: data.md5,
title: data.title,
ec: ecValue,
ecStr,
hc: hcValue,
hcStr,
});
}
return returns;
}
+103
View File
@@ -0,0 +1,103 @@
/* eslint-disable no-await-in-loop */
import { BMSTablesDataset, BMSTableChart } from "./types";
import fetch from "node-fetch";
export const BMS_TABLES = {
insane: "🟊",
overjoy: "🟊🟊",
normal: "☆",
normal2: "▽",
insane2: "▼",
stella: "st",
satellite: "sl",
dpNormal: "δ",
dpInsane: "★",
};
const registeredTables: BMSTablesDataset[] = [
{
name: "Insane",
playtype: "7K",
description: "The 7K GENOSIDE insane table.",
url: "http://www.ribbit.xyz/bms/tables/insane_body.json",
prefix: BMS_TABLES.insane,
},
{
name: "Normal",
playtype: "7K",
description: "The 7K GENOSIDE normal table.",
url: "http://www.ribbit.xyz/bms/tables/normal_body.json",
prefix: BMS_TABLES.normal,
},
{
name: "Stella",
playtype: "7K",
description: "The stella table, from Stellaverse.",
url: "https://stellabms.xyz/st/score.json",
prefix: BMS_TABLES.stella,
},
{
name: "Satellite",
playtype: "7K",
description: "The satellite table, from Stellaverse.",
url: "https://stellabms.xyz/sl/score.json",
prefix: BMS_TABLES.satellite,
},
{
name: "Insane2",
playtype: "7K",
description: "A successor to the original insane table, but is generally less consistent.",
url: "http://rattoto10.jounin.jp/js/insane_data.json",
prefix: BMS_TABLES.insane2,
},
{
name: "Normal2",
playtype: "7K",
description: "A successor to the original normal table.",
url: "http://rattoto10.jounin.jp/js/score.json",
prefix: BMS_TABLES.normal2,
},
{
name: "Overjoy",
description: "The overjoy table. Level 1 is roughly equivalent to Insane 20.",
playtype: "7K",
url: "http://lr2.sakura.ne.jp/data/score.json",
prefix: BMS_TABLES.overjoy,
},
// {
// name: "DP Insane",
// description: "The 14K Insane table.",
// playtype: "14K",
// url: "http://dpbmsdelta.web.fc2.com/table/data/insane_data.json",
// },
// {
// name: "DP Normal",
// description: "The 14K Normal table, Sometimes called the delta table.",
// playtype: "14K",
// url: "http://dpbmsdelta.web.fc2.com/table/data/dpdelta_data.json",
// },
// {
// name: "DP Satellite",
// description: "The 14K Satellite table.",
// playtype: "14K",
// url: "https://stellabms.xyz/dp/score.json",
// },
];
export interface TableRes {
table: BMSTablesDataset;
charts: BMSTableChart[];
}
export default async function GetTableData(): Promise<TableRes[]> {
const out = [];
// only 7k supported atm
for (const table of registeredTables.filter((e) => e.playtype === "7K")) {
const charts = await fetch(table.url).then((r) => r.json());
out.push({ table, charts });
}
return out;
}
+5
View File
@@ -0,0 +1,5 @@
import { CreateLogger } from "mei-logger";
const logger = CreateLogger("Sieglinde");
export default logger;
+78
View File
@@ -0,0 +1,78 @@
const TableValueGetters: Record<
"Insane" | "Normal" | "Stella" | "Satellite" | "Insane2" | "Normal2" | "Overjoy",
(x: string) => number | null
> = {
Insane: (c) => {
const n = Number(c);
if (Number.isNaN(n)) {
return null;
}
return n + 12;
},
Insane2: (c) => {
const n = Number(c);
if (Number.isNaN(n)) {
return null;
}
return n + 12;
},
Normal: (c) => {
const n = Number(c);
if (Number.isNaN(n)) {
return null;
}
return n;
},
Normal2: (c) => {
const n = Number(c);
if (Number.isNaN(n)) {
return null;
}
return n;
},
Overjoy: (c) => {
const n = Number(c);
if (Number.isNaN(n)) {
return null;
}
return n + 12 + 20;
},
Satellite: (c) => {
const n = Number(c);
if (Number.isNaN(n)) {
return null;
}
return (
[0, 1.5, 3, 4.5, 6.5, 8.5, 10.5, 12, 13.5, 15.5, 16.5, 17.5, 19].map((e) => e + 12)[
n
] ?? null
);
},
Stella: (c) => {
const n = Number(c);
if (Number.isNaN(n)) {
return null;
}
return (
[19.5, 21, 22, 22.5, 23.5, 24, 24.25, 24.5, 24.75, 25, 25.5, 26, 27, 27.5].map(
(e) => e + 12
)[n] ?? null
);
},
} as const;
export default TableValueGetters;
+42
View File
@@ -0,0 +1,42 @@
import logger from "./logger";
import fs from "fs";
import { Command } from "commander";
import SieglindeV0Calc from "./calc/v0";
import GetTableData from "./fetch-tables";
if (require.main !== module) {
logger.error(`The script main.ts must be invoked directly!`);
process.exit(-1);
}
const program = new Command();
program
.option("-v, --version <The Sieglinde Version to calculate>")
.option("-o, --out <Where to output JSON>");
program.parse(process.argv);
const options = program.opts();
if (!options.out) {
logger.error(`Need to provide an --out parameter for output!`);
process.exit(-1);
}
const version = Number(options.version) ?? 0;
function WriteOut(data: string) {
fs.writeFileSync(options.out, data);
}
(async () => {
if (version === 0) {
const tableInfo = await GetTableData();
const calcData = await Promise.all(tableInfo.map(SieglindeV0Calc));
WriteOut(JSON.stringify(calcData));
} else {
logger.error(`Unsupported/Unknown version ${version}.`);
process.exit(-1);
}
})();
+26
View File
@@ -0,0 +1,26 @@
export interface BMSTablesDataset {
url: string;
name: string;
description: string;
playtype: "7K" | "14K";
prefix: string;
}
export interface BMSTableChart {
title: string;
artist: string;
url: string;
url_diff: string;
md5: string;
sha256: string;
level: string;
}
export interface CalcReturns {
md5: string;
title: string;
ec: number;
hc: number;
ecStr: string;
hcStr: string;
}
+42
View File
@@ -0,0 +1,42 @@
import fetch from "node-fetch";
import { XMLParser } from "fast-xml-parser";
import { BMSTableChart, BMSTablesDataset } from "./types";
import TableValueGetters from "./lookups";
const parser = new XMLParser();
export async function GetScoresForMD5(md5: string) {
const scores = await fetch("https://dream-pro.info/~lavalse/LR2IR/2/getrankingxml.cgi", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `songmd5=${md5}`,
}).then((r) => r.text());
const data = parser.parse(scores);
return data;
}
export function Mean(d: number[]) {
return d.reduce((a, r) => a + r, 0) / d.length;
}
export function GetSigmoidalValue(x: number) {
if (x > 1) {
return 1;
}
// https://math.stackexchange.com/a/2063195
return 0.5 * (1 + Math.sin(x * Math.PI - Math.PI / 2));
}
export function GetBaseline(table: BMSTablesDataset, level: string) {
// @ts-expect-error don't care it's exhaustive
return TableValueGetters[table.name](level);
}
export function GetFString(table: BMSTablesDataset, chart: BMSTableChart) {
return table.prefix + chart.level;
}
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": [
"ES2019"
],
"declaration": true,
"strict": true,
"allowJs": false,
"esModuleInterop": true,
"sourceMap": true,
"outDir": "./js",
"typeRoots": [
"@types",
"node_modules/@types"
],
},
"include": [
"src/**/*.ts",
],
"exclude": [
"node_modules",
]
}