feat: streamline search results and open captured analyses
Publish GitHub Pages output / publish (push) Successful in 54s

This commit is contained in:
2026-09-09 19:15:25 +09:00
parent 28035110f9
commit d26b9ca884
8 changed files with 365 additions and 53 deletions
+50
View File
@@ -0,0 +1,50 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { combinationAnalysisSettings, parseAnalysisTransfer, transferredAnalysisKey, type AnalysisTransfer } from '../models/analysis-transfer.ts';
import { calculateExactScore } from './score.ts';
import { optimizeExactOrderAndCooldowns } from './exact-optimizer.ts';
import type { GrowthSnapshot } from '../models/analysis-history.ts';
const growth: GrowthSnapshot = { collection: { e: { owned: true, awakening: 5 } },
memberBoards: { e: ['G1'] }, boardCards: {}, memoryCount: 30, memberPowerUpPercent: 4.37 };
const state = { songId: 'song', difficulty: 'expert' as const, fixedCardIds: ['c', 'd', 'a', 'b'],
fixedAwakenings: [5, 3, 4, 4], fixedCooldownReductions: [null, 12, null, 4] as (null | 12 | 4)[],
fixedAllBlueNodes: [false, true, false, false], fixedGenerationBonusOverrides: [null, 0, 900, null],
awakening: 4, candidateGenerationBonus: 450, songScoreBonusOverride: 10,
leaderCharacterId: 'leader', leaderOutfitCardId: 'outfit' };
test('조합의 표시 순서로 바뀌어도 고정 카드 조건과 자동 후보 보드·보유 개화를 정확히 전달한다', () => {
const settings = combinationAnalysisSettings(state, ['a', 'b', 'c', 'd', 'e'], growth);
assert.deepEqual(settings.awakenings, [4, 4, 5, 3, 5]);
assert.deepEqual(settings.fixedCooldownReductions, [null, 4, null, 12, null]);
assert.deepEqual(settings.allBlueNodes, [false, false, false, true, true]);
assert.deepEqual(settings.generationBonusOverrides, [900, null, null, 0, 450]);
assert.equal(settings.leaderOutfitCardId, 'outfit');
assert.equal(settings.songScoreBonusOverride, 10);
assert.deepEqual(growth.memberBoards, { e: ['G1'] });
});
test('전달 결과는 재탐색 없이 점수·확률을 유지하고 원래 카드·채보로 시뮬레이션한다', () => {
const cards = ['a', 'b', 'c', 'd', 'e'].map((id, i) => ({ id, name: id,
active: { cooldownSeconds: 2 + i, durationSeconds: 1, scoreUpPercent: 100, probabilityPercent: 55 },
special: { durationSeconds: 1, scoreUpPercent: 100 }, allowedCooldownReductions: [0] as const }));
const chart = { songId: 'song', difficulty: 'expert', durationSeconds: 8, scoreRatio: 1,
specialStartSeconds: [1, 2, 3, 4, 5], notes: Array.from({ length: 8 }, (_, timeSeconds) => ({timeSeconds, type: 'regular' as const, isFlick: false})) };
const result = optimizeExactOrderAndCooldowns(cards, chart, { perfectNoteScore: 100, directSongScoreBonusPercent: 10 });
const capture = { settings: combinationAnalysisSettings(state, cards.map(c => c.id), growth), growth,
title: 'song', durationSeconds: 8, overallPower: 100, analyzedAt: '2026-09-09T09:00:00Z' };
const transfer: AnalysisTransfer = { schemaVersion: 1, capture, result, cards, chart, directSongScoreBonusPercent: 10,
achievementProbabilities: { maximum: 0.1, atLeast99Percent: 0.2, atLeast95Percent: 0.4, samples: 25000 } };
const loaded = parseAnalysisTransfer(JSON.stringify(transfer));
assert.deepEqual(loaded, transfer);
const score = calculateExactScore(loaded.result.order.map(id => loaded.cards.find(c => c.id === id)!), loaded.result.cooldownReductions, loaded.chart,
{ perfectNoteScore: loaded.result.perfectNoteScore, directSongScoreBonusPercent: loaded.directSongScoreBonusPercent });
assert.equal(score.theoreticalMaxScore, result.theoreticalMaxScore);
assert.equal(score.expectedScore, result.expectedScore);
const originalKey = transferredAnalysisKey(loaded.capture);
loaded.capture.growth.memoryCount += 1;
assert.notEqual(transferredAnalysisKey(loaded.capture), originalKey);
assert.equal(growth.memoryCount, 30);
assert.throws(() => parseAnalysisTransfer(JSON.stringify({ ...transfer, cards: [] })));
assert.throws(() => parseAnalysisTransfer(JSON.stringify({ ...transfer, chart: { ...chart, songId: 'another song' } })));
});
@@ -0,0 +1,32 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFileSync } from 'node:fs';
import { createContext, runInContext } from 'node:vm';
import ts from 'typescript';
const source = readFileSync(new URL('../workers/score-optimizer.worker.ts', import.meta.url), 'utf8');
const code = ts.transpileModule(source.replace(/^import .*;\r?\n/gm, '').replace('export {};', ''), {
compilerOptions: { target: ts.ScriptTarget.ES2022 }
}).outputText;
test('조합 워커는 요청한 결과 수를 점수순으로 반환하고 미지정·잘못된 값은 10개로 처리한다', () => {
for (const [limit, expected] of [[undefined, 10], [10, 10], [20, 20], [30, 30], [50, 50], [0, 10]]) {
const messages: any[] = [];
const context = createContext({
self: { postMessage: (message: unknown) => messages.push(message) },
countOptimizationCandidates: () => 1,
optimizeExactOrderAndCooldowns: (_cards: unknown, _chart: unknown, options: { perfectNoteScore: number }) => ({
theoreticalMaxScore: options.perfectNoteScore, expectedScore: options.perfectNoteScore
})
});
runInContext(code, context);
context.request = { type: 'optimize-combinations', resultLimit: limit, chart: {},
candidates: Array.from({ length: 60 }, (_, i) => ({ cards: [], overallPower: i, perfectNoteScore: i })) };
runInContext('self.onmessage({ data: request });', context);
const result = messages.at(-1);
assert.equal(result.type, 'combination-result');
assert.equal(result.rankings.length, expected);
assert.deepEqual(Array.from(result.rankings, (row: any) => row.result.theoreticalMaxScore),
Array.from({ length: expected! }, (_, i) => 59 - i));
}
});