Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: floating-point profile rating calculation #1227

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions common/src/config/game-support/chunithm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FAST_SLOW_MAXCOMBO } from "./_common";
import { FmtNum } from "../../utils/util";
import { ClassValue, zodNonNegativeInt } from "../config-utils";
import { ClassValue, ToDecimalPlaces, zodNonNegativeInt } from "../config-utils";
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
Expand Down Expand Up @@ -87,15 +87,20 @@ export const CHUNITHM_SINGLE_CONF = {
rating: {
description:
"The rating value of this score. This is identical to the system used in game.",
formatter: ToDecimalPlaces(2),
},
},
sessionRatingAlgs: {
naiveRating: { description: "The average of your best 10 ratings this session." },
naiveRating: {
description: "The average of your best 10 ratings this session.",
formatter: ToDecimalPlaces(2),
},
},
profileRatingAlgs: {
naiveRating: {
description:
"The average of your best 30 ratings. This is different to in-game, as it does not take into account your recent scores in any way.",
formatter: ToDecimalPlaces(2),
},
},

Expand Down
9 changes: 7 additions & 2 deletions common/src/config/game-support/ongeki.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FAST_SLOW_MAXCOMBO } from "./_common";
import { FmtNum } from "../../utils/util";
import { ClassValue } from "../config-utils";
import { ClassValue, ToDecimalPlaces } from "../config-utils";
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
Expand Down Expand Up @@ -112,14 +112,19 @@ export const ONGEKI_SINGLE_CONF = {
rating: {
description:
"The rating value of this score. This is identical to the system used in game.",
formatter: ToDecimalPlaces(2),
},
},
sessionRatingAlgs: {
naiveRating: { description: "The average of your best 10 ratings this session." },
naiveRating: {
description: "The average of your best 10 ratings this session.",
formatter: ToDecimalPlaces(2),
},
},
profileRatingAlgs: {
naiveRating: {
description: "The average of your best 45 scores.",
formatter: ToDecimalPlaces(2),
},
},

Expand Down
40 changes: 38 additions & 2 deletions server/src/game-implementations/games/chunithm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CHUNITHM_GRADES, CHUNITHM_LAMPS } from "tachi-common";
import t from "tap";
import { dmf, mkMockPB, mkMockScore } from "test-utils/misc";
import ResetDBState from "test-utils/resets";
import { CHUNITHMBBKKChart } from "test-utils/test-data";
import { CHUNITHMBBKKChart, TestingChunithmScorePB } from "test-utils/test-data";
import type { ProvidedMetrics, ScoreData } from "tachi-common";

