281 lines
9.2 KiB
TypeScript
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))
|
|
}
|
|
})
|
|
})
|