Files
HolodoriCalc/src/lib/engine/event-optimization.test.ts
T
dal4segno 29e70c565d
Publish GitHub Pages output / publish (push) Successful in 33s
data: add 64 charts and event challenge
2026-09-25 20:31:50 +09:00

112 lines
6.3 KiB
TypeScript

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);
});