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( sources: readonly (readonly T[])[], candidates: readonly T[], fixedIds: ReadonlySet, prepare: (cards: T[]) => P, priority: (candidate: P) => number, accepts: (cards: readonly T[]) => boolean ): P[] { const unique = new Map(); 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( candidates: readonly T[], evaluatedKeys: ReadonlySet, limit: number ): T[] { const unique = new Map(); 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)); }