fix: reconsider center card upgrades after placement fallback
Publish GitHub Pages output / publish (push) Successful in 38s
Publish GitHub Pages output / publish (push) Successful in 38s
This commit is contained in:
@@ -188,7 +188,55 @@ test('현재 배치가 더 좋으면 단독 평가 결과 대신 현재 배치
|
||||
assert.deepEqual(refined, current);
|
||||
});
|
||||
|
||||
test('54개 소켓과 카드 100장에서도 노란 소켓 주변만 정해진 횟수 안에서 비교한다', async () => {
|
||||
test('중앙 카드 단독 점수가 같아도 전체 배치에서 점수가 오르는 미사용 카드를 추천한다', async () => {
|
||||
const source: BoardConnectionOptimizerSource = {
|
||||
members: [{ id: 'a', positionGroupId: 'model' }],
|
||||
models: { model: [{ groupId: 'S-center', x: 0, y: 0 }, { groupId: 'S-side', x: 2, y: 0 }] },
|
||||
nodeGroups: {
|
||||
'S-center': [{ characterIds: [], type: 'connection', effect: null }],
|
||||
'S-side': [{ characterIds: [], type: 'connection', effect: null }]
|
||||
}
|
||||
};
|
||||
const boards = { a: ['S-side'] };
|
||||
const cards = ['old', 'new', 'anchor'].map((id) => ({ id, awakening: 0,
|
||||
connect: [{ level: 1, effectPermilUp: 100, extent: [{ x: 1, y: 0 }] }] }));
|
||||
const current = { a: { 'S-center': 'old', 'S-side': 'anchor' } };
|
||||
// Integer score rounding makes isolated gains tie, but not their combined gain.
|
||||
const evaluate = (p: Record<string, Record<string, string>>) => ({
|
||||
score: 100 + Math.floor(((p.a?.['S-center'] === 'new' ? 2 : p.a?.['S-center'] === 'old' ? 1 : 0)
|
||||
+ (p.a?.['S-side'] === 'anchor' ? 8 : 0)) / 10), activationRateUp: 0
|
||||
});
|
||||
const initial = optimizeBoardConnectionCards(source, boards, cards, {
|
||||
currentPlacements: current, preservePlacementOnTie: () => true,
|
||||
placementWeight: (card, member, socket) => evaluate({ [member]: { [socket]: card.id } }).score - evaluate({}).score
|
||||
}).placements;
|
||||
assert.deepEqual(initial, current);
|
||||
const refined = await refineBoardConnectionSongBonus(source, boards, cards, initial, {
|
||||
currentPlacements: current, songBonusEnabled: false, evaluate
|
||||
});
|
||||
assert.deepEqual(refined, { a: { 'S-center': 'new', 'S-side': 'anchor' } });
|
||||
assert.ok(evaluate(refined).score > evaluate(current).score);
|
||||
});
|
||||
|
||||
test('초기 추천의 손해로 기존 배치로 돌아가도 중앙 신규 카드 교체를 다시 탐색한다', async () => {
|
||||
const { source, boards, cards } = workFixture(false);
|
||||
cards.push({ ...cards[1], id: 'new' });
|
||||
const current = { work: { S: 'worker' }, score: { S: 'spare' } };
|
||||
const initial = { other: { S: 'worker' }, score: { S: 'new' } };
|
||||
const evaluate = (p: Record<string, Record<string, string>>) => ({
|
||||
score: 100 + (p.work?.S === 'worker' ? 50 : 0) + (p.score?.S === 'new' ? 10 : 0),
|
||||
activationRateUp: 0
|
||||
});
|
||||
assert.ok(evaluate(initial).score < evaluate(current).score);
|
||||
const refined = await refineBoardConnectionSongBonus(source, boards, cards, initial, {
|
||||
currentPlacements: current, songBonusEnabled: false, evaluate
|
||||
});
|
||||
assert.equal(refined.work.S, 'worker');
|
||||
assert.equal(refined.score.S, 'new');
|
||||
assert.ok(evaluate(refined).score > evaluate(current).score);
|
||||
});
|
||||
|
||||
test('54개 소켓과 카드 100장에서도 정해진 횟수 안에서 비교한다', async () => {
|
||||
const { source, boards, cards, evaluate, singers } = fixture();
|
||||
for (let index = 0; index < 51; index += 1) {
|
||||
const id = `green-${index}`;
|
||||
@@ -203,5 +251,5 @@ test('54개 소켓과 카드 100장에서도 노란 소켓 주변만 정해진
|
||||
evaluate: (placements) => { checks += 1; return evaluate(placements); }
|
||||
});
|
||||
assert.ok(evaluate(refined).score >= evaluate(initial).score);
|
||||
assert.ok(checks <= 1 + 3 * 2 * (54 + 100 + 1));
|
||||
assert.ok(checks <= 1 + 3 * 54 * (54 + 100 + 1));
|
||||
});
|
||||
|
||||
@@ -4,8 +4,9 @@ import type { BoardConnectionOptimizerSource, OptimizableConnectionCard } from '
|
||||
type Placements = Record<string, Record<string, string>>;
|
||||
type Metric = { score: number; activationRateUp: number };
|
||||
|
||||
/** Bounded local correction for song-bonus saturation, not a global search.
|
||||
* Keep the original matching result and try moves/swaps involving song/work sockets.
|
||||
/** Bounded local correction for non-additive board scores, not a global search.
|
||||
* Try moves/swaps involving center, song and work sockets, including when the
|
||||
* original matching result loses to the current placements.
|
||||
* Every comparison scores the whole board, including caps and rounding.
|
||||
*/
|
||||
export async function refineBoardConnectionSongBonus(
|
||||
@@ -53,7 +54,8 @@ export async function refineBoardConnectionSongBonus(
|
||||
}));
|
||||
return { member: member.id, socket: socket.groupId, required: required.length > 0, preserveWork,
|
||||
allowed: cards.map((card) => required.every((position) => covers(card, position))),
|
||||
focus: cards.some((card) => yellow.some((position) => covers(card, position))) };
|
||||
focus: (socket.x === 0 && socket.y === 0)
|
||||
|| cards.some((card) => yellow.some((position) => covers(card, position))) };
|
||||
});
|
||||
});
|
||||
const encode = (assignment: number[]) => {
|
||||
@@ -83,7 +85,9 @@ export async function refineBoardConnectionSongBonus(
|
||||
let metric = evaluate(best);
|
||||
if (current && valid(current)) { const value = evaluate(current); if (better(value, metric)) { best = current; metric = value; } }
|
||||
const focus = slots.flatMap((slot, index) => slot.focus || slot.preserveWork ? [index] : []);
|
||||
// At most three search passes over song and existing work sockets.
|
||||
// Center upgrades must also be checked against the complete board: isolated
|
||||
// matching weights can overvalue other moves and cause fallback to current.
|
||||
// At most three passes over center, song and existing work sockets.
|
||||
for (let pass = 0; pass < 3 && focus.length; pass += 1) {
|
||||
let next = best;
|
||||
let nextMetric = metric;
|
||||
|
||||
Reference in New Issue
Block a user