141 lines
6.7 KiB
TypeScript
141 lines
6.7 KiB
TypeScript
import {
|
||
MONSTER_PRESETS,
|
||
FAMOUS_ITEMS_META,
|
||
classifyItemCategory,
|
||
loadCanonicalTreasureClasses,
|
||
calculateAnalyticProbabilities,
|
||
runMonteCarloSimulation,
|
||
buildReverseLookupIndex,
|
||
reverseLookup,
|
||
} from '../src/loot';
|
||
|
||
async function main() {
|
||
console.log('=== [1] 验证 MONSTER_PRESETS 与 TC 完整性 ===');
|
||
const { tcTable, autoTcs } = loadCanonicalTreasureClasses();
|
||
console.log(`载入系统 TC 节点数: ${tcTable.all.length}, 自动生成 TC 节点数: ${autoTcs.size}`);
|
||
|
||
let missingTcCount = 0;
|
||
for (const mon of MONSTER_PRESETS) {
|
||
for (const diff of ['normal', 'nightmare', 'hell'] as const) {
|
||
const tcName = mon.tc[diff];
|
||
if (!tcTable.get(tcName) && !autoTcs.has(tcName)) {
|
||
console.error(`❌ [缺失] 怪物 ${mon.nameCn} 在 ${diff} 难度的 TC 不存在: ${tcName}`);
|
||
missingTcCount++;
|
||
}
|
||
if (mon.questTc) {
|
||
const questTcName = mon.questTc[diff];
|
||
if (!tcTable.get(questTcName) && !autoTcs.has(questTcName)) {
|
||
console.error(`❌ [缺失] 怪物 ${mon.nameCn} 在 ${diff} 难度的 首杀TC 不存在: ${questTcName}`);
|
||
missingTcCount++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (missingTcCount === 0) {
|
||
console.log(`✅ 所有 ${MONSTER_PRESETS.length} 个怪物预设在全难度的基础 TC 与首杀 TC 校验 100% 存在!`);
|
||
} else {
|
||
throw new Error(`存在 ${missingTcCount} 个无效的 TC 引用`);
|
||
}
|
||
|
||
console.log('\n=== [2] 验证 FAMOUS_ITEMS_META 与物品分类器 ===');
|
||
const famousCount = Object.keys(FAMOUS_ITEMS_META).length;
|
||
console.log(`已收录核心暗金与极品底模元数据条目: ${famousCount} 条`);
|
||
|
||
// Test classifyItemCategory
|
||
const r30Cat = classifyItemCategory('r30');
|
||
const shakoCat = classifyItemCategory('uap', 'unique');
|
||
const phaseBladeCat = classifyItemCategory('7cr');
|
||
const sojCat = classifyItemCategory('rin', 'unique');
|
||
const tokenCat = classifyItemCategory('tes');
|
||
|
||
console.log(`- 30# 贝 (r30) 分类: ${r30Cat} (期望: rune)`);
|
||
console.log(`- 军帽 (uap, unique) 分类: ${shakoCat} (期望: unique)`);
|
||
console.log(`- 幻化之刃 (7cr) 分类: ${phaseBladeCat} (期望: base)`);
|
||
console.log(`- 戒指/护身符 (rin) 分类: ${sojCat} (期望: charm_gem)`);
|
||
console.log(`- 洗点徽章 (tes) 分类: ${tokenCat} (期望: misc)`);
|
||
|
||
if (r30Cat !== 'rune' || shakoCat !== 'unique' || phaseBladeCat !== 'base' || sojCat !== 'charm_gem' || tokenCat !== 'misc') {
|
||
throw new Error('分类器逻辑测试失败');
|
||
}
|
||
console.log('✅ 物品多维分类器运行正确!');
|
||
|
||
console.log('\n=== [3] 验证精确概率表解析 (Baal H 与 Andariel H) ===');
|
||
const baalOdds = calculateAnalyticProbabilities('Baal (H)', { tcTable, autoTcs, gamePlayers: 1, partyPlayers: 1 });
|
||
console.log(`Baal (H) - 叶子节点数: ${baalOdds.leaves.length}, Picks: ${baalOdds.picks}, NoDrop: ${(baalOdds.noDropCompoundProb * 100).toFixed(2)}%`);
|
||
|
||
// Verify Shako / uap odds for Baal
|
||
const uapLeaf = baalOdds.leaves.find(l => l.item === 'uap');
|
||
if (uapLeaf) {
|
||
console.log(`- 军帽底模 (uap) 单次概率: ${(uapLeaf.singleProbability * 100).toFixed(4)}%, 综合概率: ${(uapLeaf.compoundProbability * 100).toFixed(4)}%, 几率比: 1 : ${uapLeaf.oneInOdds.toFixed(1)}`);
|
||
}
|
||
|
||
// Verify Quest Drop for Andariel
|
||
const andyNormalOdds = calculateAnalyticProbabilities('Andariel (H)', { tcTable, autoTcs, gamePlayers: 1, partyPlayers: 1 });
|
||
const andyQuestOdds = calculateAnalyticProbabilities('Andarielq (H)', { tcTable, autoTcs, gamePlayers: 1, partyPlayers: 1 });
|
||
console.log(`Andariel (H) 普通杀 NoDrop: ${(andyNormalOdds.noDropCompoundProb * 100).toFixed(2)}%`);
|
||
console.log(`Andarielq (H) Bug首杀 NoDrop: ${(andyQuestOdds.noDropCompoundProb * 100).toFixed(2)}%`);
|
||
|
||
const andyNormalUap = andyNormalOdds.leaves.find(l => l.item === 'uap')?.compoundProbability || 0;
|
||
const andyQuestUap = andyQuestOdds.leaves.find(l => l.item === 'uap')?.compoundProbability || 0;
|
||
console.log(`- 安姐普通杀 军帽(uap) 几率: ${(andyNormalUap * 100).toFixed(4)}%`);
|
||
console.log(`- 安姐Bug首杀 军帽(uap) 几率: ${(andyQuestUap * 100).toFixed(4)}% (提升 +${(((andyQuestUap - andyNormalUap) / andyNormalUap) * 100).toFixed(1)}%)`);
|
||
|
||
const andyQuestHasJunk = andyQuestOdds.leaves.some(l => l.item === 'tsc' || l.item === 'isc' || l.item === 'gld');
|
||
if (andyQuestHasJunk) {
|
||
throw new Error('安姐首杀 TC 不应包含卷轴或金币等垃圾杂项!');
|
||
}
|
||
if (andyQuestUap <= andyNormalUap) {
|
||
throw new Error('安姐首杀装备出货率应当显著高于普通击杀!');
|
||
}
|
||
console.log('✅ 安姐首杀剔除杂质与装备出货率暴增验证通过!');
|
||
|
||
console.log('\n=== [4] 验证 1,000 次蒙特卡洛掉落模拟 (实测出货与品质分布) ===');
|
||
const simRes = await runMonteCarloSimulation({
|
||
tcName: 'Baal (H)',
|
||
runs: 1000,
|
||
gamePlayers: 3,
|
||
partyPlayers: 1,
|
||
magicFind: 250,
|
||
difficulty: 'hell',
|
||
tcTable,
|
||
autoTcs,
|
||
});
|
||
|
||
console.log(`模拟总轮数: ${simRes.totalRuns}`);
|
||
console.log(`掉落总件数: ${simRes.itemsDropped} 件 (平均每轮 ${(simRes.itemsDropped / simRes.totalRuns).toFixed(2)} 件)`);
|
||
console.log(`品质分布:`);
|
||
console.log(` - Unique (暗金): ${simRes.qualityCounts.unique} 件`);
|
||
console.log(` - Set (套装): ${simRes.qualityCounts.set} 件`);
|
||
console.log(` - Rare (稀有): ${simRes.qualityCounts.rare} 件`);
|
||
console.log(` - Magic (魔法): ${simRes.qualityCounts.magic} 件`);
|
||
console.log(` - Rune (符文): ${simRes.qualityCounts.rune} 件`);
|
||
console.log(` - Normal/Sup (底模): ${simRes.qualityCounts.normal + simRes.qualityCounts.superior} 件`);
|
||
console.log(` - Gold (金币): ${simRes.qualityCounts.gold} 堆`);
|
||
|
||
if (simRes.itemsDropped === 0) {
|
||
throw new Error('1000次模拟依然为0件掉落!');
|
||
}
|
||
console.log(`✅ 1,000 次模拟正常出货,共 ${simRes.itemSummaries.length} 种不同物品!`);
|
||
|
||
console.log('\n=== [5] 验证反查索引 (Reverse Lookup) ===');
|
||
const revIndex = buildReverseLookupIndex(tcTable, autoTcs);
|
||
const shakoRev = reverseLookup(revIndex, 'shako');
|
||
console.log(`搜索 "shako" 匹配到的掉落源数量: ${shakoRev[0]?.entries.length || 0} 个 TC`);
|
||
if (shakoRev.length > 0 && shakoRev[0]!.entries.length > 0) {
|
||
console.log(`- 最佳掉落源 TOP 1: ${shakoRev[0]!.entries[0]!.tcName}, 综合几率: ${(shakoRev[0]!.entries[0]!.compoundProbability * 100).toFixed(4)}%`);
|
||
} else {
|
||
throw new Error('反查 shako 失败');
|
||
}
|
||
console.log('✅ 反查索引测试通过!');
|
||
|
||
console.log('\n========================================');
|
||
console.log('🎉 全部 Maxroll 怪物选择器与掉落列表核心功能实测 100% 成功!');
|
||
console.log('========================================');
|
||
}
|
||
|
||
main().catch(err => {
|
||
console.error(err);
|
||
process.exit(1);
|
||
});
|