import test from 'node:test'; import assert from 'node:assert/strict'; import { events } from '../data/events.ts'; import { activeEventAt, compareEventRankings, eventBonusBreakdown, eventChallengeSongScoreBonusPercent, eventOptimizationCardEligible, eventPointIndex } from './event-optimization.ts'; const currentEvent = events[0]; test('솔로 이벤트는 챕터 경계에서 대상 멤버와 과제곡을 함께 전환한다', () => { assert.equal(activeEventAt(events, Date.parse('2026-09-19T10:59:59.999Z')), null); const expected = [ [19, 'chr-00004', 'm0349'], [21, 'chr-04013', 'm0351'], [23, 'chr-00035', 'm0350'], [25, 'chr-03005', 'm0352'] ] as const; for (const [day, characterId, songId] of expected) { const start = Date.parse(`2026-09-${day}T11:00:00.000Z`); const chapter = activeEventAt(events, start)!; assert.deepEqual(chapter.bonusCharacterIds, [characterId]); assert.deepEqual(Object.keys(chapter.challengeSongBonusCardIds), [songId]); assert.equal(activeEventAt(events, start + 48 * 60 * 60 * 1000 - 1)?.id, chapter.id); assert.notEqual(activeEventAt(events, start - 1)?.id, chapter.id); } assert.equal(activeEventAt(events, Date.parse('2026-09-27T11:00:00.000Z')), null); }); test('신규 카드 공통 보너스와 현재 챕터 멤버 보너스를 따로 적용한다', () => { const aki = activeEventAt(events, Date.parse('2026-09-19T11:00:00Z'))!; const shiori = activeEventAt(events, Date.parse('2026-09-21T11:00:00Z'))!; const cards = [ { id: aki.featuredCardIds[0], characterId: 'chr-00004', rarity: 5 }, { id: 'shiori-regular', characterId: 'chr-04013', rarity: 3 } ]; assert.deepEqual(eventBonusBreakdown(aki, cards, () => 5), { formationPercent: 50, featuredCardPercent: 50, awakeningPercent: 33, totalPercent: 133 }); assert.equal(eventBonusBreakdown(shiori, [cards[0]], () => 0).totalPercent, 50); assert.equal(eventBonusBreakdown(shiori, [cards[1]], () => 0).totalPercent, 50); assert.equal(eventChallengeSongScoreBonusPercent(aki, 'm0349', [cards[0].id]), 10); assert.equal(eventChallengeSongScoreBonusPercent(shiori, 'm0349', [cards[0].id]), 0); assert.equal(eventChallengeSongScoreBonusPercent(aki, 'm0349', [aki.featuredCardIds[1]]), 0); }); test('9월 25일 20시부터 아냐와 64.가 마지막 챕터 보너스를 받는다', () => { const before = activeEventAt(events, Date.parse('2026-09-25T10:59:59.999Z'))!; const anya = activeEventAt(events, Date.parse('2026-09-25T11:00:00.000Z'))!; const featuredAnya = { id: 'card-03005-5-uniq-0084-00', characterId: 'chr-03005', rarity: 5 }; assert.equal(before.id, 'marathon_event-relay-002-chapter-03'); assert.equal(anya.id, 'marathon_event-relay-002-chapter-04'); assert.deepEqual(anya.bonusCharacterIds, ['chr-03005']); assert.deepEqual(anya.challengeSongBonusCardIds, { m0352: [featuredAnya.id] }); assert.equal(eventBonusBreakdown(anya, [featuredAnya], () => 0).totalPercent, 100); assert.equal(eventChallengeSongScoreBonusPercent(anya, 'm0352', [featuredAnya.id]), 10); assert.equal(eventChallengeSongScoreBonusPercent(before, 'm0352', [featuredAnya.id]), 0); }); test('이벤트 기간 안에서만 진행 중 이벤트를 반환한다', () => { assert.equal(activeEventAt(events, Date.parse('2026-08-29T02:59:59.999Z')), null); assert.equal(activeEventAt(events, Date.parse('2026-08-31T00:00:00.000Z'))?.id, currentEvent.id); assert.equal(activeEventAt(events, Date.parse('2026-09-06T11:00:00.000Z')), null); }); test('이벤트 최적화는 모든 4·5성과 보너스 멤버의 저등급 카드를 후보로 사용한다', () => { const bonusCharacters = new Set(['bonus']); assert.equal(eventOptimizationCardEligible({ id: 'five', characterId: 'other', rarity: 5 }, bonusCharacters), true); assert.equal(eventOptimizationCardEligible({ id: 'four', characterId: 'other', rarity: 4 }, bonusCharacters), true); assert.equal(eventOptimizationCardEligible({ id: 'bonus-three', characterId: 'bonus', rarity: 3 }, bonusCharacters), true); assert.equal(eventOptimizationCardEligible({ id: 'regular-three', characterId: 'other', rarity: 3 }, bonusCharacters), false); }); test('편성·신규 카드·개화 보너스를 카드별로 합산한다', () => { const cards = [ { id: currentEvent.featuredCardIds[0], characterId: currentEvent.bonusCharacterIds[0], rarity: 5 }, { id: 'regular-event-member', characterId: currentEvent.bonusCharacterIds[1], rarity: 4 }, { id: 'other-member', characterId: 'chr-other', rarity: 3 } ]; const awakening = new Map([[cards[0].id, 5], [cards[1].id, 2], [cards[2].id, 1]]); const bonus = eventBonusBreakdown(currentEvent, cards, (card) => awakening.get(card.id) ?? 0); assert.deepEqual(bonus, { formationPercent: 60, featuredCardPercent: 30, awakeningPercent: 38, totalPercent: 128 }); }); test('과제곡에 대응하는 신규 카드가 있을 때만 스코어 보너스를 적용한다', () => { assert.equal(eventChallengeSongScoreBonusPercent(currentEvent, 'm0338', [currentEvent.featuredCardIds[0]]), 10); assert.equal(eventChallengeSongScoreBonusPercent(currentEvent, 'm0338', [currentEvent.featuredCardIds[1]]), 0); assert.equal(eventChallengeSongScoreBonusPercent(currentEvent, 'm0342', [currentEvent.featuredCardIds[3]]), 10); }); test('예상 점수에 이벤트 획득 배율을 적용해 비교 지수를 만든다', () => { assert.equal(eventPointIndex(1_000_000, 128), 2_280_000); }); test('이벤트 순위는 최고점보다 평균 점수를 우선한다', () => { const highMaximum = { result: { expectedScore: 100, theoreticalMaxScore: 300 } }; const highAverage = { result: { expectedScore: 120, theoreticalMaxScore: 200 } }; assert.deepEqual([highMaximum, highAverage].sort(compareEventRankings), [highAverage, highMaximum]); }); test('평균 이벤트 Pt에는 획득 보너스를 반영하고 동률은 평균 점수로 정렬한다', () => { const bonus = { result: { expectedScore: 100 }, eventBonusPercent: 100 }; const score = { result: { expectedScore: 150 }, eventBonusPercent: 0 }; assert.ok(compareEventRankings(bonus, score) < 0); assert.ok(compareEventRankings(bonus, { result: { expectedScore: 200 } }) > 0); const sameAverage = { ...bonus, result: { expectedScore: 100, theoreticalMaxScore: 999 } }; assert.equal(compareEventRankings(bonus, sameAverage), 0); });