81 lines
3.5 KiB
JavaScript
81 lines
3.5 KiB
JavaScript
import { access } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { finalNoteScore } from '../src/lib/engine/score.ts';
|
|
|
|
const DIFFICULTIES = new Set(['easy', 'normal', 'hard', 'expert']);
|
|
|
|
function assertFiniteNumber(value, label, { minimum = -Infinity, maximum = Infinity } = {}) {
|
|
if (typeof value !== 'number' || !Number.isFinite(value) || value < minimum || value > maximum) {
|
|
throw new Error(`${label} 값이 유효하지 않습니다: ${JSON.stringify(value)}`);
|
|
}
|
|
}
|
|
|
|
export async function assertValidScoreCalibrations(calibrations, options = {}) {
|
|
const chartDirectory = options.chartDirectory ?? path.resolve('static/data/charts');
|
|
|
|
if (calibrations?.schemaVersion !== 1 || !Array.isArray(calibrations.charts)) {
|
|
throw new Error('score-calibrations.json은 schemaVersion 1과 charts 배열이 필요합니다.');
|
|
}
|
|
|
|
const chartKeys = new Set();
|
|
|
|
for (const [chartIndex, calibration] of calibrations.charts.entries()) {
|
|
const prefix = `charts[${chartIndex}]`;
|
|
if (typeof calibration.songId !== 'string' || !/^m\d{4}$/.test(calibration.songId)) {
|
|
throw new Error(`${prefix}.songId 형식이 올바르지 않습니다: ${JSON.stringify(calibration.songId)}`);
|
|
}
|
|
|
|
const difficulty = typeof calibration.difficulty === 'string'
|
|
? calibration.difficulty.toLowerCase()
|
|
: '';
|
|
if (!DIFFICULTIES.has(difficulty)) {
|
|
throw new Error(`${prefix}.difficulty가 올바르지 않습니다: ${JSON.stringify(calibration.difficulty)}`);
|
|
}
|
|
|
|
const chartKey = `${calibration.songId}.${difficulty}`;
|
|
if (chartKeys.has(chartKey)) throw new Error(`중복 실측 보정 항목입니다: ${chartKey}`);
|
|
chartKeys.add(chartKey);
|
|
|
|
assertFiniteNumber(calibration.scoreRatio, `${prefix}.scoreRatio`, { minimum: Number.EPSILON });
|
|
if (!Array.isArray(calibration.measurements) || calibration.measurements.length === 0) {
|
|
throw new Error(`${prefix}.measurements에는 한 개 이상의 실측값이 필요합니다.`);
|
|
}
|
|
|
|
const measurementKeys = new Set();
|
|
for (const [measurementIndex, measurement] of calibration.measurements.entries()) {
|
|
const measurementPrefix = `${prefix}.measurements[${measurementIndex}]`;
|
|
assertFiniteNumber(measurement.overallPower, `${measurementPrefix}.overallPower`, { minimum: 1 });
|
|
assertFiniteNumber(
|
|
measurement.directSongScoreBonusPercent,
|
|
`${measurementPrefix}.directSongScoreBonusPercent`,
|
|
{ minimum: 0, maximum: 100 }
|
|
);
|
|
assertFiniteNumber(measurement.normalPerfectScore, `${measurementPrefix}.normalPerfectScore`, { minimum: 1 });
|
|
|
|
const measurementKey = [
|
|
measurement.overallPower,
|
|
measurement.directSongScoreBonusPercent,
|
|
measurement.normalPerfectScore
|
|
].join(':');
|
|
if (measurementKeys.has(measurementKey)) {
|
|
throw new Error(`${chartKey}에 중복 실측값이 있습니다: ${measurementKey}`);
|
|
}
|
|
measurementKeys.add(measurementKey);
|
|
|
|
const reproducedScore = finalNoteScore(
|
|
measurement.overallPower / calibration.scoreRatio,
|
|
measurement.directSongScoreBonusPercent
|
|
);
|
|
if (reproducedScore !== measurement.normalPerfectScore) {
|
|
throw new Error(
|
|
`${chartKey} 실측값을 재현하지 못했습니다: 기대 ${measurement.normalPerfectScore}, 계산 ${reproducedScore}`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (difficulty === 'hard' || difficulty === 'expert') {
|
|
await access(path.join(chartDirectory, `${chartKey}.json`));
|
|
}
|
|
}
|
|
}
|