const baseMetrics: ProvidedMetrics["chunithm:Single"] = {
Expand Down Expand Up @@ -69,7 +69,43 @@ t.test("CHUNITHM Implementation", (t) => {
});

t.todo("Session Calcs");
t.todo("Profile Calcs");

t.test("Profile Calcs", (t) => {
t.beforeEach(ResetDBState);

const mockPBs = async (ratings: Array<number>) => {
await Promise.all(
ratings.map((rating, idx) =>
db["personal-bests"].insert({
...TestingChunithmScorePB,
chartID: `TEST${idx}`,
calculatedData: {
...TestingChunithmScorePB.calculatedData,
rating,
},
})
)
);
};

t.test("Floating-point edge case", async (t) => {
await mockPBs(Array(30).fill(17.15));

t.equal(await CHUNITHM_IMPL.profileCalcs.naiveRating("chunithm", "Single", 1), 17.15);

t.end();
});

t.test("Profile with fewer than 30 scores", async (t) => {
await mockPBs([16, 16, 16, 16]);

t.equal(await CHUNITHM_IMPL.profileCalcs.naiveRating("chunithm", "Single", 1), 2.13);

t.end();
});

t.end();
});

t.test("Colour Deriver", (t) => {
const f = (v: number | null, expected: any) =>
Expand Down
2 changes: 1 addition & 1 deletion server/src/game-implementations/games/chunithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const CHUNITHM_IMPL: GPTServerImplementation<"chunithm:Single"> = {
rating: (scoreData, chart) => CHUNITHMRating.calculate(scoreData.score, chart.levelNum),
},
sessionCalcs: { naiveRating: SessionAvgBest10For("rating") },
profileCalcs: { naiveRating: ProfileAvgBestN("rating", 30) },
profileCalcs: { naiveRating: ProfileAvgBestN("rating", 30, false, 100) },
classDerivers: {
colour: (ratings) => {
const rating = ratings.naiveRating;
Expand Down
40 changes: 38 additions & 2 deletions server/src/game-implementations/games/ongeki.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ONGEKI_BELL_LAMPS, ONGEKI_GRADES, ONGEKI_NOTE_LAMPS } from "tachi-commo
import t from "tap";
import { dmf, mkMockPB, mkMockScore } from "test-utils/misc";
import ResetDBState from "test-utils/resets";
import { TestingOngekiChart } from "test-utils/test-data";
import { TestingOngekiChart, TestingOngekiScorePB } from "test-utils/test-data";
import type { ProvidedMetrics, ScoreData } from "tachi-common";

const baseMetrics: ProvidedMetrics["ongeki:Single"] = {
Expand Down Expand Up @@ -71,7 +71,43 @@ t.test("ONGEKI Implementation", (t) => {
});

t.todo("Session Calcs");
t.todo("Profile Calcs");

t.test("Profile Calcs", (t) => {
t.beforeEach(ResetDBState);

const mockPBs = async (ratings: Array<number>) => {
await Promise.all(
ratings.map((rating, idx) =>
db["personal-bests"].insert({
...TestingOngekiScorePB,
chartID: `TEST${idx}`,
calculatedData: {
...TestingOngekiScorePB.calculatedData,
rating,
},
})
)
);
};

t.test("Floating-point edge case", async (t) => {
await mockPBs(Array(45).fill(16.27));

t.equal(await ONGEKI_IMPL.profileCalcs.naiveRating("ongeki", "Single", 1), 16.27);

t.end();
});

t.test("Profile with fewer than 45 scores", async (t) => {
await mockPBs([16, 16, 16, 16]);

t.equal(await ONGEKI_IMPL.profileCalcs.naiveRating("ongeki", "Single", 1), 1.42);

t.end();
});

t.end();
});

t.test("Colour Deriver", (t) => {
const f = (v: number | null, expected: any) =>
Expand Down
2 changes: 1 addition & 1 deletion server/src/game-implementations/games/ongeki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const ONGEKI_IMPL: GPTServerImplementation<"ongeki:Single"> = {
},
sessionCalcs: { naiveRating: SessionAvgBest10For("rating") },
profileCalcs: {
naiveRating: ProfileAvgBestN("rating", 45),
naiveRating: ProfileAvgBestN("rating", 45, false, 100),
},
classDerivers: {
colour: (ratings) => {
Expand Down
27 changes: 22 additions & 5 deletions server/src/game-implementations/utils/profile-calc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
* @param n - The amount of rating values to pull.
* @param returnMean - Optionally, if true, return the sum of these values divided by N.
* @param nullIfNotEnoughScores - If true, return null if the total scores this user has is less than N.
* @param multiplier - If defined, ratings will be multiplied by this value and converted to integers.
*
* @returns - Number if the user has scores with that rating algorithm, null if they have
* no scores with this rating algorithm that are non-null.
Expand All @@ -23,7 +24,8 @@ function CalcN<GPT extends GPTString>(
key: ScoreRatingAlgorithms[GPT],
n: integer,
returnMean = false,
nullIfNotEnoughScores = false
nullIfNotEnoughScores = false,
multiplier = 1
) {
return async (game: Game, playtype: Playtype, userID: integer) => {
const sc = await db["personal-bests"].find(
Expand All @@ -48,6 +50,19 @@ function CalcN<GPT extends GPTString>(
return null;
}

if (multiplier !== 1) {
const result = sc.reduce(
(a, e) => a + Math.round((e.calculatedData[key] ?? 0) * multiplier),
0
);

if (returnMean) {
return Math.floor(result / n) / multiplier;
}

return result / multiplier;
}

let result = sc.reduce((a, e) => a + e.calculatedData[key]!, 0);

if (returnMean) {
Expand All @@ -61,17 +76,19 @@ function CalcN<GPT extends GPTString>(
export function ProfileSumBestN<GPT extends GPTString>(
key: ScoreRatingAlgorithms[GPT],
n: integer,
nullIfNotEnoughScores = false
nullIfNotEnoughScores = false,
multiplier = 1
) {
return CalcN(key, n, false, nullIfNotEnoughScores);
return CalcN(key, n, false, nullIfNotEnoughScores, multiplier);
}

export function ProfileAvgBestN<GPT extends GPTString>(
key: ScoreRatingAlgorithms[GPT],
n: integer,
nullIfNotEnoughScores = false
nullIfNotEnoughScores = false,
multiplier = 1
) {
return CalcN(key, n, true, nullIfNotEnoughScores);
return CalcN(key, n, true, nullIfNotEnoughScores, multiplier);
}

export async function GetBestRatingOnSongs(
Expand Down
68 changes: 68 additions & 0 deletions server/src/test-utils/test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,39 @@ export const CHUNITHMBBKKChart: ChartDocument<"chunithm:Single"> = {
versions: ["paradiselost"],
};

export const TestingChunithmScorePB: PBScoreDocument<"chunithm:Single"> = {
chartID: "192b96bdb6150f80ba6412ce02df1249e16c0cb0",
userID: 1,
calculatedData: {
rating: 5,
},
composedFrom: [{ name: "Best Score", scoreID: "TESTING_SCORE_ID" }],
highlight: false,
isPrimary: true,
scoreData: {
score: 1010000,
lamp: "ALL JUSTICE CRITICAL",
grade: "SSS+",
enumIndexes: {
grade: 13,
lamp: 4,
},
judgements: {},
optional: {
enumIndexes: {},
},
},
rankingData: {
rank: 1,
outOf: 1,
rivalRank: null,
},
songID: 3,
game: "chunithm",
playtype: "Single",
timeAchieved: 10000,
};

export const TestingDoraChart: ChartDocument<"gitadora:Dora"> = {
songID: 0,
chartID: "29f0bfab357ba54e3fd0176fb3cbc578c9ec8df5",
Expand Down Expand Up @@ -1380,6 +1413,41 @@ export const TestingOngekiChart: ChartDocument<"ongeki:Single"> = {
versions: ["brightMemory3", "brightMemory3Omni"],
};

export const TestingOngekiScorePB: PBScoreDocument<"ongeki:Single"> = {
chartID: "213796bdb6150f80ba6412ce69df1249e16c0cb0",
userID: 1,
calculatedData: {
rating: 17,
},
composedFrom: [{ name: "Best Score", scoreID: "TESTING_SCORE_ID" }],
highlight: false,
isPrimary: true,
scoreData: {
score: 1010000,
noteLamp: "ALL BREAK",
bellLamp: "FULL BELL",
grade: "SSS+",
enumIndexes: {
grade: 11,
noteLamp: 3,
bellLamp: 1,
},
judgements: {},
optional: {
enumIndexes: {},
},
},
rankingData: {
rank: 1,
outOf: 1,
rivalRank: null,
},
songID: 19,
game: "ongeki",
playtype: "Single",
timeAchieved: 10000,
};

export const TestingOngekiChartConverter: ChartDocument<"ongeki:Single"> = {
chartID: "e5e4ee3d4feb233c399751b3ba3daf8ba149c9e6",
data: {
Expand Down
Loading