feat: support every song difficulty including Easy and Normal
Publish GitHub Pages output / publish (push) Successful in 41s

This commit is contained in:
2026-09-07 14:18:10 +09:00
parent 8f97683271
commit 0af64052c6
405 changed files with 12211 additions and 25 deletions
+31
View File
@@ -0,0 +1,31 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFile } from 'node:fs/promises';
import { difficulties, isDifficulty } from '../data/difficulty.ts';
import { calculateExactScore, perfectNoteScoreFromPower } from './score.ts';
test('모든 악곡의 네 난이도 채보를 불러와 실제 점수를 계산한다', async () => {
const catalog = JSON.parse(await readFile(new URL('../data/exact-chart-catalog.json', import.meta.url), 'utf8'));
const bySong = new Map<string, Set<string>>();
for (const row of catalog.charts) {
assert.ok(isDifficulty(row.difficulty));
const seen = bySong.get(row.songId) ?? new Set<string>();
assert.ok(!seen.has(row.difficulty), `${row.songId}: 중복 난이도`);
seen.add(row.difficulty);
bySong.set(row.songId, seen);
const chart = JSON.parse(await readFile(new URL(`../../../static${row.path}`, import.meta.url), 'utf8'));
assert.equal(chart.songId, row.songId);
assert.equal(chart.difficulty, row.difficulty);
assert.equal(chart.notes.length, row.totalNotes);
assert.ok(chart.notes.length > 0);
// 튜토리얼용 짧은 곡은 스페셜 구간이 두 개뿐이다.
assert.equal(chart.specialStartSeconds.length, row.songId === 'm9999' ? 2 : 5);
assert.equal(chart.scoreCalibrationSource, 'formula-v1');
const score = calculateExactScore([], {}, chart, {
perfectNoteScore: perfectNoteScoreFromPower(100000, chart.scoreRatio)
});
assert.ok(Number.isFinite(score.expectedScore) && score.expectedScore > 0, `${row.songId}.${row.difficulty}`);
}
assert.ok(bySong.size > 0);
for (const [songId, actual] of bySong) assert.deepEqual(actual, new Set(difficulties), songId);
});
+7
View File
@@ -41,6 +41,13 @@ test('성장 상태를 공유하는 기록 백업을 손실 없이 읽고 잘못
const parsed = parseAnalysisArchive(JSON.stringify(archive));
assert.equal(parsed.records.length, 2); assert.equal(parsed.snapshots.length, 1);
assert.deepEqual(parsed.snapshots[0].growth, growth);
for (const difficulty of ['easy', 'normal', 'hard', 'expert']) {
record.settings.difficulty = difficulty;
assert.equal(parseAnalysisArchive(JSON.stringify(archive)).records[0].settings.difficulty, difficulty);
}
record.settings.difficulty = 'invalid';
assert.throws(() => parseAnalysisArchive(JSON.stringify(archive)));
record.settings.difficulty = 'expert';
record.settings.awakenings[0] = 8;
assert.throws(() => parseAnalysisArchive(JSON.stringify(archive)));
});
+11
View File
@@ -31,3 +31,14 @@ test('고점 순위와 상위 3곡 합계는 동일 곡의 난이도를 중복
assert.equal(leaderRating([]), 0);
assert.equal(results[0].theoreticalMaxScore, 5001);
});
test('전체 난이도 추천은 Easy와 Normal을 포함하고 곡별 최고점만 합산한다', () => {
const charts = ['easy', 'normal', 'hard', 'expert'].map((difficulty, index) => ({
songId: 'song', title: 'Song', difficulty, singerCharacterIds: ['leader'],
theoreticalMaxScore: 20000 - index * 5000, expectedScore: 1000
}));
assert.deepEqual(leaderRatingCharts(charts, 'leader', 'all', Date.now()), charts);
assert.deepEqual(leaderRatingCharts(charts, 'leader', 'normal', Date.now()), [charts[1]]);
assert.equal(rankSongs(charts)[0].difficulty, 'easy');
assert.equal(leaderRating(charts), 4);
});
+2 -1
View File
@@ -1,3 +1,4 @@
import { isDifficulty } from '../data/difficulty.ts';
import { releasedSongCharts } from '../data/song-release.ts';
export type RatingChart = {
@@ -17,7 +18,7 @@ export function songRating(score: number): number {
export function leaderRatingCharts<T extends RatingChart>(charts: readonly T[], leaderId: string, difficulty: string, now: number): T[] {
return releasedSongCharts(charts, now).filter((chart) =>
Boolean(leaderId) && chart.singerCharacterIds?.includes(leaderId) &&
(difficulty === 'both' ? chart.difficulty === 'hard' || chart.difficulty === 'expert' : chart.difficulty === difficulty));
(difficulty === 'all' ? isDifficulty(chart.difficulty) : difficulty === 'both' ? chart.difficulty === 'hard' || chart.difficulty === 'expert' : chart.difficulty === difficulty));
}
export type SongRatingScore = {
+1 -1
View File
@@ -50,7 +50,7 @@ test('공식 적용 카탈로그와 실제 채보가 같은 분모 및 환산값
assert.equal(chart.notes.length, ref.fullComboNoteCount);
formulaCount += 1;
}
assert.equal(formulaCount, 388);
assert.equal(formulaCount, catalog.charts.length);
});
test('잘못된 분모와 난이도로 공식 환산값을 만들지 않는다', () => {