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([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) }) })