feat: limit medium-probability cards and sync Docker dev dependencies

This commit is contained in:
2026-09-02 12:28:55 +09:00
parent 7308428945
commit 1e3d1d0b08
7 changed files with 138 additions and 11 deletions
+18 -3
View File
@@ -23,6 +23,15 @@ export type CombinationScoringStrategy<T extends CombinationSearchCard> = {
const SCREENING_LIMIT_PER_OPEN_SLOT = 5_000;
const EXACT_FINALIST_LIMIT_PER_OPEN_SLOT = 56;
export function withinMediumProbabilityCardLimit(
cards: readonly { details: { active: readonly { probabilityPercent?: number | null }[] } }[],
maximum: number | null
): boolean {
return maximum === null || cards.filter((card) =>
card.details.active.some((level) => level.probabilityPercent === 46)
).length <= maximum;
}
export function combinationSearchLimits(fixedCardCount: number, targetSize = 5): CombinationSearchLimits {
const normalizedTargetSize = Math.max(1, Math.floor(targetSize));
const normalizedFixedCardCount = Math.min(normalizedTargetSize, Math.max(0, Math.floor(fixedCardCount)));
@@ -49,12 +58,15 @@ export function generateCombinationCandidates<T extends CombinationSearchCard>(
targetSize: number,
scoreCard: (card: T) => number,
beamWidth = 800,
scoreCombination?: (cards: readonly T[]) => number
scoreCombination?: (cards: readonly T[]) => number,
// Must accept valid partial combinations, as it is checked at every expansion.
acceptsCombination?: (cards: readonly T[]) => boolean
): T[][] {
if (targetSize < fixedCards.length) throw new Error('목표 카드 수가 고정 카드 수보다 작습니다.');
if (new Set(fixedCards.map((card) => card.characterId)).size !== fixedCards.length) {
throw new Error('같은 멤버를 두 번 고정할 수 없습니다.');
}
if (acceptsCombination && !acceptsCombination(fixedCards)) return [];
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));
@@ -72,6 +84,7 @@ export function generateCombinationCandidates<T extends CombinationSearchCard>(
if (usedIds.has(card.id)) continue;
if (usedCharacters.has(card.characterId)) continue;
const cards = [...state.cards, card];
if (acceptsCombination && !acceptsCombination(cards)) continue;
const nextState = {
cards,
score: scoreCombination?.(cards) ?? state.score + scoreCard(card)
@@ -96,7 +109,8 @@ export function generateAnchoredCombinationCandidates<T extends CombinationSearc
candidateCards: readonly T[],
targetSize: number,
strategies: readonly CombinationScoringStrategy<T>[],
perAnchorLimit: number
perAnchorLimit: number,
acceptsCombination?: (cards: readonly T[]) => boolean
): T[][] {
if (strategies.length === 0 || perAnchorLimit <= 0 || fixedCards.length >= targetSize) return [];
const fixedCharacters = new Set(fixedCards.map((card) => card.characterId));
@@ -113,7 +127,8 @@ export function generateAnchoredCombinationCandidates<T extends CombinationSearc
targetSize,
strategy.scoreCard,
perStrategyLimit,
strategy.scoreCombination
strategy.scoreCombination,
acceptsCombination
)) {
anchored.set(cards.map((card) => card.id).sort().join('|'), cards);
}