feat: compare scores with and without special-start timing bonus
Publish GitHub Pages output / publish (push) Successful in 31s

This commit is contained in:
2026-09-27 20:11:05 +09:00
parent b87c5d2e18
commit dbe95d19a0
5 changed files with 58 additions and 7 deletions
+14 -4
View File
@@ -74,10 +74,13 @@ function specialIsActive(
function specialSupportAt(
cardsInOrder: readonly OptimizerCard[],
chart: ExactSongChart,
timeSeconds: number
timeSeconds: number,
excludeStart = false
): number {
return cardsInOrder.reduce((sum, card, slot) =>
sum + (specialIsActive(card, chart.specialStartSeconds[slot], timeSeconds) ? card.special.scoreUpPercent : 0), 0);
sum + (specialIsActive(card, chart.specialStartSeconds[slot], timeSeconds) &&
!(excludeStart && Math.abs(timeSeconds - chart.specialStartSeconds[slot]!) < 1e-9)
? card.special.scoreUpPercent : 0), 0);
}
function activationRateBonusAt(
@@ -96,9 +99,10 @@ function activeCandidates(
reductions: Readonly<Record<string, CooldownReduction>>,
chart: ExactSongChart,
timeSeconds: number,
excludedActiveWindows: ExactScoreOptions['excludedActiveWindows']
excludedActiveWindows: ExactScoreOptions['excludedActiveWindows'],
excludeSpecialStart = false
): ActiveCandidate[] {
const specialSupport = specialSupportAt(cardsInOrder, chart, timeSeconds);
const specialSupport = specialSupportAt(cardsInOrder, chart, timeSeconds, excludeSpecialStart);
const candidates = cardsInOrder.flatMap((card, memberIndex) => {
const reduction = reductions[card.id] ?? 0;
const attempt = activeAttempt(card, reduction, timeSeconds);
@@ -149,6 +153,7 @@ export function calculateExactScore(
): ExactScoreResult {
const directBonus = options.directSongScoreBonusPercent ?? 0;
const scoreNotes = chart.notes;
let specialStartBonus = 0;
const notePoints = scoreNotes.map((note, index) => {
const combo = index + 1;
const baseNoteScore = note.type === 'mid'
@@ -159,6 +164,10 @@ export function calculateExactScore(
const baselineScore = finalNoteScore(rawBaselineScore, directBonus);
const expectedScore = finalNoteScore(rawBaselineScore * (1 + expectedStrongestBoost(candidates)), directBonus);
const theoreticalMaxScore = finalNoteScore(rawBaselineScore * (1 + (candidates[0]?.boost ?? 0)), directBonus);
if (chart.specialStartSeconds.some((start) => start != null && Math.abs(start - note.timeSeconds) < 1e-9)) {
const withoutStart = activeCandidates(cardsInOrder, reductions, chart, note.timeSeconds, options.excludedActiveWindows, true);
specialStartBonus += theoreticalMaxScore - finalNoteScore(rawBaselineScore * (1 + (withoutStart[0]?.boost ?? 0)), directBonus);
}
return { timeSeconds: note.timeSeconds, combo, expectedScore, theoreticalMaxScore, baselineScore };
});
@@ -174,6 +183,7 @@ export function calculateExactScore(
return {
expectedScore: rawExpected,
theoreticalMaxScore: rawMaximum,
withoutSpecialStartBonusScore: rawMaximum - specialStartBonus,
baselineScore: rawBaseline,
perfectNoteScore: options.perfectNoteScore,
scoreBySecond,
@@ -0,0 +1,29 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFile } from 'node:fs/promises';
import { calculateExactScore } from './score.ts';
import type { OptimizerCard, ExactSongChart } from './types.ts';
test('스페셜 시작 보너스만 제외하고 기존 스페셜, 종료 경계 및 액티브 실패는 유지한다', () => {
const cards: OptimizerCard[] = [
{ id: 'a', name: 'a', active: { cooldownSeconds: 1, durationSeconds: 10, scoreUpPercent: 100 }, special: { durationSeconds: 3, scoreUpPercent: 100 } },
{ id: 'b', name: 'b', active: { cooldownSeconds: 999, durationSeconds: 1, scoreUpPercent: 100 }, special: { durationSeconds: 1, scoreUpPercent: 50 } }
];
const chart: ExactSongChart = { songId: 'test', difficulty: 'hard', durationSeconds: 5, scoreRatio: 1,
specialStartSeconds: [1, 2], notes: [1, 2, 2.5, 3, 4].map(timeSeconds => ({ timeSeconds, type: 'regular', isFlick: false })) };
const score = calculateExactScore(cards, {}, chart, { perfectNoteScore: 100 });
assert.equal(score.theoreticalMaxScore, 1500);
assert.equal(score.withoutSpecialStartBonusScore, 1350);
const excluded = calculateExactScore(cards, {}, chart, { perfectNoteScore: 100, excludedActiveWindows: [{cardId: 'a', startSeconds: 1}] });
assert.equal(excluded.theoreticalMaxScore, 1300);
assert.equal(excluded.withoutSpecialStartBonusScore, 1250);
});
test('PSYCHO Hard 첨부 편성의 밀어치기 적용 및 미적용 점수를 재현한다', async () => {
const chart = JSON.parse(await readFile(new URL('../../../static/data/charts/m0121.hard.json', import.meta.url), 'utf8'));
const rows = [[30,11,110,14,115], [27,9,125,10,160], [15,6,110,12,135], [19,7,110,10,160], [23,8,120,11,145]];
const cards: OptimizerCard[] = rows.map(([cooldownSeconds,durationSeconds,scoreUpPercent,sd,sb],i) => ({id: String(i),name: String(i),active:{cooldownSeconds,durationSeconds,scoreUpPercent},special:{durationSeconds:sd,scoreUpPercent:sb},scoreSupportPercent:57}));
const result = calculateExactScore(cards, {'0':4,'1':12,'2':12,'3':12,'4':12}, chart, {perfectNoteScore:396815/chart.scoreRatio,directSongScoreBonusPercent:10});
assert.equal(result.theoreticalMaxScore, 4067011);
assert.equal(result.withoutSpecialStartBonusScore, 4043015);
});
+2
View File
@@ -111,6 +111,8 @@ export type NoteScorePoint = {
export type ExactScoreResult = {
expectedScore: number;
theoreticalMaxScore: number;
/** 스페셜 시작과 같은 시각의 노트에 새 스페셜 점수 효과를 적용하지 않은 최고점. */
withoutSpecialStartBonusScore?: number;
baselineScore: number;
perfectNoteScore: number;
scoreBySecond: readonly number[];