210 lines
8.5 KiB
TypeScript
210 lines
8.5 KiB
TypeScript
/**
|
|
* scripts/verify-challenger-elite-stats.ts
|
|
*
|
|
* Empirical Challenger Verification Suite for Diablo II v1.13c Elite Monster Generation.
|
|
*
|
|
* Objective:
|
|
* 1. Monte Carlo simulation (>= 10,000 trials) testing `planMonsterGroups` for elite selection:
|
|
* - Champion ratio convergence to 20% within 3-sigma confidence interval.
|
|
* - Unique ratio convergence to 80% within 3-sigma confidence interval.
|
|
* - Minion counts for Uniques strictly bounded in [3, 6].
|
|
* - Champion pack sizes strictly bounded in [2, 4].
|
|
* - HP multipliers: Champions 3x, Boss 4x, Minions 2x.
|
|
*/
|
|
|
|
import * as fs from 'fs'
|
|
import { MountedArchives } from '../src/mpq/mount.ts'
|
|
import { MpqArchive } from '../src/mpq/archive.ts'
|
|
import { fileSource } from '../src/mpq/file-source.ts'
|
|
import { loadActTables } from '../src/game/acts.ts'
|
|
import {
|
|
readLevelMonsterPlan,
|
|
readMonsterKinds,
|
|
selectLevelTypes,
|
|
planMonsterGroups,
|
|
planLevelMonsters,
|
|
ELITE_HEALTH_MULTIPLIER,
|
|
MONUMOD_CONSTANTS,
|
|
type MonsterKind,
|
|
} from '../src/game/monsters.ts'
|
|
import { Rng } from '../src/game/rng.ts'
|
|
|
|
export interface MonteCarloReport {
|
|
readonly trials: number
|
|
readonly totalEliteGroups: number
|
|
readonly championGroups: number
|
|
readonly uniqueGroups: number
|
|
readonly championRatio: number
|
|
readonly uniqueRatio: number
|
|
readonly threeSigmaMargin: number
|
|
readonly championRatioWithin3Sigma: boolean
|
|
readonly uniqueRatioWithin3Sigma: boolean
|
|
readonly championPackSizeMin: number
|
|
readonly championPackSizeMax: number
|
|
readonly championPackSizeValid: boolean
|
|
readonly championPackSizeHistogram: Record<number, number>
|
|
readonly uniqueMinionCountMin: number
|
|
readonly uniqueMinionCountMax: number
|
|
readonly uniqueMinionCountValid: boolean
|
|
readonly uniqueMinionCountHistogram: Record<number, number>
|
|
readonly hpMultipliersValid: boolean
|
|
readonly hpMultipliers: {
|
|
champion: number
|
|
uniqueBoss: number
|
|
minion: number
|
|
}
|
|
}
|
|
|
|
export async function runEliteMonteCarloSimulation(trials = 10000): Promise<MonteCarloReport> {
|
|
const dir = fs.existsSync('samples/d2/Patch_D2.mpq')
|
|
? 'samples/d2'
|
|
: (fs.existsSync('/usr/local/google/home/taodao/d2-data/Patch_D2.mpq')
|
|
? '/usr/local/google/home/taodao/d2-data'
|
|
: 'samples/d2')
|
|
|
|
const archives = new MountedArchives()
|
|
for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
|
|
archives.add(name, await MpqArchive.open(await fileSource(`${dir}/${name}`)))
|
|
}
|
|
const tables = await loadActTables(archives)
|
|
const kinds = readMonsterKinds(tables.monstats)
|
|
|
|
const plan = readLevelMonsterPlan(tables.levels, 3, 'nightmare')!
|
|
const types = selectLevelTypes(plan, kinds, new Rng(1), 'nightmare')
|
|
|
|
let totalEliteGroups = 0
|
|
let championGroups = 0
|
|
let uniqueGroups = 0
|
|
|
|
let minChampPackSize = Infinity
|
|
let maxChampPackSize = -Infinity
|
|
const champPackSizes = new Map<number, number>()
|
|
|
|
let minUniqueMinionCount = Infinity
|
|
let maxUniqueMinionCount = -Infinity
|
|
const uniqueMinionCounts = new Map<number, number>()
|
|
|
|
for (let trial = 0; trial < trials; trial++) {
|
|
const rng = new Rng(200000 + trial)
|
|
// Budget 200 ensures packs are not artificially truncated by remaining budget
|
|
const groups = planMonsterGroups(plan, types, kinds, 200, rng)
|
|
for (const g of groups) {
|
|
if (g.rank === 'champion') {
|
|
championGroups++
|
|
totalEliteGroups++
|
|
minChampPackSize = Math.min(minChampPackSize, g.count)
|
|
maxChampPackSize = Math.max(maxChampPackSize, g.count)
|
|
champPackSizes.set(g.count, (champPackSizes.get(g.count) || 0) + 1)
|
|
} else if (g.rank === 'unique') {
|
|
uniqueGroups++
|
|
totalEliteGroups++
|
|
const minions = g.count - 1
|
|
minUniqueMinionCount = Math.min(minUniqueMinionCount, minions)
|
|
maxUniqueMinionCount = Math.max(maxUniqueMinionCount, minions)
|
|
uniqueMinionCounts.set(minions, (uniqueMinionCounts.get(minions) || 0) + 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
const championRatio = championGroups / totalEliteGroups
|
|
const uniqueRatio = uniqueGroups / totalEliteGroups
|
|
const threeSigmaMargin = 3 * Math.sqrt((0.20 * 0.80) / totalEliteGroups)
|
|
|
|
const championRatioWithin3Sigma = Math.abs(championRatio - 0.20) <= threeSigmaMargin
|
|
const uniqueRatioWithin3Sigma = Math.abs(uniqueRatio - 0.80) <= threeSigmaMargin
|
|
|
|
const championPackSizeValid = minChampPackSize >= 2 && maxChampPackSize <= 4
|
|
const uniqueMinionCountValid = minUniqueMinionCount >= 3 && maxUniqueMinionCount <= 6
|
|
|
|
const championHp = ELITE_HEALTH_MULTIPLIER.champion
|
|
const uniqueHp = ELITE_HEALTH_MULTIPLIER.unique
|
|
const minionHp = 1 + (MONUMOD_CONSTANTS.minionHpPct / 100)
|
|
const hpMultipliersValid = championHp === 3 && uniqueHp === 4 && minionHp === 2
|
|
|
|
return {
|
|
trials,
|
|
totalEliteGroups,
|
|
championGroups,
|
|
uniqueGroups,
|
|
championRatio,
|
|
uniqueRatio,
|
|
threeSigmaMargin,
|
|
championRatioWithin3Sigma,
|
|
uniqueRatioWithin3Sigma,
|
|
championPackSizeMin: minChampPackSize,
|
|
championPackSizeMax: maxChampPackSize,
|
|
championPackSizeValid,
|
|
championPackSizeHistogram: Object.fromEntries(champPackSizes),
|
|
uniqueMinionCountMin: minUniqueMinionCount,
|
|
uniqueMinionCountMax: maxUniqueMinionCount,
|
|
uniqueMinionCountValid,
|
|
uniqueMinionCountHistogram: Object.fromEntries(uniqueMinionCounts),
|
|
hpMultipliersValid,
|
|
hpMultipliers: {
|
|
champion: championHp,
|
|
uniqueBoss: uniqueHp,
|
|
minion: minionHp,
|
|
},
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log('======================================================================')
|
|
console.log('🔬 EMPIRICAL CHALLENGER MONTE CARLO TEST: ELITE STATISTICAL PROPERTIES')
|
|
console.log('======================================================================')
|
|
|
|
const report = await runEliteMonteCarloSimulation(10000)
|
|
|
|
console.log(`\nTrials Run: ${report.trials}`)
|
|
console.log(`Total Elite Groups Sampled: ${report.totalEliteGroups}`)
|
|
console.log(`Champion Groups: ${report.championGroups}`)
|
|
console.log(`Unique Groups: ${report.uniqueGroups}`)
|
|
|
|
console.log('\n--- Criterion 1 & 2: Statistical Convergence (3-Sigma) ---')
|
|
console.log(`Observed Champion Ratio: ${(report.championRatio * 100).toFixed(3)}% (Target: 20.000%)`)
|
|
console.log(`3-Sigma Confidence Interval: [${((0.20 - report.threeSigmaMargin) * 100).toFixed(3)}%, ${((0.20 + report.threeSigmaMargin) * 100).toFixed(3)}%]`)
|
|
console.log(`Champion Ratio within 3-Sigma: ${report.championRatioWithin3Sigma ? '✅ PASS' : '❌ FAIL'}`)
|
|
|
|
console.log(`\nObserved Unique Ratio: ${(report.uniqueRatio * 100).toFixed(3)}% (Target: 80.000%)`)
|
|
console.log(`3-Sigma Confidence Interval: [${((0.80 - report.threeSigmaMargin) * 100).toFixed(3)}%, ${((0.80 + report.threeSigmaMargin) * 100).toFixed(3)}%]`)
|
|
console.log(`Unique Ratio within 3-Sigma: ${report.uniqueRatioWithin3Sigma ? '✅ PASS' : '❌ FAIL'}`)
|
|
|
|
console.log('\n--- Criterion 3: Minion Counts for Uniques strictly bounded in [3, 6] ---')
|
|
console.log(`Observed Minion Range: [${report.uniqueMinionCountMin}, ${report.uniqueMinionCountMax}]`)
|
|
console.log(`Histogram (minion count: occurrences):`, report.uniqueMinionCountHistogram)
|
|
console.log(`Strictly Bounded in [3, 6]: ${report.uniqueMinionCountValid ? '✅ PASS' : '❌ FAIL'}`)
|
|
|
|
console.log('\n--- Criterion 4: Champion Pack Sizes strictly bounded in [2, 4] ---')
|
|
console.log(`Observed Champion Pack Size Range: [${report.championPackSizeMin}, ${report.championPackSizeMax}]`)
|
|
console.log(`Histogram (pack size: occurrences):`, report.championPackSizeHistogram)
|
|
console.log(`Strictly Bounded in [2, 4]: ${report.championPackSizeValid ? '✅ PASS' : '❌ FAIL'}`)
|
|
|
|
console.log('\n--- Criterion 5: HP Multipliers (Champions 3x, Boss 4x, Minions 2x) ---')
|
|
console.log(`Champion HP Multiplier: ${report.hpMultipliers.champion}x (Expected: 3x)`)
|
|
console.log(`Unique Boss HP Multiplier: ${report.hpMultipliers.uniqueBoss}x (Expected: 4x)`)
|
|
console.log(`Minion HP Multiplier: ${report.hpMultipliers.minion}x (Expected: 2x)`)
|
|
console.log(`HP Multipliers Valid: ${report.hpMultipliersValid ? '✅ PASS' : '❌ FAIL'}`)
|
|
|
|
console.log('\n======================================================================')
|
|
const overallPass =
|
|
report.championRatioWithin3Sigma &&
|
|
report.uniqueRatioWithin3Sigma &&
|
|
report.uniqueMinionCountValid &&
|
|
report.championPackSizeValid &&
|
|
report.hpMultipliersValid
|
|
|
|
console.log(`FINAL EMPIRICAL VERDICT: ${overallPass ? 'APPROVE' : 'REQUEST_CHANGES'}`)
|
|
console.log('======================================================================')
|
|
|
|
if (!overallPass) {
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
main().catch((err) => {
|
|
console.error(err)
|
|
process.exit(1)
|
|
})
|
|
}
|