import { describe, expect, test } from "vitest" import { classifyBorderPiece, getCellOrientation, selectBorderPiece, selectBorderVariant, isClosedBorder, getPerimeterOpenings, DRLGOUTPLACE_PlaceAct1245OutdoorBorders, layBorder, generateWilderness, type WildernessPiece, type WildernessStats, type BorderOrientation, } from "../src/game/wilderness.ts" import { createCanvas } from "../src/game/wilderness.ts" import { Rng } from "../src/game/rng.ts" import type { Ds1, Ds1Cell } from "../src/formats/ds1.ts" function makeMockBorderDs1(width: number, height: number, orientation: BorderOrientation, isOpen: boolean): Ds1 { const cells: Ds1Cell[][] = [] for (let y = 0; y < height; y += 1) { const row: Ds1Cell[] = [] for (let x = 0; x < width; x += 1) { let isWall: boolean if (isOpen) { // Open variant: gap in the middle of the border edge if (orientation === "south" || orientation === "north") { isWall = x !== Math.floor(width / 2) } else if (orientation === "west" || orientation === "east") { isWall = y !== Math.floor(height / 2) } else { isWall = true } } else { // Closed variant: continuous wall along the border isWall = true } row.push({ walls: isWall ? [{ prop1: 129, 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, } } function createEmptyStats(): WildernessStats { return { substitutions: [], borderPieces: {}, unresolved: [], notes: [], borderStamped: 0, groundCells: 0, groundTile: null, sizeSource: "test", roadCells: 0, roadSegments: 0, anchors: 0, specialPresets: [], entrances: [], proceduralRoadCells: 0, proceduralDividerCells: 0, proceduralWaterCells: 0, proceduralWallCells: 0, } } describe("Issue #67: DRLGOUTPLACE_PlaceAct1245OutdoorBorders & Multi-Act Border Classification", () => { describe("Task 1A: classifyBorderPiece across Acts 1, 2, 3, 4, 5", () => { test("Act 1: Wild Border 1..12 and Wild Cliff Border 2..10 (including 6A-C and corner 10)", () => { // Edges: 1=S, 2=W, 3=N, 4=E expect(classifyBorderPiece("Act 1 - Wild Border 1")).toEqual({ orientation: "south", isCliff: false }) expect(classifyBorderPiece("Act 1 - Wild Border 2")).toEqual({ orientation: "west", isCliff: false }) expect(classifyBorderPiece("Act 1 - Wild Border 3")).toEqual({ orientation: "north", isCliff: false }) expect(classifyBorderPiece("Act 1 - Wild Border 4")).toEqual({ orientation: "east", isCliff: false }) // Corners: 5/9=SW, 6/10=NW, 7/11=NE, 8/12=SE expect(classifyBorderPiece("Act 1 - Wild Border 5")).toEqual({ orientation: "corner-sw", isCliff: false }) expect(classifyBorderPiece("Act 1 - Wild Border 6")).toEqual({ orientation: "corner-nw", isCliff: false }) expect(classifyBorderPiece("Act 1 - Wild Border 7")).toEqual({ orientation: "corner-ne", isCliff: false }) expect(classifyBorderPiece("Act 1 - Wild Border 8")).toEqual({ orientation: "corner-se", isCliff: false }) expect(classifyBorderPiece("Act 1 - Wild Border 9")).toEqual({ orientation: "corner-sw", isCliff: false }) expect(classifyBorderPiece("Act 1 - Wild Border 10")).toEqual({ orientation: "corner-nw", isCliff: false }) expect(classifyBorderPiece("Act 1 - Wild Border 11")).toEqual({ orientation: "corner-ne", isCliff: false }) expect(classifyBorderPiece("Act 1 - Wild Border 12")).toEqual({ orientation: "corner-se", isCliff: false }) // Wild Cliff borders: isCliff: true expect(classifyBorderPiece("Act 1 - Wild Cliff Border 2")).toEqual({ orientation: "west", isCliff: true }) expect(classifyBorderPiece("Act 1 - Wild Cliff Border 3")).toEqual({ orientation: "north", isCliff: true }) expect(classifyBorderPiece("Act 1 - Wild Cliff Border 5")).toEqual({ orientation: "corner-sw", isCliff: true }) expect(classifyBorderPiece("Act 1 - Wild Cliff Border 6A")).toEqual({ orientation: "corner-nw", isCliff: true }) expect(classifyBorderPiece("Act 1 - Wild Cliff Border 6B")).toEqual({ orientation: "corner-nw", isCliff: true }) expect(classifyBorderPiece("Act 1 - Wild Cliff Border 6C")).toEqual({ orientation: "corner-nw", isCliff: true }) expect(classifyBorderPiece("Act 1 - Wild Cliff Border 7")).toEqual({ orientation: "corner-ne", isCliff: true }) expect(classifyBorderPiece("Act 1 - Wild Cliff Border 10")).toEqual({ orientation: "corner-nw", isCliff: true }) // Caves are excluded from generic borders expect(classifyBorderPiece("Act 1 - Wild Cliff Cave Right")).toEqual({ orientation: null, isCliff: false }) expect(classifyBorderPiece("Act 1 - Wild Cliff Cave Left")).toEqual({ orientation: null, isCliff: false }) }) test("Act 2: Desert Border 1..12 and Desert Cliff Left/Right/Top/Tomb variants", () => { // Desert Border 1..12 expect(classifyBorderPiece("Act 2 - Desert Border 1")).toEqual({ orientation: "south", isCliff: false }) expect(classifyBorderPiece("Act 2 - Desert Border 2")).toEqual({ orientation: "west", isCliff: false }) expect(classifyBorderPiece("Act 2 - Desert Border 3")).toEqual({ orientation: "north", isCliff: false }) expect(classifyBorderPiece("Act 2 - Desert Border 4")).toEqual({ orientation: "east", isCliff: false }) expect(classifyBorderPiece("Act 2 - Desert Border 5")).toEqual({ orientation: "corner-sw", isCliff: false }) expect(classifyBorderPiece("Act 2 - Desert Border 6")).toEqual({ orientation: "corner-nw", isCliff: false }) expect(classifyBorderPiece("Act 2 - Desert Border 7")).toEqual({ orientation: "corner-ne", isCliff: false }) expect(classifyBorderPiece("Act 2 - Desert Border 8")).toEqual({ orientation: "corner-se", isCliff: false }) expect(classifyBorderPiece("Act 2 - Desert Border 9")).toEqual({ orientation: "corner-sw", isCliff: false }) expect(classifyBorderPiece("Act 2 - Desert Border 10")).toEqual({ orientation: "corner-nw", isCliff: false }) expect(classifyBorderPiece("Act 2 - Desert Border 11")).toEqual({ orientation: "corner-ne", isCliff: false }) expect(classifyBorderPiece("Act 2 - Desert Border 12")).toEqual({ orientation: "corner-se", isCliff: false }) // Desert Cliff Left / Right / Top variants expect(classifyBorderPiece("Act 2 - Desert Cliff Left Wall")).toEqual({ orientation: "west", isCliff: true }) expect(classifyBorderPiece("Act 2 - Desert Cliff Left Path")).toEqual({ orientation: "west", isCliff: true }) expect(classifyBorderPiece("Act 2 - Desert Cliff Left Ends")).toEqual({ orientation: "west", isCliff: true }) expect(classifyBorderPiece("Act 2 - Desert Cliff Left King Tomb")).toEqual({ orientation: "west", isCliff: true }) expect(classifyBorderPiece("Act 2 - Desert Cliff Right Wall")).toEqual({ orientation: "east", isCliff: true }) expect(classifyBorderPiece("Act 2 - Desert Cliff Right Path")).toEqual({ orientation: "east", isCliff: true }) expect(classifyBorderPiece("Act 2 - Desert Cliff Right Ends")).toEqual({ orientation: "east", isCliff: true }) expect(classifyBorderPiece("Act 2 - Desert Cliff Right King Tomb")).toEqual({ orientation: "east", isCliff: true }) expect(classifyBorderPiece("Act 2 - Desert Cliff Top")).toEqual({ orientation: "north", isCliff: true }) expect(classifyBorderPiece("Act 2 - Desert Cliff Top King Tomb")).toEqual({ orientation: "north", isCliff: true }) }) test("Act 3: Slums/Burbs/Metro Border cardinal directions (N, S, E, W, NE, NW, SE, SW)", () => { expect(classifyBorderPiece("Act 3 - Slums Border N")).toEqual({ orientation: "north", isCliff: false }) expect(classifyBorderPiece("Act 3 - Slums Border S")).toEqual({ orientation: "south", isCliff: false }) expect(classifyBorderPiece("Act 3 - Slums Border E")).toEqual({ orientation: "east", isCliff: false }) expect(classifyBorderPiece("Act 3 - Slums Border W")).toEqual({ orientation: "west", isCliff: false }) expect(classifyBorderPiece("Act 3 - Slums Border NE")).toEqual({ orientation: "corner-ne", isCliff: false }) expect(classifyBorderPiece("Act 3 - Slums Border NW")).toEqual({ orientation: "corner-nw", isCliff: false }) expect(classifyBorderPiece("Act 3 - Slums Border SE")).toEqual({ orientation: "corner-se", isCliff: false }) expect(classifyBorderPiece("Act 3 - Slums Border SW")).toEqual({ orientation: "corner-sw", isCliff: false }) expect(classifyBorderPiece("Act 3 - Burbs Border N")).toEqual({ orientation: "north", isCliff: false }) expect(classifyBorderPiece("Act 3 - Burbs Border SW")).toEqual({ orientation: "corner-sw", isCliff: false }) expect(classifyBorderPiece("Act 3 - Metro Border E")).toEqual({ orientation: "east", isCliff: false }) expect(classifyBorderPiece("Act 3 - Metro Border SE")).toEqual({ orientation: "corner-se", isCliff: false }) }) test("Act 4: Mesa Border 1..12", () => { expect(classifyBorderPiece("Act 4 - Mesa Border 1")).toEqual({ orientation: "south", isCliff: false }) expect(classifyBorderPiece("Act 4 - Mesa Border 2")).toEqual({ orientation: "west", isCliff: false }) expect(classifyBorderPiece("Act 4 - Mesa Border 3")).toEqual({ orientation: "north", isCliff: false }) expect(classifyBorderPiece("Act 4 - Mesa Border 4")).toEqual({ orientation: "east", isCliff: false }) expect(classifyBorderPiece("Act 4 - Mesa Border 5")).toEqual({ orientation: "corner-sw", isCliff: false }) expect(classifyBorderPiece("Act 4 - Mesa Border 6")).toEqual({ orientation: "corner-nw", isCliff: false }) expect(classifyBorderPiece("Act 4 - Mesa Border 7")).toEqual({ orientation: "corner-ne", isCliff: false }) expect(classifyBorderPiece("Act 4 - Mesa Border 8")).toEqual({ orientation: "corner-se", isCliff: false }) expect(classifyBorderPiece("Act 4 - Mesa Border 9")).toEqual({ orientation: "corner-sw", isCliff: false }) expect(classifyBorderPiece("Act 4 - Mesa Border 10")).toEqual({ orientation: "corner-nw", isCliff: false }) expect(classifyBorderPiece("Act 4 - Mesa Border 11")).toEqual({ orientation: "corner-ne", isCliff: false }) expect(classifyBorderPiece("Act 4 - Mesa Border 12")).toEqual({ orientation: "corner-se", isCliff: false }) }) test("Act 5: Barricade Cliff Border 1..12, Barricade Ravine Border 1..6, and Snow variants", () => { // Barricade Cliff: isCliff: true expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 1")).toEqual({ orientation: "south", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 2")).toEqual({ orientation: "west", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 3")).toEqual({ orientation: "north", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 4")).toEqual({ orientation: "east", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 5")).toEqual({ orientation: "corner-sw", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 6")).toEqual({ orientation: "corner-nw", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 7")).toEqual({ orientation: "corner-ne", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 8")).toEqual({ orientation: "corner-se", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 9")).toEqual({ orientation: "corner-sw", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 10")).toEqual({ orientation: "corner-nw", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 11")).toEqual({ orientation: "corner-ne", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 12")).toEqual({ orientation: "corner-se", isCliff: true }) // Barricade Ravine: isCliff: false expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 1")).toEqual({ orientation: "south", isCliff: false }) expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 2")).toEqual({ orientation: "west", isCliff: false }) expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 3")).toEqual({ orientation: "north", isCliff: false }) expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 4")).toEqual({ orientation: "east", isCliff: false }) expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 5")).toEqual({ orientation: "corner-sw", isCliff: false }) expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 6")).toEqual({ orientation: "corner-nw", isCliff: false }) // Corner join / transition pieces expect(classifyBorderPiece("Act 5 - Barricade Ravine-Cliff Border 5")).toEqual({ orientation: "corner-sw", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Cliff Ravine Border 7")).toEqual({ orientation: "corner-ne", isCliff: true }) // Snow variants expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 1 Snow")).toEqual({ orientation: "south", isCliff: true }) expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 1 Snow")).toEqual({ orientation: "south", isCliff: false }) expect(classifyBorderPiece("Act 5 - Snow Border 1")).toEqual({ orientation: "south", isCliff: false }) expect(classifyBorderPiece("Act 5 - Snow Border 2")).toEqual({ orientation: "west", isCliff: false }) expect(classifyBorderPiece("Act 5 - Snow Border 3")).toEqual({ orientation: "north", isCliff: false }) expect(classifyBorderPiece("Act 5 - Snow Border 4")).toEqual({ orientation: "east", isCliff: false }) expect(classifyBorderPiece("Act 5 - Snow Border 6")).toEqual({ orientation: "corner-nw", isCliff: false }) }) }) describe("Task 1B: selectBorderPiece cliff preference", () => { test("prefers cliff pieces when preferCliff is true and non-cliff when false", () => { const cliffPiece: WildernessPiece = { name: "Act 1 - Wild Cliff Border 3", border: true, levels: [makeMockBorderDs1(8, 8, "north", false)], } const fencePiece: WildernessPiece = { name: "Act 1 - Wild Border 3", border: true, levels: [makeMockBorderDs1(8, 8, "north", false)], } const classified = [ { piece: cliffPiece, orientation: "north" as const, isCliff: true }, { piece: fencePiece, orientation: "north" as const, isCliff: false }, ] const rng = new Rng(100) const pickedCliff = selectBorderPiece("north", classified, rng, true) expect(pickedCliff?.name).toBe("Act 1 - Wild Cliff Border 3") const pickedFence = selectBorderPiece("north", classified, rng, false) expect(pickedFence?.name).toBe("Act 1 - Wild Border 3") }) test("falls back gracefully when only one cliff status is available", () => { const fencePiece: WildernessPiece = { name: "Act 4 - Mesa Border 3", border: true, levels: [makeMockBorderDs1(8, 8, "north", false)], } const classified = [{ piece: fencePiece, orientation: "north" as const, isCliff: false }] const rng = new Rng(200) // Even if preferCliff is true, falls back to fencePiece const picked = selectBorderPiece("north", classified, rng, true) expect(picked?.name).toBe("Act 4 - Mesa Border 3") }) }) describe("Task 1C & 2: DRLGOUTPLACE_PlaceAct1245OutdoorBorders & layBorder outer ring coverage", () => { function buildMultiActPieces(prefix: string, hasCliffs = false): WildernessPiece[] { const pieces: WildernessPiece[] = [] // Numbers 1..8 for (let i = 1; i <= 8; i += 1) { const orientation = i === 1 ? "south" : i === 2 ? "west" : i === 3 ? "north" : i === 4 ? "east" : i === 5 ? "corner-sw" : i === 6 ? "corner-nw" : i === 7 ? "corner-ne" : "corner-se" const closedDs1 = makeMockBorderDs1(8, 8, orientation, false) const openDs1 = makeMockBorderDs1(8, 8, orientation, true) pieces.push({ name: `${prefix} Border ${i}`, border: true, // 4 variants: 0..2 closed, 3 open levels: [closedDs1, closedDs1, closedDs1, openDs1], }) } if (hasCliffs) { // Add cliff pieces for north and west pieces.push({ name: `${prefix} Cliff Border 2`, border: true, levels: [makeMockBorderDs1(8, 8, "west", false)], }) pieces.push({ name: `${prefix} Cliff Border 3`, border: true, levels: [makeMockBorderDs1(8, 8, "north", false)], }) } return pieces } test.each([ { act: 1, name: "Act 1 - Wild", hasCliffs: true }, { act: 2, name: "Act 2 - Desert", hasCliffs: false }, { act: 4, name: "Act 4 - Mesa", hasCliffs: false }, { act: 5, name: "Act 5 - Barricade", hasCliffs: true }, ])("Act $act ($name): 100% outer ring coverage with 0 procedural fallback walls", ({ name, hasCliffs }) => { const gridWidth = 8 const gridHeight = 8 const canvas = createCanvas(gridWidth * 8, gridHeight * 8, 1, 1, 0) const pieces = buildMultiActPieces(name, hasCliffs) const stats = createEmptyStats() const rng = new Rng(42) const stamped = DRLGOUTPLACE_PlaceAct1245OutdoorBorders( canvas, pieces, gridWidth, gridHeight, rng, stats, 2, // Blood Moor level ID with South and East openings ) // Expected ring block count: 2 * (w + h) - 4 = 2 * (8 + 8) - 4 = 28 blocks const expectedRingBlocks = 2 * (gridWidth + gridHeight) - 4 expect(stamped).toBe(expectedRingBlocks) expect(stats.borderStamped).toBe(expectedRingBlocks) expect(stats.proceduralWallCells).toBe(0) // Verify every perimeter cell on the canvas has floor and valid wall placement for (let x = 0; x < canvas.width; x += 1) { // North edge (y=0) and South edge (y=canvas.height-1) expect(canvas.cells[0]?.[x]?.floors.length).toBeGreaterThan(0) expect(canvas.cells[canvas.height - 1]?.[x]?.floors.length).toBeGreaterThan(0) } for (let y = 0; y < canvas.height; y += 1) { // West edge (x=0) and East edge (x=canvas.width-1) expect(canvas.cells[y]?.[0]?.floors.length).toBeGreaterThan(0) expect(canvas.cells[y]?.[canvas.width - 1]?.floors.length).toBeGreaterThan(0) } }) test("Opening blocks select Open DS1 variants while closed blocks select Closed DS1 variants", () => { const gridWidth = 8 const gridHeight = 8 const canvas = createCanvas(gridWidth * 8, gridHeight * 8, 1, 1, 0) const pieces = buildMultiActPieces("Act 1 - Wild", true) const stats = createEmptyStats() const rng = new Rng(12345) // Level 2 has default entrance on South edge (mid-X, y=7) and East edge (x=7, mid-Y) DRLGOUTPLACE_PlaceAct1245OutdoorBorders(canvas, pieces, gridWidth, gridHeight, rng, stats, 2) const midX = Math.floor(gridWidth / 2) // block (4, 7) const midY = Math.floor(gridHeight / 2) // block (7, 4) // South opening block: cell (4*8 + 4, 7*8 + y) should have the open gateway gap const southOpenCenterCell = canvas.cells[7 * 8 + 4]?.[midX * 8 + 4] expect(southOpenCenterCell).toBeDefined() // Gateway center in open variant has no wall expect(southOpenCenterCenterWallCount(canvas, midX * 8, 7 * 8, 8, 8)).toBe(false) // Closed South block: cell (1*8, 7*8) should have solid continuous wall expect(isBlockClosed(canvas, 1 * 8, 7 * 8, 8, 8)).toBe(true) }) function southOpenCenterCenterWallCount(canvas: any, bx: number, by: number, w: number, h: number): boolean { const centerX = bx + Math.floor(w / 2) // Check if all cells in column centerX of this block are walls for (let y = 0; y < h; y += 1) { if (!canvas.cells[by + y]?.[centerX]?.walls.some((w: any) => !w.hidden && w.prop1 !== 0)) { return false // There is an open path through this column! } } return true } function isBlockClosed(canvas: any, bx: number, by: number, w: number, h: number): boolean { for (let y = 0; y < h; y += 1) { let hasWallRow = true for (let x = 0; x < w; x += 1) { if (!canvas.cells[by + y]?.[bx + x]?.walls.some((w: any) => !w.hidden && w.prop1 !== 0)) { hasWallRow = false break } } if (hasWallRow) return true } return false } test("Docking support: claimedBorderBlocks are skipped during border stamping", () => { const gridWidth = 8 const gridHeight = 8 const canvas = createCanvas(gridWidth * 8, gridHeight * 8, 1, 1, 0) const pieces = buildMultiActPieces("Act 1 - Wild", false) const stats = createEmptyStats() const rng = new Rng(777) // Claim 3 blocks along the West edge for a Town Transition / Facade docking const claimed = new Set(["0,2", "0,3", "0,4"]) const stamped = DRLGOUTPLACE_PlaceAct1245OutdoorBorders( canvas, pieces, gridWidth, gridHeight, rng, stats, 2, undefined, claimed, ) // 28 total perimeter blocks - 3 claimed blocks = 25 stamped blocks expect(stamped).toBe(25) expect(stats.borderStamped).toBe(25) // Verified claimed blocks were skipped: canvas floor/walls at block (0, 3) were NOT painted by border expect(canvas.cells[3 * 8]?.[0]?.walls[0]?.style).toBe(0) expect(canvas.cells[1 * 8]?.[0]?.walls[0]?.style).toBe(1) }) test("layBorder is an exported alias for DRLGOUTPLACE_PlaceAct1245OutdoorBorders", () => { expect(layBorder).toBe(DRLGOUTPLACE_PlaceAct1245OutdoorBorders) }) test("getCellOrientation correctly maps 4 corners and 4 edges", () => { expect(getCellOrientation(0, 0, 10, 10)).toBe("corner-nw") expect(getCellOrientation(9, 0, 10, 10)).toBe("corner-ne") expect(getCellOrientation(9, 9, 10, 10)).toBe("corner-se") expect(getCellOrientation(0, 9, 10, 10)).toBe("corner-sw") expect(getCellOrientation(5, 0, 10, 10)).toBe("north") expect(getCellOrientation(9, 5, 10, 10)).toBe("east") expect(getCellOrientation(5, 9, 10, 10)).toBe("south") expect(getCellOrientation(0, 5, 10, 10)).toBe("west") }) }) })