feat: show cumulative maximum score probability in timeline tooltip
Publish GitHub Pages output / publish (push) Successful in 56s

This commit is contained in:
2026-09-10 14:40:07 +09:00
parent b1b99e6cc0
commit 1fa15cbe08
5 changed files with 88 additions and 13 deletions
+28 -4
View File
@@ -62,7 +62,8 @@ function prepareModel(
cards: readonly OptimizerCard[],
reductions: Readonly<Record<string, CooldownReduction>>,
chart: ExactSongChart,
options: ExactScoreOptions
options: ExactScoreOptions,
onNote?: (probability: number) => void
): PreparedProbabilityModel {
const attempts: Attempt[] = [];
const attemptIndexes = new Map<string, number>();
@@ -70,6 +71,8 @@ function prepareModel(
const grouped = new Map<string, { candidates: NoteCandidate[]; scores: number[] }>();
const clauses: number[][] = [];
let maximumScore = 0;
let prefixProbability = 1;
let prefixClauses: number[][] = [];
scoreNotes(chart).forEach((note, noteIndex) => {
const candidates: NoteCandidate[] = [];
@@ -80,6 +83,8 @@ function prepareModel(
const attemptNumber = Math.floor((note.timeSeconds + 1e-9) / interval);
if (attemptNumber <= 0) return;
const activationTime = attemptNumber * interval;
if (options.excludedActiveWindows?.some((window) =>
window.cardId === card.id && Math.abs(window.startSeconds - activationTime) < 1e-9)) return;
if (note.timeSeconds >= activationTime + card.active.durationSeconds) return;
const key = `${card.id}:${attemptNumber}`;
let attemptIndex = attemptIndexes.get(key);
@@ -110,11 +115,18 @@ function prepareModel(
];
maximumScore += scores[0];
if (candidates.length > 0) {
clauses.push(candidates
// No activation is required when rounding gives the baseline the same score.
if (scores[0] > scores[scores.length - 1]) {
const clause = candidates
.filter((_, index) => scores[index] === scores[0])
.map((candidate) => candidate.attemptIndex));
.map((candidate) => candidate.attemptIndex);
clauses.push(clause);
if (onNote && !prefixClauses.some((existing) => existing.every((variable) => clause.includes(variable)))) {
prefixClauses = simplifyClauses([...prefixClauses, clause]);
prefixProbability = exactClauseProbability(prefixClauses, attempts);
}
}
onNote?.(prefixProbability);
const signature = candidates.map((candidate) => `${candidate.attemptIndex}:${candidate.boost.toPrecision(15)}`).join('|');
const existing = grouped.get(signature);
@@ -130,6 +142,18 @@ function prepareModel(
};
}
/** Exact cumulative maximum-score probability after each chart note, in note order. */
export function calculateMaximumScoreProbabilityTimeline(
cards: readonly OptimizerCard[],
reductions: Readonly<Record<string, CooldownReduction>>,
chart: ExactSongChart,
options: ExactScoreOptions
): number[] {
const probabilities: number[] = [];
prepareModel(cards, reductions, chart, options, (probability) => probabilities.push(probability));
return probabilities;
}
function simplifyClauses(clauses: readonly (readonly number[])[]): number[][] {
const unique = new Map<string, number[]>();
for (const clause of clauses) {