fix: prioritize theoretical max score
This commit is contained in:
@@ -83,3 +83,50 @@ test('스페셜 발동 순서를 탐색해 가장 높은 액티브를 강한 구
|
||||
assert.equal(firstActiveWinner?.cardId, 'c');
|
||||
assert.ok(Math.abs((firstActiveWinner?.scoreUpPercent ?? 0) - 242) < 1e-9);
|
||||
});
|
||||
|
||||
test('예상 점수보다 이론상 고점을 우선해 스페셜 순서를 선택한다', () => {
|
||||
const formation: OptimizerCard[] = [
|
||||
{
|
||||
id: 'active',
|
||||
name: 'active',
|
||||
active: { cooldownSeconds: 1, durationSeconds: 1, scoreUpPercent: 100, probabilityPercent: 10 },
|
||||
special: { durationSeconds: 0, scoreUpPercent: 0 },
|
||||
allowedCooldownReductions: [0]
|
||||
},
|
||||
{
|
||||
id: 'score-support',
|
||||
name: 'score-support',
|
||||
active: { cooldownSeconds: 1, durationSeconds: 0, scoreUpPercent: 0 },
|
||||
special: { durationSeconds: 1, scoreUpPercent: 200 },
|
||||
allowedCooldownReductions: [0]
|
||||
},
|
||||
{
|
||||
id: 'rate-support',
|
||||
name: 'rate-support',
|
||||
active: { cooldownSeconds: 1, durationSeconds: 0, scoreUpPercent: 0 },
|
||||
special: { durationSeconds: 1, scoreUpPercent: 0, activationRateUpPercent: 900 },
|
||||
allowedCooldownReductions: [0]
|
||||
},
|
||||
...['filler-a', 'filler-b'].map((id): OptimizerCard => ({
|
||||
id,
|
||||
name: id,
|
||||
active: { cooldownSeconds: 1, durationSeconds: 0, scoreUpPercent: 0 },
|
||||
special: { durationSeconds: 0, scoreUpPercent: 0 },
|
||||
allowedCooldownReductions: [0]
|
||||
}))
|
||||
];
|
||||
const targetChart: ExactSongChart = {
|
||||
songId: 'maximum-priority-test',
|
||||
difficulty: 'expert',
|
||||
durationSeconds: 2,
|
||||
scoreRatio: 1,
|
||||
notes: [{ timeSeconds: 1.1, type: 'regular', isFlick: false }],
|
||||
specialStartSeconds: [0.5, null, null, null, null]
|
||||
};
|
||||
|
||||
const result = optimizeExactOrderAndCooldowns(formation, targetChart, { perfectNoteScore: 100 });
|
||||
|
||||
assert.equal(result.order[0], 'score-support');
|
||||
assert.equal(result.theoreticalMaxScore, 400);
|
||||
assert.equal(result.expectedScore, 130);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { effectiveActiveProbability } from './active-probability.ts';
|
||||
import { calculateExactScore } from './score.ts';
|
||||
import { calculateExactScore, finalNoteScore } from './score.ts';
|
||||
import { passiveScoreSupportByCard } from './passive-support.ts';
|
||||
import { effectiveCooldownSeconds } from './timeline.ts';
|
||||
import { COOLDOWN_REDUCTIONS } from './types.ts';
|
||||
@@ -180,7 +180,7 @@ function scoreAssignment(
|
||||
profiles: ReadonlyMap<string, CandidateProfile>,
|
||||
base: Float64Array,
|
||||
directBonus: number
|
||||
): number {
|
||||
): { expectedScore: number; theoreticalMaxScore: number } {
|
||||
const selected = cards.map((card) => {
|
||||
const reduction = reductions[card.id] ?? 0;
|
||||
const profile = profiles.get(`${card.id}:${reduction}`);
|
||||
@@ -189,7 +189,8 @@ function scoreAssignment(
|
||||
});
|
||||
const boosts = new Float64Array(cards.length);
|
||||
const probabilities = new Float64Array(cards.length);
|
||||
let rawScore = 0;
|
||||
let expectedScore = 0;
|
||||
let theoreticalMaxScore = 0;
|
||||
|
||||
for (let noteIndex = 0; noteIndex < base.length; noteIndex += 1) {
|
||||
let count = 0;
|
||||
@@ -212,9 +213,10 @@ function scoreAssignment(
|
||||
expectedBoost += previousFailed * probabilities[position] * boosts[position];
|
||||
previousFailed *= 1 - probabilities[position];
|
||||
}
|
||||
rawScore += base[noteIndex] * (1 + expectedBoost);
|
||||
expectedScore += finalNoteScore(base[noteIndex] * (1 + expectedBoost), directBonus);
|
||||
theoreticalMaxScore += finalNoteScore(base[noteIndex] * (1 + (count > 0 ? boosts[0] : 0)), directBonus);
|
||||
}
|
||||
return Math.floor(Math.floor(rawScore) * (1 + directBonus / 100) + Number.EPSILON * Math.max(1, rawScore) * 4);
|
||||
return { expectedScore, theoreticalMaxScore };
|
||||
}
|
||||
|
||||
export function optimizeExactOrderAndCooldowns(
|
||||
@@ -230,7 +232,8 @@ export function optimizeExactOrderAndCooldowns(
|
||||
const orderCount = cards.reduce((value, _, index) => value * (index + 1), 1);
|
||||
const totalCandidates = orderCount * reductionAssignments.length;
|
||||
let evaluatedCandidates = 0;
|
||||
let bestScore = Number.NEGATIVE_INFINITY;
|
||||
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;
|
||||
@@ -240,8 +243,14 @@ export function optimizeExactOrderAndCooldowns(
|
||||
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);
|
||||
if (score > bestScore || (score === bestScore && reductionTotal < bestReductionTotal)) {
|
||||
bestScore = score;
|
||||
const isHigherMaximum = score.theoreticalMaxScore > bestTheoreticalMaxScore;
|
||||
const isHigherExpectedTie = score.theoreticalMaxScore === bestTheoreticalMaxScore &&
|
||||
score.expectedScore > bestExpectedScore;
|
||||
const isCheaperTie = score.theoreticalMaxScore === bestTheoreticalMaxScore &&
|
||||
score.expectedScore === bestExpectedScore && reductionTotal < bestReductionTotal;
|
||||
if (isHigherMaximum || isHigherExpectedTie || isCheaperTie) {
|
||||
bestTheoreticalMaxScore = score.theoreticalMaxScore;
|
||||
bestExpectedScore = score.expectedScore;
|
||||
bestOrder = order;
|
||||
bestReductions = reductions;
|
||||
bestReductionTotal = reductionTotal;
|
||||
|
||||
@@ -108,7 +108,7 @@ function expectedStrongestBoost(candidates: readonly ActiveCandidate[]): number
|
||||
return expectedBoost;
|
||||
}
|
||||
|
||||
function finalNoteScore(score: number, percent: number): number {
|
||||
export function finalNoteScore(score: number, percent: number): number {
|
||||
const scaled = score * (1 + percent / 100);
|
||||
return Math.ceil(scaled - Number.EPSILON * Math.max(1, scaled) * 4);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user