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(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(); 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(), 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(), 0), []); assert.equal(selectRefinementCandidates([a, duplicate, b], new Set(), 10).length, 2); });