/** * Diablo II 1.13c Drop Parity Statistical Audit CLI (Issue #126). * * Usage: * npx tsx scripts/audit-parity.ts [kills=1000] [boss=mephisto] [difficulty=hell] * or: * npx tsx scripts/audit-parity.ts --kills=1000 --boss=mephisto --difficulty=hell --mf=250 --players=1 */ import { BOSS_LIST, type Difficulty } from '../src/boss.ts' import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts' import { executeDropPipeline } from '../src/game/drop-pipeline.ts' import type { UpgradableItemBase } from '../src/game/item-upgrade.ts' import type { Item } from '../src/game/items.ts' export interface ParityAuditSummary { readonly config: { readonly boss: string readonly difficulty: Difficulty readonly kills: number readonly monsterType: number readonly mlvl: number readonly tcName: string readonly playerMf: number readonly gamePlayers: number readonly isQuestKill: boolean } readonly totalItems: number readonly qualityDistribution: { readonly counts: Record readonly ratios: Record } readonly baseTierUpgrade: { readonly totalEquipment: number readonly normalCount: number readonly exceptionalCount: number readonly eliteCount: number readonly exceptionalRatio: number readonly eliteRatio: number readonly upgradedRatio: number } readonly affixCountDistribution: { readonly magic: Record readonly rare: Record } } function classifyBaseTier(item: Item): 'normal' | 'exceptional' | 'elite' | null { if (item.base.kind !== 'weapon' && item.base.kind !== 'armor') { return null } const b = item.base as UpgradableItemBase const id = (b.id ?? '').trim().toLowerCase() const uber = (b.ubercode ?? '').trim().toLowerCase() const ultra = (b.ultracode ?? '').trim().toLowerCase() if (ultra && ultra !== 'xxx' && id === ultra) { return 'elite' } if (uber && uber !== 'xxx' && id === uber) { return 'exceptional' } return 'normal' } export function runParityAudit(options?: { kills?: number bossId?: string difficulty?: Difficulty playerMf?: number gamePlayers?: number isQuestKill?: boolean baseSeed?: number }): ParityAuditSummary { const kills = options?.kills ?? 1000 const bossId = (options?.bossId ?? 'mephisto').toLowerCase() const difficulty: Difficulty = options?.difficulty ?? 'hell' const playerMf = options?.playerMf ?? 250 const gamePlayers = options?.gamePlayers ?? 1 const isQuestKill = options?.isQuestKill ?? true const baseSeed = options?.baseSeed ?? 0x12600001 const boss = BOSS_LIST.find(b => b.id === bossId) ?? BOSS_LIST[2]! const monsterType = boss.monsterType ?? 4 const mlvl = boss.mlvl[difficulty] const tcName = isQuestKill ? boss.questTc[difficulty] : boss.tc[difficulty] const dropTables = getEmbeddedDropTables() let totalItems = 0 const qualityCounts: Record = { unique: 0, set: 0, rare: 0, magic: 0, superior: 0, normal: 0, low: 0, } let totalEquipment = 0 let normalCount = 0 let exceptionalCount = 0 let eliteCount = 0 const magicAffixDist: Record = { '1': 0, '2': 0 } const rareAffixDist: Record = { '3': 0, '4': 0, '5': 0, '6': 0 } for (let i = 0; i < kills; i++) { const seed = (baseSeed + i * 104729) >>> 0 const drops = executeDropPipeline({ tcName, nLevel: mlvl, monsterType, dropTables, difficulty, playerMf, gamePlayers, partyPlayers: 1, monsterRng: seed, }) for (const item of drops) { totalItems++ const rarity = (item.rarity || 'normal').toLowerCase() qualityCounts[rarity] = (qualityCounts[rarity] ?? 0) + 1 const tier = classifyBaseTier(item) if (tier !== null) { totalEquipment++ if (tier === 'elite') eliteCount++ else if (tier === 'exceptional') exceptionalCount++ else normalCount++ } if (rarity === 'magic') { const count = (item.prefix ? 1 : 0) + (item.suffix ? 1 : 0) const key = String(Math.max(1, count)) magicAffixDist[key] = (magicAffixDist[key] ?? 0) + 1 } else if (rarity === 'rare') { const rareAffixes = (item as any).rolledRareAffixes?.affixes const count = Array.isArray(rareAffixes) ? rareAffixes.length : 0 if (count > 0) { const key = String(count) rareAffixDist[key] = (rareAffixDist[key] ?? 0) + 1 } } } } const qualityRatios: Record = {} for (const [k, v] of Object.entries(qualityCounts)) { qualityRatios[k] = totalItems > 0 ? Number((v / totalItems).toFixed(4)) : 0 } const exceptionalRatio = totalEquipment > 0 ? Number((exceptionalCount / totalEquipment).toFixed(4)) : 0 const eliteRatio = totalEquipment > 0 ? Number((eliteCount / totalEquipment).toFixed(4)) : 0 const upgradedRatio = totalEquipment > 0 ? Number(((exceptionalCount + eliteCount) / totalEquipment).toFixed(4)) : 0 return { config: { boss: boss.id, difficulty, kills, monsterType, mlvl, tcName, playerMf, gamePlayers, isQuestKill, }, totalItems, qualityDistribution: { counts: qualityCounts, ratios: qualityRatios, }, baseTierUpgrade: { totalEquipment, normalCount, exceptionalCount, eliteCount, exceptionalRatio, eliteRatio, upgradedRatio, }, affixCountDistribution: { magic: magicAffixDist, rare: rareAffixDist, }, } } // CLI entry point const isMain = typeof process !== 'undefined' && Array.isArray(process.argv) && process.argv[1] && process.argv[1].includes('audit-parity') if (isMain) { const args = process.argv.slice(2) let kills = 1000 let bossId = 'mephisto' let difficulty: Difficulty = 'hell' let playerMf = 250 let gamePlayers = 1 const positional: string[] = [] for (const arg of args) { if (arg.startsWith('--kills=')) kills = Number(arg.slice('--kills='.length)) || 1000 else if (arg.startsWith('--boss=')) bossId = arg.slice('--boss='.length) else if (arg.startsWith('--difficulty=')) difficulty = arg.slice('--difficulty='.length) as Difficulty else if (arg.startsWith('--mf=')) playerMf = Number(arg.slice('--mf='.length)) || 250 else if (arg.startsWith('--players=')) gamePlayers = Number(arg.slice('--players='.length)) || 1 else if (!arg.startsWith('-')) positional.push(arg) } if (positional[0]) kills = Number(positional[0]) || kills if (positional[1]) bossId = positional[1] if (positional[2]) difficulty = positional[2] as Difficulty const summary = runParityAudit({ kills, bossId, difficulty, playerMf, gamePlayers }) console.log(JSON.stringify(summary, null, 2)) }