fix: calculate chart scores with song formulas and hidden note weights
Publish GitHub Pages output / publish (push) Successful in 54s
Publish GitHub Pages output / publish (push) Successful in 54s
This commit is contained in:
+3104
-1164
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,25 @@ type ActiveCandidate = {
|
||||
// QUEEN 및 Our Promise Expert의 인게임 실측값으로 보정한 전역 환산 상수다.
|
||||
export const CHART_SCORE_WEIGHT_COEFFICIENT = 0.3713416563352296;
|
||||
|
||||
export type SongScoreFormula = {
|
||||
/** 일반 PERFECT=1000 단위. 비가시 지점을 포함하며 득점 노트 합과 다르다. */
|
||||
perfectNoteWeight: number;
|
||||
liveScoreCoefficientPermil: number;
|
||||
difficultyLevel: number;
|
||||
};
|
||||
|
||||
export function scoreRatioFromFormula(formula: SongScoreFormula): number {
|
||||
const { perfectNoteWeight, liveScoreCoefficientPermil, difficultyLevel } = formula;
|
||||
if (!Number.isSafeInteger(perfectNoteWeight) || perfectNoteWeight <= 0 ||
|
||||
!Number.isSafeInteger(liveScoreCoefficientPermil) || liveScoreCoefficientPermil < 0 ||
|
||||
!Number.isSafeInteger(difficultyLevel) || difficultyLevel < 1) {
|
||||
throw new Error('악곡 점수 공식의 입력값이 유효하지 않습니다.');
|
||||
}
|
||||
const musicCoefficient = 1 + liveScoreCoefficientPermil / 1000 * (difficultyLevel - 5);
|
||||
if (musicCoefficient <= 0) throw new Error('악곡 점수 배율은 양수여야 합니다.');
|
||||
return perfectNoteWeight / 1000 / (2.3 * musicCoefficient);
|
||||
}
|
||||
|
||||
export function scoreRatioFromNotes(notes: ExactSongChart['notes']): number {
|
||||
let weight = 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import reference from '../../../docs/research/song-score-formula-reference.json' with { type: 'json' };
|
||||
import susReference from '../../../docs/research/sus-score-formulas.json' with { type: 'json' };
|
||||
import calibrations from '../../../data/score-calibrations.json' with { type: 'json' };
|
||||
import catalog from '../data/exact-chart-catalog.json' with { type: 'json' };
|
||||
import { calculateExactScore, perfectNoteScoreFromPower, scoreRatioFromFormula } from './score.ts';
|
||||
|
||||
test('곡별 공식과 실제 점수 엔진이 일반 PERFECT 실측 19건을 재현한다', () => {
|
||||
let count = 0;
|
||||
for (const calibration of calibrations.charts) {
|
||||
const formula = reference.charts.find((row) => row.songId === calibration.songId && row.difficulty === calibration.difficulty);
|
||||
assert.ok(formula);
|
||||
for (const measurement of calibration.measurements) {
|
||||
const result = calculateExactScore([], {}, {
|
||||
songId: calibration.songId, difficulty: calibration.difficulty,
|
||||
durationSeconds: 2, scoreRatio: scoreRatioFromFormula(formula),
|
||||
notes: [{ timeSeconds: 1, type: 'regular', isFlick: false }], specialStartSeconds: []
|
||||
}, {
|
||||
perfectNoteScore: perfectNoteScoreFromPower(measurement.overallPower, scoreRatioFromFormula(formula)),
|
||||
directSongScoreBonusPercent: measurement.directSongScoreBonusPercent
|
||||
});
|
||||
assert.equal(result.baselineScore, measurement.normalPerfectScore, `${calibration.songId}.${calibration.difficulty}`);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
assert.equal(count, 19);
|
||||
});
|
||||
|
||||
test('공식 적용 카탈로그와 실제 채보가 같은 분모 및 환산값을 사용한다', async () => {
|
||||
let formulaCount = 0;
|
||||
for (const row of catalog.charts) {
|
||||
const chart = JSON.parse(await readFile(new URL(`../../../static${row.path}`, import.meta.url), 'utf8'));
|
||||
const ref = [...reference.charts, ...susReference.charts].find((item) => item.songId === row.songId && item.difficulty === row.difficulty);
|
||||
assert.equal(chart.scoreRatio, row.scoreRatio);
|
||||
assert.equal(chart.scoreCalibrationSource, row.scoreCalibrationSource);
|
||||
if (!ref) {
|
||||
assert.notEqual(row.scoreCalibrationSource, 'formula-v1');
|
||||
continue;
|
||||
}
|
||||
assert.equal(row.scoreCalibrationSource, 'formula-v1');
|
||||
assert.equal(row.scoreRatio, scoreRatioFromFormula(ref));
|
||||
assert.deepEqual(chart.scoreFormula, row.scoreFormula);
|
||||
assert.deepEqual(chart.scoreFormula, {
|
||||
perfectNoteWeight: ref.perfectNoteWeight,
|
||||
liveScoreCoefficientPermil: ref.liveScoreCoefficientPermil,
|
||||
difficultyLevel: ref.difficultyLevel
|
||||
});
|
||||
assert.equal(chart.notes.length, ref.fullComboNoteCount);
|
||||
formulaCount += 1;
|
||||
}
|
||||
assert.equal(formulaCount, 388);
|
||||
});
|
||||
|
||||
test('잘못된 분모와 난이도로 공식 환산값을 만들지 않는다', () => {
|
||||
const valid = { perfectNoteWeight: 633950, liveScoreCoefficientPermil: 5, difficultyLevel: 24 };
|
||||
for (const invalid of [{ perfectNoteWeight: 0 }, { perfectNoteWeight: NaN }, { difficultyLevel: 0 }, { liveScoreCoefficientPermil: -1 }]) {
|
||||
assert.throws(() => scoreRatioFromFormula({ ...valid, ...invalid }));
|
||||
}
|
||||
});
|
||||
@@ -93,6 +93,7 @@ export type ExactSongChart = {
|
||||
durationSeconds: number;
|
||||
scoreRatio: number | null;
|
||||
scoreCalibrationSource?: 'measured' | 'local-estimated-v1' | string;
|
||||
scoreFormula?: import('./score.ts').SongScoreFormula;
|
||||
notes: readonly ExactChartNote[];
|
||||
specialStartSeconds: readonly (number | null)[];
|
||||
singerType?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user