mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-07 22:38:02 +03:00
Sieglinde Initial Draft
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
js
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/node_modules
|
||||
|
||||
.nyc_output
|
||||
coverage
|
||||
|
||||
/js
|
||||
|
||||
.pnpm-debug
|
||||
logs
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+2981
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CreateLogger } from "mei-logger";
|
||||
|
||||
const logger = CreateLogger("Sieglinde");
|
||||
|
||||
export default logger;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user