190 lines
6.6 KiB
TypeScript
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)
|
|
})
|
|
})
|