diablo2-web/tests/adversarial-drop-parity-aud...

206 lines
7.1 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest'
import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts'
import {
getMonsterTreasureClass,
} from '../src/game/monsters.ts'
import {
executeDropPipeline,
loadDropTables,
} from '../src/game/drop-pipeline.ts'
import * as dropPipeline from '../src/game/drop-pipeline.ts'
import * as treasureClass from '../src/game/treasure-class.ts'
import { GameEngine } from '../src/game/engine.ts'
import { auditDropParity, openDropDataArchives } from '../scripts/pack-canonical-drop-data.ts'
import * as fs from 'node:fs'
const hasD2 = fs.existsSync('samples/d2/d2data.mpq')
describe('Adversarial Reviewer Verification - Drop Parity & SuperUnique Contracts', () => {
const dropTables = getEmbeddedDropTables()
const dummyTerrain = {
widthPx: 1000,
heightPx: 1000,
overlap: () => 0,
}
function createTestEngine(difficulty: 'normal' | 'nightmare' | 'hell' = 'normal') {
return new GameEngine(dummyTerrain, {
spawn: { x: 0, y: 0 },
stats: [],
xpTable: [0, 100, 200],
dropTables,
difficulty,
skills: [],
npcDefs: [],
questDefs: [],
combatOptions: {} as any,
talkRadius: 50,
pickupRadius: 50,
inventoryCols: 10,
inventoryRows: 4,
})
}
function lastDropArgs(spy: any) {
expect(spy).toHaveBeenCalled()
const calls = spy.mock.calls
return calls[calls.length - 1][1]
}
it('Requirement R1: dropTables.monsterKinds has exactly 734 kinds conforming to 1.13c', () => {
expect(dropTables.monsterKinds.size).toBe(734)
// Key iconic 1.13c entities
expect(dropTables.monsterKinds.has('fallen1')).toBe(true)
expect(dropTables.monsterKinds.has('andariel')).toBe(true)
expect(dropTables.monsterKinds.has('duriel')).toBe(true)
expect(dropTables.monsterKinds.has('mephisto')).toBe(true)
expect(dropTables.monsterKinds.has('diablo')).toBe(true)
expect(dropTables.monsterKinds.has('baalcrab')).toBe(true)
expect(dropTables.monsterKinds.has('Expansion')).toBe(true)
// Verify all 734 monster kinds define valid TCs that exist in tcTable
for (const [id, kind] of dropTables.monsterKinds) {
expect(kind.id).toBe(id)
for (const diff of ['normal', 'nightmare', 'hell'] as const) {
for (const type of [1, 2, 3, 4] as const) {
const tc = getMonsterTreasureClass(kind, diff, type)
if (tc) {
expect(
Boolean(dropTables.tcTable.get(tc)),
`TC "${tc}" from monster "${id}" (${diff}, type ${type}) must exist in tcTable`,
).toBe(true)
}
}
}
}
})
it('Requirement R1: MonStats noRatio contract correctly hydrates across summons and suppresses TC upgrade', () => {
const noRatioMonsters = [
'claygolem',
'bloodgolem',
'irongolem',
'firegolem',
'valkyrie',
'necroskeleton',
'necromage',
'wakeofdestruction',
]
for (const id of noRatioMonsters) {
const kind = dropTables.monsterKinds.get(id)
expect(kind, `Monster ${id} should exist`).toBeDefined()
expect(kind!.noRatio, `Monster ${id} should have noRatio: true`).toBe(true)
}
// Standard monsters must NOT have noRatio
expect(dropTables.monsterKinds.get('fallen1')!.noRatio).toBeUndefined()
expect(dropTables.monsterKinds.get('andariel')!.noRatio).toBeUndefined()
// Test that isNoRatio strictly gates TC upgrade in executeDropPipeline
const upgradeSpy = vi.spyOn(treasureClass, 'resolveTreasureClassGroup')
dropPipeline.executeDropPipeline({
tcName: 'Act 1 H2H A',
nLevel: 90,
difficulty: 'hell',
isNoRatio: true,
dropTables,
})
expect(upgradeSpy).not.toHaveBeenCalled()
upgradeSpy.mockRestore()
})
it('Requirement R2: dropTables.superUniques has exactly 66 SuperUniques with authentic 3-difficulty TCs', () => {
expect(dropTables.superUniques.size).toBe(66)
expect(dropTables.superUniques.has('Expansion')).toBe(false)
for (const [id, su] of dropTables.superUniques) {
expect(su.id).toBe(id)
expect(su.monsterId, `SuperUnique ${id} must specify a monsterId`).toBeTruthy()
expect(
dropTables.monsterKinds.has(su.monsterId),
`SuperUnique ${id} monsterId "${su.monsterId}" must exist in monsterKinds`,
).toBe(true)
for (const diff of ['normal', 'nightmare', 'hell'] as const) {
const tc = typeof su.getTreasureClass === 'function' ? su.getTreasureClass(diff) : su.treasureClass
if (id.startsWith('Ancient Barbarian')) {
expect(tc, `Ancient Barbarian ${id} on ${diff} must have empty TC`).toBe('')
} else {
expect(tc, `SuperUnique ${id} on ${diff} must have non-empty TC`).toBeTruthy()
expect(
Boolean(dropTables.tcTable.get(tc)),
`SuperUnique ${id} TC "${tc}" on ${diff} must exist in tcTable`,
).toBe(true)
}
}
}
})
it('Requirement R2: SuperUnique minion inheritance and case-insensitive resolution under extreme inputs', () => {
const engine = createTestEngine('hell')
const dropSpy = vi.spyOn(dropPipeline, 'executeDropPipeline')
// Adversarial whitespace, mixed casing, and minion rank
engine.world.pendingKills = [
{
kind: 'kill',
x: 100,
y: 100,
subjectId: '', // missing subjectId
monsterRank: 'minion',
superUniqueId: '\r\n\t BiShIbOsH \t ',
monsterLevel: 85,
monsterType: 1, // Adversarial: tries to downgrade minion to normal type 1
} as any,
]
engine.tick({
movement: { x: 0, y: 0 },
attacking: false,
pickingUp: false,
talking: false,
saving: false,
loading: false,
digits: [],
})
expect(engine.metrics.dropsRolled).toBe(1)
const minionArgs = lastDropArgs(dropSpy)
// Bishibosh host monster is fallenshaman1. On Hell, fallenshaman1 TreasureClass3 is 'Act 1 (H) Unique A'
expect(minionArgs.tcName).toBe('Act 1 (H) Unique A')
expect(minionArgs.monsterType).toBe(3) // Minion strictly resolves to 3
expect(minionArgs.isBoss).toBe(false)
expect(minionArgs.difficulty).toBe('hell')
dropSpy.mockRestore()
})
it('Requirement R2: loadDropTables throws explicit Error when SuperUniques.txt fails to load', async () => {
if (!hasD2) return
const archives = await openDropDataArchives('samples/d2')
const realRead = archives.read.bind(archives)
archives.read = async (path: string) => {
if (path.includes('SuperUniques.txt')) {
throw new Error('Simulated I/O disk failure reading SuperUniques.txt')
}
return realRead(path)
}
await expect(loadDropTables(archives)).rejects.toThrow(
/Failed to load SuperUniques\.txt: Simulated I\/O disk failure/,
)
})
it('Requirement R1: pack-canonical-drop-data audit achieves 100% 1.13c parity', async () => {
if (!hasD2) return
const audit = await auditDropParity()
expect(audit.monstatsMatch).toBe(true)
expect(audit.superUniquesMatch).toBe(true)
expect(audit.monsterKindsCount).toBe(734)
expect(audit.superUniquesCount).toBe(66)
})
})