diablo2-web/tests/wilderness-jungles.test.ts

402 lines
12 KiB
TypeScript

import { describe, expect, test } from 'vitest'
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
import {
generateWilderness,
UNIMPLEMENTED_PASSES,
buildJungleTopology,
DRLG_GenerateJungles,
type WildernessPiece,
type WildernessRequest,
} from '../src/game/wilderness.ts'
import { Rng } from '../src/game/rng.ts'
/**
* Creates a mock DS1 piece of arbitrary size (e.g. 32x32 for jungle macro-cells, 64x32 for head/tail).
*/
function makeMockDs1(width: number, height: number, isWater = false): 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: isWater ? 194 : 2,
sequence: 0,
style: isWater ? 1 : 0,
unknown1: 0,
unknown2: 0,
hidden: false,
},
],
shadows: [],
substitutions: [],
})
}
cells.push(row)
}
return {
version: 18,
width,
height,
act: 3,
substitutionType: 0,
wallLayers: 1,
floorLayers: 1,
cells,
objects: [],
npcPathOffset: null,
}
}
/**
* Creates a complete mock suite of Act 3 Jungle pieces.
*/
function makeMockJunglePieces(): WildernessPiece[] {
const pieces: WildernessPiece[] = []
// 1. Head (TransL.ds1, 64x32) and Tail (TravL.ds1, 64x32)
pieces.push({
name: 'Act 3 - Jungle Head',
border: false,
levels: [makeMockDs1(64, 32)],
})
pieces.push({
name: 'Act 3 - Jungle Tail',
border: false,
levels: [makeMockDs1(64, 32)],
})
// 2. Wooden Bridge (Act 3 - Bridge, 32x32)
pieces.push({
name: 'Act 3 - Bridge',
border: false,
levels: [makeMockDs1(32, 32)],
})
// 3. Directional River Presets (32x32)
const riverTokens = [
'NS', 'EW', 'NE', 'NW', 'SE', 'SW',
'NSE', 'NSW', 'NEW', 'SEW', 'NSEW',
'NS W', 'NS E', 'EW N', 'EW S',
]
for (const token of riverTokens) {
pieces.push({
name: `Act 3 - Jungle ${token}`,
border: false,
levels: [makeMockDs1(32, 32, true)],
})
}
// 4. Clearings for each of the 3 jungle areas (32x32)
const clearingThemes = ['Webby', 'Boggy', 'Pygmy']
const openings = ['', ' E', ' W', ' EW', ' NE', ' NW', ' SE', ' SW']
for (const theme of clearingThemes) {
for (const opening of openings) {
pieces.push({
name: `Act 3 - Clearing ${theme}${opening}`,
border: false,
levels: [makeMockDs1(32, 32)],
})
}
}
return pieces
}
describe('Act 3 Jungle Branching Topology (buildJungleTopology)', () => {
test('guarantees Flayer Jungle (78) is always reachable from Spider Forest (76)', () => {
// Test across 100 random seeds
for (let seed = 1; seed <= 100; seed += 1) {
const topology = buildJungleTopology(seed)
expect(topology.zones).toHaveLength(3)
const spider = topology.zones.find(z => z.levelId === 76)!
const marsh = topology.zones.find(z => z.levelId === 77)!
const flayer = topology.zones.find(z => z.levelId === 78)!
expect(spider.name).toBe('Spider Forest')
expect(marsh.name).toBe('Great Marsh')
expect(flayer.name).toBe('Flayer Jungle')
// Check graph reachability: BFS from 76 to 78
const visited = new Set<number>([76])
const queue = [76]
while (queue.length > 0) {
const curr = queue.shift()!
const outgoing = topology.connections.filter(c => c.fromLevelId === curr)
for (const conn of outgoing) {
if (!visited.has(conn.toLevelId)) {
visited.add(conn.toLevelId)
queue.push(conn.toLevelId)
}
}
}
expect(visited.has(78)).toBe(true)
}
})
test('exhibits both Great Marsh skip active and inactive across different seeds', () => {
let skipsSeen = 0
let noSkipsSeen = 0
let marshDeadEndsSeen = 0
let marshThroughwaysSeen = 0
for (let seed = 1; seed <= 100; seed += 1) {
const topology = buildJungleTopology(seed)
if (topology.hasMarshSkip) {
skipsSeen += 1
const directSkipConn = topology.connections.find(
c => c.fromLevelId === 76 && c.toLevelId === 78,
)
expect(directSkipConn).toBeDefined()
expect(directSkipConn!.connectionType).toBe('skip')
} else {
noSkipsSeen += 1
// Without skip, Great Marsh must connect forward to Flayer Jungle
const marshToFlayer = topology.connections.find(
c => c.fromLevelId === 77 && c.toLevelId === 78,
)
expect(marshToFlayer).toBeDefined()
expect(topology.marshIsDeadEnd).toBe(false)
}
if (topology.marshIsDeadEnd) {
marshDeadEndsSeen += 1
expect(topology.hasMarshSkip).toBe(true)
} else {
marshThroughwaysSeen += 1
}
}
expect(skipsSeen).toBeGreaterThan(0)
expect(noSkipsSeen).toBeGreaterThan(0)
expect(marshDeadEndsSeen).toBeGreaterThan(0)
expect(marshThroughwaysSeen).toBeGreaterThan(0)
})
})
describe('DRLG_GenerateJungles River Network Generation', () => {
test('generates meandering river paths that wander across columns', () => {
const pieces = makeMockJunglePieces()
const request: WildernessRequest = {
levelId: 76,
levelName: 'Spider Forest',
levelTypeName: 'Act 3 - Jungle',
seed: 42,
sizeX: 64,
sizeY: 192,
subType: 0,
subTheme: 0,
pieces,
substitutions: [],
}
const result = generateWilderness(request)
const layout = result.stats.jungleLayout as any
expect(layout).toBeDefined()
expect(layout.riverNetwork).toBeDefined()
const { meanderingPath, meanderCount } = layout.riverNetwork
expect(meanderingPath.length).toBeGreaterThanOrEqual(6)
expect(meanderCount).toBeGreaterThanOrEqual(1)
// Verify that the river visits multiple columns
const columnsVisited = new Set(meanderingPath.map((p: any) => p.col))
expect(columnsVisited.size).toBeGreaterThanOrEqual(2)
})
test('places river forks / tributary junctions with branching presets', () => {
const pieces = makeMockJunglePieces()
const request: WildernessRequest = {
levelId: 77,
levelName: 'Great Marsh',
levelTypeName: 'Act 3 - Jungle',
seed: 12345,
sizeX: 64,
sizeY: 192,
subType: 0,
subTheme: 0,
pieces,
substitutions: [],
}
const result = generateWilderness(request)
const layout = result.stats.jungleLayout as any
expect(layout.riverNetwork.forks.length).toBeGreaterThan(0)
const fork = layout.riverNetwork.forks[0]!
expect(fork.row).toBeGreaterThanOrEqual(1)
expect(fork.row).toBeLessThanOrEqual(4)
expect(result.stats.riverForks).toBe(layout.riverNetwork.forks.length)
})
test('identifies and creates river island clearings flanked by river channels', () => {
const pieces = makeMockJunglePieces()
const request: WildernessRequest = {
levelId: 76,
levelName: 'Spider Forest',
levelTypeName: 'Act 3 - Jungle',
seed: 999,
sizeX: 64,
sizeY: 192,
subType: 0,
subTheme: 0,
pieces,
substitutions: [],
}
const result = generateWilderness(request)
const layout = result.stats.jungleLayout as any
expect(layout.riverNetwork.islands.length).toBeGreaterThan(0)
const island = layout.riverNetwork.islands[0]!
expect(island.col).toBeGreaterThanOrEqual(0)
expect(island.row).toBeGreaterThanOrEqual(1)
expect(result.stats.riverIslands).toBe(layout.riverNetwork.islands.length)
// Check that clearings array flags the island
const islandClearing = layout.riverNetwork.clearings.find(
(c: any) => c.col === island.col && c.row === island.row,
)
expect(islandClearing).toBeDefined()
expect(islandClearing.isIsland).toBe(true)
})
test('places wooden bridges crossing river channels to connect islands and trails', () => {
const pieces = makeMockJunglePieces()
const request: WildernessRequest = {
levelId: 78,
levelName: 'Flayer Jungle',
levelTypeName: 'Act 3 - Jungle',
seed: 777,
sizeX: 64,
sizeY: 192,
subType: 0,
subTheme: 0,
pieces,
substitutions: [],
}
const result = generateWilderness(request)
const layout = result.stats.jungleLayout as any
expect(layout.riverNetwork.bridges.length).toBeGreaterThan(0)
// Bridge recorded in stats.substitutions and special presets
const bridgeSub = (result.stats.substitutions as any[]).find(
s => s.name === 'Act 3 - Bridge',
)
expect(bridgeSub).toBeDefined()
expect(bridgeSub.enabled).toBe(true)
expect(result.stats.bridgesPlaced).toBe(layout.riverNetwork.bridges.length)
// Bridge coordinates must align to grid cells
for (const bridge of layout.riverNetwork.bridges) {
expect(bridge.x).toBe(bridge.col * 32)
expect(bridge.y).toBe(bridge.row * 32)
expect(bridge.pieceName).toBe('Act 3 - Bridge')
}
})
})
describe('End-to-End generateWilderness Integration for Act 3 Jungles', () => {
test('DRLG_GenerateJungles is removed from UNIMPLEMENTED_PASSES', () => {
expect(UNIMPLEMENTED_PASSES).not.toContain('DRLG_GenerateJungles')
})
test('generates Spider Forest (76) with Webby clearings and complete jungle metadata', () => {
const pieces = makeMockJunglePieces()
const result = generateWilderness({
levelId: 76,
levelName: 'Spider Forest',
levelTypeName: 'Act 3 - Jungle',
seed: 555,
sizeX: 64,
sizeY: 192,
subType: 0,
subTheme: 0,
pieces,
substitutions: [],
})
expect(result.level.width).toBe(64)
expect(result.level.height).toBe(192)
expect(result.level.act).toBe(3)
const subs = result.stats.substitutions as any[]
expect(subs.some(s => s.name.startsWith('Act 3 - Clearing Webby'))).toBe(true)
expect(subs.some(s => s.name === 'Act 3 - Jungle Tail')).toBe(true)
expect(subs.some(s => s.name === 'Act 3 - Jungle Head')).toBe(true)
expect(subs.some(s => s.name === 'Act 3 - Bridge')).toBe(true)
expect(result.stats.jungleLayout).toBeDefined()
expect(result.stats.jungleTopology).toBeDefined()
})
test('generates Great Marsh (77) with Boggy clearings', () => {
const pieces = makeMockJunglePieces()
const result = generateWilderness({
levelId: 77,
levelName: 'Great Marsh',
levelTypeName: 'Act 3 - Jungle',
seed: 888,
sizeX: 64,
sizeY: 192,
subType: 0,
subTheme: 0,
pieces,
substitutions: [],
})
const subs = result.stats.substitutions as any[]
expect(subs.some(s => s.name.startsWith('Act 3 - Clearing Boggy'))).toBe(true)
})
test('generates Flayer Jungle (78) with Pygmy clearings', () => {
const pieces = makeMockJunglePieces()
const result = generateWilderness({
levelId: 78,
levelName: 'Flayer Jungle',
levelTypeName: 'Act 3 - Jungle',
seed: 999,
sizeX: 64,
sizeY: 192,
subType: 0,
subTheme: 0,
pieces,
substitutions: [],
})
const subs = result.stats.substitutions as any[]
expect(subs.some(s => s.name.startsWith('Act 3 - Clearing Pygmy'))).toBe(true)
})
test('supports wider 3-column (96x192) jungle layouts', () => {
const pieces = makeMockJunglePieces()
const result = generateWilderness({
levelId: 76,
levelName: 'Spider Forest',
levelTypeName: 'Act 3 - Jungle',
seed: 1234,
sizeX: 96,
sizeY: 192,
subType: 0,
subTheme: 0,
pieces,
substitutions: [],
})
expect(result.level.width).toBe(96)
expect(result.level.height).toBe(192)
const layout = result.stats.jungleLayout as any
expect(layout.gridColumns).toBe(3)
expect(layout.gridRows).toBe(6)
expect(layout.riverNetwork.meanderCount).toBeGreaterThanOrEqual(1)
})
})