Publish GitHub Pages output / publish (push) Successful in 30s
고정 멤버와 탐색 목적을 유지하며 제한적인 두 카드 교체 후보를 정밀 평가합니다. 기존 평가 예산 안에서 이미 평가한 편성을 제외하고 후보를 재선택합니다.
84 lines
3.8 KiB
TypeScript
84 lines
3.8 KiB
TypeScript
import { effectiveActiveProbability } from './active-probability.ts';
|
|
import type { CombinationSearchCard } from './combination-search.ts';
|
|
import type { OptimizerCard } from './types.ts';
|
|
|
|
type RefinementCandidate = {
|
|
cards: readonly OptimizerCard[];
|
|
overallPower: number;
|
|
optimizationObjective?: 'maximum' | 'expected';
|
|
directSongScoreBonusPercent?: number;
|
|
finalScoreBonusPercent?: number;
|
|
rankingMultiplier?: number;
|
|
};
|
|
|
|
export function refinementCandidatePriority(candidate: RefinementCandidate): number {
|
|
const expected = candidate.optimizationObjective === 'expected';
|
|
const skillPotential = candidate.cards.reduce((sum, card) => sum +
|
|
card.active.scoreUpPercent * card.active.durationSeconds / Math.max(1, card.active.cooldownSeconds) *
|
|
(expected ? effectiveActiveProbability(card) : 1) +
|
|
card.special.scoreUpPercent * card.special.durationSeconds / 100 +
|
|
(card.scoreSupportPercent ?? 0) +
|
|
(card.passiveScoreSupportEffects ?? []).reduce((total, effect) => total +
|
|
effect.percent * (effect.target === 'all' ? candidate.cards.length : effect.targetCount ?? 1), 0), 0);
|
|
return candidate.overallPower * (1 + skillPotential / 500) *
|
|
(1 + (candidate.directSongScoreBonusPercent ?? 0) / 100) *
|
|
(1 + (candidate.finalScoreBonusPercent ?? 0) / 100) * (candidate.rankingMultiplier ?? 1);
|
|
}
|
|
|
|
export function refinementCombinationKey(cards: readonly { id: string }[]): string {
|
|
return cards.map((card) => card.id).sort().join('|');
|
|
}
|
|
|
|
/** Bounded two-card neighborhoods; all final scores still use the exact evaluator. */
|
|
export function generateTwoCardReplacementCandidates<T extends CombinationSearchCard, P>(
|
|
sources: readonly (readonly T[])[],
|
|
candidates: readonly T[],
|
|
fixedIds: ReadonlySet<string>,
|
|
prepare: (cards: T[]) => P,
|
|
priority: (candidate: P) => number,
|
|
accepts: (cards: readonly T[]) => boolean
|
|
): P[] {
|
|
const unique = new Map<string, P>();
|
|
const valid = (cards: readonly T[]) => new Set(cards.map((card) => card.characterId)).size === cards.length && accepts(cards);
|
|
for (const source of sources.slice(0, 3)) {
|
|
if (source.length !== 5 || !valid(source)) continue;
|
|
for (let first = 0; first < 5; first++) {
|
|
if (fixedIds.has(source[first].id)) continue;
|
|
for (let second = first + 1; second < 5; second++) {
|
|
if (fixedIds.has(source[second].id)) continue;
|
|
const kept = source.filter((_, index) => index !== first && index !== second);
|
|
const shortlist = candidates
|
|
.filter((card) => valid([...kept, card]))
|
|
.map((card) => ({ card, score: priority(prepare([...kept, card])) }))
|
|
.sort((left, right) => right.score - left.score || left.card.id.localeCompare(right.card.id))
|
|
.slice(0, 8);
|
|
for (let left = 0; left < shortlist.length; left++) {
|
|
for (let right = left + 1; right < shortlist.length; right++) {
|
|
const cards = [...kept, shortlist[left].card, shortlist[right].card];
|
|
if (!valid(cards)) continue;
|
|
const key = refinementCombinationKey(cards);
|
|
if (!unique.has(key)) unique.set(key, prepare(cards));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return Array.from(unique.values());
|
|
}
|
|
|
|
export function selectRefinementCandidates<T extends RefinementCandidate>(
|
|
candidates: readonly T[],
|
|
evaluatedKeys: ReadonlySet<string>,
|
|
limit: number
|
|
): T[] {
|
|
const unique = new Map<string, T>();
|
|
for (const candidate of candidates) {
|
|
const key = refinementCombinationKey(candidate.cards);
|
|
if (!evaluatedKeys.has(key) && !unique.has(key)) unique.set(key, candidate);
|
|
}
|
|
return Array.from(unique.values())
|
|
.sort((left, right) => refinementCandidatePriority(right) - refinementCandidatePriority(left) ||
|
|
refinementCombinationKey(left.cards).localeCompare(refinementCombinationKey(right.cards)))
|
|
.slice(0, Math.max(0, limit));
|
|
}
|