import { describe, expect, test } from "vitest" import { isClosedBorder, selectBorderVariant, getPerimeterOpenings, SPECIAL_PRESETS_BY_LEVEL, generateWilderness, resolveUnsetSize, UnresolvedLevelSizeError, } from "../src/game/wilderness.ts" import type { WildernessPiece } from "../src/game/wilderness.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): 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: 1, type: 0, unknown1: 0, unknown2: 0, hidden: false }], floors: [{ prop1: 2, sequence: 0, style: 1, 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, } } describe("Wilderness Features & Border Rules", () => { test("isClosedBorder identifies continuous vs open wall presets", () => { const closed = makeMockDs1(8, 8, false) const open = makeMockDs1(8, 8, true) expect(isClosedBorder(closed, "south")).toBe(true) expect(isClosedBorder(closed, "north")).toBe(true) expect(isClosedBorder(open, "south")).toBe(false) expect(isClosedBorder(open, "north")).toBe(false) }) test("getPerimeterOpenings enforces 0 exits for Cow Level and 1 entrance/1 exit for Blood Moor", () => { const cowOpenings = getPerimeterOpenings(39, 10, 10) expect(cowOpenings.size).toBe(0) const bloodMoorOpenings = getPerimeterOpenings(2, 10, 10) expect(bloodMoorOpenings.size).toBe(2) expect(bloodMoorOpenings.has("5,9")).toBe(true) expect(bloodMoorOpenings.has("9,5")).toBe(true) }) test("getPerimeterOpenings defaults to open borders for Act 3 Jungles and outdoor levels", () => { // Act 3: Spider Forest (76), Great Marsh (77), Flayer Jungle (78) const spiderForestOpenings = getPerimeterOpenings(76, 10, 10) expect(spiderForestOpenings.size).toBe(2) expect(spiderForestOpenings.has("5,9")).toBe(true) expect(spiderForestOpenings.has("9,5")).toBe(true) // Act 5: Arreat Plateau (113) const arreatOpenings = getPerimeterOpenings(113, 10, 10) expect(arreatOpenings.size).toBe(2) }) test("getPerimeterOpenings maps planned gates to perimeter block coordinates", () => { const plannedGates = [ { side: 'north' as const, toLevelId: 75, label: 'To Town' }, { side: 'east' as const, toLevelId: 77, label: 'To Marsh' }, ] const openings = getPerimeterOpenings(76, 12, 14, plannedGates) expect(openings.size).toBe(2) expect(openings.has("6,0")).toBe(true) // north: (12/2, 0) expect(openings.has("11,7")).toBe(true) // east: (12-1, 14/2) }) test("selectBorderVariant picks open variant for openings and closed variant for closed walls", () => { const closedDs1 = makeMockDs1(8, 8, false) const openDs1 = makeMockDs1(8, 8, true) const piece: WildernessPiece = { name: "Act 1 - Wild Border 1", border: true, levels: [closedDs1, openDs1], } const rng = new Rng(12345) const pickedOpen = selectBorderVariant(piece, "south", true, rng) expect(isClosedBorder(pickedOpen, "south")).toBe(false) const pickedClosed = selectBorderVariant(piece, "south", false, rng) expect(isClosedBorder(pickedClosed, "south")).toBe(true) }) test("SPECIAL_PRESETS_BY_LEVEL contains stone walls and river presets", () => { const bloodMoorPresets = SPECIAL_PRESETS_BY_LEVEL[2] expect(bloodMoorPresets).toBeDefined() expect(bloodMoorPresets).toContain("Act 1 - Corral Fill") expect(bloodMoorPresets).toContain("Act 1 - Fence Fill 4") expect(bloodMoorPresets).toContain("Act 1 - River Upper") expect(bloodMoorPresets).toContain("Act 1 - Bridge") const cowPresets = SPECIAL_PRESETS_BY_LEVEL[39] expect(cowPresets).toBeDefined() expect(cowPresets).toContain("Act 1 - Corral Fill") expect(cowPresets).toContain("Act 1 - Fence Fill 4") expect(cowPresets).toContain("Act 1 - Fence Fill 5") expect(cowPresets).toContain("Act 1 - Bivouac") expect(cowPresets).toContain("Act 1 - Pond") }) test("generateWilderness generates Level 2 with river or pond deterministically by seed", () => { const closedDs1 = makeMockDs1(8, 8, false) const piece: WildernessPiece = { name: "Act 1 - Wild Border 1", border: true, levels: [closedDs1], } const riverPiece: WildernessPiece = { name: "Act 1 - River Upper", border: false, levels: [closedDs1], } const pondPiece: WildernessPiece = { name: "Act 1 - Pond", border: false, levels: [closedDs1], } const corralPiece: WildernessPiece = { name: "Act 1 - Corral Fill", border: false, levels: [closedDs1], } const result = generateWilderness({ levelId: 2, levelName: "Blood Moor", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 12345, pieces: [piece, riverPiece, pondPiece, corralPiece], substitutions: [], }) expect(result.stats.blockGrid).toEqual({ width: 10, height: 10 }) expect(result.level.width).toBe(80) expect(result.level.height).toBe(80) }) test("Act 1 Wilderness generates interior stone walls and dirt road network", () => { const closedDs1 = makeMockDs1(8, 8, false) const piece: WildernessPiece = { name: "Act 1 - Wild Border 1", border: true, levels: [closedDs1], } const result = generateWilderness({ levelId: 3, levelName: "Cold Plains", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 99999, pieces: [piece], substitutions: [], }) let stoneWallCount = 0 let dirtRoadCount = 0 for (let y = 8; y < 72; y += 1) { for (let x = 8; x < 72; x += 1) { const cell = result.level.cells[y]?.[x] if (!cell) continue if (cell.walls.some(w => w.style === 2 && w.prop1 === 129)) { stoneWallCount += 1 } if (cell.floors.some(f => (f.style === 0 || f.style === 5) && f.prop1 === 194)) { dirtRoadCount += 1 } } } expect(stoneWallCount).toBe(0) expect(dirtRoadCount).toBeGreaterThan(30) }) test("Act 5 Bloody Foothills (Level 110) stamps 15 canonical siege strips in West-to-East order", () => { const stripNames = [ "Act 5 - Siege To Barricade", "Act 5 - Siege Strip 13", "Act 5 - Siege Strip 12", "Act 5 - Siege Strip 11", "Act 5 - Siege Strip 10", "Act 5 - Siege Strip 9", "Act 5 - Siege Strip 8", "Act 5 - Siege Strip 7", "Act 5 - Siege Strip 6", "Act 5 - Siege Strip 5", "Act 5 - Siege Strip 4", "Act 5 - Siege Strip 3", "Act 5 - Siege Strip 2", "Act 5 - Siege Strip 1", "Act 5 - Siege To Town", ] const pieces: WildernessPiece[] = stripNames.map((name, idx) => { const baseDs1 = makeMockDs1(17, 49, false) // Create a copy with tagged floor sequence at (0,0) and one object const cells = baseDs1.cells.map((row, y) => row.map((cell, x) => x === 0 && y === 0 ? { ...cell, floors: [{ ...cell.floors[0], sequence: idx + 10 }], } : cell, ), ) const ds1: Ds1 = { ...baseDs1, cells, objects: [ { type: 2, id: 100 + idx, x: 5, y: 5, flags: 0, }, ], } return { name, border: false, levels: [ds1], } }) const result = generateWilderness({ levelId: 110, levelName: "Bloody Foothills", levelTypeName: "Act 5 - Siege", sizeX: 240, sizeY: 48, subType: -1, subTheme: -1, seed: 42, pieces, substitutions: [], }) expect(result.stats.blockGrid).toEqual({ width: 30, height: 6 }) expect(result.stats.substitutedClusters).toBe(15) expect(result.level.width).toBe(240) expect(result.level.height).toBe(48) expect(result.level.objects.filter(o => o.type === 2).length).toBe(15) expect(result.level.objects.length).toBe(17) // Verify strip 0 (Siege To Barricade) is at x=0 and strip 14 (Siege To Town) is at x=224 expect(result.level.cells[0][0].floors[0].sequence).toBe(10) expect(result.level.cells[0][224].floors[0].sequence).toBe(24) }) test("Act 3 Travincal (Level 83) stamps all 6 temple complex pieces even with trailing spaces", () => { const names = [ "Act 3 - Travincal NW", "Act 3 - Travincal N ", // trailing space as in LvlPrest.txt "Act 3 - Travincal NE", "Act 3 - Travincal SW", "Act 3 - Travincal S ", // trailing space as in LvlPrest.txt "Act 3 - Travincal SE", ] const pieces: WildernessPiece[] = names.map((name, idx) => { const baseDs1 = makeMockDs1(name.includes(" N ") || name.includes(" S ") ? 33 : 17, 33, false) const ds1: Ds1 = { ...baseDs1, objects: [{ type: 2, id: 300 + idx, x: 5, y: 5, flags: 0 }], } return { name, border: false, levels: [ds1] } }) const result = generateWilderness({ levelId: 83, levelName: "Travincal", levelTypeName: "Act 3 - Kurast", sizeX: 64, sizeY: 64, subType: -1, subTheme: -1, seed: 83, pieces, substitutions: [], }) expect(result.level.width).toBe(64) expect(result.level.height).toBe(64) expect(result.stats.substitutedClusters).toBe(6) expect(result.level.objects.length).toBe(6) }) test("Act 1 Blood Moor (Level 2) connects South/East border gates with 3-tile road and uses authentic stone wall sequences", () => { const borderDs1 = makeMockDs1(8, 8, false) const openBorderDs1 = makeMockDs1(8, 8, true) const presetDs1 = makeMockDs1(16, 16, false) const riverDs1 = makeMockDs1(8, 8, false) const pieces: WildernessPiece[] = [ { name: "Act 1 - Wild Border 1", border: true, levels: [borderDs1, borderDs1, borderDs1, openBorderDs1] }, { name: "Act 1 - Wild Border 2", border: true, levels: [borderDs1] }, { name: "Act 1 - Wild Border 3", border: true, levels: [borderDs1] }, { name: "Act 1 - Wild Border 4", border: true, levels: [borderDs1, borderDs1, borderDs1, openBorderDs1] }, { name: "Act 1 - Wild Border 5", border: true, levels: [borderDs1] }, { name: "Act 1 - Wild Border 6", border: true, levels: [borderDs1] }, { name: "Act 1 - Wild Border 7", border: true, levels: [borderDs1] }, { name: "Act 1 - Wild Border 8", border: true, levels: [borderDs1] }, { name: "Act 1 - DOE Entrance", border: false, levels: [presetDs1] }, { name: "Act 1 - River Upper", border: false, levels: [riverDs1, riverDs1, riverDs1] }, { name: "Act 1 - River Lower", border: false, levels: [riverDs1, riverDs1, riverDs1] }, { name: "Act 1 - Bridge", border: false, levels: [riverDs1, riverDs1] }, { name: "Act 1 - Cottages 1", border: false, levels: [presetDs1] }, ] const result = generateWilderness({ levelId: 2, levelName: "Blood Moor", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: -1, subTheme: -1, seed: 1234, pieces, substitutions: [], specialPresets: [ "Act 1 - DOE Entrance", "Act 1 - River Upper", "Act 1 - River Lower", "Act 1 - Bridge", "Act 1 - Cottages 1", ], }) // 1. Verify 3-tile wide main highway at central crossroads (44, 44) expect(result.level.cells[44]![44]!.floors[0]!.style).toBe(0) expect(result.level.cells[44]![43]!.floors[0]!.style).toBe(0) expect(result.level.cells[44]![45]!.floors[0]!.style).toBe(0) // 2. Verify South border gate road connection at (44, 75) and East border gate connection at (75, 44) expect(result.level.cells[75]![44]!.floors[0]!.style).toBe(0) expect(result.level.cells[44]![75]!.floors[0]!.style).toBe(0) // 3. Verify no artificial interior stone wall segments (style=2, prop1=129) are stamped (Issue #65) let foundContinuousEW = false let foundEndCapEW = false for (let y = 10; y < 70; y += 1) { for (let x = 10; x < 70; x += 1) { const wall = result.level.cells[y]![x]!.walls.find(w => w.style === 2 && w.prop1 === 129) if (wall && wall.type === 2) { if (wall.sequence === 0) foundContinuousEW = true if (wall.sequence === 1 || wall.sequence === 2) foundEndCapEW = true } } } expect(foundContinuousEW).toBe(false) expect(foundEndCapEW).toBe(false) }) test("records unresolved presets and notes when requested preset piece is missing", () => { const borderDs1 = makeMockDs1(8, 8, false) const piece: WildernessPiece = { name: "Act 1 - Wild Border 1", border: true, levels: [borderDs1], } const result = generateWilderness({ levelId: 2, levelName: "Blood Moor", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 0, subTheme: 0, seed: 9999, pieces: [piece], // Missing Den of Evil Entrance preset! substitutions: [], }) const unresolved = result.stats.unresolved as string[] const notes = result.stats.notes as string[] expect(unresolved.length).toBeGreaterThan(0) expect(unresolved.some((u: string) => u.includes('Act 1 - DOE Entrance'))).toBe(true) expect(notes.some((n: string) => n.includes('Den of Evil Entrance'))).toBe(true) }) test("resolveUnsetSize dynamically derives vertical corridor dimensions for Level 111 and 112", () => { const entranceDs1 = makeMockDs1(17, 33, false) const exitDs1 = makeMockDs1(17, 33, false) const wallDs1 = makeMockDs1(17, 17, false) const cliffDs1 = makeMockDs1(17, 17, false) 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 resolved = resolveUnsetSize({ levelId, levelName: `Act 5 - Barricade ${levelId - 110}`, levelTypeName: "Act 5 - Barricade", sizeX: -1, sizeY: -1, subType: 10, subTheme: 0, seed: 12345, pieces, substitutions: [], }) expect(resolved.sizeX).toBeGreaterThanOrEqual(48) expect(resolved.sizeX).toBeLessThanOrEqual(64) expect(resolved.sizeY).toBeGreaterThanOrEqual(160) expect(resolved.sizeY).toBeLessThanOrEqual(192) expect(resolved.sizeY).toBeGreaterThan(resolved.sizeX) expect(resolved.sizeY / resolved.sizeX).toBeGreaterThanOrEqual(2.5) expect(resolved.source).toBe("LvlPrest Barricade corridor dynamic extent") } }) test("resolveUnsetSize throws UnresolvedLevelSizeError when pieces are absent (anti-silent failure)", () => { expect(() => { resolveUnsetSize({ levelId: 111, levelName: "Act 5 - Barricade 1", levelTypeName: "Act 5 - Barricade", sizeX: -1, sizeY: -1, subType: 10, subTheme: 0, seed: 12345, pieces: [], substitutions: [], }) }).toThrow(UnresolvedLevelSizeError) }) test("resolveUnsetSize throws UnresolvedLevelSizeError when non-barricade level lacks barricade pieces", () => { const mockDs1 = makeMockDs1(8, 8, false) expect(() => { resolveUnsetSize({ levelId: 2, levelName: "Blood Moor", levelTypeName: "Act 1 - Wilderness", sizeX: -1, sizeY: -1, subType: 6, subTheme: 0, seed: 12345, pieces: [{ name: "Act 1 - Wild Border 1", border: true, levels: [mockDs1] }], substitutions: [], }) }).toThrow(UnresolvedLevelSizeError) }) test("resolveUnsetSize respects sizeOverride when provided", () => { const resolved = resolveUnsetSize({ levelId: 111, levelName: "Act 5 - Barricade 1", levelTypeName: "Act 5 - Barricade", sizeX: -1, sizeY: -1, sizeOverride: { x: 80, y: 120 }, subType: 10, subTheme: 0, seed: 12345, pieces: [], substitutions: [], }) expect(resolved.sizeX).toBe(80) expect(resolved.sizeY).toBe(120) expect(resolved.source).toBe("sizeOverride") }) })