fix(monsters): enforce 1.13c champion [2,4] and unique [3,6] minion pack sizes (Fixes #448)

This commit is contained in:
troytt 2026-09-24 14:00:31 +00:00
parent d9ad6df63f
commit a7110143fc
5 changed files with 880 additions and 5 deletions

View File

@ -0,0 +1,209 @@
/**
* 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)
})
}

View File

@ -1180,9 +1180,11 @@ export function planMonsterGroups(
// 1.13c MonUMod.txt: 20% Champions, 80% Uniques + Minions
const isChampion = rng.next() < (MONUMOD_CONSTANTS.championChance / 100)
const rank: Exclude<MonsterRank, 'minion'> = isChampion ? 'champion' : 'unique'
const minGrp = Math.max(1, kind.minGroup)
const maxGrp = Math.max(minGrp, kind.maxGroup)
const count = Math.min(remaining, rng.int(minGrp, maxGrp))
// 1.13c: Champions spawn in groups of 2 to 4 (2 + rand() % 3).
// Uniques spawn with 1 boss + 3 to 6 minions (1 + (3 + rand() % 4)).
const count = rank === 'champion'
? Math.min(remaining, rng.int(2, 4))
: Math.min(remaining, 1 + rng.int(3, 6))
const modSeed = ((i + 1) * 0x9e3779b9) ^ (count * 0x85ebca6b) ^ (rank === 'champion' ? 1 : 2)
const modifiers = rollEliteModifiers(rank, new Rng(modSeed))
groups.push({ kind, count, rank, modifiers })

View File

@ -0,0 +1,189 @@
/**
* 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)
})
})

View File

@ -0,0 +1,470 @@
/**
* Challenger M1 Empirical Stress Test Suite
*
* Exhaustively stress-tests monsterBudget and planLevelMonsters:
* 1. Extreme & adversarial inputs (0 cells, 1 cell, 100k cells, 0 density, 10k density,
* fractionals, negatives, over-cap density).
* 2. Monotonicity verification (cells monotonicity, density monotonicity, joint monotonicity).
* 3. 100,000 differential fuzzing iterations against an independent mathematical oracle.
* 4. All 136 levels across all 3 difficulties (408 level configurations) verifying no NaN,
* no negative values, no crashes, and 0 budget for all town hubs under all multipliers.
* 5. planLevelMonsters extreme & adversarial parameter resilience (invalid IDs, negative multipliers,
* extreme seeds, extreme cells).
*/
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 {
monsterBudget,
planLevelMonsters,
readLevelMonsterPlan,
readMonsterKinds,
type Difficulty,
type MonsterKind,
} from '../src/game/monsters.ts'
import { Rng } from '../src/game/rng.ts'
/**
* Independent ground-truth mathematical oracle for monsterBudget.
* Defined strictly from D2Common/D2Game DRLG MonsterRegion room density specifications.
*/
function referenceMonsterBudget(cells: number, density: number): number {
if (cells <= 0 || density <= 0) return 0
const attempts = Math.floor((cells * 25) / 9)
const clampedDen = Math.min(10000, density)
const packs = attempts * (clampedDen / 100000)
return Math.max(1, Math.round(packs * 3))
}
describe('Challenger M1: monsterBudget Empirical Stress Tests', () => {
describe('Extreme & Adversarial Inputs', () => {
it('returns 0 for zero or negative cells regardless of density', () => {
const testCells = [0, -1, -5, -100, -100000, -Infinity]
const testDensities = [0, 1, 50, 1000, 10000, 50000]
for (const cells of testCells) {
for (const density of testDensities) {
expect(monsterBudget(cells, density)).toBe(0)
expect(monsterBudget({ density } as any, cells)).toBe(0)
}
}
})
it('returns 0 for zero or negative density regardless of cells', () => {
const testCells = [0, 1, 10, 100, 6400, 100000, 1000000]
const testDensities = [0, -1, -10, -500, -10000, -Infinity]
for (const cells of testCells) {
for (const density of testDensities) {
expect(monsterBudget(cells, density)).toBe(0)
expect(monsterBudget({ density } as any, cells)).toBe(0)
}
}
})
it('handles minimal positive input (1 cell, 1 density)', () => {
const budget = monsterBudget(1, 1)
expect(budget).toBe(1)
expect(Number.isInteger(budget)).toBe(true)
expect(budget).toBe(referenceMonsterBudget(1, 1))
})
it('handles minimal positive cells with maximum density cap (1 cell, 10,000 density)', () => {
const budget = monsterBudget(1, 10000)
expect(budget).toBe(1)
expect(Number.isInteger(budget)).toBe(true)
expect(budget).toBe(referenceMonsterBudget(1, 10000))
})
it('handles maximum area (100,000 cells) and high density without overflow or NaN', () => {
const budget100k = monsterBudget(100000, 10000)
// attempts = floor(2500000 / 9) = 277777
// packs = 277777 * 0.1 = 27777.7
// round(packs * 3) = round(83333.1) = 83333
expect(budget100k).toBe(83333)
expect(Number.isInteger(budget100k)).toBe(true)
expect(Number.isFinite(budget100k)).toBe(true)
expect(budget100k).toBe(referenceMonsterBudget(100000, 10000))
})
it('enforces density cap at 10,000 per 1.13c ground truth (Math.min(10000, density))', () => {
const cells = 6400
const atCap = monsterBudget(cells, 10000)
const overCap1 = monsterBudget(cells, 15000)
const overCap2 = monsterBudget(cells, 50000)
const overCap3 = monsterBudget(cells, 1000000)
expect(overCap1).toBe(atCap)
expect(overCap2).toBe(atCap)
expect(overCap3).toBe(atCap)
})
it('handles fractional cells and fractional densities correctly', () => {
const fractionalCases = [
{ cells: 0.5, density: 100 },
{ cells: 1.5, density: 50.5 },
{ cells: 100.25, density: 0.1 },
{ cells: 6400.75, density: 999.99 },
{ cells: 99999.9, density: 10000.5 },
]
for (const { cells, density } of fractionalCases) {
const actual = monsterBudget(cells, density)
const expected = referenceMonsterBudget(cells, density)
expect(actual).toBe(expected)
expect(actual).toBeGreaterThanOrEqual(1)
expect(Number.isInteger(actual)).toBe(true)
}
})
it('maintains strict parity between dual function signatures', () => {
const testCases = [
[100, 500],
[6400, 750],
[10000, 1000],
[0, 500],
[500, 0],
[-10, 500],
[500, -10],
[100000, 10000],
] as const
for (const [cells, density] of testCases) {
const direct = monsterBudget(cells, density)
const objectPlan = monsterBudget({ density } as any, cells)
expect(direct).toBe(objectPlan)
}
})
})
describe('Monotonicity Invariants', () => {
it('verifies cells monotonicity: budget never decreases as cells increase for fixed density', () => {
const testDensities = [1, 5, 20, 100, 500, 1000, 5000, 10000, 20000]
for (const density of testDensities) {
let prevBudget = 0
// Fine-grained sweep 0..1000 (step 1)
for (let cells = 0; cells <= 1000; cells += 1) {
const budget = monsterBudget(cells, density)
expect(budget).toBeGreaterThanOrEqual(prevBudget)
prevBudget = budget
}
// Medium sweep 1000..10000 (step 10)
for (let cells = 1000; cells <= 10000; cells += 10) {
const budget = monsterBudget(cells, density)
expect(budget).toBeGreaterThanOrEqual(prevBudget)
prevBudget = budget
}
// Large sweep 10000..100000 (step 100)
for (let cells = 10000; cells <= 100000; cells += 100) {
const budget = monsterBudget(cells, density)
expect(budget).toBeGreaterThanOrEqual(prevBudget)
prevBudget = budget
}
}
})
it('verifies density monotonicity: budget never decreases as density increases for fixed cells', () => {
const testCells = [1, 5, 20, 100, 500, 1000, 6400, 10000, 100000]
for (const cells of testCells) {
let prevBudget = 0
// Fine-grained sweep 0..1000 (step 1)
for (let density = 0; density <= 1000; density += 1) {
const budget = monsterBudget(cells, density)
expect(budget).toBeGreaterThanOrEqual(prevBudget)
prevBudget = budget
}
// Sweep up to cap 1000..10000 (step 10)
for (let density = 1000; density <= 10000; density += 10) {
const budget = monsterBudget(cells, density)
expect(budget).toBeGreaterThanOrEqual(prevBudget)
prevBudget = budget
}
// Sweep past cap 10000..20000 (step 100) - should stay flat
const capBudget = monsterBudget(cells, 10000)
for (let density = 10000; density <= 20000; density += 100) {
const budget = monsterBudget(cells, density)
expect(budget).toBe(capBudget)
}
}
})
it('verifies joint monotonicity across 50,000 random non-decreasing pairs', () => {
const rng = new Rng(0x1337cafe)
for (let i = 0; i < 50000; i++) {
const c1 = rng.int(0, 50000)
const c2 = c1 + rng.int(0, 50000)
const d1 = rng.int(0, 10000)
const d2 = d1 + rng.int(0, 10000)
const b1 = monsterBudget(c1, d1)
const b2 = monsterBudget(c2, d2)
expect(b2).toBeGreaterThanOrEqual(b1)
}
})
})
describe('Differential Fuzzing (100,000 iterations against Oracle)', () => {
it('matches reference oracle across 100,000 pseudo-random and adversarial inputs', () => {
const rng = new Rng(0xdeadbeef)
let failureCount = 0
for (let iter = 0; iter < 100000; iter++) {
let cells: number
let density: number
const category = iter % 5
if (category === 0) {
// Boundary / extreme values
const boundaryCells = [0, 1, 2, 3, 8, 9, 10, -1, -100, 100000, 500000]
const boundaryDen = [0, 1, 2, -1, -500, 9999, 10000, 10001, 20000, 100000]
cells = boundaryCells[rng.int(0, boundaryCells.length - 1)]!
density = boundaryDen[rng.int(0, boundaryDen.length - 1)]!
} else if (category === 1) {
// Random small values
cells = rng.int(0, 50)
density = rng.int(0, 500)
} else if (category === 2) {
// Random medium values
cells = rng.int(50, 5000)
density = rng.int(0, 2000)
} else if (category === 3) {
// Random large values
cells = rng.int(5000, 150000)
density = rng.int(0, 20000)
} else {
// Fractional / noisy values
cells = rng.range(-10, 10000)
density = rng.range(-100, 15000)
}
const actual = monsterBudget(cells, density)
const expected = referenceMonsterBudget(cells, density)
if (actual !== expected) {
failureCount++
if (failureCount <= 5) {
console.error(`Mismatch at iter ${iter}: cells=${cells}, density=${density} => actual=${actual}, expected=${expected}`)
}
}
}
expect(failureCount).toBe(0)
})
})
})
describe('Challenger M1: planLevelMonsters Full Spectrum (136 Levels x 3 Difficulties)', () => {
let tables: {
readonly levels: D2Table
readonly monstats: D2Table
readonly monlvl: D2Table
readonly monstats2: D2Table
readonly montype: D2Table
readonly monumod: D2Table
readonly superuniques: D2Table
}
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)
})
const TOWNS = [
{ id: 1, name: 'Rogue Encampment' },
{ id: 40, name: 'Lut Gholein' },
{ id: 75, name: 'Kurast Docks' },
{ id: 103, name: 'Pandemonium Fortress' },
{ id: 109, name: 'Harrogath' },
]
describe('Town Invariant Stress Testing', () => {
it.each(TOWNS)('guarantees 0 monsters, 0 packs, and 0 elites for $name ($id) across all difficulties and multipliers', ({ id }) => {
const multipliers = [
{ densityMultiplier: 1, eliteMultiplier: 1 },
{ densityMultiplier: 2, eliteMultiplier: 2 },
{ densityMultiplier: 8, eliteMultiplier: 4 },
{ densityMultiplier: 8, eliteMultiplier: 'all' as const },
{ densityMultiplier: 100, eliteMultiplier: 'all' as const },
]
const testCells = [0, 1, 6400, 100000]
for (const diff of ['normal', 'nightmare', 'hell'] as const) {
for (const cells of testCells) {
for (const opts of multipliers) {
const planned = planLevelMonsters(tables, id, cells, 12345, 170, diff, opts)
expect(planned.budget).toBe(0)
expect(planned.packs).toHaveLength(0)
expect(planned.elitePacks).toBe(0)
}
}
}
})
})
describe('All 136 Levels Sanity & Numeric Robustness', () => {
it('produces valid numbers (no NaN, no negatives, no crashes) for all 136 levels across all 3 difficulties', () => {
const cells = 6400
let totalLevelsTested = 0
for (let id = 1; id <= 136; id++) {
for (const diff of ['normal', 'nightmare', 'hell'] as const) {
const planned = planLevelMonsters(tables, id, cells, id * 100 + 7, 170, diff)
totalLevelsTested++
// Basic contract checks
expect(planned.levelId).toBe(id)
expect(Number.isFinite(planned.budget)).toBe(true)
expect(Number.isNaN(planned.budget)).toBe(false)
expect(planned.budget).toBeGreaterThanOrEqual(0)
expect(Number.isFinite(planned.elitePacks)).toBe(true)
expect(Number.isNaN(planned.elitePacks)).toBe(false)
expect(planned.elitePacks).toBeGreaterThanOrEqual(0)
// Inspect every pack and every member
let totalSpawnedMembers = 0
for (const pack of planned.packs) {
expect(pack.members.length).toBeGreaterThan(0)
for (const member of pack.members) {
totalSpawnedMembers++
expect(member.id).toBeTruthy()
expect(Number.isFinite(member.hp)).toBe(true)
expect(Number.isNaN(member.hp)).toBe(false)
expect(member.hp).toBeGreaterThanOrEqual(1)
expect(Number.isFinite(member.damage)).toBe(true)
expect(Number.isNaN(member.damage)).toBe(false)
expect(member.damage).toBeGreaterThanOrEqual(1)
expect(Number.isFinite(member.speed)).toBe(true)
expect(Number.isNaN(member.speed)).toBe(false)
expect(member.speed).toBeGreaterThanOrEqual(1)
expect(Number.isFinite(member.level)).toBe(true)
expect(Number.isNaN(member.level)).toBe(false)
expect(member.level).toBeGreaterThanOrEqual(1)
expect(member.level).toBeLessThanOrEqual(99)
expect(Number.isFinite(member.cooldownTicks)).toBe(true)
expect(Number.isFinite(member.reach)).toBe(true)
expect(Number.isFinite(member.aggroRadius)).toBe(true)
expect(Number.isFinite(member.xp)).toBe(true)
expect(['normal', 'champion', 'unique', 'minion']).toContain(member.rank)
}
}
// If budget is 0, packs must be 0
if (planned.budget === 0) {
// Only fixed SuperUniques (if any) could exist in a 0-budget level
const nonSuperUniquePacks = planned.packs.filter(p => !p.superUniqueId)
expect(nonSuperUniquePacks).toHaveLength(0)
}
}
}
expect(totalLevelsTested).toBe(136 * 3)
})
})
describe('planLevelMonsters Monotonicity & Scaling Invariants', () => {
it('verifies increasing cells increases or maintains planned budget across non-town levels', () => {
const cellSteps = [100, 500, 1000, 6400, 20000, 100000]
// Test representative levels from each Act:
// Act 1: 2 (Blood Moor), 3 (Cold Plains)
// Act 2: 41 (Rocky Waste), 43 (Far Oasis)
// Act 3: 76 (Spider Forest), 78 (Flayer Jungle)
// Act 4: 104 (Outer Steppes), 105 (Plains of Despair)
// Act 5: 111 (Frigid Highlands), 112 (Arreat Plateau)
const testLevels = [2, 3, 41, 43, 76, 78, 104, 105, 111, 112]
for (const id of testLevels) {
for (const diff of ['normal', 'nightmare', 'hell'] as const) {
let prevBudget = 0
for (const cells of cellSteps) {
const planned = planLevelMonsters(tables, id, cells, 42, 170, diff)
expect(planned.budget).toBeGreaterThanOrEqual(prevBudget)
prevBudget = planned.budget
}
}
}
})
it('verifies densityMultiplier scales budget monotonically without breaking bounds', () => {
const multipliers = [0.5, 1, 2, 4, 8]
const testLevels = [2, 3, 41, 76, 104, 111]
for (const id of testLevels) {
let prevBudget = 0
for (const densityMultiplier of multipliers) {
const planned = planLevelMonsters(tables, id, 6400, 42, 170, 'normal', { densityMultiplier })
expect(planned.budget).toBeGreaterThanOrEqual(prevBudget)
prevBudget = planned.budget
}
}
})
it('verifies eliteMultiplier all option turns all groups into elites or fills elite quota', () => {
const plannedAll = planLevelMonsters(tables, 3, 6400, 42, 170, 'normal', { eliteMultiplier: 'all' })
expect(plannedAll.elitePacks).toBeGreaterThan(0)
const elitePacksCount = plannedAll.packs.filter(p =>
p.superUniqueId || p.members.some(m => m.rank === 'champion' || m.rank === 'unique'),
).length
expect(elitePacksCount).toBe(plannedAll.elitePacks)
})
})
describe('Adversarial & Malformed Inputs to planLevelMonsters', () => {
it('gracefully handles non-existent or invalid level IDs without throwing', () => {
const invalidIds = [-1, 0, 137, 999, 9999]
for (const id of invalidIds) {
const planned = planLevelMonsters(tables, id, 6400, 42, 170, 'normal')
expect(planned.levelId).toBe(id)
expect(planned.budget).toBe(0)
expect(planned.packs).toHaveLength(0)
expect(planned.elitePacks).toBe(0)
}
})
it('handles negative or zero cells gracefully', () => {
const plannedZero = planLevelMonsters(tables, 2, 0, 42, 170, 'normal')
expect(plannedZero.budget).toBe(0)
const plannedNeg = planLevelMonsters(tables, 2, -500, 42, 170, 'normal')
expect(plannedNeg.budget).toBe(0)
})
it('handles extreme 32-bit integer seeds without degradation', () => {
const extremeSeeds = [-2147483648, 2147483647, 0, -1, 0x7fffffff]
for (const seed of extremeSeeds) {
const planned = planLevelMonsters(tables, 2, 6400, seed, 170, 'normal')
expect(planned.budget).toBeGreaterThan(0)
expect(planned.packs.length).toBeGreaterThan(0)
}
})
it('handles negative multipliers by clamping/safely returning 0', () => {
const plannedNegDensity = planLevelMonsters(tables, 2, 6400, 42, 170, 'normal', { densityMultiplier: -1 })
expect(plannedNegDensity.budget).toBe(0)
// On level 2 (no superuniques), negative eliteMultiplier yields 0 elite packs
const plannedNegEliteLvl2 = planLevelMonsters(tables, 2, 6400, 42, 170, 'normal', { eliteMultiplier: -1 })
expect(plannedNegEliteLvl2.elitePacks).toBe(0)
// On level 3 (has Bishibosh superunique), random elites must be 0, leaving only the 1 landmark superunique
const plannedNegEliteLvl3 = planLevelMonsters(tables, 3, 6400, 42, 170, 'normal', { eliteMultiplier: -1 })
const randomElites = plannedNegEliteLvl3.packs.filter(
p => !p.superUniqueId && p.members.some(m => m.rank === 'champion' || m.rank === 'unique'),
)
expect(randomElites).toHaveLength(0)
expect(plannedNegEliteLvl3.elitePacks).toBe(1) // exactly 1 for Bishibosh
})
})
})

View File

@ -451,9 +451,14 @@ describe('planMonsterGroups', () => {
}
})
it('never builds a pack larger than the monster\'s MaxGrp', () => {
it('never builds a normal pack larger than the monster\'s MaxGrp and sizes elites per 1.13c', () => {
const groups = planMonsterGroups(plan, types, kinds, 40, new Rng(6))
expect(groups.every(group => group.count >= 1 && group.count <= group.kind.maxGroup)).toBe(true)
const normal = groups.filter(group => group.rank === 'normal')
expect(normal.every(group => group.count >= 1 && group.count <= group.kind.maxGroup)).toBe(true)
const champions = groups.filter(group => group.rank === 'champion')
expect(champions.every(group => group.count >= 2 && group.count <= 4)).toBe(true)
const uniques = groups.filter(group => group.rank === 'unique')
expect(uniques.every(group => group.count >= 4 && group.count <= 7)).toBe(true)
})
it('draws elite packs from umon, not from the ordinary pool', () => {