feat: add combination search workflow

This commit is contained in:
2026-08-18 16:30:51 +09:00
parent 7b713bffe3
commit f953fe9970
10 changed files with 1294 additions and 24 deletions
+55
View File
@@ -0,0 +1,55 @@
export type CombinationSearchCard = {
id: string;
characterId: string;
};
type BeamState<T extends CombinationSearchCard> = {
cards: T[];
score: number;
};
export function generateCombinationCandidates<T extends CombinationSearchCard>(
fixedCards: readonly T[],
candidateCards: readonly T[],
targetSize: number,
scoreCard: (card: T) => number,
beamWidth = 800
): T[][] {
if (targetSize < fixedCards.length) throw new Error('목표 카드 수가 고정 카드 수보다 작습니다.');
if (new Set(fixedCards.map((card) => card.characterId)).size !== fixedCards.length) {
throw new Error('같은 멤버를 두 번 고정할 수 없습니다.');
}
const fixedIds = new Set(fixedCards.map((card) => card.id));
const fixedCharacters = new Set(fixedCards.map((card) => card.characterId));
const pool = candidateCards.filter((card) => !fixedIds.has(card.id) && !fixedCharacters.has(card.characterId));
let beam: BeamState<T>[] = [{
cards: Array.from(fixedCards),
score: fixedCards.reduce((sum, card) => sum + scoreCard(card), 0)
}];
for (let size = fixedCards.length; size < targetSize; size += 1) {
const expanded = new Map<string, BeamState<T>>();
for (const state of beam) {
const usedCharacters = new Set(state.cards.map((card) => card.characterId));
const usedIds = new Set(state.cards.map((card) => card.id));
for (const card of pool) {
if (usedIds.has(card.id)) continue;
if (usedCharacters.has(card.characterId)) continue;
const nextState = {
cards: [...state.cards, card],
score: state.score + scoreCard(card)
};
const key = nextState.cards.map((entry) => entry.id).sort().join('|');
if (!expanded.has(key)) expanded.set(key, nextState);
}
}
beam = Array.from(expanded.values())
.sort((left, right) => right.score - left.score || left.cards.map((card) => card.id).join().localeCompare(right.cards.map((card) => card.id).join()))
.slice(0, Math.max(1, beamWidth));
if (beam.length === 0) break;
}
return beam
.filter((state) => state.cards.length === targetSize)
.sort((left, right) => right.score - left.score)
.map((state) => state.cards);
}