diablo2-web/tests/wilderness-barricade-stress...

281 lines
9.2 KiB
TypeScript

import { describe, expect, test } from "vitest"
import {
resolveUnsetSize,
UnresolvedLevelSizeError,
generateWilderness,
type WildernessPiece,
} from "../src/game/wilderness.ts"
import type { Ds1, Ds1Cell } from "../src/formats/ds1.ts"
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: 5,
substitutionType: 0,
wallLayers: 1,
floorLayers: 1,
cells,
objects: [],
npcPathOffset: null,
}
}
describe("Adversarial Stress Testing: resolveUnsetSize and Barricade Extent", () => {
test("resolveUnsetSize throws UnresolvedLevelSizeError when pieces array is empty for Level 111 and 112", () => {
for (const levelId of [111, 112]) {
expect(() => {
resolveUnsetSize({
levelId,
levelName: `Act 5 - Barricade ${levelId - 110}`,
levelTypeName: "Act 5 - Barricade",
sizeX: -1,
sizeY: -1,
subType: 10,
subTheme: 0,
seed: 12345,
pieces: [],
substitutions: [],
})
}).toThrow(UnresolvedLevelSizeError)
}
})
test("resolveUnsetSize throws UnresolvedLevelSizeError for non-barricade levels without pieces across Acts 1..4", () => {
const testCases = [
{ levelId: 2, name: "Blood Moor", type: "Act 1 - Wilderness" },
{ levelId: 41, name: "Rocky Waste", type: "Act 2 - Desert" },
{ levelId: 76, name: "Spider Forest", type: "Act 3 - Jungle" },
{ levelId: 104, name: "Outer Steppes", type: "Act 4 - Mesa" },
]
for (const tc of testCases) {
expect(() => {
resolveUnsetSize({
levelId: tc.levelId,
levelName: tc.name,
levelTypeName: tc.type,
sizeX: -1,
sizeY: -1,
subType: 0,
subTheme: 0,
seed: 12345,
pieces: [],
substitutions: [],
})
}).toThrow(UnresolvedLevelSizeError)
}
})
test("resolveUnsetSize throws UnresolvedLevelSizeError for non-barricade levels with non-barricade pieces", () => {
const mockBorderPiece: WildernessPiece = {
name: "Act 1 - Wild Border 1",
border: true,
levels: [makeMockDs1(8, 8)],
}
expect(() => {
resolveUnsetSize({
levelId: 2,
levelName: "Blood Moor",
levelTypeName: "Act 1 - Wilderness",
sizeX: -1,
sizeY: -1,
subType: 6,
subTheme: 0,
seed: 12345,
pieces: [mockBorderPiece],
substitutions: [],
})
}).toThrow(UnresolvedLevelSizeError)
})
test("resolveUnsetSize handles combinations of entrance only, exit only, multiple entrances, and multiple exits", () => {
const entranceDs1 = makeMockDs1(16, 32)
const exitDs1 = makeMockDs1(16, 32)
const wallDs1 = makeMockDs1(17, 17)
// Entrance only
const entranceOnly: WildernessPiece[] = [
{ name: "Act 5 - Barricade Entrance 16x32", border: false, levels: [entranceDs1] },
{ name: "Act 5 - Barricade 1", border: false, levels: [wallDs1] },
]
const resEntranceOnly = resolveUnsetSize({
levelId: 111,
levelName: "Act 5 - Barricade 1",
levelTypeName: "Act 5 - Barricade",
sizeX: -1,
sizeY: -1,
subType: 10,
subTheme: 0,
seed: 12345,
pieces: entranceOnly,
substitutions: [],
})
expect(resEntranceOnly.sizeY).toBeGreaterThan(resEntranceOnly.sizeX)
expect(resEntranceOnly.sizeX).toBeGreaterThanOrEqual(48)
expect(resEntranceOnly.sizeX).toBeLessThanOrEqual(64)
expect(resEntranceOnly.sizeY).toBeGreaterThanOrEqual(160)
expect(resEntranceOnly.sizeY).toBeLessThanOrEqual(192)
// Exit only
const exitOnly: WildernessPiece[] = [
{ name: "Act 5 - Barricade Exit 16x32", border: false, levels: [exitDs1] },
{ name: "Act 5 - Barricade 1", border: false, levels: [wallDs1] },
]
const resExitOnly = resolveUnsetSize({
levelId: 111,
levelName: "Act 5 - Barricade 1",
levelTypeName: "Act 5 - Barricade",
sizeX: -1,
sizeY: -1,
subType: 10,
subTheme: 0,
seed: 12345,
pieces: exitOnly,
substitutions: [],
})
expect(resExitOnly.sizeY).toBeGreaterThan(resExitOnly.sizeX)
expect(resExitOnly.sizeX).toBeGreaterThanOrEqual(48)
expect(resExitOnly.sizeX).toBeLessThanOrEqual(64)
expect(resExitOnly.sizeY).toBeGreaterThanOrEqual(160)
expect(resExitOnly.sizeY).toBeLessThanOrEqual(192)
// Multiple entrances & exits
const multiplePieces: WildernessPiece[] = [
{ name: "Act 5 - Barricade Entrance A", border: false, levels: [entranceDs1] },
{ name: "Act 5 - Barricade Entrance B", border: false, levels: [makeMockDs1(24, 40)] },
{ name: "Act 5 - Barricade Exit A", border: false, levels: [exitDs1] },
{ name: "Act 5 - Barricade Exit B", border: false, levels: [makeMockDs1(32, 48)] },
{ name: "Act 5 - Barricade 1", border: false, levels: [wallDs1] },
]
const resMultiple = resolveUnsetSize({
levelId: 111,
levelName: "Act 5 - Barricade 1",
levelTypeName: "Act 5 - Barricade",
sizeX: -1,
sizeY: -1,
subType: 10,
subTheme: 0,
seed: 12345,
pieces: multiplePieces,
substitutions: [],
})
expect(resMultiple.sizeY).toBeGreaterThan(resMultiple.sizeX)
expect(resMultiple.sizeX).toBeGreaterThanOrEqual(48)
expect(resMultiple.sizeX).toBeLessThanOrEqual(64)
expect(resMultiple.sizeY).toBeGreaterThanOrEqual(160)
expect(resMultiple.sizeY).toBeLessThanOrEqual(192)
})
test("resolveUnsetSize guarantees sizeY > sizeX across 1,000 combinatorial fuzzing trials", () => {
const mockWallPiece: WildernessPiece = {
name: "Act 5 - Barricade Wall 1",
border: false,
levels: [makeMockDs1(17, 17)],
}
for (let trial = 0; trial < 1000; trial += 1) {
const numEntrances = trial % 5
const numExits = (trial * 3) % 5
const pieces: WildernessPiece[] = [mockWallPiece]
for (let i = 0; i < numEntrances; i += 1) {
const w = 1 + ((trial * 7 + i * 11) % 128)
const h = 1 + ((trial * 13 + i * 17) % 256)
pieces.push({
name: `Act 5 - Barricade Entrance ${i}`,
border: false,
levels: [makeMockDs1(w, h)],
})
}
for (let i = 0; i < numExits; i += 1) {
const w = 1 + ((trial * 19 + i * 23) % 128)
const h = 1 + ((trial * 29 + i * 31) % 256)
pieces.push({
name: `Act 5 - Barricade Exit ${i}`,
border: false,
levels: [makeMockDs1(w, h)],
})
}
const res = resolveUnsetSize({
levelId: 111,
levelName: "Act 5 - Barricade 1",
levelTypeName: "Act 5 - Barricade",
sizeX: -1,
sizeY: -1,
subType: 10,
subTheme: 0,
seed: trial,
pieces,
substitutions: [],
})
expect(res.sizeX).toBeGreaterThanOrEqual(48)
expect(res.sizeX).toBeLessThanOrEqual(64)
expect(res.sizeX % 8).toBe(0)
expect(res.sizeY).toBeGreaterThanOrEqual(160)
expect(res.sizeY).toBeLessThanOrEqual(192)
expect(res.sizeY % 8).toBe(0)
expect(res.sizeY).toBeGreaterThan(res.sizeX)
expect(res.sizeY / res.sizeX).toBeGreaterThanOrEqual(2.5)
expect(res.source).toBe("LvlPrest Barricade corridor dynamic extent")
}
})
test("generateWilderness generates Level 111 and 112 conforming to vertical corridor bounds", () => {
const entranceDs1 = makeMockDs1(17, 33)
const exitDs1 = makeMockDs1(17, 33)
const wallDs1 = makeMockDs1(17, 17)
const cliffDs1 = makeMockDs1(17, 17)
const pieces: WildernessPiece[] = [
{ name: "Act 5 - Barricade Entrance 16x32", border: false, levels: [entranceDs1] },
{ name: "Act 5 - Barricade Exit 16x32", border: false, levels: [exitDs1] },
{ name: "Act 5 - Barricade 1", border: false, levels: [wallDs1] },
{ name: "Act 5 - Barricade 2", border: false, levels: [wallDs1] },
{ name: "Act 5 - Barricade Cliff Border 1", border: true, levels: [cliffDs1] },
]
for (const levelId of [111, 112]) {
const generated = generateWilderness({
levelId,
levelName: `Act 5 - Barricade ${levelId - 110}`,
levelTypeName: "Act 5 - Barricade",
sizeX: -1,
sizeY: -1,
subType: 10,
subTheme: 0,
seed: 0x301cd095 + levelId,
pieces,
substitutions: [],
})
expect(Number(generated.stats.sizeX)).toBeGreaterThanOrEqual(48)
expect(Number(generated.stats.sizeX)).toBeLessThanOrEqual(64)
expect(Number(generated.stats.sizeY)).toBeGreaterThanOrEqual(160)
expect(Number(generated.stats.sizeY)).toBeLessThanOrEqual(192)
expect(Number(generated.stats.sizeY)).toBeGreaterThan(Number(generated.stats.sizeX))
expect(generated.level.width).toBe(Number(generated.stats.sizeX))
expect(generated.level.height).toBe(Number(generated.stats.sizeY))
}
})
})