feat: complete placement analysis workflow
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { calculateExactScore, perfectNoteScoreFromPower, scoreRatioFromNotes } from './index.ts';
|
||||
import type { ExactSongChart, OptimizerCard } from './types.ts';
|
||||
|
||||
const inactiveCard: OptimizerCard = {
|
||||
id: 'card',
|
||||
name: '카드',
|
||||
active: { cooldownSeconds: 999, durationSeconds: 1, scoreUpPercent: 100, probabilityPercent: 100 },
|
||||
special: { durationSeconds: 0, scoreUpPercent: 0 }
|
||||
};
|
||||
|
||||
function chart(notes: ExactSongChart['notes']): ExactSongChart {
|
||||
return { songId: 'test', difficulty: 'expert', durationSeconds: 120, scoreRatio: 100, notes, specialStartSeconds: [] };
|
||||
}
|
||||
|
||||
test('종합력과 악곡 비율로 PERFECT 1노트 기본점수를 계산한다', () => {
|
||||
assert.equal(perfectNoteScoreFromPower(120_000, 200), 600);
|
||||
assert.equal(perfectNoteScoreFromPower(120_100, 200), 600.5);
|
||||
assert.equal(perfectNoteScoreFromPower(0, 200, 777), 777);
|
||||
});
|
||||
|
||||
test('플릭 1.05배와 같은 시각 홀드 중간 판정의 단일 가중치로 환산비율을 계산한다', () => {
|
||||
const ratio = scoreRatioFromNotes([
|
||||
{ timeSeconds: 1, type: 'regular', isFlick: false },
|
||||
{ timeSeconds: 2, type: 'regular', isFlick: true },
|
||||
{ timeSeconds: 3, type: 'mid', isFlick: false },
|
||||
{ timeSeconds: 3, type: 'mid', isFlick: false }
|
||||
]);
|
||||
assert.ok(Math.abs(ratio - 2.15 * 0.3713416563352296) < 1e-12);
|
||||
});
|
||||
|
||||
test('기본점수의 소수점을 유지하고 가창 보너스 적용 후 노트별로 올림한다', () => {
|
||||
const result = calculateExactScore([inactiveCard], {}, chart([
|
||||
{ timeSeconds: 1, type: 'regular', isFlick: false }
|
||||
]), { perfectNoteScore: 1_354.507, directSongScoreBonusPercent: 10 });
|
||||
assert.equal(result.baselineScore, 1_490);
|
||||
});
|
||||
|
||||
test('100콤보부터 1% 콤보 보너스를 적용한다', () => {
|
||||
const notes = Array.from({ length: 100 }, (_, index) => ({ timeSeconds: index, type: 'regular' as const, isFlick: false }));
|
||||
const result = calculateExactScore([inactiveCard], {}, chart(notes), { perfectNoteScore: 600 });
|
||||
assert.equal(result.baselineScore, 99 * 600 + 606);
|
||||
});
|
||||
|
||||
test('홀드 중간 노트는 10%, 플릭은 1.05배로 계산한다', () => {
|
||||
const result = calculateExactScore([inactiveCard], {}, chart([
|
||||
{ timeSeconds: 1, type: 'mid', isFlick: false },
|
||||
{ timeSeconds: 2, type: 'regular', isFlick: true }
|
||||
]), { perfectNoteScore: 600 });
|
||||
assert.equal(result.baselineScore, 60 + 630);
|
||||
});
|
||||
|
||||
test('같은 시각에 중복된 홀드 중간 판정은 한 번만 계산한다', () => {
|
||||
const result = calculateExactScore([inactiveCard], {}, chart([
|
||||
{ timeSeconds: 1, type: 'mid', isFlick: false },
|
||||
{ timeSeconds: 1, type: 'mid', isFlick: false }
|
||||
]), { perfectNoteScore: 600 });
|
||||
assert.equal(result.baselineScore, 60);
|
||||
assert.equal(result.notes.length, 1);
|
||||
});
|
||||
|
||||
test('액티브 발동 확률과 스페셜 스코어 서포트를 기대 점수에 반영한다', () => {
|
||||
const card: OptimizerCard = {
|
||||
id: 'score',
|
||||
name: '스코어 카드',
|
||||
active: { cooldownSeconds: 1, durationSeconds: 1, scoreUpPercent: 100, probabilityPercent: 50 },
|
||||
special: { durationSeconds: 2, scoreUpPercent: 100 }
|
||||
};
|
||||
const result = calculateExactScore([card], {}, {
|
||||
...chart([{ timeSeconds: 1, type: 'regular', isFlick: false }]),
|
||||
specialStartSeconds: [0]
|
||||
}, { perfectNoteScore: 600 });
|
||||
assert.equal(result.expectedScore, 1_200);
|
||||
assert.equal(result.theoreticalMaxScore, 1_800);
|
||||
});
|
||||
Reference in New Issue
Block a user