diablo2-web/tests/challenger-m1-elite-stats.t...

190 lines
6.6 KiB
TypeScript

/**
* tests/challenger-m1-elite-stats.test.ts
*
* Empirical Challenger Verification Suite for Diablo II v1.13c Elite Monster Generation.
* Milestone 1 (Issue #448) Elite Statistical Properties.
*/
import { describe, expect, it, beforeAll } from 'vitest'
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, type D2Table } 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'
describe('Challenger M1: Elite Monster Generation Statistical Verification', () => {
let tables: {
readonly levels: D2Table
readonly monstats: D2Table
readonly monlvl: D2Table
readonly monstats2: D2Table
readonly montype: D2Table
readonly monumod: D2Table
readonly superuniques: D2Table
}
let kinds: Map<string, MonsterKind>
beforeAll(async () => {
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}`)))
}
tables = await loadActTables(archives)
kinds = readMonsterKinds(tables.monstats)
})
it('verifies Champion (20%) and Unique (80%) ratios converge within 3-sigma across 10,000 trials', () => {
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
const TRIALS = 10000
for (let trial = 0; trial < TRIALS; trial++) {
const rng = new Rng(300000 + trial)
const groups = planMonsterGroups(plan, types, kinds, 200, rng)
for (const g of groups) {
if (g.rank === 'champion') {
championGroups++
totalEliteGroups++
} else if (g.rank === 'unique') {
uniqueGroups++
totalEliteGroups++
}
}
}
expect(totalEliteGroups).toBeGreaterThan(40000)
const champRatio = championGroups / totalEliteGroups
const uniqueRatio = uniqueGroups / totalEliteGroups
const threeSigma = 3 * Math.sqrt((0.20 * 0.80) / totalEliteGroups)
// Champion ratio converges to 20% within 3-sigma
expect(Math.abs(champRatio - 0.20)).toBeLessThanOrEqual(threeSigma)
// Unique ratio converges to 80% within 3-sigma
expect(Math.abs(uniqueRatio - 0.80)).toBeLessThanOrEqual(threeSigma)
})
it('verifies authentic 1.13c HP multipliers (Champions 3x, Boss 4x, Minions 2x)', () => {
// MonUMod constants: championHpPct = 200, uniqueHpPct = 300, minionHpPct = 100
expect(MONUMOD_CONSTANTS.championHpPct).toBe(200)
expect(MONUMOD_CONSTANTS.uniqueHpPct).toBe(300)
expect(MONUMOD_CONSTANTS.minionHpPct).toBe(100)
// Applied multipliers: 1 + pct / 100
const champMultiplier = ELITE_HEALTH_MULTIPLIER.champion
const uniqueBossMultiplier = ELITE_HEALTH_MULTIPLIER.unique
const minionMultiplier = 1 + (MONUMOD_CONSTANTS.minionHpPct / 100)
expect(champMultiplier).toBe(3)
expect(uniqueBossMultiplier).toBe(4)
expect(minionMultiplier).toBe(2)
// Verify in generated pack members
const planned = planLevelMonsters(tables, 4, 6400, 42, 170, 'normal')
let foundChampion = false
let foundUnique = false
let foundMinion = false
for (const pack of planned.packs) {
if (pack.superUniqueId) continue
for (const member of pack.members) {
if (member.rank === 'champion') foundChampion = true
if (member.rank === 'unique') foundUnique = true
if (member.rank === 'minion') foundMinion = true
}
}
expect(foundChampion).toBe(true)
expect(foundUnique).toBe(true)
expect(foundMinion).toBe(true)
})
it('empirically audits minion count bounds for Uniques across 10,000 trials', () => {
const plan = readLevelMonsterPlan(tables.levels, 3, 'nightmare')!
const types = selectLevelTypes(plan, kinds, new Rng(1), 'nightmare')
let minMinions = Infinity
let maxMinions = -Infinity
let violations = 0
let totalUniques = 0
for (let trial = 0; trial < 10000; trial++) {
const rng = new Rng(400000 + trial)
const groups = planMonsterGroups(plan, types, kinds, 200, rng)
for (const g of groups) {
if (g.rank === 'unique') {
totalUniques++
const minions = g.count - 1
minMinions = Math.min(minMinions, minions)
maxMinions = Math.max(maxMinions, minions)
if (minions < 3 || minions > 6) {
violations++
}
}
}
}
// Diagnostic assertion reporting empirical findings
// Current implementation rolls kind.minGroup to kind.maxGroup instead of [3, 6]
const violationRate = violations / totalUniques
// Expectation: Strictly comply with 1.13c parity: minMinions >= 3 and maxMinions <= 6
expect(violations).toBe(0)
expect(minMinions).toBe(3)
expect(maxMinions).toBe(6)
})
it('empirically audits champion pack size bounds across 10,000 trials', () => {
const plan = readLevelMonsterPlan(tables.levels, 3, 'nightmare')!
const types = selectLevelTypes(plan, kinds, new Rng(1), 'nightmare')
let minPackSize = Infinity
let maxPackSize = -Infinity
let violations = 0
let totalChampions = 0
for (let trial = 0; trial < 10000; trial++) {
const rng = new Rng(500000 + trial)
const groups = planMonsterGroups(plan, types, kinds, 200, rng)
for (const g of groups) {
if (g.rank === 'champion') {
totalChampions++
minPackSize = Math.min(minPackSize, g.count)
maxPackSize = Math.max(maxPackSize, g.count)
if (g.count < 2 || g.count > 4) {
violations++
}
}
}
}
// Diagnostic assertion reporting empirical findings
const violationRate = violations / totalChampions
console.log(`[Challenger Audit] Champion pack size range: [${minPackSize}, ${maxPackSize}], violation rate: ${(violationRate * 100).toFixed(2)}%`)
// Expectation: Strictly comply with 1.13c parity: minPackSize >= 2 and maxPackSize <= 4
expect(violations).toBe(0)
expect(minPackSize).toBe(2)
expect(maxPackSize).toBe(4)
})
})