feat: compare top five cooldown combinations in analysis
Publish GitHub Pages output / publish (push) Successful in 42s

This commit is contained in:
dal4segno committed 2026-09-25 23:09:49 +09:00
1 parent 29e70c565d
commit 7c7f0d40c0
5 files changed
+133 -43

No files matched your search

+32 -29
View File
@@ -226,6 +226,16 @@ export function optimizeExactOrderAndCooldowns(
chart: ExactSongChart,
options: ExactOptimizeOptions
): ExactOptimizationResult {
return rankExactCooldownCombinations(cards, chart, options, 1)[0];
}
/** Keep the best special order for each distinct per-card cooldown assignment. */
export function rankExactCooldownCombinations(
cards: readonly OptimizerCard[],
chart: ExactSongChart,
options: ExactOptimizeOptions,
limit = 5
): ExactOptimizationResult[] {
if (cards.length !== 5 || new Set(cards.map((card) => card.id)).size !== 5) {
throw new Error('서로 다른 카드 5장이 필요합니다.');
}
@@ -234,46 +244,39 @@ export function optimizeExactOrderAndCooldowns(
const orderCount = cards.reduce((value, _, index) => value * (index + 1), 1);
const totalCandidates = orderCount * reductionAssignments.length;
let evaluatedCandidates = 0;
let bestTheoreticalMaxScore = Number.NEGATIVE_INFINITY;
let bestExpectedScore = Number.NEGATIVE_INFINITY;
let bestOrder: OptimizerCard[] | null = null;
let bestReductions: Record<string, CooldownReduction> | null = null;
let bestReductionTotal = Number.POSITIVE_INFINITY;
type Candidate = {
order: OptimizerCard[];
reductions: Record<string, CooldownReduction>;
theoreticalMaxScore: number;
expectedScore: number;
reductionTotal: number;
};
const bestByAssignment = new Map<Record<string, CooldownReduction>, Candidate>();
const compare = (left: Candidate, right: Candidate): number =>
(options.objective === 'expected'
? right.expectedScore - left.expectedScore || right.theoreticalMaxScore - left.theoreticalMaxScore
: right.theoreticalMaxScore - left.theoreticalMaxScore || right.expectedScore - left.expectedScore) ||
left.reductionTotal - right.reductionTotal;
for (const order of permutations(cards)) {
const profiles = makeProfiles(order, chart);
for (const reductions of reductionAssignments) {
const score = scoreAssignment(order, reductions, profiles, base, options.directSongScoreBonusPercent ?? 0);
const reductionTotal = Object.values(reductions).reduce<number>((sum, value) => sum + value, 0);
const expectedFirst = options.objective === 'expected';
const isBetterPrimary = expectedFirst
? score.expectedScore > bestExpectedScore
: score.theoreticalMaxScore > bestTheoreticalMaxScore;
const isBetterSecondary = expectedFirst
? score.expectedScore === bestExpectedScore && score.theoreticalMaxScore > bestTheoreticalMaxScore
: score.theoreticalMaxScore === bestTheoreticalMaxScore && score.expectedScore > bestExpectedScore;
const isCheaperTie = score.theoreticalMaxScore === bestTheoreticalMaxScore &&
score.expectedScore === bestExpectedScore && reductionTotal < bestReductionTotal;
if (isBetterPrimary || isBetterSecondary || isCheaperTie) {
bestTheoreticalMaxScore = score.theoreticalMaxScore;
bestExpectedScore = score.expectedScore;
bestOrder = order;
bestReductions = reductions;
bestReductionTotal = reductionTotal;
}
const candidate = { ...score, order, reductions, reductionTotal };
const previous = bestByAssignment.get(reductions);
if (!previous || compare(candidate, previous) < 0) bestByAssignment.set(reductions, candidate);
evaluatedCandidates += 1;
}
options.onProgress?.({ evaluatedCandidates, totalCandidates });
}
if (!bestOrder || !bestReductions) throw new Error('분석 결과를 만들지 못했습니다.');
const windows = buildExactSkillWindows(bestOrder, bestReductions, chart);
return {
...calculateExactScore(bestOrder, bestReductions, chart, options),
order: bestOrder.map((card) => card.id),
return [...bestByAssignment.values()].sort(compare).slice(0, Math.max(1, Math.floor(limit))).map((candidate) => ({
...calculateExactScore(candidate.order, candidate.reductions, chart, options),
order: candidate.order.map((card) => card.id),
formationOrder: cards.map((card) => card.id),
cooldownReductions: { ...bestReductions },
...windows,
cooldownReductions: { ...candidate.reductions },
...buildExactSkillWindows(candidate.order, candidate.reductions, chart),
evaluatedCandidates
};
}));
}