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
+34 -4
View File
@@ -29,7 +29,8 @@
perfectNoteScoreFromPower,
resolveCandidateAwakening,
selectMemberDiverseCandidates,
toOptimizerCard
toOptimizerCard,
withinMediumProbabilityCardLimit
} from '$lib/engine/index.ts';
import type { CooldownReduction, ExactOptimizationResult, ExactSongChart, OptimizationProgress, OptimizerCard, ScoreAchievementProbabilities } from '$lib/engine/index.ts';
import { USER_DATA_STORAGE_KEY } from '$lib/storage/browser-storage';
@@ -77,6 +78,7 @@
fixedGenerationBonusOverrides?: (number | null)[];
songScoreBonusOverride?: number | null;
ownedCardsOnly?: boolean;
maxMediumProbabilityCards?: number | null;
maximizeCompute?: boolean;
leaderCharacterId: string;
leaderOutfitCardId: string;
@@ -136,6 +138,7 @@
let fixedGenerationBonusOverrides: (number | null)[] = [null, null, null, null];
let songScoreBonusOverride: number | null = 10;
let ownedCardsOnly = false;
let maxMediumProbabilityCards: number | null = null;
let maximizeCompute = false;
let leaderCharacterId = leaderMembers[0]?.characterId ?? '';
let leaderOutfitCardId = leaderMembers[0]?.id ?? '';
@@ -220,6 +223,7 @@
fixedGenerationBonusOverrides,
songScoreBonusOverride,
ownedCardsOnly,
maxMediumProbabilityCards,
maximizeCompute,
leaderCharacterId,
leaderOutfitCardId
@@ -237,6 +241,7 @@
fixedGenerationBonusOverrides,
songScoreBonusOverride,
ownedCardsOnly,
maxMediumProbabilityCards,
maximizeCompute,
leaderCharacterId,
leaderOutfitCardId
@@ -382,6 +387,9 @@
? null
: normalizeSongScoreBonusOverride(state.songScoreBonusOverride) ?? 10;
ownedCardsOnly = state.ownedCardsOnly === true;
maxMediumProbabilityCards = typeof state.maxMediumProbabilityCards === 'number' &&
Number.isInteger(state.maxMediumProbabilityCards) && state.maxMediumProbabilityCards >= 0 && state.maxMediumProbabilityCards < 5
? state.maxMediumProbabilityCards : null;
if (ownedCardsOnly) toggleOwnedCardsOnly(true);
maximizeCompute = state.maximizeCompute === true;
if (typeof state.leaderCharacterId === 'string' && leaderMembers.some((member) => member.characterId === state.leaderCharacterId)) {
@@ -598,11 +606,15 @@
return boards;
}
function acceptsCombination(cards: readonly CardRow[]): boolean {
return withinMediumProbabilityCardLimit(cards, maxMediumProbabilityCards);
}
function screeningCombinations(): CardRow[][] {
const limits = combinationSearchLimits(fixedCards.length);
const screeningLimit = eventMode ? Math.min(limits.screeningLimit, 9_000) : limits.screeningLimit;
if (fixedCards.length === 4) {
return generateCombinationCandidates(fixedCards, candidateCards, 5, roughCardScore, screeningLimit);
return generateCombinationCandidates(fixedCards, candidateCards, 5, roughCardScore, screeningLimit, undefined, acceptsCombination);
}
const globalBudget = eventMode ? screeningLimit : Math.ceil(screeningLimit * 0.8);
const anchoredBudget = screeningLimit - globalBudget;
@@ -627,7 +639,8 @@
5,
strategy.scoreCard,
perStrategy,
strategy.scoreCombination
strategy.scoreCombination,
acceptsCombination
)) {
combined.set(combinationKey(cards), cards);
}
@@ -641,7 +654,8 @@
candidateCards,
5,
strategies,
perAnchorLimit
perAnchorLimit,
acceptsCombination
)) {
combined.set(combinationKey(cards), cards);
}
@@ -699,6 +713,7 @@
for (const replacement of candidateCards) {
if (replacement.id === source.id || otherCharacters.has(replacement.characterId)) continue;
const cards = sourceCards.map((card, cardIndex) => cardIndex === index ? replacement : card);
if (!acceptsCombination(cards)) continue;
const key = combinationKey(cards);
if (key === sourceKey) continue;
const candidate = alternatives.get(key) ?? { cards, roughScore: roughCombinationScore(cards) };
@@ -1001,6 +1016,10 @@
errorMessage = '같은 멤버의 일반 카드와 이격 카드는 함께 고정할 수 없습니다.';
return;
}
if (!acceptsCombination(fixedCards)) {
errorMessage = `고정 카드의 액티브 발동 확률 ‘중간’ 카드가 최대 ${maxMediumProbabilityCards}장 제한을 초과합니다. 고정 카드나 제한을 변경해 주세요.`;
return;
}
running = true;
searchPhase = 'screening';
exactPass = 'primary';
@@ -1158,6 +1177,15 @@
<label>자동 후보 기수 보너스 덮어쓰기 (0~900)<input class="number-input" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="3" value={candidateGenerationBonus ?? ''} placeholder="미사용" on:input={(event) => updateCandidateGenerationBonus(sanitizeGenerationBonusInput(event.currentTarget, candidateGenerationBonus))}></label>
<label>가창 보너스 덮어쓰기 (0~10%)<input class="number-input" type="number" min="0" max="10" step="any" value={songScoreBonusOverride ?? ''} placeholder="실제 보드값" on:input={(event) => updateSongScoreBonusOverride(event.currentTarget)}></label>
</div>
<div class="probability-limit-field">
<label>액티브 발동 확률 ‘중간’ 카드 최대 장수
<select bind:value={maxMediumProbabilityCards} disabled={running} aria-describedby="probability-limit-guide" on:change={() => { result = null; errorMessage = ''; }}>
<option value={null}>제한 없음</option>
{#each [0, 1, 2, 3, 4] as count}<option value={count}>{count}장{count === 0 ? ' (사용 안 함)' : ' 이하'}</option>{/each}
</select>
</label>
<p id="probability-limit-guide" class="candidate-guide">고정 카드를 포함한 5장 전체에 적용합니다. 보드의 발동률 증가와 관계없이 카드의 기본 확률을 기준으로 셉니다.</p>
</div>
<label class="owned-cards-option">
<input type="checkbox" checked={ownedCardsOnly} disabled={running} on:change={(event) => toggleOwnedCardsOnly(event.currentTarget.checked)}>
<span aria-hidden="true"></span>
@@ -1321,6 +1349,8 @@
.candidate-fields { display: grid; grid-template-columns: clamp(130px, 12vw, 180px) repeat(2, clamp(230px, 21vw, 340px)); gap: 9px; margin-top: 18px; }
.candidate-awakening { display: grid; gap: 7px; color: #8f98a8; font-size: 14px; font-weight: 750; }
.candidate-guide { margin: 7px 0 0; color: #737d8d; font-size: 12px; line-height: 1.5; }
.probability-limit-field { margin-top: 16px; }
.probability-limit-field > label { max-width: 340px; }
.fixed-fields { display: grid; gap: 9px; margin-top: 16px; }
.fixed-guide { margin: 8px 0 0; color: #737d8d; font-size: 12px; line-height: 1.5; }
.fixed-column-headings { display: grid; grid-template-columns: minmax(320px, 1fr) clamp(130px, 12vw, 180px) clamp(304px, 27vw, 400px) clamp(180px, 16vw, 240px); gap: 9px; padding: 0 2px; }