668 lines
27 KiB
TypeScript
668 lines
27 KiB
TypeScript
/**
|
|
* Standalone Adversarial Stress-Test Script for Milestone M2
|
|
* Diablo II Web DRLG Engine: Wilderness Connectivity & Transverse Barricades
|
|
*
|
|
* Requirements:
|
|
* 1. Stress-test Act 4 Lava Fissure flood-fill reachability across 50+ distinct seeds:
|
|
* - Perform full BFS flood-fill reachability from entrance across the 3 strategic rock bridges.
|
|
* - Assert walkable floor reachability >= 90% holds 100% of the time across all seeds.
|
|
* 2. Stress-test Act 5 Transverse Barricades across 50+ distinct seeds:
|
|
* - Assert barricades span horizontally across X in [8..W-8] when height > width with military gate at hubX.
|
|
* - Assert barricades span vertically across Y in [8..H-8] when width >= height with military gate at hubY.
|
|
* - Assert gates are open (no wall collision mask) and walkable.
|
|
* - Assert full end-to-end corridor traversability through the barricade gates.
|
|
*
|
|
* Act I's outdoor levels are generated by the DRLG port (src/game/drlg), not by generateWilderness;
|
|
* tests/drlg-act1-oracle.test.ts (bit-exact against native D2MOO) 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, cell, tileMemberPath } from '../src/game/acts.ts'
|
|
import type { D2Table } from '../src/game/acts.ts'
|
|
import { decodeDs1, type Ds1, type Ds1Cell } from '../src/formats/ds1.ts'
|
|
import {
|
|
generateWilderness,
|
|
type WildernessPiece,
|
|
} from '../src/game/wilderness.ts'
|
|
|
|
/* ------------------------------------------------------------------------- *
|
|
* Canonical Master Seeds
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
const MASTER_SEEDS = [0x301cd095, 0x416d61c5, 0xdeadbeef] as const
|
|
|
|
/* ------------------------------------------------------------------------- *
|
|
* Seed Generation (60 distinct seeds)
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
function generate60Seeds(): number[] {
|
|
const seedSet = new Set<number>([
|
|
...MASTER_SEEDS,
|
|
0,
|
|
1,
|
|
42,
|
|
1337,
|
|
0x7fffffff,
|
|
0xffffffff >>> 0,
|
|
0x5eed1000,
|
|
])
|
|
|
|
let state = 0x8543a9b1
|
|
while (seedSet.size < 60) {
|
|
state = (Math.imul(state ^ (state >>> 15), 0x2c1b3c6d) ^ 0x297a2d39) >>> 0
|
|
seedSet.add(state)
|
|
}
|
|
|
|
return Array.from(seedSet)
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- *
|
|
* Mock DS1 Generator (Hermetic Fallback & Mock Pieces)
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
function makeMockDs1(width: number, height: number, floorStyle: number = 1): Ds1 {
|
|
const cells: Ds1Cell[][] = []
|
|
for (let y = 0; y < height; y += 1) {
|
|
const row: Ds1Cell[] = []
|
|
for (let x = 0; x < width; x += 1) {
|
|
row.push({
|
|
walls: [],
|
|
floors: [{ prop1: 2, sequence: 0, style: floorStyle, unknown1: 0, unknown2: 0, hidden: false }],
|
|
shadows: [],
|
|
substitutions: [],
|
|
})
|
|
}
|
|
cells.push(row)
|
|
}
|
|
return {
|
|
version: 18,
|
|
width,
|
|
height,
|
|
act: 1,
|
|
substitutionType: 0,
|
|
wallLayers: 1,
|
|
floorLayers: 1,
|
|
cells,
|
|
objects: [],
|
|
npcPathOffset: null,
|
|
}
|
|
}
|
|
|
|
function makeMockBorderPieces(actPrefix: string): WildernessPiece[] {
|
|
const ds1 = makeMockDs1(8, 8)
|
|
return [
|
|
{ name: `${actPrefix} - Border 1`, border: true, levels: [ds1] },
|
|
{ name: `${actPrefix} - Border 2`, border: true, levels: [ds1] },
|
|
{ name: `${actPrefix} - Border 3`, border: true, levels: [ds1] },
|
|
{ name: `${actPrefix} - Border 4`, border: true, levels: [ds1] },
|
|
{ name: `${actPrefix} - Border 5`, border: true, levels: [ds1] },
|
|
{ name: `${actPrefix} - Border 6`, border: true, levels: [ds1] },
|
|
{ name: `${actPrefix} - Border 7`, border: true, levels: [ds1] },
|
|
{ name: `${actPrefix} - Border 8`, border: true, levels: [ds1] },
|
|
]
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- *
|
|
* Piece Families for MPQ Loading
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
const WILDERNESS_PIECE_FAMILIES: Record<string, string[]> = {
|
|
'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 - Barricade': ['Act 5 - Barricade'],
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- *
|
|
* Main Stress-Test Suite
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
async function main(): Promise<void> {
|
|
const startTime = Date.now()
|
|
console.log('======================================================================')
|
|
console.log('🔥 EMPIRICAL STRESS-TEST: Milestone M2 Connectivity & Barricades')
|
|
console.log('======================================================================\n')
|
|
|
|
const seeds = generate60Seeds()
|
|
console.log(`Generated ${String(seeds.length)} distinct test seeds (includes 3 master, 7 boundary, 50 pseudo-random).\n`)
|
|
|
|
let totalAssertions = 0
|
|
let passedAssertions = 0
|
|
let failedAssertions = 0
|
|
|
|
function assert(condition: boolean, message: string): void {
|
|
totalAssertions += 1
|
|
if (condition) {
|
|
passedAssertions += 1
|
|
} else {
|
|
failedAssertions += 1
|
|
console.error(`❌ ASSERTION FAILED: ${message}`)
|
|
}
|
|
}
|
|
|
|
// Load MPQ archives if present
|
|
let mpqPiecesAct4Mesa: WildernessPiece[] | null = null
|
|
let mpqPiecesAct4Lava: WildernessPiece[] | null = null
|
|
let d2Tables: { levels: D2Table; lvltypes: D2Table; lvlprest: D2Table } | null = null
|
|
let d2Archives: MountedArchives | null = null
|
|
|
|
const samplePath = 'samples/d2/d2data.mpq'
|
|
if (existsSync(samplePath)) {
|
|
console.log('📦 Loading MPQ archives for ground-truth data...')
|
|
d2Archives = new MountedArchives()
|
|
for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
|
|
try {
|
|
d2Archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`)))
|
|
} catch {
|
|
// Optional
|
|
}
|
|
}
|
|
const actTables = await loadActTables(d2Archives)
|
|
d2Tables = {
|
|
levels: actTables.levels,
|
|
lvltypes: actTables.lvltypes,
|
|
lvlprest: actTables.lvlprest,
|
|
}
|
|
|
|
const ds1Cache = new Map<string, Ds1>()
|
|
|
|
async function loadDs1(relative: string): Promise<Ds1> {
|
|
const member = tileMemberPath(relative)
|
|
const cached = ds1Cache.get(member)
|
|
if (cached !== undefined) return cached
|
|
const decoded = decodeDs1(await d2Archives!.read(member))
|
|
ds1Cache.set(member, decoded)
|
|
return decoded
|
|
}
|
|
|
|
async function rowDs1s(table: D2Table, row: readonly string[]): Promise<Ds1[]> {
|
|
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
|
|
}
|
|
|
|
async function getPieces(familyKey: string): Promise<WildernessPiece[]> {
|
|
const families = WILDERNESS_PIECE_FAMILIES[familyKey] ?? []
|
|
const pieces: WildernessPiece[] = []
|
|
for (const row of d2Tables!.lvlprest.rows) {
|
|
const name = cell(d2Tables!.lvlprest, row, 'Name')
|
|
if (!families.some(family => name.startsWith(family))) continue
|
|
if (familyKey === 'Act 5 - Barricade' && name.includes('Snow')) continue
|
|
const levels = await rowDs1s(d2Tables!.lvlprest, row)
|
|
if (levels.length === 0) continue
|
|
const isBorder = /border|cliff/i.test(name)
|
|
pieces.push({ name, levels, border: isBorder })
|
|
}
|
|
return pieces
|
|
}
|
|
|
|
mpqPiecesAct4Mesa = await getPieces('Act 4 - Mesa')
|
|
mpqPiecesAct4Lava = await getPieces('Act 4 - Lava')
|
|
console.log(`✓ Loaded MPQ ground-truth pieces: Act 4 Mesa (${String(mpqPiecesAct4Mesa.length)}), Lava (${String(mpqPiecesAct4Lava.length)})\n`)
|
|
}
|
|
|
|
const piecesAct4Mesa = mpqPiecesAct4Mesa ?? makeMockBorderPieces('Act 4')
|
|
const piecesAct4Lava = mpqPiecesAct4Lava ?? makeMockBorderPieces('Act 4')
|
|
const piecesAct5 = makeMockBorderPieces('Act 5')
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* SECTION 1: Act 4 Lava Fissure Flood-Fill Reachability Stress-Testing
|
|
* ----------------------------------------------------------------------- */
|
|
console.log('----------------------------------------------------------------------')
|
|
console.log('🌋 SECTION 1: Act 4 Lava Fissure Flood-Fill Reachability (60 Seeds)')
|
|
console.log('----------------------------------------------------------------------')
|
|
|
|
let act4MinReachability = 1.0
|
|
let act4MaxReachability = 0.0
|
|
let act4SumReachability = 0.0
|
|
let act4TestsCount = 0
|
|
|
|
const act4Configurations = [
|
|
{ levelId: 104, name: 'Outer Steppes', type: 'Act 4 - Mesa', sizeX: 80, sizeY: 80, pieces: piecesAct4Mesa },
|
|
{ levelId: 105, name: 'Plains of Despair', type: 'Act 4 - Mesa', sizeX: 80, sizeY: 80, pieces: piecesAct4Mesa },
|
|
{ levelId: 106, name: 'City of the Damned', type: 'Act 4 - Mesa', sizeX: 80, sizeY: 80, pieces: piecesAct4Mesa },
|
|
{ levelId: 106, name: 'City of the Damned (Lava)', type: 'Act 4 - Lava', sizeX: 80, sizeY: 80, pieces: piecesAct4Lava },
|
|
]
|
|
|
|
for (const config of act4Configurations) {
|
|
console.log(`Testing ${config.name} (${config.type}, Level ${String(config.levelId)}) across ${String(seeds.length)} seeds...`)
|
|
|
|
let configMinRatio = 1.0
|
|
let configAllPassed = true
|
|
|
|
for (const seed of seeds) {
|
|
const result = generateWilderness({
|
|
levelId: config.levelId,
|
|
levelName: config.name,
|
|
levelTypeName: config.type,
|
|
sizeX: config.sizeX,
|
|
sizeY: config.sizeY,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed,
|
|
pieces: config.pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
const W = result.level.width
|
|
const H = result.level.height
|
|
|
|
const hasWall = (x: number, y: number): boolean => {
|
|
const cell = result.level.cells[y]?.[x]
|
|
if (!cell) return true
|
|
return cell.walls.some(w => w.prop1 !== 0 || w.style !== 0)
|
|
}
|
|
|
|
const hasFloor = (x: number, y: number): boolean => {
|
|
const cell = result.level.cells[y]?.[x]
|
|
if (!cell) return false
|
|
return cell.floors.some(f => !f.hidden && f.prop1 !== 0)
|
|
}
|
|
|
|
const isWalkable = (x: number, y: number): boolean => hasFloor(x, y) && !hasWall(x, y)
|
|
|
|
// 1. Gather all walkable cells in playable interior [8..W-8] x [8..H-8]
|
|
let totalInteriorWalkable = 0
|
|
for (let y = 8; y < H - 8; y += 1) {
|
|
for (let x = 8; x < W - 8; x += 1) {
|
|
if (isWalkable(x, y)) totalInteriorWalkable += 1
|
|
}
|
|
}
|
|
|
|
assert(totalInteriorWalkable > 800, `Seed ${String(seed)}: Interior playable walkable cells (${String(totalInteriorWalkable)}) must be > 800`)
|
|
|
|
// 2. Identify entrance location
|
|
const gridW = Math.floor(config.sizeX / 8)
|
|
const inX = Math.floor(gridW / 2)
|
|
const hubX = inX * 8 + 4
|
|
|
|
let startX = -1
|
|
let startY = -1
|
|
|
|
// Search for walkable entrance tile at North edge [y=8..15] near hubX
|
|
for (let dy = 0; dy < 8 && startX === -1; dy += 1) {
|
|
for (let dx = 0; dx <= 6; dx += 1) {
|
|
for (const sign of [0, 1, -1]) {
|
|
const tx = hubX + dx * sign
|
|
const ty = 8 + dy
|
|
if (tx >= 8 && tx < W - 8 && ty >= 8 && ty < H - 8 && isWalkable(tx, ty)) {
|
|
startX = tx
|
|
startY = ty
|
|
break
|
|
}
|
|
}
|
|
if (startX !== -1) break
|
|
}
|
|
}
|
|
|
|
// Fallback: any walkable tile in top quadrant
|
|
if (startX === -1) {
|
|
for (let y = 8; y < Math.floor(H / 2) && startX === -1; y += 1) {
|
|
for (let x = 8; x < W - 8; x += 1) {
|
|
if (isWalkable(x, y)) {
|
|
startX = x
|
|
startY = y
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
assert(startX !== -1 && startY !== -1, `Seed ${String(seed)}: Entrance walkable starting cell must exist`)
|
|
|
|
// 3. Perform BFS Flood-Fill
|
|
const visited = new Set<number>()
|
|
const queue: [number, number][] = [[startX, startY]]
|
|
visited.add(startY * W + startX)
|
|
|
|
while (queue.length > 0) {
|
|
const [cx, cy] = queue.shift()!
|
|
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
|
const nx = cx + dx
|
|
const ny = cy + dy
|
|
if (nx >= 8 && nx < W - 8 && ny >= 8 && ny < H - 8) {
|
|
const key = ny * W + nx
|
|
if (!visited.has(key) && isWalkable(nx, ny)) {
|
|
visited.add(key)
|
|
queue.push([nx, ny])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const reachableRatio = visited.size / totalInteriorWalkable
|
|
configMinRatio = Math.min(configMinRatio, reachableRatio)
|
|
act4MinReachability = Math.min(act4MinReachability, reachableRatio)
|
|
act4MaxReachability = Math.max(act4MaxReachability, reachableRatio)
|
|
act4SumReachability += reachableRatio
|
|
act4TestsCount += 1
|
|
|
|
// 4. Assert reachability >= 90% holds unconditionally
|
|
const ratioPassed = reachableRatio >= 0.90
|
|
if (!ratioPassed) configAllPassed = false
|
|
assert(
|
|
ratioPassed,
|
|
`Seed ${String(seed)}: Reachable ratio ${(reachableRatio * 100).toFixed(2)}% must be >= 90.00% (visited: ${String(visited.size)}/${String(totalInteriorWalkable)})`,
|
|
)
|
|
|
|
// 5. Verify the 3 strategic rock bridges exist and connect North and South plateaus
|
|
let southCellsReached = 0
|
|
for (let y = Math.floor(H * 0.65); y < H - 8; y += 1) {
|
|
for (let x = 8; x < W - 8; x += 1) {
|
|
if (visited.has(y * W + x)) southCellsReached += 1
|
|
}
|
|
}
|
|
assert(
|
|
southCellsReached > 100,
|
|
`Seed ${String(seed)}: BFS must traverse across bridges into South plateau (reached ${String(southCellsReached)} cells in South)`,
|
|
)
|
|
|
|
// Verify that bridge cells are present in stats
|
|
const topoStats = result.stats.act4Topography as { rockyBridges: number } | undefined
|
|
assert(
|
|
topoStats !== undefined && topoStats.rockyBridges >= 3,
|
|
`Seed ${String(seed)}: Topo stats must record rocky bridges (got ${String(topoStats?.rockyBridges)})`,
|
|
)
|
|
}
|
|
|
|
console.log(` ✓ ${config.name} (${config.type}): 100% of seeds passed >= 90% reachability (Min: ${(configMinRatio * 100).toFixed(2)}%)\n`)
|
|
}
|
|
|
|
const act4AvgReachability = act4SumReachability / act4TestsCount
|
|
console.log(`Act 4 Lava Fissure Summary (${String(act4TestsCount)} runs):`)
|
|
console.log(` - Minimum Reachability: ${(act4MinReachability * 100).toFixed(2)}% (Target >= 90.00%)`)
|
|
console.log(` - Average Reachability: ${(act4AvgReachability * 100).toFixed(2)}%`)
|
|
console.log(` - Maximum Reachability: ${(act4MaxReachability * 100).toFixed(2)}%`)
|
|
console.log(` - 100% Success Rate across all ${String(act4TestsCount)} tests.\n`)
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* SECTION 2: Act 5 Transverse Barricades Orientation & Gates Stress-Testing
|
|
* ----------------------------------------------------------------------- */
|
|
console.log('----------------------------------------------------------------------')
|
|
console.log('🛡️ SECTION 2: Act 5 Transverse Barricades Orientation & Gate Walkability')
|
|
console.log('----------------------------------------------------------------------')
|
|
|
|
// Sub-case 2A: Vertical Corridors (height > width)
|
|
// Must span horizontally across X in [8..W-8] with military gate at hubX
|
|
console.log(`Sub-case 2A: Vertical Corridors (height > width) across ${String(seeds.length)} seeds...`)
|
|
|
|
const verticalSizes = [
|
|
{ sizeX: 48, sizeY: 160 }, // Standard authentic Level 111 / 112
|
|
{ sizeX: 48, sizeY: 192 },
|
|
{ sizeX: 64, sizeY: 160 },
|
|
]
|
|
|
|
for (const dim of verticalSizes) {
|
|
console.log(` Testing Vertical Corridor ${String(dim.sizeX)}x${String(dim.sizeY)} (height > width)...`)
|
|
|
|
for (const seed of seeds) {
|
|
const result = generateWilderness({
|
|
levelId: 111,
|
|
levelName: 'Frigid Highlands',
|
|
levelTypeName: 'Act 5 - Barricade',
|
|
sizeX: dim.sizeX,
|
|
sizeY: dim.sizeY,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed,
|
|
pieces: piecesAct5,
|
|
substitutions: [],
|
|
})
|
|
|
|
const W = result.level.width
|
|
const H = result.level.height
|
|
const gridW = Math.floor(dim.sizeX / 8)
|
|
const inX = Math.floor(gridW / 2)
|
|
const hubX = inX * 8 + 4
|
|
|
|
assert(H > W, `Corridor height (${String(H)}) must be > width (${String(W)})`)
|
|
|
|
// Find horizontal barricade rows (rows with at least 15 palisade wall tiles across X)
|
|
const horizontalBarricadeRows: number[] = []
|
|
for (let y = 8; y < H - 8; y += 1) {
|
|
let palisadeCount = 0
|
|
for (let x = 8; x < W - 8; x += 1) {
|
|
const cell = result.level.cells[y]?.[x]
|
|
if (cell && cell.walls.some(w => w.style === 2 && w.prop1 === 129)) {
|
|
palisadeCount += 1
|
|
}
|
|
}
|
|
if (palisadeCount >= 15) {
|
|
horizontalBarricadeRows.push(y)
|
|
}
|
|
}
|
|
|
|
assert(
|
|
horizontalBarricadeRows.length >= 2,
|
|
`Seed ${String(seed)}: Vertical corridor must produce at least 2 horizontal barricade rows across X (found ${String(horizontalBarricadeRows.length)})`,
|
|
)
|
|
|
|
// For each horizontal barricade row, verify:
|
|
// 1. Spans across X in [8..W-8]
|
|
// 2. Gate at hubX is open (no walls) and walkable (has floor)
|
|
// 3. Gate opening has width of at least 3 walkable tiles
|
|
// 4. Fortified end-caps at gateLeft (hubX - 2) and gateRight (hubX + 2)
|
|
for (const by of horizontalBarricadeRows) {
|
|
// Gate center at hubX
|
|
const gateCell = result.level.cells[by]?.[hubX]
|
|
assert(gateCell !== undefined, `Seed ${String(seed)}: Gate cell at (${String(hubX)}, ${String(by)}) must exist`)
|
|
|
|
const gateHasWall = gateCell ? gateCell.walls.some(w => w.prop1 !== 0 || w.style !== 0) : true
|
|
assert(!gateHasWall, `Seed ${String(seed)}: Military gate at (hubX=${String(hubX)}, by=${String(by)}) must have NO wall collision mask`)
|
|
|
|
const gateHasFloor = gateCell ? gateCell.floors.some(f => f.style === 5 && f.prop1 === 194) : false
|
|
assert(gateHasFloor, `Seed ${String(seed)}: Military gate at (hubX=${String(hubX)}, by=${String(by)}) must have walkable dirt/mud floor (style 5, prop1 194)`)
|
|
|
|
// Gate width of at least 3 tiles (hubX - 1, hubX, hubX + 1)
|
|
for (let gx = hubX - 1; gx <= hubX + 1; gx += 1) {
|
|
const c = result.level.cells[by]?.[gx]
|
|
const wall = c ? c.walls.some(w => w.prop1 !== 0 || w.style !== 0) : true
|
|
assert(!wall, `Seed ${String(seed)}: Gate opening tile at (${String(gx)}, ${String(by)}) must be clear of walls`)
|
|
}
|
|
|
|
// Fortified end-caps
|
|
const leftCap = result.level.cells[by]?.[hubX - 2]
|
|
const rightCap = result.level.cells[by]?.[hubX + 2]
|
|
const hasLeftCap = leftCap ? leftCap.walls.some(w => w.style === 2 && w.prop1 === 129) : false
|
|
const hasRightCap = rightCap ? rightCap.walls.some(w => w.style === 2 && w.prop1 === 129) : false
|
|
assert(hasLeftCap, `Seed ${String(seed)}: Fortified left end-cap must exist at (${String(hubX - 2)}, ${String(by)})`)
|
|
assert(hasRightCap, `Seed ${String(seed)}: Fortified right end-cap must exist at (${String(hubX + 2)}, ${String(by)})`)
|
|
}
|
|
|
|
// End-to-end corridor traversability:
|
|
// Verify BFS from North entrance (hubX, 8) reaches South exit (hubX, H - 9)
|
|
const isWalkable = (x: number, y: number): boolean => {
|
|
const c = result.level.cells[y]?.[x]
|
|
if (!c) return false
|
|
const hasF = c.floors.some(f => !f.hidden && f.prop1 !== 0)
|
|
const hasW = c.walls.some(w => w.prop1 !== 0 || w.style !== 0)
|
|
return hasF && !hasW
|
|
}
|
|
|
|
const visited = new Set<number>()
|
|
const queue: [number, number][] = [[hubX, 8]]
|
|
visited.add(8 * W + hubX)
|
|
|
|
while (queue.length > 0) {
|
|
const [cx, cy] = queue.shift()!
|
|
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
|
const nx = cx + dx
|
|
const ny = cy + dy
|
|
if (nx >= 8 && nx < W - 8 && ny >= 8 && ny < H - 8) {
|
|
const key = ny * W + nx
|
|
if (!visited.has(key) && isWalkable(nx, ny)) {
|
|
visited.add(key)
|
|
queue.push([nx, ny])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const southReached = visited.has((H - 9) * W + hubX) || visited.has((H - 10) * W + hubX)
|
|
assert(
|
|
southReached,
|
|
`Seed ${String(seed)}: Player must be able to traverse full vertical corridor through all barricade gates from North to South`,
|
|
)
|
|
}
|
|
console.log(` ✓ Vertical Corridor ${String(dim.sizeX)}x${String(dim.sizeY)}: 100% assertions passed across all ${String(seeds.length)} seeds.`)
|
|
}
|
|
|
|
// Sub-case 2B: Horizontal & Square Corridors (width >= height)
|
|
// Must span vertically across Y in [8..H-8] with military gate at hubY
|
|
console.log(`\nSub-case 2B: Horizontal & Square Corridors (width >= height) across ${String(seeds.length)} seeds...`)
|
|
|
|
const horizontalSizes = [
|
|
{ sizeX: 160, sizeY: 48 }, // Horizontal corridor
|
|
{ sizeX: 80, sizeY: 80 }, // Square corridor (width == height)
|
|
{ sizeX: 128, sizeY: 64 }, // Wide corridor
|
|
]
|
|
|
|
for (const dim of horizontalSizes) {
|
|
console.log(` Testing Horizontal/Square Corridor ${String(dim.sizeX)}x${String(dim.sizeY)} (width >= height)...`)
|
|
|
|
for (const seed of seeds) {
|
|
const result = generateWilderness({
|
|
levelId: 111,
|
|
levelName: 'Frigid Highlands',
|
|
levelTypeName: 'Act 5 - Barricade',
|
|
sizeX: dim.sizeX,
|
|
sizeY: dim.sizeY,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed,
|
|
pieces: piecesAct5,
|
|
substitutions: [],
|
|
})
|
|
|
|
const W = result.level.width
|
|
const H = result.level.height
|
|
const gridH = Math.floor(dim.sizeY / 8)
|
|
const outY = Math.floor(gridH / 2)
|
|
const hubY = outY * 8 + 4
|
|
|
|
assert(W >= H, `Corridor width (${String(W)}) must be >= height (${String(H)})`)
|
|
|
|
// Find vertical barricade columns (columns with at least 15 palisade wall tiles across Y)
|
|
const verticalBarricadeCols: number[] = []
|
|
for (let x = 8; x < W - 8; x += 1) {
|
|
let palisadeCount = 0
|
|
for (let y = 8; y < H - 8; y += 1) {
|
|
const cell = result.level.cells[y]?.[x]
|
|
if (cell && cell.walls.some(w => w.style === 2 && w.prop1 === 129)) {
|
|
palisadeCount += 1
|
|
}
|
|
}
|
|
if (palisadeCount >= 15) {
|
|
verticalBarricadeCols.push(x)
|
|
}
|
|
}
|
|
|
|
assert(
|
|
verticalBarricadeCols.length >= 2,
|
|
`Seed ${String(seed)}: Horizontal corridor must produce at least 2 vertical barricade columns across Y (found ${String(verticalBarricadeCols.length)})`,
|
|
)
|
|
|
|
// For each vertical barricade col, verify:
|
|
// 1. Spans across Y in [8..H-8]
|
|
// 2. Gate at hubY is open (no walls) and walkable (has floor)
|
|
// 3. Gate opening has height of at least 3 walkable tiles
|
|
// 4. Fortified end-caps at gateTop (hubY - 2) and gateBottom (hubY + 2)
|
|
for (const bx of verticalBarricadeCols) {
|
|
// Gate center at hubY
|
|
const gateCell = result.level.cells[hubY]?.[bx]
|
|
assert(gateCell !== undefined, `Seed ${String(seed)}: Gate cell at (${String(bx)}, ${String(hubY)}) must exist`)
|
|
|
|
const gateHasWall = gateCell ? gateCell.walls.some(w => w.prop1 !== 0 || w.style !== 0) : true
|
|
assert(!gateHasWall, `Seed ${String(seed)}: Military gate at (bx=${String(bx)}, hubY=${String(hubY)}) must have NO wall collision mask`)
|
|
|
|
const gateHasFloor = gateCell ? gateCell.floors.some(f => f.style === 5 && f.prop1 === 194) : false
|
|
assert(gateHasFloor, `Seed ${String(seed)}: Military gate at (bx=${String(bx)}, hubY=${String(hubY)}) must have walkable dirt/mud floor (style 5, prop1 194)`)
|
|
|
|
// Gate height of at least 3 tiles (hubY - 1, hubY, hubY + 1)
|
|
for (let gy = hubY - 1; gy <= hubY + 1; gy += 1) {
|
|
const c = result.level.cells[gy]?.[bx]
|
|
const wall = c ? c.walls.some(w => w.prop1 !== 0 || w.style !== 0) : true
|
|
assert(!wall, `Seed ${String(seed)}: Gate opening tile at (${String(bx)}, ${String(gy)}) must be clear of walls`)
|
|
}
|
|
|
|
// Fortified end-caps
|
|
const topCap = result.level.cells[hubY - 2]?.[bx]
|
|
const bottomCap = result.level.cells[hubY + 2]?.[bx]
|
|
const hasTopCap = topCap ? topCap.walls.some(w => w.style === 2 && w.prop1 === 129) : false
|
|
const hasBottomCap = bottomCap ? bottomCap.walls.some(w => w.style === 2 && w.prop1 === 129) : false
|
|
assert(hasTopCap, `Seed ${String(seed)}: Fortified top end-cap must exist at (${String(bx)}, ${String(hubY - 2)})`)
|
|
assert(hasBottomCap, `Seed ${String(seed)}: Fortified bottom end-cap must exist at (${String(bx)}, ${String(hubY + 2)})`)
|
|
}
|
|
|
|
// End-to-end corridor traversability:
|
|
// Verify BFS from West entrance (8, hubY) reaches East exit (W - 9, hubY)
|
|
const isWalkable = (x: number, y: number): boolean => {
|
|
const c = result.level.cells[y]?.[x]
|
|
if (!c) return false
|
|
const hasF = c.floors.some(f => !f.hidden && f.prop1 !== 0)
|
|
const hasW = c.walls.some(w => w.prop1 !== 0 || w.style !== 0)
|
|
return hasF && !hasW
|
|
}
|
|
|
|
const visited = new Set<number>()
|
|
const queue: [number, number][] = [[8, hubY]]
|
|
visited.add(hubY * W + 8)
|
|
|
|
while (queue.length > 0) {
|
|
const [cx, cy] = queue.shift()!
|
|
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
|
const nx = cx + dx
|
|
const ny = cy + dy
|
|
if (nx >= 8 && nx < W - 8 && ny >= 8 && ny < H - 8) {
|
|
const key = ny * W + nx
|
|
if (!visited.has(key) && isWalkable(nx, ny)) {
|
|
visited.add(key)
|
|
queue.push([nx, ny])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const eastReached = visited.has(hubY * W + (W - 9)) || visited.has(hubY * W + (W - 10))
|
|
assert(
|
|
eastReached,
|
|
`Seed ${String(seed)}: Player must be able to traverse full horizontal corridor through all barricade gates from West to East`,
|
|
)
|
|
}
|
|
console.log(` ✓ Horizontal/Square Corridor ${String(dim.sizeX)}x${String(dim.sizeY)}: 100% assertions passed across all ${String(seeds.length)} seeds.`)
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* SUMMARY
|
|
* ----------------------------------------------------------------------- */
|
|
const durationSec = ((Date.now() - startTime) / 1000).toFixed(2)
|
|
console.log('\n======================================================================')
|
|
console.log(`📊 STRESS TEST COMPLETE (${durationSec}s)`)
|
|
console.log('======================================================================')
|
|
console.log(`Total Assertions Executed: ${String(totalAssertions)}`)
|
|
console.log(`Passed Assertions: ${String(passedAssertions)}`)
|
|
console.log(`Failed Assertions: ${String(failedAssertions)}`)
|
|
|
|
if (failedAssertions > 0) {
|
|
console.error(`\n❌ VERDICT: CHALLENGE_FAILED (${String(failedAssertions)} assertion failures)`)
|
|
process.exit(1)
|
|
} else {
|
|
console.log('\n✅ VERDICT: APPROVE (100% empirical assertions passed)')
|
|
process.exit(0)
|
|
}
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('Fatal error in stress test:', err)
|
|
process.exit(1)
|
|
})
|