diablo2-web/tests/challenger-m1-stress.test.ts

471 lines
19 KiB
TypeScript

/**
* 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
})
})
})