/** * Standalone Adversarial Stress-Test Script for Milestone M1 * Diablo II Web DRLG Engine: Seed Sensitivity & Determinism Profiler * * Requirements: * 1. Stress-test seed sensitivity across 50 distinct random seeds for all legacy-generator outdoor levels (Acts 2..5). * 2. Assert determinism: re-running identical seeds yields 100% identical digests. * 3. Assert zero collision across different seeds for procedural outdoor levels. * * Act I's outdoor levels are generated by the DRLG port (src/game/drlg), not by generateWilderness; * tests/drlg-act1-oracle.test.ts and tests/drlg-act1-invariants.test.ts cover them. * * Archetype: EMPIRICAL CHALLENGER */ import { existsSync } from 'node: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, parseTable, cell, tileMemberPath } from '../src/game/acts.ts' import type { D2Table } from '../src/game/acts.ts' import { decodeDs1, type Ds1 } from '../src/formats/ds1.ts' import { generateWilderness, type WildernessPiece, type WildernessSubstitution, } from '../src/game/wilderness.ts' /* ------------------------------------------------------------------------- * * FNV-1a Canonical Map Hasher * ------------------------------------------------------------------------- */ class Hasher { private hash = 2166136261 push(value: number): this { const word = Math.trunc(value) | 0 for (let byte = 0; byte < 4; byte += 1) { this.hash ^= (word >>> (byte * 8)) & 0xff this.hash = Math.imul(this.hash, 16777619) >>> 0 } return this } get digest(): string { return (this.hash >>> 0).toString(16).padStart(8, '0') } } function canonicalHash(level: Ds1): string { const hasher = new Hasher() hasher.push(level.version).push(level.width).push(level.height).push(level.act) hasher.push(level.substitutionType).push(level.wallLayers).push(level.floorLayers) for (let y = 0; y < level.height; y += 1) { const row = level.cells[y] if (row === undefined) continue for (let x = 0; x < level.width; x += 1) { const cellRecord = row[x] if (cellRecord === undefined) continue for (const wall of cellRecord.walls) { hasher.push(wall.prop1).push(wall.sequence).push(wall.style).push(wall.type).push(wall.hidden ? 1 : 0) } for (const floor of cellRecord.floors) { hasher.push(floor.prop1).push(floor.sequence).push(floor.style).push(floor.hidden ? 1 : 0) } for (const shadow of cellRecord.shadows) hasher.push(shadow.prop1).push(shadow.sequence).push(shadow.style) for (const substitution of cellRecord.substitutions) hasher.push(substitution.value) } } for (const object of level.objects) { hasher.push(object.type).push(object.id).push(object.x).push(object.y).push(object.flags) } return hasher.digest } /* ------------------------------------------------------------------------- * * Canonical Master Seeds * ------------------------------------------------------------------------- */ const MASTER_SEEDS = [0x301cd095, 0x416d61c5, 0xdeadbeef] as const /* ------------------------------------------------------------------------- * * 50 Distinct Adversarial & Random Seeds * ------------------------------------------------------------------------- */ // Construct exactly 50 distinct seeds: // - 3 canonical master seeds // - 7 boundary & edge cases (0, 1, 0x7fffffff, 0xffffffff, 42, 1337, 0x5eed1000) // - 40 pseudo-random high-entropy seeds generated deterministically function generate50Seeds(): number[] { const seedSet = new Set([ ...MASTER_SEEDS, 0, 1, 0x7fffffff, 0xffffffff, 42, 1337, 0x5eed1000, ]) // Deterministic LCG to populate the remainder up to 50 distinct seeds let state = 0x12345678 while (seedSet.size < 50) { state = Math.imul(state, 1664525) + 1013904223 seedSet.add(state >>> 0) } return Array.from(seedSet) } /* ------------------------------------------------------------------------- * * MPQ Data Cache & Level Resolution * ------------------------------------------------------------------------- */ const WILDERNESS_PIECE_FAMILIES: Readonly> = { 'Act 2 - Desert': ['Act 2 - Desert'], 'Act 3 - Jungle': ['Act 3 - Jungle'], 'Act 3 - Kurast': ['Act 3 - Burst', 'Act 3 - Burbs', 'Act 3 - Clearing', 'Act 3 - Slums', 'Act 3 - Metro', 'Act 3 - Travincal', 'Act 3 - Bridge'], 'Act 4 - Mesa': ['Act 4 - Mesa', 'Act 4 - Fortress', 'Act 4 - Pits', 'Act 4 - Bridge'], 'Act 4 - Lava': ['Act 4 - Lava', 'Act 4 - Diablo'], 'Act 5 - Siege': ['Act 5 - Siege'], 'Act 5 - Barricade': ['Act 5 - Barricade'], } // Preset wilderness levels that have fixed layouts by definition const PRESET_WILDERNESS_LEVELS = new Set([82, 83, 108]) async function runStressTest(): Promise { const startTime = Date.now() console.log('======================================================================') console.log('šŸ”„ EMPIRICAL STRESS-TEST: Milestone M1 DRLG Wilderness Generation') console.log('======================================================================\n') const samplePath = 'samples/d2/d2data.mpq' if (!existsSync(samplePath)) { console.error(`āŒ MPQ archive missing at ${samplePath}`) process.exit(1) } const archives = new MountedArchives() for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) { try { archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`))) } catch { // Ignore missing optional archive } } const tables = await loadActTables(archives) const lvlsub = parseTable(await archives.read('data\\global\\excel\\LvlSub.txt')) const ds1Cache = new Map() async function loadDs1(relative: string): Promise { const member = tileMemberPath(relative) const cached = ds1Cache.get(member) if (cached !== undefined) return cached const decoded = decodeDs1(await archives.read(member)) ds1Cache.set(member, decoded) return decoded } async function rowDs1s(table: D2Table, row: readonly string[]): Promise { const levels: Ds1[] = [] for (let slot = 1; slot <= 6; slot += 1) { const value = cell(table, row, `File${String(slot)}`) if (value === '' || value === '0') continue levels.push(await loadDs1(value)) } return levels } const piecesCache = new Map() async function getWildernessPieces(levelTypeName: string): Promise { const cached = piecesCache.get(levelTypeName) if (cached !== undefined) return cached const families = WILDERNESS_PIECE_FAMILIES[levelTypeName] ?? [] const pieces: WildernessPiece[] = [] for (const row of tables.lvlprest.rows) { const name = cell(tables.lvlprest, row, 'Name') if (!families.some(family => name.startsWith(family))) continue if (levelTypeName === 'Act 5 - Barricade' && name.includes('Snow')) continue const levels = await rowDs1s(tables.lvlprest, row) if (levels.length === 0) continue const isBorder = /border|cliff/i.test(name) pieces.push({ name, levels, border: isBorder }) } piecesCache.set(levelTypeName, pieces) return pieces } const subsCache = new Map() async function getSubstitutions(type: number): Promise { if (type < 0) return [] const cached = subsCache.get(type) if (cached !== undefined) return cached const rows: WildernessSubstitution[] = [] for (const row of lvlsub.rows) { if (Number(cell(lvlsub, row, 'Type')) !== type) continue const file = cell(lvlsub, row, 'File') if (file === '' || file === '0') continue const levels = [await loadDs1(file)] rows.push({ name: cell(lvlsub, row, 'Name'), type, gridSize: Number(cell(lvlsub, row, 'GridSize')) || 1, bordType: Number(cell(lvlsub, row, 'BordType')), dt1Mask: Number(cell(lvlsub, row, 'Dt1Mask')) || 0, prob: [0, 1, 2, 3, 4].map(i => Number(cell(lvlsub, row, `Prob${String(i)}`)) || 0), trials: [0, 1, 2, 3, 4].map(i => Number(cell(lvlsub, row, `Trials${String(i)}`)) || 0), max: [0, 1, 2, 3, 4].map(i => Number(cell(lvlsub, row, `Max${String(i)}`)) || 0), levels, }) } subsCache.set(type, rows) return rows } // Identify all wilderness levels (DrlgType 3) interface WildernessLevelMeta { id: number name: string act: number levelTypeName: string sizeX: number sizeY: number subType: number subShrine: number subTheme: number isProcedural: boolean } const wildernessLevels: WildernessLevelMeta[] = [] const drlgPortLevels: number[] = [] for (const row of tables.levels.rows) { if (cell(tables.levels, row, 'DrlgType') !== '3') continue const id = Number(cell(tables.levels, row, 'Id')) const name = cell(tables.levels, row, 'Name') const act = Number(cell(tables.levels, row, 'Act')) + 1 // Act I outdoor levels come from the DRLG port (src/game/drlg); generateWilderness throws for them. if (act === 1) { drlgPortLevels.push(id) continue } const typeId = cell(tables.levels, row, 'LevelType') const typeRow = tables.lvltypes.rows.find(r => cell(tables.lvltypes, r, 'Id') === typeId) const levelTypeName = typeRow ? cell(tables.lvltypes, typeRow, 'Name') : '' const sizeX = Number(cell(tables.levels, row, 'SizeX')) const sizeY = Number(cell(tables.levels, row, 'SizeY')) const subType = Number(cell(tables.levels, row, 'SubType')) const subShrine = Number(cell(tables.levels, row, 'SubShrine')) const subTheme = Number(cell(tables.levels, row, 'SubTheme')) const isProcedural = !PRESET_WILDERNESS_LEVELS.has(id) wildernessLevels.push({ id, name, act, levelTypeName, sizeX, sizeY, subType, subShrine, subTheme, isProcedural, }) } console.log(`Discovered ${wildernessLevels.length} legacy-generator outdoor wilderness levels across Acts 2..5:`) console.log(`- Procedural outdoor levels: ${wildernessLevels.filter(l => l.isProcedural).length}`) console.log(`- Preset outdoor levels: ${wildernessLevels.filter(l => !l.isProcedural).length}`) console.log(`(Act I outdoor levels ${drlgPortLevels.join(', ')} are the DRLG port's; see tests/drlg-act1-*.test.ts)\n`) const seeds50 = generate50Seeds() console.log(`Generated 50 distinct test seeds (includes 3 master seeds, 7 edge/boundary seeds, 40 random seeds).`) console.log(`Seed sample: [${seeds50.slice(0, 6).map(s => '0x' + s.toString(16)).join(', ')} ... (${seeds50.length} total)]\n`) let totalAssertions = 0 let passedAssertions = 0 let failedAssertions = 0 const failureList: string[] = [] function check(condition: boolean, description: string): void { totalAssertions += 1 if (condition) { passedAssertions += 1 } else { failedAssertions += 1 failureList.push(description) console.error(`āŒ FAILED: ${description}`) } } // --------------------------------------------------------------------------- // TEST 1 & 2: Seed Sensitivity & Determinism across 50 seeds for every legacy-generator level // --------------------------------------------------------------------------- console.log('--- [Test 1 & 2] 50-Seed Sensitivity & Deterministic Re-run Invariance ---') console.log(`Testing all ${wildernessLevels.length} wilderness levels across 50 distinct seeds...\n`) for (const level of wildernessLevels) { const pieces = await getWildernessPieces(level.levelTypeName) const subs = await getSubstitutions(level.subType) const shrineSubs = await getSubstitutions(level.subShrine) const seedToDigest = new Map() const digests: string[] = [] for (const seed of seeds50) { const req = { levelId: level.id, levelName: level.name, levelTypeName: level.levelTypeName, sizeX: level.sizeX, sizeY: level.sizeY, subType: level.subType, subTheme: Math.max(0, level.subTheme), seed, pieces, substitutions: subs, shrineSubstitutions: shrineSubs, } // Run 1 const run1 = generateWilderness(req) const digest1 = canonicalHash(run1.level) // Run 2 (Determinism Verification) const run2 = generateWilderness(req) const digest2 = canonicalHash(run2.level) check( digest1 === digest2, `Level ${level.id} (${level.name}) seed 0x${seed.toString(16)} non-deterministic: ${digest1} !== ${digest2}` ) seedToDigest.set(seed, digest1) digests.push(digest1) } const uniqueDigests = new Set(digests) if (level.isProcedural) { // Assertion 3: Zero collision across different seeds for procedural outdoor levels const collisionCount = digests.length - uniqueDigests.size check( collisionCount === 0, `Level ${level.id} (${level.name}) had ${collisionCount} hash collisions across 50 distinct seeds (${uniqueDigests.size}/50 unique)` ) console.log( ` Act ${level.act} Level ${String(level.id).padStart(3)} | ${level.name.padEnd(25)} | ` + `50/50 unique (${uniqueDigests.size} distinct hashes) | 100% Deterministic (50/50 pairs matched)` ) } else { // Preset level: determinism holds, hashes are consistent console.log( ` Act ${level.act} Level ${String(level.id).padStart(3)} | ${level.name.padEnd(25)} | ` + `[PRESET] ${uniqueDigests.size} variant(s) | 100% Deterministic (50/50 pairs matched)` ) } } const elapsedSec = ((Date.now() - startTime) / 1000).toFixed(2) console.log('\n======================================================================') console.log(`šŸ“Š STRESS TEST SUMMARY (Completed in ${elapsedSec}s)`) console.log('======================================================================') console.log(`Total Assertions Executed: ${totalAssertions}`) console.log(`Passed Assertions: ${passedAssertions}`) console.log(`Failed Assertions: ${failedAssertions}`) if (failedAssertions > 0) { console.error('\nāŒ FAILURES DETECTED:') for (const f of failureList) { console.error(` - ${f}`) } process.exit(1) } const proceduralCount = wildernessLevels.filter(l => l.isProcedural).length console.log('\nāœ… ALL 3 EMPIRICAL INVARIANTS SATISFIED WITHOUT EXCEPTION:') console.log(`1. Seed sensitivity: ${seeds50.length} distinct random seeds tested across all ${wildernessLevels.length} legacy-generator outdoor levels.`) console.log(`2. Determinism: ${wildernessLevels.length * seeds50.length} duplicate generation pairs produced 100% identical FNV-1a digests.`) console.log(`3. Zero collision: ${proceduralCount}/${proceduralCount} procedural levels produced ${seeds50.length}/${seeds50.length} unique digests (0 collisions).`) console.log('======================================================================\n') } runStressTest().catch(err => { console.error('Fatal error running stress test:', err) process.exit(1) })