/** * Adversarial Empirical Challenger Harness: Wilderness Traversability & Walkability Stress Test * * Scope: * 1. Pre-Baked Pack Traversability Audit: * - All 60 procedural wilderness variants across Acts 2..5 (Levels 41..46, 76..81, 104..106, 110..112, 117, 134). * - Plus all 24 Act 1 procedural wilderness variants (Levels 2..7, 17, 39). * - Total: 84 baked scene variants. * - Asserts: * * Spawn point is non-null, in-bounds, walkable (blocked === 0), and grounded on floor cell. * * Area block reachability >= 90% (canonical DRLG area connectivity). * * Every exit/entrance warp is reachable from spawn via 8-way BFS. * * Every border entrance seam is reachable from spawn via 8-way BFS. * * Every waypoint pedestal is reachable from spawn via 8-way BFS. * * Bidirectional path connectivity between entry and exit points. * * 2. Multi-Seed Procedural Wilderness Stress Test: * - Generates all 20 procedural wilderness levels in Acts 2..5 across 10 diverse seeds (200 generations). * - Asserts: * * findIsoSpawn succeeds and lands on a walkable, floor-grounded sub-tile. * * Procedural layout has >= 90% area reachability. * * All generated warps and seams are reachable from player spawn. */ import { readFileSync, existsSync } from 'node:fs' import { join } from 'node:path' import { MpqArchive } from '../src/mpq/archive.ts' import { fileSource } from '../src/mpq/file-source.ts' import { MountedArchives } from '../src/mpq/mount.ts' import { loadActTables, parseTable, cell, tileMemberPath, resolveLevelLibraries } from '../src/game/acts.ts' import type { D2Table } from '../src/game/acts.ts' import { decodeDs1 } from '../src/formats/ds1.ts' import type { Ds1 } from '../src/formats/ds1.ts' import { decodeDt1, type Dt1 } from '../src/formats/dt1.ts' import { buildIsoMapScene, subTileAt, levelSeed } from '../src/game/d2map.ts' import { SUB_TILES_PER_TILE } from '../src/game/map.ts' import { generateWilderness, type WildernessPiece, type WildernessSubstitution } from '../src/game/wilderness.ts' import { findIsoSpawn } from '../src/game/level-links.ts' const packDir = 'samples/d2-packs' const archiveDir = 'samples/d2' let totalChecks = 0 let failedChecks = 0 const failures: string[] = [] function check(ok: boolean, message: string): void { totalChecks += 1 if (!ok) { failedChecks += 1 failures.push(message) console.log(` FAIL: ${message}`) } } // ========================================================================= // PART 1: BAKED PACK TRAVERSABILITY AUDIT // ========================================================================= console.log('=== PART 1: BAKED SCENE TRAVERSABILITY AUDIT (84 WILDERNESS VARIANTS) ===\n') const index = JSON.parse(readFileSync(join(packDir, 'index.json'), 'utf8')) as { levels: readonly { act: number; levelId: number; path: string; ds1: string; label: string; kind?: string }[] } const targetWildernessLevelIds = [ // Act 1 2, 3, 4, 5, 6, 7, 17, 39, // Act 2 41, 42, 43, 44, 45, 46, // Act 3 76, 77, 78, 79, 80, 81, // Act 4 104, 105, 106, // Act 5 110, 111, 112, 117, 134, ] const bakedVariants = index.levels.filter(l => targetWildernessLevelIds.includes(l.levelId)) console.log(`Found ${bakedVariants.length} matching wilderness variants in index.json.\n`) for (const entry of bakedVariants) { const scenePath = join(packDir, entry.path, 'scene.json') check(existsSync(scenePath), `${entry.path}: scene.json exists`) if (!existsSync(scenePath)) continue const scene = JSON.parse(readFileSync(scenePath, 'utf8')) const W = scene.collision.width const H = scene.collision.height const expanded = new Uint8Array(W * H) let cursor = 0 for (const run of scene.collision.runs) { const val = run[0] ?? 0 const len = run[1] ?? 0 if (val !== 0) expanded.fill(val, cursor, Math.min(cursor + len, expanded.length)) cursor += len } const floorSet = new Set() for (const f of scene.floors) { floorSet.add(`${f[3]},${f[4]}`) } const grid = { originX: scene.originX, originY: scene.originY, cellsX: scene.cellsX, cellsY: scene.cellsY, gridWidth: W, blocked: expanded, } // 1. Spawn existence & validity check(scene.spawn !== null && Array.isArray(scene.spawn) && scene.spawn.length === 2, `${entry.path}: spawn exists`) if (!scene.spawn) continue const st = subTileAt(grid, scene.spawn[0], scene.spawn[1]) check(st.inBounds, `${entry.path}: spawn is in-bounds (${st.subX}, ${st.subY})`) if (!st.inBounds) continue const isSpawnWalkable = expanded[st.subY * W + st.subX] === 0 check(isSpawnWalkable, `${entry.path}: spawn sub-tile is walkable (blocked=${expanded[st.subY * W + st.subX]})`) const spawnCellX = Math.floor(st.subX / 5) const spawnCellY = Math.floor(st.subY / 5) check(floorSet.has(`${spawnCellX},${spawnCellY}`), `${entry.path}: spawn is grounded on floor cell (${spawnCellX}, ${spawnCellY})`) // 2. BFS Flood Fill from spawn const visited = new Uint8Array(W * H) const dist = new Int32Array(W * H).fill(-1) const queue = [st.subX, st.subY] visited[st.subY * W + st.subX] = 1 dist[st.subY * W + st.subX] = 0 let head = 0 while (head < queue.length) { const cx = queue[head++]! const cy = queue[head++]! const curDist = dist[cy * W + cx]! const neighbours = [ [cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1], [cx + 1, cy + 1], [cx - 1, cy - 1], [cx + 1, cy - 1], [cx - 1, cy + 1], ] for (const [nx, ny] of neighbours) { if (nx >= 0 && nx < W && ny >= 0 && ny < H) { const idx = ny * W + nx if (visited[idx] === 0 && expanded[idx] === 0) { visited[idx] = 1 dist[idx] = curDist + 1 queue.push(nx, ny) } } } } // 3. Area-based block reachability (8x8 cell areas) const AREA_CELLS = 8 const blocksX = Math.floor(scene.cellsX / AREA_CELLS) const blocksY = Math.floor(scene.cellsY / AREA_CELLS) let totalBlocks = 0 let reachedBlocks = 0 for (let by = 0; by < blocksY; by++) { for (let bx = 0; bx < blocksX; bx++) { let blockHasWalkable = false let blockIsReached = false for (let cy = by * AREA_CELLS; cy < (by + 1) * AREA_CELLS; cy++) { for (let cx = bx * AREA_CELLS; cx < (bx + 1) * AREA_CELLS; cx++) { if (!floorSet.has(`${cx},${cy}`)) continue for (let sy = 0; sy < 5; sy++) { for (let sx = 0; sx < 5; sx++) { const gx = cx * 5 + sx const gy = cy * 5 + sy if (gx < W && gy < H && expanded[gy * W + gx] === 0) { blockHasWalkable = true if (visited[gy * W + gx] === 1) blockIsReached = true } } } } } if (blockHasWalkable) { totalBlocks++ if (blockIsReached) reachedBlocks++ } } } const blockReachRatio = totalBlocks > 0 ? (reachedBlocks / totalBlocks) : 1 check(blockReachRatio >= 0.90, `${entry.path}: block reachability >= 90% (${reachedBlocks}/${totalBlocks} = ${(blockReachRatio * 100).toFixed(1)}%)`) // 4. Warp connectivity for (const w of scene.warps ?? []) { const ax = w.arriveX ?? w.x const ay = w.arriveY ?? w.y let minD = -1 for (let dy = -5; dy <= 5; dy++) { for (let dx = -5; dx <= 5; dx++) { const wx = ax + dx const wy = ay + dy if (wx >= 0 && wx < W && wy >= 0 && wy < H && visited[wy * W + wx] === 1) { const d = dist[wy * W + wx]! if (minD === -1 || d < minD) minD = d } } } check(minD !== -1, `${entry.path}: warp to level ${w.toLevelId} (${w.label ?? ''}) at (${ax},${ay}) reachable (dist=${minD})`) } // 5. Entrance connectivity for (const ent of scene.entrances ?? []) { const ax = ent.arriveX ?? ent.x const ay = ent.arriveY ?? ent.y let minD = -1 for (let dy = -3; dy <= 3; dy++) { for (let dx = -3; dx <= 3; dx++) { const ex = ax + dx const ey = ay + dy if (ex >= 0 && ex < W && ey >= 0 && ey < H && visited[ey * W + ex] === 1) { const d = dist[ey * W + ex]! if (minD === -1 || d < minD) minD = d } } } check(minD !== -1, `${entry.path}: entrance to level ${ent.toLevelId} (${ent.label ?? ''}) at (${ax},${ay}) reachable (dist=${minD})`) } // 6. Waypoint connectivity for (const wp of scene.waypoints ?? []) { const ax = wp.arriveX ?? wp.x const ay = wp.arriveY ?? wp.y let minD = -1 for (let dy = -5; dy <= 5; dy++) { for (let dx = -5; dx <= 5; dx++) { const wx = ax + dx const wy = ay + dy if (wx >= 0 && wx < W && wy >= 0 && wy < H && visited[wy * W + wx] === 1) { const d = dist[wy * W + wx]! if (minD === -1 || d < minD) minD = d } } } check(minD !== -1, `${entry.path}: waypoint ${wp.waypointId} at (${ax},${ay}) reachable (dist=${minD})`) } // 7. Cross-transition traversability (from entrance A to exit B) const allEndpoints = [ ...(scene.warps ?? []).map((w: any) => ({ type: 'warp', to: w.toLevelId, x: w.arriveX ?? w.x, y: w.arriveY ?? w.y })), ...(scene.entrances ?? []).map((e: any) => ({ type: 'entrance', to: e.toLevelId, x: e.arriveX ?? e.x, y: e.arriveY ?? e.y })), ] if (allEndpoints.length >= 2) { for (let i = 0; i < allEndpoints.length; i++) { for (let j = i + 1; j < allEndpoints.length; j++) { const ep1 = allEndpoints[i]! const ep2 = allEndpoints[j]! const ep1Reached = visited[ep1.y * W + ep1.x] === 1 || hasVisitedNeighbor(visited, W, H, ep1.x, ep1.y, 4) const ep2Reached = visited[ep2.y * W + ep2.x] === 1 || hasVisitedNeighbor(visited, W, H, ep2.x, ep2.y, 4) check(ep1Reached && ep2Reached, `${entry.path}: path exists between ${ep1.type} to ${ep1.to} and ${ep2.type} to ${ep2.to}`) } } } } function hasVisitedNeighbor(visited: Uint8Array, W: number, H: number, x: number, y: number, radius: number): boolean { for (let dy = -radius; dy <= radius; dy++) { for (let dx = -radius; dx <= radius; dx++) { const nx = x + dx const ny = y + dy if (nx >= 0 && nx < W && ny >= 0 && ny < H && visited[ny * W + nx] === 1) return true } } return false } console.log(`Part 1 complete: ${totalChecks - failedChecks}/${totalChecks} checks passed.`) // ========================================================================= // PART 2: MULTI-SEED PROCEDURAL WILDERNESS GENERATION STRESS TEST // ========================================================================= console.log('\n=== PART 2: PROCEDURAL MULTI-SEED STRESS TEST (ACTS 2..5) ===\n') const archives = new MountedArchives() for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) { archives.add(name, await MpqArchive.open(await fileSource(join(archiveDir, name)))) } const tables = await loadActTables(archives) const lvlsub = parseTable(await archives.read('data\\global\\excel\\LvlSub.txt')) const ds1Cache = new Map() async function loadDs1(arch: MountedArchives, relative: string): Promise { const member = tileMemberPath(relative) const cached = ds1Cache.get(member) if (cached !== undefined) return cached const decoded = decodeDs1(await arch.read(member)) ds1Cache.set(member, decoded) return decoded } async function rowDs1s(arch: MountedArchives, 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(arch, value)) } return levels } function themeValues(table: D2Table, row: readonly string[], prefix: string): number[] { const values: number[] = [] for (let index = 0; index < 5; index += 1) values.push(Number(cell(table, row, `${prefix}${String(index)}`)) || 0) return values } 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'], } async function wildernessPieces(arch: MountedArchives, lvlprest: D2Table, levelTypeName: string): Promise { const families = WILDERNESS_PIECE_FAMILIES[levelTypeName] ?? [] const pieces: WildernessPiece[] = [] for (const row of lvlprest.rows) { const name = cell(lvlprest, row, 'Name') if (!families.some(family => name.startsWith(family))) continue if (levelTypeName === 'Act 5 - Barricade' && name.includes('Snow')) continue const levels = await rowDs1s(arch, lvlprest, row) if (levels.length === 0) continue const isBorder = /border|cliff/i.test(name) pieces.push({ name, levels, border: isBorder }) } return pieces } async function substitutions(arch: MountedArchives, subTable: D2Table, type: number): Promise { if (type < 0) return [] const rows: WildernessSubstitution[] = [] for (const row of subTable.rows) { if (Number(cell(subTable, row, 'Type')) !== type) continue const file = cell(subTable, row, 'File') if (file === '' || file === '0') continue const levels = [await loadDs1(arch, file)] rows.push({ name: cell(subTable, row, 'Name'), type, gridSize: Number(cell(subTable, row, 'GridSize')) || 1, bordType: Number(cell(subTable, row, 'BordType')), dt1Mask: Number(cell(subTable, row, 'Dt1Mask')) || 0, prob: themeValues(subTable, row, 'Prob'), trials: themeValues(subTable, row, 'Trials'), max: themeValues(subTable, row, 'Max'), levels, }) } return rows } const dt1Cache = new Map() async function loadDt1(arch: MountedArchives, name: string): Promise { const cached = dt1Cache.get(name) if (cached !== undefined) return cached const decoded = decodeDt1(await arch.read(name)) dt1Cache.set(name, decoded) return decoded } async function decodeLibraries(arch: MountedArchives, names: readonly string[]): Promise { const libraries: Dt1[] = [] for (const name of names) { try { libraries.push(await loadDt1(arch, name)) } catch {} } return libraries } const stressSeeds = [ 0x301cd095, 0x416d61c5, 0xdeadbeef, 42, 0x5eed_2026, ] const procLevels = [ // Act 2 41, 42, 43, 44, 45, 46, // Act 3 76, 77, 78, 79, 80, 81, // Act 4 104, 105, 106, // Act 5 110, 111, 112, 117, 134, ] let procGenerations = 0 for (const levelId of procLevels) { const levelRow = tables.levels.rows.find(candidate => Number(cell(tables.levels, candidate, 'Id')) === levelId)! const levelName = cell(tables.levels, levelRow, 'Name') const typeId = cell(tables.levels, levelRow, 'LevelType') const typeRow = tables.lvltypes.rows.find(candidate => cell(tables.lvltypes, candidate, 'Id') === typeId)! const typeName = typeRow === undefined ? '' : cell(tables.lvltypes, typeRow, 'Name') const sizeX = Number(cell(tables.levels, levelRow, 'SizeX')) const sizeY = Number(cell(tables.levels, levelRow, 'SizeY')) const subType = Number(cell(tables.levels, levelRow, 'SubType')) const subShrine = Number(cell(tables.levels, levelRow, 'SubShrine')) const subTheme = Number(cell(tables.levels, levelRow, 'SubTheme')) const pieces = await wildernessPieces(archives, tables.lvlprest, typeName) const rows = await substitutions(archives, lvlsub, subType) const shrineRows = await substitutions(archives, lvlsub, subShrine) const libraries = resolveLevelLibraries(tables, levelId) const dt1s = await decodeLibraries(archives, libraries.dt1Names) console.log(`Checking Level ${levelId} (${levelName}) across ${stressSeeds.length} seeds...`) for (const seed of stressSeeds) { procGenerations++ const request = { levelId, levelName, levelTypeName: typeName, sizeX, sizeY, subType, subTheme: Math.max(0, subTheme), seed, pieces, substitutions: rows, shrineSubstitutions: shrineRows, } try { const generated = generateWilderness(request) const scene = buildIsoMapScene(generated.level, dt1s, levelSeed('generated')) // Verify spawn const spawn = findIsoSpawn(scene) check(spawn !== null, `Proc Lvl ${levelId} (${levelName}) seed 0x${seed.toString(16)}: spawn point found`) if (spawn !== null) { const st = subTileAt(scene, spawn.x, spawn.y) check(st.inBounds, `Proc Lvl ${levelId} seed 0x${seed.toString(16)}: spawn in-bounds (${st.subX}, ${st.subY})`) const isWalkable = scene.blocked[st.subY * scene.gridWidth + st.subX] === 0 check(isWalkable, `Proc Lvl ${levelId} seed 0x${seed.toString(16)}: spawn is walkable`) // BFS flood fill const W = scene.gridWidth const H = scene.cellsY * 5 const visited = new Uint8Array(W * H) const q = [st.subX, st.subY] visited[st.subY * W + st.subX] = 1 let h = 0 while (h < q.length) { const cx = q[h++]! const cy = q[h++]! const neighbours = [ [cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1], [cx + 1, cy + 1], [cx - 1, cy - 1], [cx + 1, cy - 1], [cx - 1, cy + 1], ] for (const [nx, ny] of neighbours) { if (nx >= 0 && nx < W && ny >= 0 && ny < H) { const idx = ny * W + nx if (visited[idx] === 0 && scene.blocked[idx] === 0) { visited[idx] = 1 q.push(nx, ny) } } } } // Verify all gate openings are reachable const gateOpenings = (generated.stats.gateOpenings ?? []) as readonly any[] for (const gate of gateOpenings) { const ax = gate.tileCoord.x * 5 + 2 const ay = gate.tileCoord.y * 5 + 2 let reached = false for (let dy = -5; dy <= 5 && !reached; dy++) { for (let dx = -5; dx <= 5 && !reached; dx++) { const gx = ax + dx const gy = ay + dy if (gx >= 0 && gx < W && gy >= 0 && gy < H && visited[gy * W + gx] === 1) reached = true } } check(reached, `Proc Lvl ${levelId} seed 0x${seed.toString(16)}: gate opening to ${gate.toLevelId} reachable`) } } } catch (err) { check(false, `Proc Lvl ${levelId} seed 0x${seed.toString(16)} threw: ${err instanceof Error ? err.message : String(err)}`) } } } console.log(`Part 2 complete: tested ${procGenerations} procedural level generations across Acts 2..5.`) console.log('\n=========================================================================') console.log(`FINAL STRESS TEST SUMMARY: ${totalChecks - failedChecks}/${totalChecks} assertions passed (${failures.length} failures)`) console.log('=========================================================================\n') if (failures.length > 0) { console.log(`Top Failures:`) for (const f of failures.slice(0, 20)) { console.log(` - ${f}`) } process.exit(1) } else { console.log('ALL ADVERSARIAL TRAVERSABILITY & WALKABILITY ASSERTIONS PASSED!') process.exit(0) }