diablo2-web/scripts/verify-maxroll-loot.ts

141 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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