fix: preserve equivalent cards in board optimization
Publish GitHub Pages output / publish (push) Successful in 47s
Publish GitHub Pages output / publish (push) Successful in 47s
This commit is contained in:
@@ -4,6 +4,10 @@ updatedAt: 2026-09-05T10:20:00+09:00
|
||||
|
||||
## 2026년 9월 5일
|
||||
|
||||
### 보드 최적화의 불필요한 교체 제거
|
||||
|
||||
`FIXED` 배치 분석의 보드 최적화에서 연결 모양과 강화 수치가 같은 카드로 교체하도록 안내하던 문제를 수정했습니다. 효과가 같으면 기존에 배치한 카드를 유지합니다.
|
||||
|
||||
### 게이머즈 소속 조건 판정 수정
|
||||
|
||||
`FIXED` 게이머즈 멤버가 소속 조건에서 누락되던 문제를 수정했습니다. 수영복 오카유, 일반 5성 미오, 4성 오카유·코로네의 게이머즈 조건 및 대상 지정이 있는 리더 의상·패시브·스킬 효과가 정상 적용됩니다.
|
||||
|
||||
@@ -31,6 +31,46 @@ const rightCard = {
|
||||
connect: [{ level: 1, effectPermilUp: 1000, extent: [{ x: 1, y: 0 }] }]
|
||||
};
|
||||
|
||||
test('모양과 수치가 같은 카드는 기존 배치를 유지하고 중복 배치하지 않는다', () => {
|
||||
const cards = ['a', 'b', 'c'].map((id) => ({ ...rightCard, id }));
|
||||
const currentPlacements = { member: { 'S-center': 'c', 'S-right': 'a' } };
|
||||
const result = optimizeBoardConnectionCards(source, { member: ['S-right', 'G-right', 'G-far'] }, cards, { currentPlacements });
|
||||
assert.deepEqual(result.placements, currentPlacements);
|
||||
assert.equal(new Set(result.assignments.map((assignment) => assignment.cardId)).size, 2);
|
||||
assert.equal(result.totalWeight, 900_000);
|
||||
});
|
||||
|
||||
test('개화에 따른 실제 효과와 좌표 순서에 무관한 모양으로 기존 카드를 유지한다', () => {
|
||||
const effect = { level: 1, effectPermilUp: 1000, extent: [{ x: -1, y: 0 }, { x: 1, y: 0 }] };
|
||||
const cards = [
|
||||
{ id: 'a', awakening: 0, connect: [effect] },
|
||||
{ id: 'b', awakening: 5, connect: [
|
||||
{ ...effect, effectPermilUp: 100 },
|
||||
{ ...effect, level: 2, extent: [...effect.extent].reverse() }
|
||||
] }
|
||||
];
|
||||
const currentPlacements = { member: { 'S-center': 'b' } };
|
||||
const result = optimizeBoardConnectionCards(source, { member: ['G-left', 'G-right'] }, cards, { currentPlacements });
|
||||
assert.deepEqual(result.placements, currentPlacements);
|
||||
});
|
||||
|
||||
test('기존 카드와 모양 또는 수치가 다르면 실제 개선 배치를 유지한다', () => {
|
||||
for (const previous of [leftCard, { ...rightCard, id: 'weak', connect: [{ ...rightCard.connect[0], effectPermilUp: 100 }] }]) {
|
||||
const result = optimizeBoardConnectionCards(source, { member: ['G-left', 'G-right'] }, [previous, rightCard], {
|
||||
currentPlacements: { member: { 'S-center': previous.id } }
|
||||
});
|
||||
assert.equal(result.placements.member['S-center'], rightCard.id);
|
||||
}
|
||||
});
|
||||
|
||||
test('기존 카드를 유지해도 다른 소켓에 필요한 동등 카드를 중복 사용하지 않는다', () => {
|
||||
const cards = ['a', 'b'].map((id) => ({ ...rightCard, id }));
|
||||
const result = optimizeBoardConnectionCards(source, { member: ['S-right', 'G-right', 'G-far'] }, cards, {
|
||||
currentPlacements: { member: { 'S-center': 'b' } }
|
||||
});
|
||||
assert.deepEqual(result.placements, { member: { 'S-center': 'b', 'S-right': 'a' } });
|
||||
});
|
||||
|
||||
test('활성 연결 노드 전체에서 보유 카드의 최대 가중치 배치를 찾는다', () => {
|
||||
const result = optimizeBoardConnectionCards(source, { member: ['S-right', 'G-left', 'G-right', 'G-far'] }, [leftCard, rightCard]);
|
||||
assert.deepEqual(result.placements, {
|
||||
|
||||
@@ -47,6 +47,7 @@ export type BoardConnectionOptimizationResult = {
|
||||
};
|
||||
|
||||
export type BoardConnectionOptimizationOptions = {
|
||||
currentPlacements?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
||||
targetMemberId?: string;
|
||||
requireSoloSongBonus?: boolean;
|
||||
requireGroupSongBonus?: boolean;
|
||||
@@ -202,7 +203,44 @@ export function optimizeBoardConnectionCards(
|
||||
|| right.fulfillment - left.fulfillment
|
||||
|| right.totalFulfillment - left.totalFulfillment
|
||||
|| right.genericWeight - left.genericWeight)[0];
|
||||
const matches = best.matches;
|
||||
const matches = best.matches.map((match) => ({ ...match }));
|
||||
if (options.currentPlacements) {
|
||||
// Equivalent cards are interchangeable. Reserve existing cards first, then
|
||||
// distribute the remaining cards so preserving one socket cannot duplicate a card.
|
||||
const equivalentCards = new Map<string, number[]>();
|
||||
cards.forEach((card, cardIndex) => {
|
||||
const effect = selectedEffect(card)!;
|
||||
const key = JSON.stringify([
|
||||
effect.effectPermilUp,
|
||||
effect.extent.map(({ x, y }) => `${x},${y}`).sort(),
|
||||
genericWeights[cardIndex], priorityUnits[cardIndex],
|
||||
soloBoosts[cardIndex], groupBoosts[cardIndex]
|
||||
]);
|
||||
const group = equivalentCards.get(key) ?? [];
|
||||
group.push(cardIndex);
|
||||
equivalentCards.set(key, group);
|
||||
});
|
||||
for (const group of equivalentCards.values()) {
|
||||
const available = new Set(group);
|
||||
const groupMatches = matches.filter((match) => available.has(match.rowIndex));
|
||||
const pending = [];
|
||||
for (const match of groupMatches) {
|
||||
const { member, socket } = slots[match.columnIndex];
|
||||
const currentId = options.currentPlacements[member.id]?.[socket.groupId];
|
||||
const currentIndex = group.find((index) => cards[index].id === currentId);
|
||||
if (currentIndex !== undefined && available.delete(currentIndex)) {
|
||||
match.rowIndex = currentIndex;
|
||||
} else {
|
||||
pending.push(match);
|
||||
}
|
||||
}
|
||||
for (const match of pending) {
|
||||
const index = available.has(match.rowIndex) ? match.rowIndex : available.values().next().value!;
|
||||
match.rowIndex = index;
|
||||
available.delete(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
const placements: Record<string, Record<string, string>> = {};
|
||||
const assignments = matches.map((match) => {
|
||||
const card = cards[match.rowIndex];
|
||||
|
||||
@@ -592,6 +592,7 @@
|
||||
connectionOptimizationProgress = { stage: '최적 조합 선택 중', completed: totalPlacementChecks, total: totalPlacementChecks };
|
||||
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
||||
const optimized = optimizeBoardConnectionCards(boardSummary, effectiveMemberBoards, candidateCards, {
|
||||
currentPlacements: boardCards,
|
||||
placementWeight: (card, memberId, socketId) => placementMetrics(card, memberId, socketId).score,
|
||||
placementPriorityWeight: (card, memberId, socketId) => isBlueConnectionSocket(memberId, socketId)
|
||||
? placementMetrics(card, memberId, socketId).activationRateUp
|
||||
|
||||
Reference in New Issue
Block a user