고정 멤버와 탐색 목적을 유지하며 제한적인 두 카드 교체 후보를 정밀 평가합니다. 기존 평가 예산 안에서 이미 평가한 편성을 제외하고 후보를 재선택합니다.
This commit is contained in:
1 parent
da40ec4238
commit
1e893e62c9
7 files changed
+330
-8
No files matched your search
@@ -0,0 +1,60 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { generateTwoCardReplacementCandidates, refinementCandidatePriority, refinementCombinationKey, selectRefinementCandidates } from './combination-refinement.ts';
|
||||
import type { OptimizerCard } from './types.ts';
|
||||
|
||||
function optimizer(id: string, percent = 0, probability = 100): OptimizerCard {
|
||||
return { id, name: id, active: { cooldownSeconds: 10, durationSeconds: 10, scoreUpPercent: percent, probabilityPercent: probability }, special: { durationSeconds: 0, scoreUpPercent: 0 } };
|
||||
}
|
||||
|
||||
test('refinement priority distinguishes maximum from expected and retains event/song multipliers', () => {
|
||||
const risky = { cards: [optimizer('risky', 150, 20)], overallPower: 1000 };
|
||||
const steady = { cards: [optimizer('steady', 100)], overallPower: 1000 };
|
||||
assert.ok(refinementCandidatePriority(risky) > refinementCandidatePriority(steady));
|
||||
assert.ok(refinementCandidatePriority({ ...risky, optimizationObjective: 'expected' }) <
|
||||
refinementCandidatePriority({ ...steady, optimizationObjective: 'expected' }));
|
||||
const plain = { cards: [optimizer('plain')], overallPower: 1000 };
|
||||
assert.ok(refinementCandidatePriority({ ...plain, overallPower: 900, rankingMultiplier: 2, optimizationObjective: 'expected' }) > refinementCandidatePriority(plain));
|
||||
assert.ok(refinementCandidatePriority({ ...plain, overallPower: 900, directSongScoreBonusPercent: 10, finalScoreBonusPercent: 10 }) > refinementCandidatePriority(plain));
|
||||
});
|
||||
|
||||
type Card = { id: string; characterId: string; medium: boolean };
|
||||
const cards: Card[] = Array.from({ length: 7 }, (_, index) => ({ id: String(index), characterId: String(index), medium: index >= 5 }));
|
||||
const accepts = (selected: readonly Card[]) => selected.filter((card) => card.medium).length <= 1;
|
||||
const prepare = (selected: Card[]) => ({ cards: selected.map((card) => optimizer(card.id)), overallPower: 100 + selected.reduce((sum, card) => sum + Number(card.id), 0) });
|
||||
function choose<T>(items: readonly T[], count: number): T[][] {
|
||||
if (!count) return [[]];
|
||||
return items.flatMap((item, index) => choose(items.slice(index + 1), count - 1).map((rest) => [item, ...rest]));
|
||||
}
|
||||
|
||||
test('bounded two-card search matches the exhaustive tiny neighborhood and preserves fixed members', () => {
|
||||
const source = cards.slice(0, 5), fixed = new Set(['0', '1']), expected = new Set<string>();
|
||||
for (const removed of choose([2, 3, 4], 2)) {
|
||||
const kept = source.filter((_, index) => !removed.includes(index));
|
||||
for (const pair of choose(cards, 2)) {
|
||||
const result = [...kept, ...pair];
|
||||
if (new Set(result.map((card) => card.characterId)).size === 5 && accepts(result)) expected.add(refinementCombinationKey(result));
|
||||
}
|
||||
}
|
||||
const actual = generateTwoCardReplacementCandidates([source], cards, fixed, prepare, refinementCandidatePriority, accepts);
|
||||
assert.deepEqual(actual.map((candidate) => refinementCombinationKey(candidate.cards)).sort(), [...expected].sort());
|
||||
assert.ok(actual.every((candidate) => ['0', '1'].every((id) => candidate.cards.some((card) => card.id === id))));
|
||||
});
|
||||
|
||||
test('replacement search uses only the first three sources and returns no changes with four fixed members', () => {
|
||||
const pool = Array.from({ length: 12 }, (_, index) => ({ id: String(index), characterId: String(index), medium: false }));
|
||||
const sources = [pool.slice(0, 5), [pool[0], pool[1], pool[2], pool[3], pool[5]], [pool[0], pool[1], pool[2], pool[3], pool[6]], pool.slice(7, 12)];
|
||||
const found = generateTwoCardReplacementCandidates(sources, pool, new Set<string>(), prepare, refinementCandidatePriority, () => true);
|
||||
assert.ok(found.every((candidate) => sources.slice(0, 3).some((source) => source.filter((card) => candidate.cards.some((item) => item.id === card.id)).length >= 3)));
|
||||
assert.deepEqual(generateTwoCardReplacementCandidates([sources[0]], pool, new Set(['0', '1', '2', '3']), prepare, refinementCandidatePriority, () => true), []);
|
||||
});
|
||||
|
||||
test('mixed refinement removes already evaluated teams and permutations before applying the shared budget', () => {
|
||||
const a = { cards: [optimizer('a'), optimizer('b')], overallPower: 100 };
|
||||
const duplicate = { ...a, cards: [...a.cards].reverse() };
|
||||
const b = { cards: [optimizer('c'), optimizer('d')], overallPower: 200 };
|
||||
const c = { cards: [optimizer('e'), optimizer('f')], overallPower: 300 };
|
||||
assert.deepEqual(selectRefinementCandidates([a, duplicate, b, c], new Set([refinementCombinationKey(c.cards)]), 1), [b]);
|
||||
assert.deepEqual(selectRefinementCandidates([a], new Set<string>(), 0), []);
|
||||
assert.equal(selectRefinementCandidates([a, duplicate, b], new Set<string>(), 10).length, 2);
|
||||
});
|
||||
Reference in new issue
Block a user