402 lines
12 KiB
TypeScript
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)
|
|
})
|
|
})
|