fix: prioritize theoretical max score
This commit is contained in:
@@ -83,3 +83,50 @@ test('스페셜 발동 순서를 탐색해 가장 높은 액티브를 강한 구
|
|||||||
assert.equal(firstActiveWinner?.cardId, 'c');
|
assert.equal(firstActiveWinner?.cardId, 'c');
|
||||||
assert.ok(Math.abs((firstActiveWinner?.scoreUpPercent ?? 0) - 242) < 1e-9);
|
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 { effectiveActiveProbability } from './active-probability.ts';
|
||||||
import { calculateExactScore } from './score.ts';
|
import { calculateExactScore, finalNoteScore } from './score.ts';
|
||||||
import { passiveScoreSupportByCard } from './passive-support.ts';
|
import { passiveScoreSupportByCard } from './passive-support.ts';
|
||||||
import { effectiveCooldownSeconds } from './timeline.ts';
|
import { effectiveCooldownSeconds } from './timeline.ts';
|
||||||
import { COOLDOWN_REDUCTIONS } from './types.ts';
|
import { COOLDOWN_REDUCTIONS } from './types.ts';
|
||||||
@@ -180,7 +180,7 @@ function scoreAssignment(
|
|||||||
profiles: ReadonlyMap<string, CandidateProfile>,
|
profiles: ReadonlyMap<string, CandidateProfile>,
|
||||||
base: Float64Array,
|
base: Float64Array,
|
||||||
directBonus: number
|
directBonus: number
|
||||||
): number {
|
): { expectedScore: number; theoreticalMaxScore: number } {
|
||||||
const selected = cards.map((card) => {
|
const selected = cards.map((card) => {
|
||||||
const reduction = reductions[card.id] ?? 0;
|
const reduction = reductions[card.id] ?? 0;
|
||||||
const profile = profiles.get(`${card.id}:${reduction}`);
|
const profile = profiles.get(`${card.id}:${reduction}`);
|
||||||
@@ -189,7 +189,8 @@ function scoreAssignment(
|
|||||||
});
|
});
|
||||||
const boosts = new Float64Array(cards.length);
|
const boosts = new Float64Array(cards.length);
|
||||||
const probabilities = 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) {
|
for (let noteIndex = 0; noteIndex < base.length; noteIndex += 1) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -212,9 +213,10 @@ function scoreAssignment(
|
|||||||
expectedBoost += previousFailed * probabilities[position] * boosts[position];
|
expectedBoost += previousFailed * probabilities[position] * boosts[position];
|
||||||
previousFailed *= 1 - probabilities[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(
|
export function optimizeExactOrderAndCooldowns(
|
||||||
@@ -230,7 +232,8 @@ export function optimizeExactOrderAndCooldowns(
|
|||||||
const orderCount = cards.reduce((value, _, index) => value * (index + 1), 1);
|
const orderCount = cards.reduce((value, _, index) => value * (index + 1), 1);
|
||||||
const totalCandidates = orderCount * reductionAssignments.length;
|
const totalCandidates = orderCount * reductionAssignments.length;
|
||||||
let evaluatedCandidates = 0;
|
let evaluatedCandidates = 0;
|
||||||
let bestScore = Number.NEGATIVE_INFINITY;
|
let bestTheoreticalMaxScore = Number.NEGATIVE_INFINITY;
|
||||||
|
let bestExpectedScore = Number.NEGATIVE_INFINITY;
|
||||||
let bestOrder: OptimizerCard[] | null = null;
|
let bestOrder: OptimizerCard[] | null = null;
|
||||||
let bestReductions: Record<string, CooldownReduction> | null = null;
|
let bestReductions: Record<string, CooldownReduction> | null = null;
|
||||||
let bestReductionTotal = Number.POSITIVE_INFINITY;
|
let bestReductionTotal = Number.POSITIVE_INFINITY;
|
||||||
@@ -240,8 +243,14 @@ export function optimizeExactOrderAndCooldowns(
|
|||||||
for (const reductions of reductionAssignments) {
|
for (const reductions of reductionAssignments) {
|
||||||
const score = scoreAssignment(order, reductions, profiles, base, options.directSongScoreBonusPercent ?? 0);
|
const score = scoreAssignment(order, reductions, profiles, base, options.directSongScoreBonusPercent ?? 0);
|
||||||
const reductionTotal = Object.values(reductions).reduce<number>((sum, value) => sum + value, 0);
|
const reductionTotal = Object.values(reductions).reduce<number>((sum, value) => sum + value, 0);
|
||||||
if (score > bestScore || (score === bestScore && reductionTotal < bestReductionTotal)) {
|
const isHigherMaximum = score.theoreticalMaxScore > bestTheoreticalMaxScore;
|
||||||
bestScore = score;
|
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;
|
bestOrder = order;
|
||||||
bestReductions = reductions;
|
bestReductions = reductions;
|
||||||
bestReductionTotal = reductionTotal;
|
bestReductionTotal = reductionTotal;
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ function expectedStrongestBoost(candidates: readonly ActiveCandidate[]): number
|
|||||||
return expectedBoost;
|
return expectedBoost;
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalNoteScore(score: number, percent: number): number {
|
export function finalNoteScore(score: number, percent: number): number {
|
||||||
const scaled = score * (1 + percent / 100);
|
const scaled = score * (1 + percent / 100);
|
||||||
return Math.ceil(scaled - Number.EPSILON * Math.max(1, scaled) * 4);
|
return Math.ceil(scaled - Number.EPSILON * Math.max(1, scaled) * 4);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user