checkpoint: 20

This commit is contained in:
zkldi
2023-01-19 16:39:37 +00:00
parent a576bc6438
commit d385c35eb7
16 changed files with 272 additions and 131 deletions
+1
View File
@@ -266,6 +266,7 @@ export const BMS_7K_CONF = {
export const BMS_14K_CONF = {
...BMS_7K_CONF,
classes: {
genocideDan: { type: "PROVIDED", values: BMSGenocideDans },
stslDan: { type: "PROVIDED", values: BMSDPSlDans },
+72 -15
View File
@@ -7,6 +7,19 @@ import type {
ConfProvidedMetrics,
} from "./game-config";
// WARNING! THIS FILE IS VERY COMPLEX. THERE IS SOME HIGH-TIER TYPESCRIPT MAGIC GOING
// ON HERE.
// The rough outline is that Tachi allows games to have up to 5 kinds of metrics
// "DECIMAL" | "INTEGER" | "ENUM" | "GRAPH" and "NULLABLE_GRAPH".
// Metrics themselves then come in three groups:
// - Provided: They **must** be provided for a score import to be usable
// - Derived: They **will** be put on a score document, but are a function
// of the provided metrics and the chart this score is on (i.e. Grades for IIDX)
// - Additional: They **may** exist. We want to store them if they exist, but don't
// mandate their existence (i.e. fast/slow/maxCombo)
export type DecimalMetricValidator<GPT extends GPTString> = (
metric: number,
chart: ChartDocument<GPT>
@@ -66,6 +79,8 @@ export type ConfScoreMetric =
/**
* When we store and interact with enum values, we want them to be both strings
* and integers.
*
* This is used by score documents that are stored in the DB.
*/
export interface EnumValue<S extends string> {
string: S;
@@ -74,9 +89,9 @@ export interface EnumValue<S extends string> {
/**
* Given a Metric Type, turn it into its evaluated form. An IntegerScoreMetric
* becomes an integer, etc.
* becomes an integer and an enum becomes an EnumValue.
*/
export type ExtractMetricType<M extends ConfScoreMetric> = M extends ConfDecimalScoreMetric
export type ExtractMetricValue<M extends ConfScoreMetric> = M extends ConfDecimalScoreMetric
? number
: M extends ConfIntegerScoreMetric
? integer
@@ -88,6 +103,26 @@ export type ExtractMetricType<M extends ConfScoreMetric> = M extends ConfDecimal
? Array<number | null>
: never;
/**
* Given a Metric Type, turn it into its evaluated form with the caveat that
* enums become strings instead of EnumValue<string>.
*
* This is used on the intermediate score importing step, where ENUM metrics aren't
* fully realised. We'd be working with { lamp: "FAILED" } instead of
* { lamp: { string: "FAILED", index: 1 }}.
*/
export type DryExtractMetricType<M extends ConfScoreMetric> = M extends ConfDecimalScoreMetric
? number
: M extends ConfIntegerScoreMetric
? integer
: M extends ConfEnumScoreMetric<infer V>
? V
: M extends ConfGraphScoreMetric
? Array<number>
: M extends ConfNullableGraphScoreMetric
? Array<number | null>
: never;
/**
* Extract all the names of enum types from this record of score metrics.
*
@@ -109,6 +144,9 @@ export type ExtractEnumMetricNames<R extends Record<string, ConfScoreMetric>> =
[K in keyof R]: R[K] extends ConfEnumScoreMetric<infer _> ? K : never;
}[keyof R];
/**
* What are all the metrics available for this GPT?
*/
export type AllMetrics = {
[GPT in GPTString]: ConfDerivedMetrics[GPT] &
ConfOptionalMetrics[GPT] &
@@ -125,28 +163,47 @@ export type GetEnumValue<
MetricName extends ExtractEnumMetricNames<AllMetrics[GPT]>
> = AllMetrics[GPT][MetricName] extends ConfEnumScoreMetric<infer EnumValues> ? EnumValues : never;
type PossibleMetrics = ExtractMetricType<ConfScoreMetric>;
/**
* Turn a record of ScoreMetrics into their actual literal values.
* Turn a record of ConfigScoreMetrics into their actual literal values.
*
* @example ExtractMetrics<{ score: IntegerScoreMetric; lamp: EnumScoreMetric<"FAILED"|"CLEAR"> }>
* @example ExtractMetrics<{
* score: ConfIntegerScoreMetric; lamp: ConfEnumScoreMetric<"FAILED"|"CLEAR">
* }>
* will equal
* { score: integer; lamp: "FAILED" | "CLEAR" }
* { score: integer; lamp: { string: "FAILED" | "CLEAR "..., index: number } }
*/
export type ExtractMetrics<R extends Record<string, ConfScoreMetric>> = {
[K in keyof R]: ExtractMetricType<R[K]>;
[K in keyof R]: ExtractMetricValue<R[K]>;
};
/**
* Turn a record of score metrics in to their "dry" values. These are used by dry
* scores and consist of one change:
*
* Enums would normally be { string: "FAILED" | ..., index: number }. In dry extraction
* they are extracted to just "FAILED" | ...
*
* This is because dry scores don't fill out these values.
*/
export type DryExtractMetrics<R extends Record<string, ConfScoreMetric>> = {
[K in keyof R]: DryExtractMetricType<R[K]>;
};
// We want some signatures for implementing metric "derivers".
// This complex type nonsense effectively gives us a typesafe form for:
// MetricDeriver<"iidx:SP", number>
// (metrics: IIDXSPMetrics, chart: Chart<"iidx:SP"> ) => number
export type DerivedMetricValue = Array<number | null> | Array<number> | integer | number | string;
export type MetricValue = ExtractMetricValue<ConfScoreMetric>;
export type MetricDeriver<
M extends Record<string, PossibleMetrics>,
GPT extends GPTString,
// possible return values
// from a derived fn
V extends DerivedMetricValue = DerivedMetricValue
> = (mandatoryMetrics: M, chart: ChartDocument<GPT>) => V;
> = (mandatoryMetrics: DryExtractMetrics<ConfProvidedMetrics[GPT]>, chart: ChartDocument<GPT>) => V;
/**
* A function that will derive this metric, given a function of other metrics and
@@ -155,14 +212,14 @@ export type MetricDeriver<
export type ScoreMetricDeriver<M extends ConfScoreMetric, GPT extends GPTString> =
// graph score metrics correspond to Array<number>
M extends ConfGraphScoreMetric
? MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, Array<number>>
? MetricDeriver<GPT, Array<number>>
: // nullable graphs correspond to Array<number | null>
M extends ConfNullableGraphScoreMetric
? MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, Array<number | null>>
? MetricDeriver<GPT, Array<number | null>>
: // enums correspond to their string unions ("FAILED"|"CLEAR")
M extends ConfEnumScoreMetric<infer V>
? MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, V>
? MetricDeriver<GPT, V>
: // the other two are obvious
M extends ConfIntegerScoreMetric
? MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, integer>
: MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, number>;
? MetricDeriver<GPT, integer>
: MetricDeriver<GPT, number>;
@@ -0,0 +1,26 @@
import type { CalculateDataFunction, GPTScoreCalculators } from "../types";
const SGLCalc: CalculateDataFunction<"bms:7K" | "bms:14K"> = (dryScore, chart) => {
const ecValue = chart.data.sglEC ?? 0;
const hcValue = chart.data.sglHC ?? 0;
switch (dryScore.scoreData.lamp) {
case "FULL COMBO":
case "EX HARD CLEAR":
case "HARD CLEAR":
return Math.max(hcValue, ecValue);
case "CLEAR":
case "EASY CLEAR":
return ecValue;
default:
return 0;
}
};
export const BMS_7K_SCORE_CALC: GPTScoreCalculators["bms:7K"] = {
sieglinde: SGLCalc,
};
export const BMS_14K_SCORE_CALC: GPTScoreCalculators["bms:14K"] = {
sieglinde: SGLCalc,
};
@@ -0,0 +1,6 @@
import { CHUNITHMRating } from "rg-stats";
import type { GPTScoreCalculators } from "../types";
export const CHUNITHM_SCORE_CALC: GPTScoreCalculators["chunithm:Single"] = {
rating: (dryScore, chart) => CHUNITHMRating.calculate(dryScore.scoreData.score, chart.levelNum),
};
@@ -0,0 +1,61 @@
import { PoyashiBPI } from "rg-stats";
import type { GPTScoreCalculators, CalculateDataFunction } from "../types";
const BPICalc: CalculateDataFunction<"iidx:DP" | "iidx:SP"> = (dryScore, chart) => {
if (chart.data.kaidenAverage === null || chart.data.worldRecord === null) {
return null;
}
return PoyashiBPI.calculate(
dryScore.scoreData.score,
chart.data.kaidenAverage,
chart.data.worldRecord,
chart.data.notecount * 2,
chart.data.bpiCoefficient
);
};
export const IIDX_SP_SCORE_CALC: GPTScoreCalculators["iidx:SP"] = {
BPI: BPICalc,
ktLampRating: (dryScore, chart) => {
// if chart has no ncValue, use the rating of the chart instead.
const ncValue = chart.data.ncTier?.value ?? chart.levelNum;
// if hc < nc, hcValue = ncValue
// this means you can't lose score from getting a better clear.
// same for exhc < hc.
const hcValue = Math.max(chart.data.hcTier?.value ?? 0, ncValue);
const exhcValue = Math.max(chart.data.exhcTier?.value ?? 0, hcValue);
switch (dryScore.scoreData.lamp) {
case "FULL COMBO":
case "EX HARD CLEAR":
return exhcValue;
case "HARD CLEAR":
return hcValue;
case "CLEAR":
return ncValue;
default:
return 0;
}
},
};
export const IIDX_DP_SCORE_CALC: GPTScoreCalculators["iidx:DP"] = {
BPI: BPICalc,
ktLampRating: (dryScore, chart) => {
// if chart has no tier, use the rating of the chart instead.
const ecValue = chart.data.dpTier?.value ?? chart.levelNum;
switch (dryScore.scoreData.lamp) {
case "FULL COMBO":
case "EX HARD CLEAR":
case "HARD CLEAR":
case "CLEAR":
case "EASY CLEAR":
return ecValue;
default:
return 0;
}
},
};
@@ -0,0 +1,7 @@
import { Jubility } from "rg-stats";
import type { GPTScoreCalculators } from "../types";
export const JUBEAT_SCORE_CALC: GPTScoreCalculators["jubeat:Single"] = {
jubility: (dryScore, chart) =>
Jubility.calculate(dryScore.scoreData.score, dryScore.scoreData.musicRate, chart.levelNum),
};
@@ -0,0 +1,12 @@
import { PopnClearMedalToLamp } from "../../derivers/games/popn";
import { PopnClassPoints } from "rg-stats";
import type { GPTScoreCalculators } from "../types";
export const POPN_9B_SCORE_CALC: GPTScoreCalculators["popn:9B"] = {
classPoints: (dryScore, chart) =>
PopnClassPoints.calculate(
dryScore.scoreData.score,
PopnClearMedalToLamp(dryScore.scoreData.clearMedal),
chart.levelNum
),
};
@@ -0,0 +1,6 @@
import { WACCARate } from "rg-stats";
import type { GPTScoreCalculators } from "../types";
export const WACCA_SCORE_CALC: GPTScoreCalculators["wacca:Single"] = {
rate: (dryScore, chart) => WACCARate.calculate(dryScore.scoreData.score, chart.levelNum),
};
@@ -0,0 +1,25 @@
import { BMS_14K_SCORE_CALC, BMS_7K_SCORE_CALC } from "./games/bms";
import { CHUNITHM_SCORE_CALC } from "./games/chunithm";
import { IIDX_DP_SCORE_CALC, IIDX_SP_SCORE_CALC } from "./games/iidx";
import type { GPTScoreCalculators } from "./types";
export const SCORE_CALCULATORS: GPTScoreCalculators = {
"iidx:SP": IIDX_SP_SCORE_CALC,
"iidx:DP": IIDX_DP_SCORE_CALC,
"bms:7K": BMS_7K_SCORE_CALC,
"bms:14K": BMS_14K_SCORE_CALC,
"chunithm:Single": CHUNITHM_SCORE_CALC,
"jubeat:Single": JUBEAT_SCORE_CALC,
"gitadora:Dora": GITADORA_DORA_SCORE_CALC,
"gitadora:Gita": GITADORA_GITA_SCORE_CALC,
"itg:Stamina": ITG_STAMINA_SCORE_CALC,
"maimaidx:Single": MAIMAI_DX_SCORE_CALC,
"museca:Single": MUSECA_SCORE_CALC,
"pms:Controller": PMS_CONTROLLER_SCORE_CALC,
"pms:Keyboard": PMS_KEYBOARD_SCORE_CALC,
"popn:9B": POPN_9B_SCORE_CALC,
"sdvx:Single": SDVX_SCORE_CALC,
"usc:Controller": USC_CONTROLLER_SCORE_CALC,
"usc:Keyboard": USC_KEYBOARD_SCORE_CALC,
"wacca:Single": WACCA_SCORE_CALC,
};
@@ -0,0 +1,13 @@
import type { DryScore } from "../common/types";
import type { KtLogger } from "lib/logger/logger";
import type { GPTString, ChartDocument, ScoreRatingAlgorithms } from "tachi-common";
export type CalculateDataFunction<GPT extends GPTString> = (
dryScore: DryScore<GPT>,
chart: ChartDocument<GPT>,
logger: KtLogger
) => number | null;
export type GPTScoreCalculators = {
[G in GPTString]: Record<ScoreRatingAlgorithms[G], CalculateDataFunction<G>>;
};
@@ -6,8 +6,9 @@ import type {
ScoreDocument,
integer,
OptionalMetrics,
ConfProvidedMetrics,
} from "tachi-common";
import type { EnumValue } from "tachi-common/types/metrics";
import type { DryExtractMetrics, EnumValue } from "tachi-common/types/metrics";
import type { Mutable } from "utils/types";
/**
@@ -21,7 +22,7 @@ export type ExtractEnumValues<TMetrics extends Record<string, unknown>> = {
[K in keyof TMetrics]: TMetrics[K] extends EnumValue<infer V> ? V : TMetrics[K];
};
type DryScoreData<GPT extends GPTString> = ExtractEnumValues<ProvidedMetrics[GPT]> & {
export type DryScoreData<GPT extends GPTString> = DryExtractMetrics<ConfProvidedMetrics[GPT]> & {
optional: Mutable<ExtractEnumValues<OptionalMetrics[GPT]>>;
judgements: Partial<Record<Judgements[GPT], integer | null>>;
};
@@ -8,6 +8,7 @@
import { GetGrade } from "./common-utils";
import { IIDXLIKE_DERIVERS } from "./games/iidx-like";
import { PopnClearMedalToLamp } from "./games/popn";
import { SDVXLIKE_DERIVERS } from "./games/sdvx-like";
import { InternalFailure } from "../common/converter-failures";
import {
@@ -21,16 +22,10 @@ import {
POPN_GBOUNDARIES,
WACCA_GBOUNDARIES,
} from "tachi-common";
import type { DryScore } from "../common/types";
import type { DryScore, DryScoreData } from "../common/types";
import type { GPTDerivers } from "./types";
import type {
GPTString,
ChartDocument,
ConfProvidedMetrics,
ConfDerivedMetrics,
ScoreData,
} from "tachi-common";
import type { MetricDeriver } from "tachi-common/types/metrics";
import type { ChartDocument, DerivedMetrics, GPTString, ScoreData } from "tachi-common";
import type { MetricValue } from "tachi-common/types/metrics";
type AllGPTDerivers = {
[GPT in GPTString]: GPTDerivers<GPT>;
@@ -74,14 +69,14 @@ const GPT_DERIVERS: AllGPTDerivers = {
// don't check if metrics.survivedPercent === 100, as due to floating
// point inaccuracies, it's possible to have a 100% fail
// (on extremely long charts, for example)
if (metrics.lamp.string === "FAILED") {
if (metrics.lamp === "FAILED") {
return metrics.survivedPercent;
}
return 100 + metrics.scorePercent;
},
grade: ({ scorePercent, lamp }) => {
if (lamp.string === "FAILED") {
if (lamp === "FAILED") {
return "F";
}
@@ -95,35 +90,16 @@ const GPT_DERIVERS: AllGPTDerivers = {
grade: ({ score }) => GetGrade(MUSECA_GBOUNDARIES, score),
},
"popn:9B": {
lamp: ({ clearMedal }) => {
switch (clearMedal.string) {
case "perfect":
return "PERFECT";
case "fullComboCircle":
case "fullComboDiamond":
case "fullComboStar":
return "FULL COMBO";
case "clearCircle":
case "clearDiamond":
case "clearStar":
return "CLEAR";
case "easyClear":
return "EASY CLEAR";
case "failedCircle":
case "failedDiamond":
case "failedStar":
return "FAILED";
}
},
lamp: ({ clearMedal }) => PopnClearMedalToLamp(clearMedal),
grade: ({ score, clearMedal }) => {
const gradeString = GetGrade(POPN_GBOUNDARIES, score);
const c = clearMedal.string;
// grades are kneecapped at "A" if you failed.
if (
score >= 90_000 &&
(c === "failedCircle" || c === "failedDiamond" || c === "failedStar")
(clearMedal === "failedCircle" ||
clearMedal === "failedDiamond" ||
clearMedal === "failedStar")
) {
return "A";
}
@@ -138,16 +114,13 @@ const GPT_DERIVERS: AllGPTDerivers = {
* we want to store.
*/
function DeriveMetrics<GPT extends GPTString>(
gpt: GPTString,
metrics: ConfProvidedMetrics[GPT],
gpt: GPT,
metrics: DryScoreData<GPT>,
chart: ChartDocument<GPT>
) {
const deriverImplementation: Record<
string,
MetricDeriver<ConfProvidedMetrics[GPT], GPT>
> = GPT_DERIVERS[gpt];
const deriverImplementation: GPTDerivers<GPT> = GPT_DERIVERS[gpt];
const derivedMetrics: Record<string, DerivedMetricValue> = {};
const derivedMetrics: Record<string, MetricValue> = {};
const gptConfig = GetGPTConfig(gpt);
@@ -162,15 +135,12 @@ function DeriveMetrics<GPT extends GPTString>(
const value = fn(metrics, chart);
// enum values on scores are stored as { string, index }. One is convenient
// for sorting and stuff in the DB, the other is convenient to know what
// actually is what. Integers only sucks.
if (metricConfig.type === "ENUM") {
const index = metricConfig.values.indexOf(value);
if (index === -1) {
throw new InternalFailure(
`Failed to get the index for ENUM ${gpt} '${key}' for value ${value}. This should never happen!`
`Tried to turn a ${key} enum value of ${value} into an index, got -1?`
);
}
@@ -183,7 +153,7 @@ function DeriveMetrics<GPT extends GPTString>(
}
}
return derivedMetrics as ConfDerivedMetrics[GPT];
return derivedMetrics as DerivedMetrics[GPT];
}
/**
@@ -0,0 +1,24 @@
import type { GetEnumValue } from "tachi-common/types/metrics";
export function PopnClearMedalToLamp(
clearMedal: GetEnumValue<"popn:9B", "clearMedal">
): GetEnumValue<"popn:9B", "lamp"> {
switch (clearMedal) {
case "perfect":
return "PERFECT";
case "fullComboCircle":
case "fullComboDiamond":
case "fullComboStar":
return "FULL COMBO";
case "clearCircle":
case "clearDiamond":
case "clearStar":
return "CLEAR";
case "easyClear":
return "EASY CLEAR";
case "failedCircle":
case "failedDiamond":
case "failedStar":
return "FAILED";
}
}
@@ -4,32 +4,6 @@ import type { KtLogger } from "lib/logger/logger";
import type { ChartDocument, Game, integer, Playtype, GPTStrings } from "tachi-common";
import type { GetEnumValue } from "tachi-common/types/metrics";
export function CalculateKTLampRatingIIDXSP(dryScore: DryScore, chart: ChartDocument<"iidx:SP">) {
const ncValue = chart.tierlistInfo["kt-NC"]?.value ?? 0;
const hcValue = Math.max(chart.tierlistInfo["kt-HC"]?.value ?? 0, ncValue);
const exhcValue = Math.max(chart.tierlistInfo["kt-EXHC"]?.value ?? 0, hcValue);
if (!exhcValue && !hcValue && !ncValue) {
return LampRatingNoTierlistInfo(dryScore, "iidx", chart.playtype, chart);
}
const lamp = dryScore.scoreData.lamp;
const gptConfig = GetGamePTConfig("iidx", chart.playtype);
const lampIndex = gptConfig.lamps.indexOf(lamp);
if (exhcValue && lampIndex >= IIDX_LAMPS.EX_HARD_CLEAR) {
return exhcValue;
} else if (hcValue && lampIndex >= IIDX_LAMPS.HARD_CLEAR) {
return hcValue;
} else if (ncValue && lampIndex >= IIDX_LAMPS.CLEAR) {
return ncValue;
}
return 0;
}
export function CalculateKTLampRatingIIDXDP(dryScore: DryScore, chart: ChartDocument<"iidx:DP">) {
const tierlistValue = chart.tierlistInfo["dp-tier"]?.value ?? 0;
@@ -49,45 +23,3 @@ export function CalculateKTLampRatingIIDXDP(dryScore: DryScore, chart: ChartDocu
return 0;
}
function LampRatingNoTierlistInfo(
dryScore: DryScore,
game: Game,
playtype: Playtype,
chart: ChartDocument
) {
const gptConfig = GetGamePTConfig(game, playtype);
const lamps = gptConfig.lamps;
const CLEAR_LAMP_INDEX = lamps.indexOf(gptConfig.clearLamp);
// if this is a clear
if (lamps.indexOf(dryScore.scoreData.lamp) >= CLEAR_LAMP_INDEX) {
// return this chart's numeric level as the lamp rating
return chart.levelNum;
}
// else, this score is worth 0.
return 0;
}
export function CalculateSieglinde(
chart: ChartDocument<GPTStrings["bms" | "pms"]>,
lamp: GetEnumValue<GPTStrings["bms" | "pms"], "lamp">
) {
const ecValue = chart.data.sglEC ?? 0;
const hcValue = chart.data.sglHC ?? 0;
switch (lamp) {
case "FULL COMBO":
case "EX HARD CLEAR":
case "HARD CLEAR":
return Math.max(hcValue, ecValue);
case "CLEAR":
case "EASY CLEAR":
return ecValue;
}
return 0;
}