diablo2-web/tests/challenger-m1-adversarial.t...

528 lines
18 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest'
import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts'
import { GameEngine } from '../src/game/engine.ts'
import type { MonsterKind } from '../src/game/monsters.ts'
import { getMonsterTreasureClass } from '../src/game/monsters.ts'
import * as dropPipeline from '../src/game/drop-pipeline.ts'
import { executeDropPipeline } from '../src/game/drop-pipeline.ts'
import {
rollBaseUpgrade,
DifficultyLevelsTable,
parseDifficultyLevelsTable,
ITEM_UPGRADE_FLAG_EXCEPTIONAL,
ITEM_UPGRADE_FLAG_ELITE,
type UpgradableItemBase,
} from '../src/game/item-upgrade.ts'
import { D2Rng } from '../src/game/d2-rng.ts'
describe('Challenger M1 Adversarial Stress Test Suite', () => {
const dropTables = getEmbeddedDropTables()
const dummyTerrain = {
widthPx: 1000,
heightPx: 1000,
overlap: () => 0,
}
const idleInput = {
movement: { x: 0, y: 0 },
attacking: false,
pickingUp: false,
talking: false,
saving: false,
loading: false,
digits: [],
}
function createTestEngine(
difficulty: 'normal' | 'nightmare' | 'hell' = 'normal',
customMonsterKinds?: Map<string, MonsterKind>,
customDropTables?: any,
) {
return new GameEngine(dummyTerrain, {
spawn: { x: 0, y: 0 },
stats: [],
xpTable: [0, 100, 200],
dropTables: customDropTables ?? dropTables,
difficulty,
monsterKinds: customMonsterKinds,
skills: [],
npcDefs: [],
questDefs: [],
combatOptions: {} as any,
talkRadius: 50,
pickupRadius: 50,
inventoryCols: 10,
inventoryRows: 4,
})
}
// =========================================================================
// Dimension 1: Fuzz and test invalid inputs to GameEngine drop processing
// =========================================================================
describe('Dimension 1: Invalid & Adversarial Inputs to GameEngine Drop Processing', () => {
it('rejects missing monsterLevel (undefined, null)', () => {
const engine = createTestEngine('normal')
for (const mlvl of [undefined, null]) {
engine.world.pendingKills = [
{
kind: 'kill',
x: 10,
y: 10,
subjectId: 'fallen1',
monsterRank: 'normal',
monsterLevel: mlvl as any,
} as any,
]
expect(() => engine.tick(idleInput)).toThrow(
/GameEngine drop failure: missing or invalid monsterLevel for death event/,
)
}
})
it('rejects invalid numeric monsterLevel (0, negative, NaN, strings, objects)', () => {
const invalidLevels = [
0,
-1,
-100,
-Infinity,
Number.NaN,
'10',
'',
false,
true,
{},
[],
]
for (const mlvl of invalidLevels) {
const engine = createTestEngine('normal')
engine.world.pendingKills = [
{
kind: 'kill',
x: 10,
y: 10,
subjectId: 'fallen1',
monsterRank: 'normal',
monsterLevel: mlvl as any,
} as any,
]
expect(() => engine.tick(idleInput)).toThrow(
/GameEngine drop failure: missing or invalid monsterLevel for death event/,
)
}
})
it('fuzzes unknown monsters and injection payloads in subjectId and superUniqueId', () => {
const adversarialIds = [
'__proto__',
'constructor',
'toString',
'valueOf',
'',
' ',
'DROP TABLE monsters;',
'<script>alert(1)</script>',
'null',
'undefined',
'non_existent_monster_666',
'!!!invalid!!!',
'1234567890',
]
for (const id of adversarialIds) {
const engine = createTestEngine('normal')
engine.world.pendingKills = [
{
kind: 'kill',
x: 10,
y: 10,
subjectId: id,
monsterRank: 'normal',
monsterLevel: 10,
} as any,
]
expect(() => engine.tick(idleInput)).toThrow(
/GameEngine drop failure: unknown monster/,
)
}
})
it('rejects unknown superUnique with unknown subjectId', () => {
const engine = createTestEngine('normal')
engine.world.pendingKills = [
{
kind: 'kill',
x: 10,
y: 10,
superUniqueId: 'NonExistentSuperUnique_XYZ',
monsterRank: 'unique',
monsterLevel: 50,
} as any,
]
expect(() => engine.tick(idleInput)).toThrow(
/GameEngine drop failure: unknown monster \(subjectId: "undefined", superUniqueId: "NonExistentSuperUnique_XYZ"\) not found in canonical drop tables/,
)
})
it('rejects corrupt combat monster with empty getTreasureClass() result', () => {
const customKinds = new Map<string, MonsterKind>()
customKinds.set('corrupt_combatant', {
id: 'corrupt_combatant',
baseId: 'corrupt_combatant',
name: 'Corrupt Combatant',
boss: false,
levels: { normal: 10, nightmare: 40, hell: 70 },
treasureClasses: ['Act 1 H2H A'],
getTreasureClass: () => '',
} as any)
const engine = createTestEngine('normal', customKinds)
engine.world.pendingKills = [
{
kind: 'kill',
x: 10,
y: 10,
subjectId: 'corrupt_combatant',
monsterRank: 'normal',
monsterLevel: 10,
} as any,
]
expect(() => engine.tick(idleInput)).toThrow(
/GameEngine drop failure: corrupt or unresolvable treasure class for monster "corrupt_combatant" on difficulty "normal"/,
)
})
it('rejects combat monster resolving to unregistered TC', () => {
const customKinds = new Map<string, MonsterKind>()
customKinds.set('unregistered_tc_demon', {
id: 'unregistered_tc_demon',
baseId: 'unregistered_tc_demon',
name: 'Unregistered Demon',
levels: { normal: 10, nightmare: 40, hell: 70 },
treasureClasses: ['Totally_Fake_TC_Name_999'],
} as any)
const engine = createTestEngine('normal', customKinds)
engine.world.pendingKills = [
{
kind: 'kill',
x: 10,
y: 10,
subjectId: 'unregistered_tc_demon',
monsterRank: 'normal',
monsterLevel: 10,
} as any,
]
expect(() => engine.tick(idleInput)).toThrow(
/GameEngine drop failure: treasure class "Totally_Fake_TC_Name_999" resolved for monster "unregistered_tc_demon" not found in drop tables/,
)
})
it('asserts executeDropPipeline is NEVER called when validation fails', () => {
const dropSpy = vi.spyOn(dropPipeline, 'executeDropPipeline')
const engine = createTestEngine('normal')
const invalidEvents = [
{ kind: 'kill', x: 0, y: 0, subjectId: 'fallen1', monsterLevel: 0 },
{ kind: 'kill', x: 0, y: 0, subjectId: 'fallen1', monsterLevel: -1 },
{ kind: 'kill', x: 0, y: 0, subjectId: 'unknown_mob', monsterLevel: 10 },
]
for (const ev of invalidEvents) {
engine.world.pendingKills = [ev as any]
expect(() => engine.tick(idleInput)).toThrow()
}
expect(dropSpy).not.toHaveBeenCalled()
dropSpy.mockRestore()
})
})
// =========================================================================
// Dimension 2: Verify all 251 authentic zero-drop critters and Ancient Barbarians
// =========================================================================
describe('Dimension 2: All 251 Authentic Zero-Drop Critters & Ancient Barbarians', () => {
it('verifies exactly 251 monsterKinds have authentic empty TCs on Normal', () => {
const normalEmptyKinds: string[] = []
for (const [id, kind] of dropTables.monsterKinds) {
const tc = typeof kind.getTreasureClass === 'function'
? kind.getTreasureClass('normal', 1)
: ''
if (!tc || tc.trim() === '') {
normalEmptyKinds.push(id)
}
}
expect(normalEmptyKinds.length).toBe(251)
})
it('slays all 251 authentic zero-drop critters on Normal without throwing, yielding 0 drops', () => {
const engine = createTestEngine('normal')
for (const [id, kind] of dropTables.monsterKinds) {
const tc = typeof kind.getTreasureClass === 'function'
? kind.getTreasureClass('normal', 1)
: ''
if (!tc || tc.trim() === '') {
engine.world.pendingKills = [
{
kind: 'kill',
x: 10,
y: 10,
subjectId: id,
monsterRank: 'normal',
monsterLevel: 1,
} as any,
]
expect(() => engine.tick(idleInput), `Critter ${id} failed`).not.toThrow()
expect(engine.metrics.dropsRolled, `Critter ${id} rolled drops`).toBe(0)
expect(engine.ground.length, `Critter ${id} dropped ground items`).toBe(0)
}
}
})
it('slays all 3 Arreat Summit Ancient Barbarians across Normal, Nightmare, and Hell with 0 drops', () => {
const ancientSuperUniques = [
'Ancient Barbarian 1',
'Ancient Barbarian 2',
'Ancient Barbarian 3',
]
for (const diff of ['normal', 'nightmare', 'hell'] as const) {
const engine = createTestEngine(diff)
for (const ancientName of ancientSuperUniques) {
const suDef = dropTables.superUniques.get(ancientName)
expect(suDef, `SuperUnique ${ancientName} must exist`).toBeDefined()
expect(suDef?.treasureClass, `Ancient ${ancientName} TC must be empty`).toBe('')
engine.world.pendingKills = [
{
kind: 'kill',
x: 10,
y: 10,
subjectId: suDef!.monsterId,
superUniqueId: ancientName,
monsterRank: 'unique',
monsterLevel: diff === 'hell' ? 90 : diff === 'nightmare' ? 68 : 37,
} as any,
]
expect(() => engine.tick(idleInput), `Ancient ${ancientName} on ${diff} threw error`).not.toThrow()
expect(engine.metrics.dropsRolled, `Ancient ${ancientName} on ${diff} rolled drops`).toBe(0)
expect(engine.ground.length, `Ancient ${ancientName} on ${diff} dropped ground items`).toBe(0)
}
}
})
it('verifies ambient critters (chicken, rat, bird1, bird2) drop 0 items across all 3 difficulties', () => {
const critters = ['chicken', 'rat', 'bird1', 'bird2']
for (const diff of ['normal', 'nightmare', 'hell'] as const) {
const engine = createTestEngine(diff)
for (const critter of critters) {
engine.world.pendingKills = [
{
kind: 'kill',
x: 10,
y: 10,
subjectId: critter,
monsterRank: 'normal',
monsterLevel: 1,
} as any,
]
expect(() => engine.tick(idleInput)).not.toThrow()
expect(engine.metrics.dropsRolled).toBe(0)
expect(engine.ground.length).toBe(0)
}
}
})
})
// =========================================================================
// Dimension 3: Verify base upgrade divisor calculations across all 3 difficulties
// =========================================================================
describe('Dimension 3: Base Upgrade Divisor Calculations & Difficulty Odds', () => {
it('verifies DifficultyLevelsTable parsing from authentic 1.13c table', () => {
const diffTable = dropTables.difficultyLevels
expect(diffTable).toBeDefined()
expect(diffTable.Normal.UberCodeOddsNormal).toBe(0)
expect(diffTable.Normal.UberCodeOddsGood).toBe(0)
expect(diffTable.Normal.UltraCodeOddsNormal).toBe(0)
expect(diffTable.Normal.UltraCodeOddsGood).toBe(0)
expect(diffTable.Nightmare.UberCodeOddsNormal).toBe(10)
expect(diffTable.Nightmare.UberCodeOddsGood).toBe(20)
expect(diffTable.Nightmare.UltraCodeOddsNormal).toBe(0)
expect(diffTable.Nightmare.UltraCodeOddsGood).toBe(0)
expect(diffTable.Hell.UberCodeOddsNormal).toBe(20)
expect(diffTable.Hell.UberCodeOddsGood).toBe(40)
expect(diffTable.Hell.UltraCodeOddsNormal).toBe(30)
expect(diffTable.Hell.UltraCodeOddsGood).toBe(40)
})
it('fails fast if DifficultyLevelsTable constructor receives incomplete difficulty entries', () => {
expect(() => new DifficultyLevelsTable([], new Map())).toThrow(
/Failed to initialize DifficultyLevelsTable: expected Normal, Nightmare, and Hell entries/,
)
})
it('fails fast if executeDropPipeline cannot resolve difficulty odds', () => {
const corruptedTables = {
...dropTables,
difficultyOdds: new Map(),
difficultyLevels: new Map(),
}
expect(() =>
executeDropPipeline(corruptedTables as any, {
tcName: 'Act 1 Equip A',
nLevel: 10,
monsterType: 1,
difficulty: 'normal',
}),
).toThrow(/Failed to resolve difficulty odds for difficulty: "normal"/)
})
it('verifies rollBaseUpgrade on Normal difficulty NEVER upgrades base items (10,000 rolls)', () => {
const mockBase: UpgradableItemBase = {
id: 'cap',
name: 'Cap',
kind: 'armor',
invWidth: 2,
invHeight: 2,
maxStack: 1,
value: 12,
damage: 0,
defense: 5,
tags: ['helm'],
level: 1,
normcode: 'cap',
ubercode: 'xap',
ultracode: 'uap',
}
const xapBase: UpgradableItemBase = { ...mockBase, id: 'xap', name: 'War Hat', level: 34 }
const uapBase: UpgradableItemBase = { ...mockBase, id: 'uap', name: 'Shako', level: 58 }
const lookup = (code: string) => (code === 'xap' ? xapBase : code === 'uap' ? uapBase : mockBase)
const rng = new D2Rng(1337)
// Normal difficulty: qf4 = 0, qf5 = 0
for (let i = 0; i < 10000; i++) {
const res = rollBaseUpgrade(mockBase, 0, 0, rng, lookup)
expect(res.isUber).toBe(false)
expect(res.base.id).toBe('cap')
}
})
it('verifies statistical distribution of base upgrades on Nightmare difficulty (50,000 rolls)', () => {
const mockBase: UpgradableItemBase = {
id: 'cap',
name: 'Cap',
kind: 'armor',
invWidth: 2,
invHeight: 2,
maxStack: 1,
value: 12,
damage: 0,
defense: 5,
tags: ['helm'],
level: 1,
normcode: 'cap',
ubercode: 'xap',
ultracode: 'uap',
}
const xapBase: UpgradableItemBase = { ...mockBase, id: 'xap', name: 'War Hat', level: 34 }
const uapBase: UpgradableItemBase = { ...mockBase, id: 'uap', name: 'Shako', level: 58 }
const lookup = (code: string) => (code === 'xap' ? xapBase : code === 'uap' ? uapBase : mockBase)
const rng = new D2Rng(4242)
// Nightmare Normal monster: qf4 = 10, qf5 = 0 (10 / 1024 ~= 0.009765625)
let upgrades = 0
const N = 50000
for (let i = 0; i < N; i++) {
const res = rollBaseUpgrade(mockBase, 10, 0, rng, lookup)
if (res.isUber) {
upgrades++
expect(res.base.id).toBe('xap')
}
}
const expectedRate = 10 / 1024
const actualRate = upgrades / N
// 3 sigma tolerance
const sigma = Math.sqrt((expectedRate * (1 - expectedRate)) / N)
expect(Math.abs(actualRate - expectedRate)).toBeLessThan(3.5 * sigma)
})
it('verifies statistical distribution of base upgrades on Hell difficulty (50,000 rolls)', () => {
const mockBase: UpgradableItemBase = {
id: 'cap',
name: 'Cap',
kind: 'armor',
invWidth: 2,
invHeight: 2,
maxStack: 1,
value: 12,
damage: 0,
defense: 5,
tags: ['helm'],
level: 1,
normcode: 'cap',
ubercode: 'xap',
ultracode: 'uap',
}
const xapBase: UpgradableItemBase = { ...mockBase, id: 'xap', name: 'War Hat', level: 34 }
const uapBase: UpgradableItemBase = { ...mockBase, id: 'uap', name: 'Shako', level: 58 }
const lookup = (code: string) => (code === 'xap' ? xapBase : code === 'uap' ? uapBase : mockBase)
const rng = new D2Rng(9999)
// Hell Good monster: qf4 = 40, qf5 = 40 (40/1024 ~= 0.0390625)
let eliteUpgrades = 0
let exceptionalUpgrades = 0
const N = 50000
for (let i = 0; i < N; i++) {
const res = rollBaseUpgrade(mockBase, 40, 40, rng, lookup)
if (res.isUber) {
if (res.base.id === 'uap') {
eliteUpgrades++
} else if (res.base.id === 'xap') {
exceptionalUpgrades++
}
}
}
// Expected elite rate = 40/1024
const pElite = 40 / 1024
const actualElite = eliteUpgrades / N
const sigmaElite = Math.sqrt((pElite * (1 - pElite)) / N)
expect(Math.abs(actualElite - pElite)).toBeLessThan(3.5 * sigmaElite)
expect(exceptionalUpgrades).toBeGreaterThan(0)
})
it('handles bases with xxx or missing upgrade codes without throwing', () => {
const unupgradableBase: UpgradableItemBase = {
id: 'amu',
name: 'Amulet',
kind: 'misc',
invWidth: 1,
invHeight: 1,
maxStack: 1,
value: 100,
damage: 0,
defense: 0,
tags: ['jewl'],
level: 1,
ubercode: 'xxx',
ultracode: 'xxx',
}
const rng = new D2Rng(100)
const res = rollBaseUpgrade(unupgradableBase, 1000, 1000, rng, () => undefined)
expect(res.isUber).toBe(false)
expect(res.base.id).toBe('amu')
})
})
})