mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-01 19:38:13 +03:00
checkpoint: 4
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* Utilities *specifically* for game/gpt configuration.
|
||||
*/
|
||||
import type { integer } from "../types";
|
||||
import type { ClassInfo } from "../types/_TEMPNAME-game-config-inner";
|
||||
import type { ClassInfo } from "../types/game-config";
|
||||
|
||||
export function NoDecimalPlace(value: number): string {
|
||||
return value.toFixed(0);
|
||||
|
||||
@@ -162,7 +162,19 @@ export const BMS_7K_CONF = {
|
||||
|
||||
additionalMetrics: {
|
||||
...FAST_SLOW_MAXCOMBO,
|
||||
// TODO
|
||||
bp: { type: "INTEGER" },
|
||||
gauge: { type: "DECIMAL" },
|
||||
gaugeHistory: { type: "GRAPH" },
|
||||
epg: { type: "INTEGER" },
|
||||
egr: { type: "INTEGER" },
|
||||
egd: { type: "INTEGER" },
|
||||
ebd: { type: "INTEGER" },
|
||||
epr: { type: "INTEGER" }, //
|
||||
lpg: { type: "INTEGER" },
|
||||
lgr: { type: "INTEGER" },
|
||||
lgd: { type: "INTEGER" },
|
||||
lbd: { type: "INTEGER" },
|
||||
lpr: { type: "INTEGER" },
|
||||
},
|
||||
|
||||
scoreRatingAlgs: {
|
||||
@@ -218,7 +230,7 @@ export const BMS_7K_CONF = {
|
||||
|
||||
orderedJudgements: ["pgreat", "great", "good", "bad", "poor"],
|
||||
|
||||
supportedVersions: [],
|
||||
chartSets: [],
|
||||
|
||||
supportedTierlists: {
|
||||
"sgl-EC": { description: "Sieglinde Easy Clear ratings." },
|
||||
|
||||
@@ -90,7 +90,7 @@ export const CHUNITHM_SINGLE_CONF = {
|
||||
|
||||
orderedJudgements: ["jcrit", "justice", "attack", "miss"],
|
||||
|
||||
supportedVersions: ["paradiselost"],
|
||||
chartSets: ["Paradise Lost"],
|
||||
|
||||
supportedTierlists: {},
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ export const GITADORA_GITA_CONF = {
|
||||
|
||||
orderedJudgements: ["perfect", "great", "good", "ok", "miss"],
|
||||
|
||||
supportedVersions: ["Konaste"],
|
||||
chartSets: ["Konaste"],
|
||||
|
||||
supportedTierlists: {},
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { ExtractMetrics } from "../../types/metrics";
|
||||
import { ClassValue } from "../config-utils";
|
||||
import type {
|
||||
import {
|
||||
INTERNAL_GAME_CONFIG,
|
||||
INTERNAL_GPT_CONFIG as INTERNAL_GPT_CONFIG,
|
||||
INTERNAL_GPT_CONFIG,
|
||||
} from "../../types/internals";
|
||||
import { ClassValue } from "../config-utils";
|
||||
|
||||
export const IIDX_CONF = {
|
||||
defaultPlaytype: "SP",
|
||||
@@ -159,7 +158,7 @@ export const IIDX_SP_CONF = {
|
||||
|
||||
orderedJudgements: ["pgreat", "great", "good", "bad", "poor"],
|
||||
|
||||
supportedVersions: [
|
||||
chartSets: [
|
||||
"3rd Style CS",
|
||||
"4th Style CS",
|
||||
"5th Style CS",
|
||||
|
||||
@@ -92,7 +92,7 @@ export const ITG_STAMINA_CONF = {
|
||||
|
||||
orderedJudgements: ["15ms", "fantastic", "excellent", "great", "decent", "wayoff", "miss"],
|
||||
|
||||
supportedVersions: [],
|
||||
chartSets: [],
|
||||
|
||||
supportedTierlists: {},
|
||||
|
||||
|
||||
@@ -86,14 +86,14 @@ export const JUBEAT_SINGLE_CONF = {
|
||||
|
||||
orderedJudgements: ["perfect", "great", "good", "poor", "miss"],
|
||||
|
||||
supportedVersions: [
|
||||
chartSets: [
|
||||
"jubeat",
|
||||
"ripples",
|
||||
"knit",
|
||||
"copious",
|
||||
"saucer",
|
||||
"prop",
|
||||
"qubell",
|
||||
"Qubell",
|
||||
"clan",
|
||||
"festo",
|
||||
],
|
||||
|
||||
@@ -155,7 +155,7 @@ export const MAIMAI_DX_SINGLE_CONF = {
|
||||
|
||||
orderedJudgements: ["pcrit", "perfect", "great", "good", "miss"],
|
||||
|
||||
supportedVersions: ["universeplus"],
|
||||
chartSets: ["universeplus"],
|
||||
|
||||
supportedTierlists: {},
|
||||
|
||||
|
||||
@@ -67,18 +67,13 @@ export const MUSECA_SINGLE_CONF = {
|
||||
difficultyOrder: ["Green", "Yellow", "Red"],
|
||||
difficultyShorthand: { Green: "G", Yellow: "Y", Red: "R" },
|
||||
defaultDifficulty: "Red",
|
||||
difficultyColours: {
|
||||
Green: COLOUR_SET.green,
|
||||
Yellow: COLOUR_SET.vibrantYellow,
|
||||
Red: COLOUR_SET.red,
|
||||
},
|
||||
},
|
||||
|
||||
supportedClasses: {},
|
||||
|
||||
orderedJudgements: ["critical", "near", "miss"],
|
||||
|
||||
supportedVersions: ["1 + 1/2", "1 + 1/2 Rev. B"],
|
||||
chartSets: ["1 + 1/2", "1 + 1/2 Rev. B"],
|
||||
|
||||
supportedTierlists: {},
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ export const PMS_CONTROLLER_CONF = {
|
||||
|
||||
orderedJudgements: ["cool", "great", "good", "bad", "poor"],
|
||||
|
||||
supportedVersions: [],
|
||||
chartSets: [],
|
||||
|
||||
supportedTierlists: {
|
||||
"sgl-EC": { description: "Sieglinde Easy Clear ratings." },
|
||||
|
||||
@@ -110,7 +110,7 @@ export const POPN_9B_CONF = {
|
||||
|
||||
orderedJudgements: ["cool", "great", "good", "bad"],
|
||||
|
||||
supportedVersions: ["peace", "kaimei"],
|
||||
chartSets: ["peace", "Kaimei Riddles"],
|
||||
|
||||
supportedTierlists: {},
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ export const SDVX_SINGLE_CONF = {
|
||||
|
||||
orderedJudgements: ["critical", "near", "miss"],
|
||||
|
||||
supportedVersions: [
|
||||
chartSets: [
|
||||
"BOOTH",
|
||||
"infinite infection",
|
||||
"GRAVITY WARS",
|
||||
|
||||
@@ -78,7 +78,7 @@ export const USC_CONTROLLER_CONF = {
|
||||
|
||||
orderedJudgements: ["critical", "near", "miss"],
|
||||
|
||||
supportedVersions: [],
|
||||
chartSets: [],
|
||||
|
||||
supportedTierlists: {},
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ export const WACCA_SINGLE_CONF = {
|
||||
|
||||
orderedJudgements: ["marvelous", "great", "good", "miss"],
|
||||
|
||||
supportedVersions: ["REVERSE"],
|
||||
chartSets: ["REVERSE"],
|
||||
|
||||
supportedTierlists: {},
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
import { FormatSieglindeBMS, FormatSieglindePMS, FormatMaimaiDXRating } from "./config-utils";
|
||||
import { COLOUR_SET } from "../constants/colour-set";
|
||||
import {
|
||||
FormatSieglindeBMS,
|
||||
FormatSieglindePMS,
|
||||
FormatMaimaiDXRating,
|
||||
} from "./config-utils";
|
||||
import type { GPTTierlists } from "..";
|
||||
import type { ClassInfo } from "./game-classes";
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { GetGameConfig } from "./old-config";
|
||||
import type { IDStrings, GPTSupportedVersions, Game, Playtype } from "../types";
|
||||
|
||||
export type Versions = {
|
||||
[I in IDStrings]: {
|
||||
[K in GPTSupportedVersions[I]]: string;
|
||||
};
|
||||
};
|
||||
|
||||
const prettyGitadoraVersions: Versions["gitadora:Dora"] = {
|
||||
konaste: "Konaste",
|
||||
};
|
||||
|
||||
// WHEN YOU UPDATE THIS, **MAKE SURE** YOU UPDATE CONFIG.TS ACCORDINGLY.
|
||||
export const PrettyVersions: Versions = {
|
||||
"popn:9B": { peace: "peace", kaimei: "Kaimei Riddles" },
|
||||
"bms:7K": {},
|
||||
"bms:14K": {},
|
||||
"chunithm:Single": {
|
||||
paradiselost: "Paradise Lost",
|
||||
},
|
||||
"gitadora:Gita": prettyGitadoraVersions,
|
||||
"gitadora:Dora": prettyGitadoraVersions,
|
||||
"museca:Single": {
|
||||
1.5: "1 + 1/2",
|
||||
"1.5-b": "1 + 1/2 Rev. B",
|
||||
},
|
||||
"sdvx:Single": {
|
||||
booth: "BOOTH",
|
||||
inf: "Infinite Infection",
|
||||
gw: "GRAVITY WARS",
|
||||
heaven: "HEAVENLY HAVEN",
|
||||
vivid: "VIVID WAVE",
|
||||
exceed: "EXCEED GEAR",
|
||||
konaste: "Konaste",
|
||||
},
|
||||
"usc:Keyboard": {},
|
||||
"usc:Controller": {},
|
||||
"wacca:Single": {
|
||||
reverse: "REVERSE",
|
||||
},
|
||||
"jubeat:Single": {
|
||||
festo: "festo",
|
||||
clan: "clan",
|
||||
qubell: "Qubell",
|
||||
saucer: "saucer",
|
||||
prop: "prop",
|
||||
copious: "copious",
|
||||
knit: "knit",
|
||||
ripples: "ripples",
|
||||
jubeat: "jubeat",
|
||||
},
|
||||
"pms:Controller": {},
|
||||
"pms:Keyboard": {},
|
||||
"itg:Stamina": {},
|
||||
"maimaidx:Single": {
|
||||
universeplus: "UNiVERSE PLUS",
|
||||
},
|
||||
};
|
||||
|
||||
export function PrettyFormatGameVersion<I extends IDStrings>(
|
||||
idString: I,
|
||||
version: GPTSupportedVersions[I]
|
||||
) {
|
||||
// lol
|
||||
const [game] = idString.split(":") as [Game, Playtype];
|
||||
|
||||
const ver = PrettyVersions[idString][version];
|
||||
|
||||
const gameConfig = GetGameConfig(game);
|
||||
|
||||
return `${gameConfig.name} (${ver})`;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { CalculateESD, ESDCompare, PercentCompare } from "./esd";
|
||||
import { GetGamePTConfig } from "../config/old-config";
|
||||
import t from "tap";
|
||||
|
||||
function approx(t: Tap.Test, given: number, recieved: number, acc = 0.01) {
|
||||
if (Math.abs(given - recieved) < acc) {
|
||||
return t.pass();
|
||||
}
|
||||
|
||||
return t.fail(`Expected ${given} to be within ${acc} of ${recieved}.`);
|
||||
}
|
||||
|
||||
const gptConfig = GetGamePTConfig("iidx", "SP");
|
||||
|
||||
if (!gptConfig.supportsESD) {
|
||||
throw new Error(`IIDX:SP no longer supports esd, these tests won't work.`);
|
||||
}
|
||||
|
||||
const judgementWindows = gptConfig.judgementWindows;
|
||||
|
||||
t.test("#CalculateESD", (t) => {
|
||||
const jdg = 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 = 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();
|
||||
});
|
||||
@@ -1,192 +0,0 @@
|
||||
/**
|
||||
* Cumulative Distribution Function
|
||||
* 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)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Error Function
|
||||
* https://en.wikipedia.org/wiki/Error_function
|
||||
*/
|
||||
function erf(x: number) {
|
||||
// save the sign of x
|
||||
const sign = x >= 0 ? 1 : -1;
|
||||
|
||||
const absX = 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 * absX);
|
||||
const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-absX * absX);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// 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: Array<ESDJudgementFormat>,
|
||||
stddev: number,
|
||||
largestValue: number
|
||||
) {
|
||||
let lastJudgeMSBorder = 0;
|
||||
let prbSum = 0;
|
||||
|
||||
for (const judge of judgements) {
|
||||
const nVal = CDFBetween(lastJudgeMSBorder, judge.msBorder, MEAN, stddev ** 2) * judge.value;
|
||||
|
||||
lastJudgeMSBorder = judge.msBorder;
|
||||
prbSum = prbSum + nVal;
|
||||
}
|
||||
|
||||
prbSum = prbSum / largestValue;
|
||||
|
||||
return prbSum;
|
||||
}
|
||||
|
||||
const ACCEPTABLE_ERROR = 0.001;
|
||||
const MAX_ITERATIONS = 50;
|
||||
|
||||
export interface ESDJudgementFormat {
|
||||
name: string;
|
||||
msBorder: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function CalculateESD(
|
||||
judgements: Array<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 (largestValue === undefined) {
|
||||
throw new Error(`Empty array of judgements passed?`);
|
||||
}
|
||||
|
||||
// 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 new Error(`(ESD-JS) Did not reach value within MAX_ITERATIONS (${MAX_ITERATIONS})`);
|
||||
}
|
||||
|
||||
return estSD;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
let besdc = BASE_CASE - esdc;
|
||||
|
||||
if (inv) {
|
||||
besdc = besdc * -1;
|
||||
}
|
||||
|
||||
return besdc * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts two percents to ESD, then runs ESDCompare.
|
||||
*/
|
||||
export function PercentCompare(
|
||||
judgements: Array<ESDJudgementFormat>,
|
||||
baseP: number,
|
||||
compareP: number,
|
||||
cdeg = 1
|
||||
): number {
|
||||
const e1 = CalculateESD(judgements, baseP);
|
||||
const e2 = CalculateESD(judgements, compareP);
|
||||
|
||||
return ESDCompare(e1, e2, cdeg);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Types related to game configuration. These are used by our `internal` game config
|
||||
// types
|
||||
|
||||
export interface RatingAlgorithmConfig {
|
||||
/**
|
||||
* Write a short descrption for this rating algorithm.
|
||||
@@ -12,14 +15,6 @@ export interface RatingAlgorithmConfig {
|
||||
formatter?: (value: number) => string;
|
||||
}
|
||||
|
||||
export interface TierlistConfig {
|
||||
/**
|
||||
* Write a short description explaining what this tierlist is and what the values
|
||||
* generally mean.
|
||||
*/
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ClassInfo {
|
||||
display: string;
|
||||
id: string;
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { GAME_CONFIGS, GAME_PT_CONFIGS } from "../config/config";
|
||||
import type { FixedDifficulties } from "./_TEMPNAME-game-config-inner";
|
||||
import type { MatchTypes } from "./batch-manual";
|
||||
import type { ClassConfig, FixedDifficulties, RatingAlgorithmConfig } from "./game-config";
|
||||
import type { ExtractEnumMetricNames, ExtractMetrics } from "./metrics";
|
||||
import type { ExtractArrayElementType } from "./utils";
|
||||
|
||||
@@ -81,6 +82,10 @@ export type Judgements = {
|
||||
[G in GPTString]: ExtractArrayElementType<typeof GAME_PT_CONFIGS[G]["orderedJudgements"]>;
|
||||
};
|
||||
|
||||
export type ChartSets = {
|
||||
[G in GPTString]: ExtractArrayElementType<typeof GAME_PT_CONFIGS[G]["chartSets"]>;
|
||||
};
|
||||
|
||||
export type ScoreRatingAlgorithms = {
|
||||
[G in GPTString]: keyof typeof GAME_PT_CONFIGS[G]["scoreRatingAlgs"];
|
||||
};
|
||||
@@ -186,7 +191,7 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
|
||||
* The idea of additionalMetrics allow us to store useful metrics about scores
|
||||
* without necessitating that they exist on arrival. Incredibly convenient.
|
||||
*/
|
||||
additionalMetrics: Record<string, ScoreMetric>;
|
||||
additionalMetrics: AdditionalMetrics[GPT];
|
||||
|
||||
/**
|
||||
* What rating algorithms may a score have attached onto it for this GPT?
|
||||
@@ -195,7 +200,7 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
|
||||
* server config. By defining them here, the typesystem will enforce that you
|
||||
* implement them elsewhere.
|
||||
*/
|
||||
scoreRatingAlgs: Record<string, RatingAlgorithmConfig>;
|
||||
scoreRatingAlgs: Record<ScoreRatingAlgorithms[GPT], RatingAlgorithmConfig>;
|
||||
|
||||
/**
|
||||
* What rating algorithms may a session have attached onto it for this GPT?
|
||||
@@ -204,7 +209,7 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
|
||||
* server config. By defining them here, the typesystem will enforce that you
|
||||
* implement them elsewhere.
|
||||
*/
|
||||
sessionRatingAlgs: Record<string, RatingAlgorithmConfig>;
|
||||
sessionRatingAlgs: Record<SessionRatingAlgorithms[GPT], RatingAlgorithmConfig>;
|
||||
|
||||
/**
|
||||
* What rating algorithms may a profile have attached onto it for this GPT?
|
||||
@@ -224,7 +229,7 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
|
||||
* server config. By defining them here, the typesystem will enforce that you
|
||||
* implement them elsewhere.
|
||||
*/
|
||||
profileRatingAlgs: Record<string, RatingAlgorithmConfig>;
|
||||
profileRatingAlgs: Record<ProfileRatingAlgorithms[GPT], RatingAlgorithmConfig>;
|
||||
|
||||
/**
|
||||
* What classes may a profile have attached onto it for this GPT?
|
||||
@@ -242,21 +247,21 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
|
||||
*
|
||||
* @note This should be one of the keys in scoreRatingAlgs.
|
||||
*/
|
||||
defaultScoreRatingAlg: string;
|
||||
defaultScoreRatingAlg: ScoreRatingAlgorithms[GPT];
|
||||
|
||||
/**
|
||||
* What's the default session rating algorithm for this GPT?
|
||||
*
|
||||
* @note This should be one of the keys in sessionRatingAlgs.
|
||||
*/
|
||||
defaultSessionRatingAlg: string;
|
||||
defaultSessionRatingAlg: SessionRatingAlgorithms[GPT];
|
||||
|
||||
/**
|
||||
* What's the default profile rating algorithm for this GPT1?
|
||||
*
|
||||
* @note This should be one of the keys in sessionRatingAlgs.
|
||||
*/
|
||||
defaultProfileRatingAlg: string;
|
||||
defaultProfileRatingAlg: ProfileRatingAlgorithms[GPT];
|
||||
|
||||
/**
|
||||
* How does this GPT handle difficulties?
|
||||
@@ -276,39 +281,27 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
|
||||
*
|
||||
* These should be ordered from **best to worst**.
|
||||
*/
|
||||
orderedJudgements: ReadonlyArray<string>;
|
||||
orderedJudgements: ReadonlyArray<Judgements[GPT]>;
|
||||
|
||||
/**
|
||||
* What versions do we support for this GPT?
|
||||
* What sets of charts do we have for this GPT?
|
||||
*
|
||||
* Versions are the way tachi disambiguates cases (typically in arcade games) where
|
||||
* Chart Sets are the way tachi disambiguates cases (typically in arcade games) where
|
||||
* a chart is modified.
|
||||
* For example, Rising in the Sun
|
||||
* (https://remywiki.com/Rising_in_the_Sun(original_mix))
|
||||
* was removed in IIDX 21 ("spada"), and revived in IIDX 27 with entirely different
|
||||
* was removed in IIDX 21, and revived in IIDX 27 with entirely different
|
||||
* charts. Although these charts are completely different,
|
||||
* they use the same song and difficulty
|
||||
* so Rising in the Sun SP ANOTHER could mean two things!.
|
||||
*
|
||||
* We need to handle these cases, so we disambiguate by attaching "versions" onto
|
||||
* every chart. These "versions" indicate what sets of chart states they appeared in
|
||||
* for this GPT. Then, when a score is coming in, it can indicate what version this
|
||||
* score was from. That way, we can make sure they resolve to the right chart.
|
||||
* We need to handle these cases, so we disambiguate by attaching "chart sets" onto
|
||||
* every chart. These "chart sets" indicate what sets of chart states they
|
||||
* appeared in for this GPT. Then, when a score is coming in, it can indicate what
|
||||
* chartSet this score was on. That way, we can make sure they resolve to the right
|
||||
* chart.
|
||||
*/
|
||||
supportedVersions: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* What tierlists does this GPT have?
|
||||
*
|
||||
* Tierlists are a way of attaching custom rating values onto a chart.
|
||||
* Unlike "chart.data", which may be overloaded with anything, defined tierlists
|
||||
* *must* fit a known schema, allowing the UI to visually render any tierlists
|
||||
* defined here.
|
||||
*
|
||||
* This feature is a bit redundant given "chart.data", and should be further
|
||||
* investigated. My bad.
|
||||
*/
|
||||
supportedTierlists: Record<string, TierlistConfig>;
|
||||
chartSets: ReadonlyArray<ChartSets[GPT]>;
|
||||
|
||||
/**
|
||||
* What "matchTypes" should this game support for batch-manual imports? This
|
||||
@@ -317,8 +310,3 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
|
||||
*/
|
||||
supportedMatchTypes: ReadonlyArray<MatchTypes>;
|
||||
}
|
||||
|
||||
const gptConf: ExtractEnumMetricNames<DerivedMetrics["iidx:SP"]> = {
|
||||
lamp: "a",
|
||||
asdf: 213,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type {
|
||||
RatingAlgorithmConfig,
|
||||
ClassConfig,
|
||||
DifficultyConfig,
|
||||
TierlistConfig,
|
||||
} from "./_TEMPNAME-game-config-inner";
|
||||
import type { MatchTypes } from "./batch-manual";
|
||||
import type { RatingAlgorithmConfig, ClassConfig, DifficultyConfig } from "./game-config";
|
||||
import type { ScoreMetric } from "./metrics";
|
||||
|
||||
/**
|
||||
@@ -15,197 +10,37 @@ import type { ScoreMetric } from "./metrics";
|
||||
*
|
||||
* @see {GamePTConfig} for the intended-to-use type. This is an *outline* for a GPT
|
||||
* config, and has significantly less typesafety.
|
||||
*
|
||||
* Documentation for this type can be found in `game-support.ts`, which has the
|
||||
* GamePTConfig type, which actually has documentation.
|
||||
*/
|
||||
export type INTERNAL_GPT_CONFIG = Readonly<{
|
||||
/**
|
||||
* What metrics **must** be provided in order for this score to be usable by
|
||||
* Tachi?
|
||||
*
|
||||
* This is intended for things like Score, Lamp, etc. Things that quite fundamentally
|
||||
* *are* the metrics of the score.
|
||||
*/
|
||||
providedMetrics: Record<string, ScoreMetric>;
|
||||
|
||||
/**
|
||||
* What metrics do we want to exist on score documents, but don't need to be
|
||||
* provided?
|
||||
*
|
||||
* In simple terms, all of these metrics **MUST** be derivable by a DETERMINISTIC
|
||||
* function of f(mandatoryMetrics, chartThisScoreWasOn).
|
||||
*
|
||||
* This is for convenience/efficiency mainly. A good example would be "percent" for
|
||||
* IIDX. Technically, we could recalculate it every single time we want to display
|
||||
* it by dividing score by chart.data.notecount * 2, but that's horrendously
|
||||
* inefficient.
|
||||
*
|
||||
* Furthermore, since these things are derived deterministically, they only ever
|
||||
* need to be recalculated in extreme circumstances (an IIDX chart has changed its
|
||||
* notecounts!!!). If mandatory metrics were to change, it's just now a different
|
||||
* score.
|
||||
*
|
||||
* Another good example would be "Grade" for most games, as a grade is often just
|
||||
* cutoffs applied on score values.
|
||||
*/
|
||||
derivedMetrics: Record<string, ScoreMetric>;
|
||||
|
||||
/**
|
||||
* What's the default metric for this GPT?
|
||||
*
|
||||
* This will be used to order leaderboard rankings.
|
||||
*
|
||||
* @note This **MUST** be one of the mandatory or derived keys.
|
||||
*/
|
||||
defaultMetric: string;
|
||||
|
||||
/**
|
||||
* What's the preferred default enum for this GPT?
|
||||
*
|
||||
* Enum types are used across the UI (think folder breakdown charts), and the game
|
||||
* should generally declare a default.
|
||||
*
|
||||
* @note This **MUST** be one of the mandatory or derived keys.
|
||||
*/
|
||||
preferredDefaultEnum: string;
|
||||
|
||||
/**
|
||||
* What metrics *can* we store about scores, but don't necessarily *need*?
|
||||
*
|
||||
* Of course, in a perfect world we'd store all the metrics always all the time!
|
||||
* But a lot of import methods (eamusement CSV, etc) would be filtered out by
|
||||
* mandating the existence of a lot of these metrics.
|
||||
*
|
||||
* The idea of additionalMetrics allow us to store useful metrics about scores
|
||||
* without necessitating that they exist on arrival. Incredibly convenient.
|
||||
*/
|
||||
additionalMetrics: Record<string, ScoreMetric>;
|
||||
|
||||
/**
|
||||
* What rating algorithms may a score have attached onto it for this GPT?
|
||||
*
|
||||
* @note The implementations for these rating algorithms are handled in the
|
||||
* server config. By defining them here, the typesystem will enforce that you
|
||||
* implement them elsewhere.
|
||||
*/
|
||||
scoreRatingAlgs: Record<string, RatingAlgorithmConfig>;
|
||||
|
||||
/**
|
||||
* What rating algorithms may a session have attached onto it for this GPT?
|
||||
*
|
||||
* @note The implementations for these rating algorithms are handled in the
|
||||
* server config. By defining them here, the typesystem will enforce that you
|
||||
* implement them elsewhere.
|
||||
*/
|
||||
sessionRatingAlgs: Record<string, RatingAlgorithmConfig>;
|
||||
|
||||
/**
|
||||
* What rating algorithms may a profile have attached onto it for this GPT?
|
||||
*
|
||||
* @note This is **SPECIFICALLY** for numeric, calculatable metrics. This means
|
||||
* that the metric *must* be calculatable *at all times* from the set of all
|
||||
* scores this user has on this GPT.
|
||||
*
|
||||
* This is intended for numeric, continous data.
|
||||
* If you want to store something with a fixed set of values, such as a user's
|
||||
* "rating colour", use `supportedClasses`.
|
||||
*
|
||||
* If you want to store something that cannot be derived from the user's scores,
|
||||
* such as their "Dan", use `supportedClasses`.
|
||||
*
|
||||
* @note The implementations for these rating algorithms are handled in the
|
||||
* server config. By defining them here, the typesystem will enforce that you
|
||||
* implement them elsewhere.
|
||||
*/
|
||||
profileRatingAlgs: Record<string, RatingAlgorithmConfig>;
|
||||
|
||||
/**
|
||||
* What classes may a profile have attached onto it for this GPT?
|
||||
*
|
||||
* Classes are a *fixed*, *ordered* set of values.
|
||||
* They may be a function of existing state (like "rating colours", where a user
|
||||
* gets a new discrete colour when they go up certain ratings),
|
||||
* or they may be provided by score imports, such as "dans", which cannot be
|
||||
* derived from a player's scores or profile ratings.
|
||||
*/
|
||||
supportedClasses: Record<string, ClassConfig>;
|
||||
|
||||
/**
|
||||
* What's the default score rating algorithm for this GPT?
|
||||
*
|
||||
* @note This should be one of the keys in scoreRatingAlgs.
|
||||
*/
|
||||
defaultScoreRatingAlg: string;
|
||||
|
||||
/**
|
||||
* What's the default session rating algorithm for this GPT?
|
||||
*
|
||||
* @note This should be one of the keys in sessionRatingAlgs.
|
||||
*/
|
||||
defaultSessionRatingAlg: string;
|
||||
|
||||
/**
|
||||
* What's the default profile rating algorithm for this GPT1?
|
||||
*
|
||||
* @note This should be one of the keys in sessionRatingAlgs.
|
||||
*/
|
||||
defaultProfileRatingAlg: string;
|
||||
|
||||
/**
|
||||
* How does this GPT handle difficulties?
|
||||
*
|
||||
* "Difficulties" are used to allow one song to have multiple charts. Some games
|
||||
* may have a known set of possible difficulties, such as "Easy", "Normal" and
|
||||
* "Hard".
|
||||
*
|
||||
* Other games may have an unknown set of possible difficulties, such as osu!
|
||||
* allowing any string (as long as its unique.)
|
||||
*
|
||||
*/
|
||||
difficultyConfig: DifficultyConfig;
|
||||
|
||||
/**
|
||||
* What judgements does this GPT have? These are typically timing-window names.
|
||||
*
|
||||
* These should be ordered from **best to worst**.
|
||||
*/
|
||||
orderedJudgements: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* What versions do we support for this GPT?
|
||||
*
|
||||
* Versions are the way tachi disambiguates cases (typically in arcade games) where
|
||||
* a chart is modified.
|
||||
* For example, Rising in the Sun
|
||||
* (https://remywiki.com/Rising_in_the_Sun(original_mix))
|
||||
* was removed in IIDX 21 ("spada"), and revived in IIDX 27 with entirely different
|
||||
* charts. Although these charts are completely different,
|
||||
* they use the same song and difficulty
|
||||
* so Rising in the Sun SP ANOTHER could mean two things!.
|
||||
*
|
||||
* We need to handle these cases, so we disambiguate by attaching "versions" onto
|
||||
* every chart. These "versions" indicate what sets of chart states they appeared in
|
||||
* for this GPT. Then, when a score is coming in, it can indicate what version this
|
||||
* score was from. That way, we can make sure they resolve to the right chart.
|
||||
*/
|
||||
supportedVersions: ReadonlyArray<string>;
|
||||
chartSets: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* What tierlists does this GPT have?
|
||||
*
|
||||
* Tierlists are a way of attaching custom rating values onto a chart.
|
||||
* Unlike "chart.data", which may be overloaded with anything, defined tierlists
|
||||
* *must* fit a known schema, allowing the UI to visually render any tierlists
|
||||
* defined here.
|
||||
*
|
||||
* This feature is a bit redundant given "chart.data", and should be further
|
||||
* investigated. My bad.
|
||||
*/
|
||||
supportedTierlists: Record<string, TierlistConfig>;
|
||||
|
||||
/**
|
||||
* What "matchTypes" should this game support for batch-manual imports? This
|
||||
* allows us to disable things like "songTitle" resolutions for games like BMS,
|
||||
* where song titles are absolutely not guaranteed to be unique.
|
||||
*/
|
||||
supportedMatchTypes: ReadonlyArray<MatchTypes>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -69,13 +69,9 @@ export type ExtractMetricType<M extends ScoreMetric> = M extends DecimalScoreMet
|
||||
* ExtractEnumMetricNames<{ score: { type: "INTEGER" } }, grade: { type: "ENUM" ... }>
|
||||
* would return a type of "grade"
|
||||
*/
|
||||
export type ExtractEnumMetricNames<R extends Record<string, ScoreMetric>> = Omit<
|
||||
R,
|
||||
keyof {
|
||||
// vvvv this is stupid
|
||||
[K in keyof R]: R[K] extends EnumScoreMetric ? never : K;
|
||||
}
|
||||
>;
|
||||
export type ExtractEnumMetricNames<R extends Record<string, ScoreMetric>> = {
|
||||
[K in keyof R]: R[K] extends EnumScoreMetric ? K : never;
|
||||
}[keyof R];
|
||||
|
||||
type PossibleMetrics = ExtractMetricType<ScoreMetric>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user