feat: improve analysis and release v1.0.2

This commit is contained in:
2026-08-19 13:47:49 +09:00
parent 488d652e61
commit e259f82b69
30 changed files with 1222 additions and 205 deletions
+78 -8
View File
@@ -1,7 +1,7 @@
/// <reference lib="webworker" />
import { countOptimizationCandidates, optimizeExactOrderAndCooldowns } from '../engine/index.ts';
import type { ExactOptimizationResult, ExactOptimizeOptions, ExactSongChart, OptimizerCard } from '../engine/index.ts';
import { countOptimizationCandidates, estimateScoreAchievementProbabilities, optimizeExactOrderAndCooldowns } from '../engine/index.ts';
import type { ExactOptimizationResult, ExactOptimizeOptions, ExactSongChart, OptimizerCard, ScoreAchievementProbabilities } from '../engine/index.ts';
type OptimizeRequest = {
type: 'optimize-exact';
@@ -9,6 +9,7 @@ type OptimizeRequest = {
chart: ExactSongChart;
perfectNoteScore: number;
directSongScoreBonusPercent?: number;
includeAchievementProbabilities?: boolean;
};
type CombinationCandidate = {
@@ -24,7 +25,31 @@ type CombinationRequest = {
chart: ExactSongChart;
};
self.onmessage = (event: MessageEvent<OptimizeRequest | CombinationRequest>) => {
type RankedCombination = {
result: ExactOptimizationResult;
overallPower: number;
cards?: OptimizerCard[];
directSongScoreBonusPercent?: number;
achievementProbabilities?: ScoreAchievementProbabilities;
};
type AchievementRequest = {
type: 'estimate-achievement';
rankings: RankedCombination[];
chart: ExactSongChart;
sampleCount?: number;
};
function cardsForResult(cards: readonly OptimizerCard[], result: ExactOptimizationResult): OptimizerCard[] {
const cardById = new Map(cards.map((card) => [card.id, card]));
return result.order.map((cardId) => {
const card = cardById.get(cardId);
if (!card) throw new Error('확률 계산 대상의 스페셜 순서를 복원하지 못했습니다.');
return card;
});
}
self.onmessage = (event: MessageEvent<OptimizeRequest | CombinationRequest | AchievementRequest>) => {
try {
if (event.data.type === 'optimize-exact') {
@@ -34,14 +59,49 @@ self.onmessage = (event: MessageEvent<OptimizeRequest | CombinationRequest>) =>
onProgress: (progress) => self.postMessage({ type: 'progress', progress })
};
const result = optimizeExactOrderAndCooldowns(event.data.cards, event.data.chart, options);
self.postMessage({ type: 'result', result });
let achievementProbabilities: ScoreAchievementProbabilities | undefined;
if (event.data.includeAchievementProbabilities) {
const orderedCards = cardsForResult(event.data.cards, result);
achievementProbabilities = estimateScoreAchievementProbabilities(
orderedCards,
result.cooldownReductions,
event.data.chart,
{ ...options, sampleCount: 25_000 }
);
}
self.postMessage({ type: 'result', result, achievementProbabilities });
return;
}
if (event.data.type === 'estimate-achievement') {
const request = event.data;
const rankings = request.rankings.map((ranking, rankingIndex) => {
if (!ranking.cards) throw new Error('확률 계산에 필요한 카드 정보가 없습니다.');
const orderedCards = cardsForResult(ranking.cards, ranking.result);
const achievementProbabilities = estimateScoreAchievementProbabilities(
orderedCards,
ranking.result.cooldownReductions,
event.data.chart,
{
perfectNoteScore: ranking.result.perfectNoteScore,
directSongScoreBonusPercent: ranking.directSongScoreBonusPercent,
sampleCount: request.sampleCount
}
);
self.postMessage({
type: 'achievement-progress',
progress: { evaluatedCandidates: rankingIndex + 1, totalCandidates: request.rankings.length }
});
return { ...ranking, achievementProbabilities };
});
self.postMessage({ type: 'achievement-result', rankings });
return;
}
const totalCandidates = event.data.candidates.reduce((sum, candidate) => sum + countOptimizationCandidates(candidate.cards), 0);
let completedCandidates = 0;
let best: { result: ExactOptimizationResult; overallPower: number } | null = null;
const rankings: { result: ExactOptimizationResult; overallPower: number }[] = [];
let best: RankedCombination | null = null;
const rankings: RankedCombination[] = [];
for (const candidate of event.data.candidates) {
const result = optimizeExactOrderAndCooldowns(candidate.cards, event.data.chart, {
perfectNoteScore: candidate.perfectNoteScore,
@@ -54,9 +114,19 @@ self.onmessage = (event: MessageEvent<OptimizeRequest | CombinationRequest>) =>
completedCandidates += countOptimizationCandidates(candidate.cards);
if (!best || result.theoreticalMaxScore > best.result.theoreticalMaxScore ||
(result.theoreticalMaxScore === best.result.theoreticalMaxScore && result.expectedScore > best.result.expectedScore)) {
best = { result, overallPower: candidate.overallPower };
best = {
result,
overallPower: candidate.overallPower,
cards: candidate.cards,
directSongScoreBonusPercent: candidate.directSongScoreBonusPercent
};
}
rankings.push({ result, overallPower: candidate.overallPower });
rankings.push({
result,
overallPower: candidate.overallPower,
cards: candidate.cards,
directSongScoreBonusPercent: candidate.directSongScoreBonusPercent
});
}
if (!best) throw new Error('정밀 분석할 조합이 없습니다.');
rankings.sort((left, right) => right.result.theoreticalMaxScore - left.result.theoreticalMaxScore ||