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일
|
## 2026년 9월 5일
|
||||||
|
|
||||||
|
### 보드 최적화의 불필요한 교체 제거
|
||||||
|
|
||||||
|
`FIXED` 배치 분석의 보드 최적화에서 연결 모양과 강화 수치가 같은 카드로 교체하도록 안내하던 문제를 수정했습니다. 효과가 같으면 기존에 배치한 카드를 유지합니다.
|
||||||
|
|
||||||
### 게이머즈 소속 조건 판정 수정
|
### 게이머즈 소속 조건 판정 수정
|
||||||
|
|
||||||
`FIXED` 게이머즈 멤버가 소속 조건에서 누락되던 문제를 수정했습니다. 수영복 오카유, 일반 5성 미오, 4성 오카유·코로네의 게이머즈 조건 및 대상 지정이 있는 리더 의상·패시브·스킬 효과가 정상 적용됩니다.
|
`FIXED` 게이머즈 멤버가 소속 조건에서 누락되던 문제를 수정했습니다. 수영복 오카유, 일반 5성 미오, 4성 오카유·코로네의 게이머즈 조건 및 대상 지정이 있는 리더 의상·패시브·스킬 효과가 정상 적용됩니다.
|
||||||
|
|||||||
@@ -31,6 +31,46 @@ const rightCard = {
|
|||||||
connect: [{ level: 1, effectPermilUp: 1000, extent: [{ x: 1, y: 0 }] }]
|
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('활성 연결 노드 전체에서 보유 카드의 최대 가중치 배치를 찾는다', () => {
|
test('활성 연결 노드 전체에서 보유 카드의 최대 가중치 배치를 찾는다', () => {
|
||||||
const result = optimizeBoardConnectionCards(source, { member: ['S-right', 'G-left', 'G-right', 'G-far'] }, [leftCard, rightCard]);
|
const result = optimizeBoardConnectionCards(source, { member: ['S-right', 'G-left', 'G-right', 'G-far'] }, [leftCard, rightCard]);
|
||||||
assert.deepEqual(result.placements, {
|
assert.deepEqual(result.placements, {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export type BoardConnectionOptimizationResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type BoardConnectionOptimizationOptions = {
|
export type BoardConnectionOptimizationOptions = {
|
||||||
|
currentPlacements?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
||||||
targetMemberId?: string;
|
targetMemberId?: string;
|
||||||
requireSoloSongBonus?: boolean;
|
requireSoloSongBonus?: boolean;
|
||||||
requireGroupSongBonus?: boolean;
|
requireGroupSongBonus?: boolean;
|
||||||
@@ -202,7 +203,44 @@ export function optimizeBoardConnectionCards(
|
|||||||
|| right.fulfillment - left.fulfillment
|
|| right.fulfillment - left.fulfillment
|
||||||
|| right.totalFulfillment - left.totalFulfillment
|
|| right.totalFulfillment - left.totalFulfillment
|
||||||
|| right.genericWeight - left.genericWeight)[0];
|
|| 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 placements: Record<string, Record<string, string>> = {};
|
||||||
const assignments = matches.map((match) => {
|
const assignments = matches.map((match) => {
|
||||||
const card = cards[match.rowIndex];
|
const card = cards[match.rowIndex];
|
||||||
|
|||||||
@@ -592,6 +592,7 @@
|
|||||||
connectionOptimizationProgress = { stage: '최적 조합 선택 중', completed: totalPlacementChecks, total: totalPlacementChecks };
|
connectionOptimizationProgress = { stage: '최적 조합 선택 중', completed: totalPlacementChecks, total: totalPlacementChecks };
|
||||||
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
||||||
const optimized = optimizeBoardConnectionCards(boardSummary, effectiveMemberBoards, candidateCards, {
|
const optimized = optimizeBoardConnectionCards(boardSummary, effectiveMemberBoards, candidateCards, {
|
||||||
|
currentPlacements: boardCards,
|
||||||
placementWeight: (card, memberId, socketId) => placementMetrics(card, memberId, socketId).score,
|
placementWeight: (card, memberId, socketId) => placementMetrics(card, memberId, socketId).score,
|
||||||
placementPriorityWeight: (card, memberId, socketId) => isBlueConnectionSocket(memberId, socketId)
|
placementPriorityWeight: (card, memberId, socketId) => isBlueConnectionSocket(memberId, socketId)
|
||||||
? placementMetrics(card, memberId, socketId).activationRateUp
|
? placementMetrics(card, memberId, socketId).activationRateUp
|
||||||
|
|||||||
Reference in New Issue
Block a user