471 lines
19 KiB
TypeScript
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
|
|
})
|
|
})
|
|
})
|