perf: 조합 탐색 정밀 계산과 Worker 처리 최적화
Publish GitHub Pages output / publish (push) Successful in 34s

카드 조건과 빈도별 공통 계산을 재사용하고 준비·평가 Worker를 재사용합니다.
준비된 후보를 모두 정밀 평가하고 최종 결과를 한 번 표시합니다.
업데이트 문구를 한 항목으로 통합합니다.
This commit is contained in:
dal4segno committed 2026-10-13 12:03:10 +09:00
1 parent f009a7fd6b
commit da40ec4238
18 files changed
+1623 -467

No files matched your search

+26 -4
View File
@@ -6,6 +6,10 @@ export type CombinationSearchCard = {
type BeamState<T extends CombinationSearchCard> = {
cards: T[];
score: number;
mask0: number;
mask1: number;
mask2: number;
tieKey?: string;
};
export type CombinationSearchLimits = {
@@ -70,7 +74,18 @@ export function generateCombinationCandidates<T extends CombinationSearchCard>(
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));
const identities = [...new Set([...fixedCards, ...pool].map((card) => card.id))];
const compactKeys = identities.length <= 96 && identities.every((id) => typeof id === 'string' && !id.includes('|'));
const flags = new Map(identities.map((id, index) => [id, [
compactKeys && index < 32 ? 1 << index : 0,
compactKeys && index >= 32 && index < 64 ? 1 << (index - 32) : 0,
compactKeys && index >= 64 ? 1 << (index - 64) : 0
]]));
const entries = pool.map((card) => ({ card, flags: flags.get(card.id)! }));
const fixedMasks = fixedCards.reduce((mask, card) => mask.map((value, index) => value | flags.get(card.id)![index]), [0, 0, 0]);
const tieKey = (state: BeamState<T>): string => state.tieKey ??= state.cards.map((card) => card.id).join();
let beam: BeamState<T>[] = [{
mask0: fixedMasks[0], mask1: fixedMasks[1], mask2: fixedMasks[2], tieKey: undefined,
cards: Array.from(fixedCards),
score: scoreCombination?.(fixedCards) ?? fixedCards.reduce((sum, card) => sum + scoreCard(card), 0)
}];
@@ -80,21 +95,28 @@ export function generateCombinationCandidates<T extends CombinationSearchCard>(
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) {
for (const { card, flags: bits } of entries) {
if (usedIds.has(card.id)) continue;
if (usedCharacters.has(card.characterId)) continue;
const mask0 = state.mask0 | bits[0], mask1 = state.mask1 | bits[1], mask2 = state.mask2 | bits[2];
const compactKey = compactKeys ? mask0 + ':' + mask1 + ':' + mask2 : '';
if (compactKeys && !acceptsCombination && expanded.has(compactKey)) continue;
const cards = [...state.cards, card];
if (acceptsCombination && !acceptsCombination(cards)) continue;
// Preserve the first traversal's card order and Map insertion order.
// Scoring callbacks are pure; duplicate permutations need no evaluation.
const key = compactKeys ? compactKey : cards.map((entry) => entry.id).sort().join('|');
if (expanded.has(key)) continue;
const nextState = {
mask0, mask1, mask2, tieKey: undefined,
cards,
score: scoreCombination?.(cards) ?? state.score + scoreCard(card)
};
const key = nextState.cards.map((entry) => entry.id).sort().join('|');
if (!expanded.has(key)) expanded.set(key, nextState);
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()))
.sort((left, right) => right.score - left.score || tieKey(left).localeCompare(tieKey(right)))
.slice(0, Math.max(1, beamWidth));
if (beam.length === 0) break;
}