826 lines
28 KiB
TypeScript
826 lines
28 KiB
TypeScript
import { describe, expect, test } from 'vitest'
|
|
import {
|
|
generateWilderness,
|
|
getPerimeterOpenings,
|
|
UNIMPLEMENTED_PASSES,
|
|
} from '../src/game/wilderness.ts'
|
|
import type { WildernessPiece } from '../src/game/wilderness.ts'
|
|
import {
|
|
act2OasisRadius,
|
|
act5FrozenLakeRadius,
|
|
jitterFrac,
|
|
} from '../src/game/wilderness-acts.ts'
|
|
import type {
|
|
Act2TopographyStats,
|
|
Act3TopographyStats,
|
|
Act4TopographyStats,
|
|
Act5TopographyStats,
|
|
} from '../src/game/wilderness-acts.ts'
|
|
import { Rng } from '../src/game/rng.ts'
|
|
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
|
|
|
function makeMockDs1(
|
|
width: number,
|
|
height: number,
|
|
hasGapInWalls: boolean = false,
|
|
floorStyle: number = 1,
|
|
wallStyle: 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) {
|
|
const isGap = hasGapInWalls && x === Math.floor(width / 2)
|
|
row.push({
|
|
walls: isGap ? [] : [{ prop1: 2, sequence: 0, style: wallStyle, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
|
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 borderDs1 = makeMockDs1(8, 8, false)
|
|
const openBorderDs1 = makeMockDs1(8, 8, true)
|
|
return [
|
|
{ name: `${actPrefix} - Border 1`, border: true, levels: [borderDs1, borderDs1, borderDs1, openBorderDs1] },
|
|
{ name: `${actPrefix} - Border 2`, border: true, levels: [borderDs1] },
|
|
{ name: `${actPrefix} - Border 3`, border: true, levels: [borderDs1] },
|
|
{ name: `${actPrefix} - Border 4`, border: true, levels: [borderDs1, borderDs1, borderDs1, openBorderDs1] },
|
|
{ name: `${actPrefix} - Border 5`, border: true, levels: [borderDs1] },
|
|
{ name: `${actPrefix} - Border 6`, border: true, levels: [borderDs1] },
|
|
{ name: `${actPrefix} - Border 7`, border: true, levels: [borderDs1] },
|
|
{ name: `${actPrefix} - Border 8`, border: true, levels: [borderDs1] },
|
|
]
|
|
}
|
|
|
|
describe('Wilderness Outdoor Initializers for Acts 2-5 (Issue #51)', () => {
|
|
test('UNIMPLEMENTED_PASSES has 5 outdoor passes removed', () => {
|
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGOUTDESR_InitAct2OutdoorLevel')
|
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGOUTPLACE_InitAct3OutdoorLevel')
|
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGOUTDOORS_InitAct4OutdoorLevel')
|
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGOUTSIEGE_InitAct5OutdoorLevel')
|
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLG_GenerateJungles')
|
|
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLGVER_CreateVertices')
|
|
expect(UNIMPLEMENTED_PASSES).toEqual([
|
|
'DRLGOUTPLACE_CreateLevelConnections',
|
|
'DRLGOUTDOORS_SpawnAct3Mephisto',
|
|
])
|
|
})
|
|
|
|
describe('Act 2: DRLGOUTDESR_InitAct2OutdoorLevel', () => {
|
|
test('Rocky Waste (41) generates sand caravan trails and canyon ridge walls', () => {
|
|
const pieces = makeMockBorderPieces('Act 2')
|
|
const result = generateWilderness({
|
|
levelId: 41,
|
|
levelName: 'Rocky Waste',
|
|
levelTypeName: 'Act 2 - Desert',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 41001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act2Topography).toBeDefined()
|
|
const topo = result.stats.act2Topography as Act2TopographyStats
|
|
expect(topo.sandTrailCells).toBeGreaterThan(50)
|
|
expect(topo.canyonWalls).toBeGreaterThan(20)
|
|
|
|
let foundSandTrail = false
|
|
let foundCanyonWall = false
|
|
for (let y = 0; y < result.level.height; y += 1) {
|
|
for (let x = 0; x < result.level.width; x += 1) {
|
|
const cell = result.level.cells[y]![x]!
|
|
if (cell.floors.some(f => f.style === 5 && f.prop1 === 194)) foundSandTrail = true
|
|
if (cell.walls.some(w => w.style === 1 && w.prop1 === 129)) foundCanyonWall = true
|
|
}
|
|
}
|
|
expect(foundSandTrail).toBe(true)
|
|
expect(foundCanyonWall).toBe(true)
|
|
})
|
|
|
|
test('Far Oasis (43) generates lush water oasis pools', () => {
|
|
const pieces = makeMockBorderPieces('Act 2')
|
|
const result = generateWilderness({
|
|
levelId: 43,
|
|
levelName: 'Far Oasis',
|
|
levelTypeName: 'Act 2 - Desert',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 43001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act2Topography).toBeDefined()
|
|
const topo = result.stats.act2Topography as Act2TopographyStats
|
|
expect(topo.oasisPoolCells).toBeGreaterThan(15)
|
|
|
|
let foundOasisWater = false
|
|
for (let y = 0; y < result.level.height; y += 1) {
|
|
for (let x = 0; x < result.level.width; x += 1) {
|
|
const cell = result.level.cells[y]![x]!
|
|
if (cell.floors.some(f => f.style === 2 && f.prop1 === 2)) foundOasisWater = true
|
|
}
|
|
}
|
|
expect(foundOasisWater).toBe(true)
|
|
})
|
|
|
|
test('Valley of Snakes (45) creates narrow canyon gorge', () => {
|
|
const pieces = makeMockBorderPieces('Act 2')
|
|
const result = generateWilderness({
|
|
levelId: 45,
|
|
levelName: 'Valley of Snakes',
|
|
levelTypeName: 'Act 2 - Desert',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 45001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act2Topography).toBeDefined()
|
|
const topo = result.stats.act2Topography as Act2TopographyStats
|
|
expect(topo.canyonWalls).toBeGreaterThan(30)
|
|
})
|
|
|
|
test('Canyon of the Magi (46) places 7 Tal Rasha tombs, central dais, and has 0 perimeter openings', () => {
|
|
const pieces = makeMockBorderPieces('Act 2')
|
|
expect(getPerimeterOpenings(46, 10, 10).size).toBe(0)
|
|
|
|
const result = generateWilderness({
|
|
levelId: 46,
|
|
levelName: 'Canyon of the Magi',
|
|
levelTypeName: 'Act 2 - Desert',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 46001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act2Topography).toBeDefined()
|
|
const topo = result.stats.act2Topography as Act2TopographyStats
|
|
expect(topo.tombEntrances).toBe(7)
|
|
|
|
// Verify central Arcane Sanctuary warp platform (portal/dais)
|
|
let foundCentralDais = false
|
|
for (let y = 35; y <= 45; y += 1) {
|
|
for (let x = 35; x <= 45; x += 1) {
|
|
const cell = result.level.cells[y]![x]!
|
|
if (cell.floors.some(f => f.style === 3)) foundCentralDais = true
|
|
}
|
|
}
|
|
expect(foundCentralDais).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Act 3: DRLGOUTPLACE_InitAct3OutdoorLevel & DRLG_GenerateJungles', () => {
|
|
test('Jungles (76 Spider Forest) generate river channel, wooden river bridges, and jungle paths', () => {
|
|
const tailDs1 = makeMockDs1(64, 32)
|
|
const headDs1 = makeMockDs1(64, 32)
|
|
const clearingDs1 = makeMockDs1(32, 32)
|
|
const riverDs1 = makeMockDs1(32, 32)
|
|
|
|
const pieces: WildernessPiece[] = [
|
|
{ name: 'Act 3 - Jungle Tail', border: false, levels: [tailDs1] },
|
|
{ name: 'Act 3 - Jungle Head', border: false, levels: [headDs1] },
|
|
{ name: 'Act 3 - Clearing Webby E', border: false, levels: [clearingDs1] },
|
|
{ name: 'Act 3 - Jungle NS W', border: false, levels: [riverDs1] },
|
|
]
|
|
|
|
const result = generateWilderness({
|
|
levelId: 76,
|
|
levelName: 'Spider Forest',
|
|
levelTypeName: 'Act 3 - Jungle',
|
|
sizeX: 64,
|
|
sizeY: 192,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 76001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act3Topography).toBeDefined()
|
|
const topo = result.stats.act3Topography as Act3TopographyStats
|
|
expect(topo.riverBridges).toBeGreaterThanOrEqual(2)
|
|
expect(topo.junglePathCells).toBeGreaterThan(20)
|
|
|
|
// Verify wooden river bridge planks (style 4, prop1 2) spanning across East channel
|
|
let foundBridgePlank = false
|
|
for (let y = 70; y <= 90; y += 1) {
|
|
for (let x = 32; x < 60; x += 1) {
|
|
const cell = result.level.cells[y]![x]!
|
|
if (cell.floors.some(f => f.style === 4 && f.prop1 === 2)) foundBridgePlank = true
|
|
}
|
|
}
|
|
expect(foundBridgePlank).toBe(true)
|
|
})
|
|
|
|
test('Kurast Causeway (82) stamps causeway with central walkway, balustrades, and canal water', () => {
|
|
const groundDs1 = makeMockDs1(48, 16)
|
|
const pieces: WildernessPiece[] = [
|
|
{ name: 'Act 3 - Ground', border: false, levels: [groundDs1] },
|
|
]
|
|
|
|
const result = generateWilderness({
|
|
levelId: 82,
|
|
levelName: 'Kurast Causeway',
|
|
levelTypeName: 'Act 3 - Kurast',
|
|
sizeX: 48,
|
|
sizeY: 16,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 82001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act3Topography).toBeDefined()
|
|
const topo = result.stats.act3Topography as Act3TopographyStats
|
|
expect(topo.causewayCells).toBeGreaterThan(50)
|
|
expect(topo.canalCells).toBeGreaterThan(100)
|
|
|
|
// Verify central walkway (style 5), stone balustrade walls (style 1), and canal water (style 2)
|
|
let foundWalkway = false
|
|
let foundBalustrade = false
|
|
let foundCanalWater = false
|
|
for (let y = 0; y < result.level.height; y += 1) {
|
|
for (let x = 0; x < result.level.width; x += 1) {
|
|
const cell = result.level.cells[y]![x]!
|
|
if (cell.floors.some(f => (f.style === 5 || f.style === 20) && f.prop1 === 194)) foundWalkway = true
|
|
if (cell.walls.some(w => (w.style === 1 || w.style === 20) && w.prop1 === 129)) foundBalustrade = true
|
|
if (cell.floors.some(f => f.style === 2 && f.prop1 === 2)) foundCanalWater = true
|
|
}
|
|
}
|
|
expect(foundWalkway).toBe(true)
|
|
expect(foundBalustrade).toBe(true)
|
|
expect(foundCanalWater).toBe(true)
|
|
})
|
|
|
|
test('Kurast Causeway (82) preserves preset when Act 3 - Bridge is provided', () => {
|
|
const bridgeDs1 = makeMockDs1(48, 16, false, 29, 31)
|
|
const pieces: WildernessPiece[] = [
|
|
{ name: 'Act 3 - Bridge', border: false, levels: [bridgeDs1] },
|
|
]
|
|
|
|
const result = generateWilderness({
|
|
levelId: 82,
|
|
levelName: 'Kurast Causeway',
|
|
levelTypeName: 'Act 3 - Kurast',
|
|
sizeX: 48,
|
|
sizeY: 16,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 82001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
let foundPresetFloor = false
|
|
let foundPresetWall = false
|
|
for (let y = 0; y < result.level.height; y += 1) {
|
|
for (let x = 0; x < result.level.width; x += 1) {
|
|
const cell = result.level.cells[y]![x]!
|
|
if (cell.floors.some(f => f.style === 29)) foundPresetFloor = true
|
|
if (cell.walls.some(w => w.style === 31)) foundPresetWall = true
|
|
}
|
|
}
|
|
expect(foundPresetFloor).toBe(true)
|
|
expect(foundPresetWall).toBe(true)
|
|
})
|
|
|
|
test('Kurast Bazaar (79) generates paved stone avenues, canals, and stone bridges', () => {
|
|
const pieces = makeMockBorderPieces('Act 3')
|
|
const result = generateWilderness({
|
|
levelId: 79,
|
|
levelName: 'Kurast Bazaar',
|
|
levelTypeName: 'Act 3 - Kurast',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 79001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act3Topography).toBeDefined()
|
|
const topo = result.stats.act3Topography as Act3TopographyStats
|
|
expect(topo.causewayCells).toBeGreaterThan(50)
|
|
expect(topo.canalCells).toBeGreaterThan(30)
|
|
expect(topo.riverBridges).toBeGreaterThanOrEqual(1)
|
|
})
|
|
})
|
|
|
|
describe('Act 4: DRLGOUTDOORS_InitAct4OutdoorLevel', () => {
|
|
test('Outer Steppes (104) generates lava chasms, basalt lips, and rocky bridge causeways', () => {
|
|
const pieces = makeMockBorderPieces('Act 4')
|
|
const result = generateWilderness({
|
|
levelId: 104,
|
|
levelName: 'Outer Steppes',
|
|
levelTypeName: 'Act 4 - Mesa',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 104001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act4Topography).toBeDefined()
|
|
const topo = result.stats.act4Topography as Act4TopographyStats
|
|
expect(topo.lavaChasmCells).toBeGreaterThan(100)
|
|
expect(topo.rockyBridges).toBeGreaterThan(5)
|
|
|
|
// Verify boiling molten lava (style 20, sequence 3, prop1 2) and rocky bridges (style 10, sequence 0, prop1 2)
|
|
let foundLava = false
|
|
let foundRockyBridge = false
|
|
for (let y = 0; y < result.level.height; y += 1) {
|
|
for (let x = 0; x < result.level.width; x += 1) {
|
|
const cell = result.level.cells[y]![x]!
|
|
if (cell.floors.some(f => (f.style === 20 || f.style === 10) && f.prop1 === 2)) foundLava = true
|
|
if (cell.floors.some(f => f.style === 10 && f.sequence === 0 && f.prop1 === 2)) foundRockyBridge = true
|
|
}
|
|
}
|
|
expect(foundLava).toBe(true)
|
|
expect(foundRockyBridge).toBe(true)
|
|
})
|
|
|
|
test('Plains of Despair (105) creates Izual prison plateau', () => {
|
|
const pieces = makeMockBorderPieces('Act 4')
|
|
const result = generateWilderness({
|
|
levelId: 105,
|
|
levelName: 'Plains of Despair',
|
|
levelTypeName: 'Act 4 - Mesa',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 105001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act4Topography).toBeDefined()
|
|
const topo = result.stats.act4Topography as Act4TopographyStats
|
|
expect(topo.plateauCells).toBeGreaterThan(20)
|
|
})
|
|
})
|
|
|
|
describe('Act 5: DRLGOUTSIEGE_InitAct5OutdoorLevel', () => {
|
|
test('Frigid Highlands (111) generates transverse siege barricades, choke-point gates, and trenches', () => {
|
|
const pieces = makeMockBorderPieces('Act 5')
|
|
const result = generateWilderness({
|
|
levelId: 111,
|
|
levelName: 'Frigid Highlands',
|
|
levelTypeName: 'Act 5 - Barricade',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 111001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act5Topography).toBeDefined()
|
|
const topo = result.stats.act5Topography as Act5TopographyStats
|
|
expect(topo.barricadeWalls).toBeGreaterThan(40)
|
|
expect(topo.trenchCells).toBeGreaterThan(30)
|
|
|
|
// Verify spiked palisade walls (style 2, prop1 129) and trench floors (style 0/6)
|
|
let foundBarricadeWall = false
|
|
let foundTrench = false
|
|
for (let y = 0; y < result.level.height; y += 1) {
|
|
for (let x = 0; x < result.level.width; x += 1) {
|
|
const cell = result.level.cells[y]![x]!
|
|
if (cell.walls.some(w => w.style === 2 && w.prop1 === 129)) foundBarricadeWall = true
|
|
if (cell.floors.some(f => (f.style === 0 || f.style === 6) && f.prop1 === 2)) foundTrench = true
|
|
}
|
|
}
|
|
expect(foundBarricadeWall).toBe(true)
|
|
expect(foundTrench).toBe(true)
|
|
})
|
|
|
|
test('Arreat Plateau (112) generates snow crater frozen lakes with ice floors', () => {
|
|
const pieces = makeMockBorderPieces('Act 5')
|
|
const result = generateWilderness({
|
|
levelId: 112,
|
|
levelName: 'Arreat Plateau',
|
|
levelTypeName: 'Act 5 - Barricade',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 112001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.act5Topography).toBeDefined()
|
|
const topo = result.stats.act5Topography as Act5TopographyStats
|
|
expect(topo.frozenLakeCells).toBeGreaterThan(20)
|
|
|
|
let foundIceFloor = false
|
|
for (let y = 0; y < result.level.height; y += 1) {
|
|
for (let x = 0; x < result.level.width; x += 1) {
|
|
const cell = result.level.cells[y]![x]!
|
|
if (cell.floors.some(f => f.style === 2 && f.sequence === 0 && f.prop1 === 2)) foundIceFloor = true
|
|
}
|
|
}
|
|
expect(foundIceFloor).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Phase D2: Organic Geometries & Fractional Jitter (Milestone M2)', () => {
|
|
function computePolarIsoperimetricQuotient(
|
|
radiusFn: (theta: number) => number,
|
|
steps: number = 1000,
|
|
): { area: number; perimeter: number; quotient: number } {
|
|
const dTheta = (2 * Math.PI) / steps
|
|
let area = 0
|
|
let perimeter = 0
|
|
for (let i = 0; i < steps; i += 1) {
|
|
const t1 = i * dTheta
|
|
const t2 = (i + 1) * dTheta
|
|
const r1 = radiusFn(t1)
|
|
const r2 = radiusFn(t2)
|
|
area += 0.5 * r1 * r2 * Math.sin(dTheta)
|
|
const x1 = r1 * Math.cos(t1)
|
|
const y1 = r1 * Math.sin(t1)
|
|
const x2 = r2 * Math.cos(t2)
|
|
const y2 = r2 * Math.sin(t2)
|
|
perimeter += Math.hypot(x2 - x1, y2 - y1)
|
|
}
|
|
const quotient = (perimeter * perimeter) / area
|
|
return { area, perimeter, quotient }
|
|
}
|
|
|
|
test('Act 2 Oasis satisfies organic isoperimetric quotient P^2/A > 4pi * 1.15', () => {
|
|
const threshold = 4 * Math.PI * 1.15
|
|
|
|
// 1. Analytical contour integration across various phase angles
|
|
const testPhases = [
|
|
[0, 0],
|
|
[0.5, 1.2],
|
|
[Math.PI / 3, Math.PI / 4],
|
|
[2.1, 0.9],
|
|
]
|
|
for (const [phi1, phi2] of testPhases) {
|
|
const { quotient } = computePolarIsoperimetricQuotient(
|
|
theta => act2OasisRadius(10, theta, phi1, phi2),
|
|
1000,
|
|
)
|
|
expect(quotient).toBeGreaterThan(threshold)
|
|
}
|
|
|
|
// 2. Discrete raster grid verification on generated Far Oasis level
|
|
const pieces = makeMockBorderPieces('Act 2')
|
|
const result = generateWilderness({
|
|
levelId: 43,
|
|
levelName: 'Far Oasis',
|
|
levelTypeName: 'Act 2 - Desert',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 43002,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
const cells = result.level.cells
|
|
const W = result.level.width
|
|
const H = result.level.height
|
|
const isOasis = (x: number, y: number): boolean => {
|
|
if (x < 0 || x >= W || y < 0 || y >= H) return false
|
|
return cells[y]![x]!.floors.some(f => f.style === 2 && f.prop1 === 2)
|
|
}
|
|
|
|
let area = 0
|
|
let perimeter = 0
|
|
for (let y = 0; y < H; y += 1) {
|
|
for (let x = 0; x < W; x += 1) {
|
|
if (isOasis(x, y)) {
|
|
area += 1
|
|
let exposedEdges = 0
|
|
if (!isOasis(x + 1, y)) exposedEdges += 1
|
|
if (!isOasis(x - 1, y)) exposedEdges += 1
|
|
if (!isOasis(x, y + 1)) exposedEdges += 1
|
|
if (!isOasis(x, y - 1)) exposedEdges += 1
|
|
perimeter += exposedEdges
|
|
}
|
|
}
|
|
}
|
|
expect(area).toBeGreaterThan(15)
|
|
const discreteQuotient = (perimeter * perimeter) / area
|
|
expect(discreteQuotient).toBeGreaterThan(threshold)
|
|
})
|
|
|
|
test('Act 5 Frozen Lake satisfies organic isoperimetric quotient P^2/A > 4pi * 1.15', () => {
|
|
const threshold = 4 * Math.PI * 1.15
|
|
|
|
// 1. Analytical contour integration across various phase angles
|
|
const testPhases = [
|
|
[0, 0],
|
|
[0.7, 1.5],
|
|
[Math.PI / 2, Math.PI / 6],
|
|
[1.8, 2.3],
|
|
]
|
|
for (const [phi1, phi2] of testPhases) {
|
|
const { quotient } = computePolarIsoperimetricQuotient(
|
|
theta => act5FrozenLakeRadius(10, theta, phi1, phi2),
|
|
1000,
|
|
)
|
|
expect(quotient).toBeGreaterThan(threshold)
|
|
}
|
|
|
|
// 2. Discrete raster grid verification on generated Arreat Plateau level
|
|
const pieces = makeMockBorderPieces('Act 5')
|
|
const result = generateWilderness({
|
|
levelId: 112,
|
|
levelName: 'Arreat Plateau',
|
|
levelTypeName: 'Act 5 - Barricade',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 112002,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
const cells = result.level.cells
|
|
const W = result.level.width
|
|
const H = result.level.height
|
|
const isLake = (x: number, y: number): boolean => {
|
|
if (x < 0 || x >= W || y < 0 || y >= H) return false
|
|
return cells[y]![x]!.floors.some(f => f.style === 2 && f.sequence === 0 && f.prop1 === 2)
|
|
}
|
|
|
|
let area = 0
|
|
let perimeter = 0
|
|
for (let y = 0; y < H; y += 1) {
|
|
for (let x = 0; x < W; x += 1) {
|
|
if (isLake(x, y)) {
|
|
area += 1
|
|
let exposedEdges = 0
|
|
if (!isLake(x + 1, y)) exposedEdges += 1
|
|
if (!isLake(x - 1, y)) exposedEdges += 1
|
|
if (!isLake(x, y + 1)) exposedEdges += 1
|
|
if (!isLake(x, y - 1)) exposedEdges += 1
|
|
perimeter += exposedEdges
|
|
}
|
|
}
|
|
}
|
|
expect(area).toBeGreaterThan(20)
|
|
const discreteQuotient = (perimeter * perimeter) / area
|
|
expect(discreteQuotient).toBeGreaterThan(threshold)
|
|
})
|
|
|
|
test('Act 4 Lava Fissure guarantees >= 90% walkable flood-fill connectivity', () => {
|
|
const pieces = makeMockBorderPieces('Act 4')
|
|
const testSeeds = [104001, 104002, 104003, 104004]
|
|
|
|
for (const seed of testSeeds) {
|
|
const result = generateWilderness({
|
|
levelId: 104,
|
|
levelName: 'Outer Steppes',
|
|
levelTypeName: 'Act 4 - Mesa',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed,
|
|
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]!
|
|
return cell.walls.some(w => w.prop1 !== 0 || w.style !== 0)
|
|
}
|
|
const isWalkable = (x: number, y: number): boolean => !hasWall(x, y)
|
|
|
|
// Find all walkable cells in the playable interior [8..W-8] x [8..H-8]
|
|
let totalWalkable = 0
|
|
let startX = -1
|
|
let startY = -1
|
|
for (let y = 8; y < H - 8; y += 1) {
|
|
for (let x = 8; x < W - 8; x += 1) {
|
|
if (isWalkable(x, y)) {
|
|
totalWalkable += 1
|
|
if (startX === -1 && y < 16) {
|
|
startX = x
|
|
startY = y
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(totalWalkable).toBeGreaterThan(1000)
|
|
expect(startX).toBeGreaterThanOrEqual(8)
|
|
|
|
// BFS flood-fill from starting walkable cell
|
|
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 / totalWalkable
|
|
expect(reachableRatio).toBeGreaterThanOrEqual(0.90)
|
|
}
|
|
})
|
|
|
|
test('Act 5 Barricade palisades cut horizontally across X when height > width with gate at hubX', () => {
|
|
const pieces = makeMockBorderPieces('Act 5')
|
|
|
|
// 1. Vertical corridor (height > width): palisades run horizontally across X
|
|
const vertResult = generateWilderness({
|
|
levelId: 111,
|
|
levelName: 'Frigid Highlands',
|
|
levelTypeName: 'Act 5 - Barricade',
|
|
sizeX: 48,
|
|
sizeY: 160,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 111001,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
const vW = vertResult.level.width
|
|
const vH = vertResult.level.height
|
|
const vGridW = Math.floor(48 / 8)
|
|
const vInX = Math.floor(vGridW / 2)
|
|
const vHubX = vInX * 8 + 4
|
|
|
|
// Locate horizontal barricade rows (at least 15 palisade wall tiles across the row)
|
|
const horizontalBarricadeRows: number[] = []
|
|
for (let y = 8; y < vH - 8; y += 1) {
|
|
let count = 0
|
|
for (let x = 8; x < vW - 8; x += 1) {
|
|
const cell = vertResult.level.cells[y]![x]!
|
|
if (cell.walls.some(w => w.style === 2 && w.prop1 === 129)) {
|
|
count += 1
|
|
}
|
|
}
|
|
if (count >= 15) {
|
|
horizontalBarricadeRows.push(y)
|
|
}
|
|
}
|
|
|
|
expect(horizontalBarricadeRows.length).toBeGreaterThanOrEqual(2)
|
|
|
|
// Verify each horizontal barricade row has an open choke-point gate at vHubX
|
|
for (const by of horizontalBarricadeRows) {
|
|
const gateCell = vertResult.level.cells[by]![vHubX]!
|
|
const hasWall = gateCell.walls.some(w => w.prop1 !== 0 || w.style !== 0)
|
|
expect(hasWall).toBe(false)
|
|
expect(gateCell.floors.some(f => (f.style === 0 || f.style === 6) && f.prop1 === 2)).toBe(true)
|
|
}
|
|
|
|
// 2. Horizontal corridor (width >= height): palisades run vertically across Y
|
|
const horizResult = generateWilderness({
|
|
levelId: 111,
|
|
levelName: 'Frigid Highlands',
|
|
levelTypeName: 'Act 5 - Barricade',
|
|
sizeX: 160,
|
|
sizeY: 48,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: 111002,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
const hW = horizResult.level.width
|
|
const hH = horizResult.level.height
|
|
const hGridH = Math.floor(48 / 8)
|
|
const hOutY = Math.floor(hGridH / 2)
|
|
const hHubY = hOutY * 8 + 4
|
|
|
|
const verticalBarricadeCols: number[] = []
|
|
for (let x = 8; x < hW - 8; x += 1) {
|
|
let count = 0
|
|
for (let y = 8; y < hH - 8; y += 1) {
|
|
const cell = horizResult.level.cells[y]![x]!
|
|
if (cell.walls.some(w => w.style === 2 && w.prop1 === 129)) {
|
|
count += 1
|
|
}
|
|
}
|
|
if (count >= 15) {
|
|
verticalBarricadeCols.push(x)
|
|
}
|
|
}
|
|
|
|
expect(verticalBarricadeCols.length).toBeGreaterThanOrEqual(2)
|
|
|
|
// Verify each vertical barricade col has an open choke-point gate at hHubY
|
|
for (const bx of verticalBarricadeCols) {
|
|
const gateCell = horizResult.level.cells[hHubY]![bx]!
|
|
const hasWall = gateCell.walls.some(w => w.prop1 !== 0 || w.style !== 0)
|
|
expect(hasWall).toBe(false)
|
|
expect(gateCell.floors.some(f => (f.style === 0 || f.style === 6) && f.prop1 === 2)).toBe(true)
|
|
}
|
|
})
|
|
|
|
test('Topographical jitter is bounded within +-0.05 and varies across seeds', () => {
|
|
const rng = new Rng(0x98765432)
|
|
const samples: number[] = []
|
|
for (let i = 0; i < 300; i += 1) {
|
|
const val = jitterFrac(rng, 0.50, 0.05)
|
|
expect(val).toBeGreaterThanOrEqual(0.45)
|
|
expect(val).toBeLessThanOrEqual(0.55)
|
|
samples.push(val)
|
|
}
|
|
|
|
const mean = samples.reduce((a, b) => a + b, 0) / samples.length
|
|
expect(Math.abs(mean - 0.50)).toBeLessThan(0.02)
|
|
const min = Math.min(...samples)
|
|
const max = Math.max(...samples)
|
|
expect(max - min).toBeGreaterThan(0.08)
|
|
|
|
// Verify that across different seeds, generated topographical feature counts vary organically
|
|
const pieces = makeMockBorderPieces('Act 2')
|
|
const seeds = [43001, 43002, 43003, 43004, 43005]
|
|
const results = seeds.map(s =>
|
|
generateWilderness({
|
|
levelId: 43,
|
|
levelName: 'Far Oasis',
|
|
levelTypeName: 'Act 2 - Desert',
|
|
sizeX: 80,
|
|
sizeY: 80,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed: s,
|
|
pieces,
|
|
substitutions: [],
|
|
}),
|
|
)
|
|
|
|
const sandTrailStats = results.map(
|
|
r => (r.stats.act2Topography as Act2TopographyStats).sandTrailCells,
|
|
)
|
|
const oasisPoolStats = results.map(
|
|
r => (r.stats.act2Topography as Act2TopographyStats).oasisPoolCells,
|
|
)
|
|
|
|
// Confirm organic variation across runs
|
|
const uniqueSandTrails = new Set(sandTrailStats)
|
|
const uniqueOasisPools = new Set(oasisPoolStats)
|
|
expect(uniqueSandTrails.size).toBeGreaterThan(1)
|
|
expect(uniqueOasisPools.size).toBeGreaterThan(1)
|
|
})
|
|
})
|
|
})
|
|
|