import { describe, expect, test } from "vitest" import { generateWilderness, getPerimeterOpenings, type WildernessPiece, type SeamConstraint, type GateOpening, type PlannedGate, } from "../src/game/wilderness.ts" import type { Ds1, Ds1Cell } from "../src/formats/ds1.ts" function makeMockDs1(width: number, height: number, hasGapInWalls = false): 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: 0, 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 makeTestPieces(): WildernessPiece[] { return [ { name: "Act 1 - Wild Border 1", border: true, levels: [makeMockDs1(8, 8, false), makeMockDs1(8, 8, true)] }, { name: "Act 1 - Wild Border 2", border: true, levels: [makeMockDs1(8, 8, false), makeMockDs1(8, 8, true)] }, { name: "Act 1 - Wild Border 3", border: true, levels: [makeMockDs1(8, 8, false), makeMockDs1(8, 8, true)] }, { name: "Act 1 - Wild Border 4", border: true, levels: [makeMockDs1(8, 8, false), makeMockDs1(8, 8, true)] }, { name: "Act 1 - Wild Border 5", border: true, levels: [makeMockDs1(8, 8, false)] }, { name: "Act 1 - Wild Border 6", border: true, levels: [makeMockDs1(8, 8, false)] }, { name: "Act 1 - Wild Border 7", border: true, levels: [makeMockDs1(8, 8, false)] }, { name: "Act 1 - Wild Border 8", border: true, levels: [makeMockDs1(8, 8, false)] }, { name: "Act 1 - Wild Border 9", border: true, levels: [makeMockDs1(8, 8, false)] }, { name: "Act 1 - Wild Border 10", border: true, levels: [makeMockDs1(8, 8, false)] }, { name: "Act 1 - Wild Border 11", border: true, levels: [makeMockDs1(8, 8, false)] }, { name: "Act 1 - Wild Border 12", border: true, levels: [makeMockDs1(8, 8, false)] }, { name: "Act 1 - Town Transition East", border: true, levels: [makeMockDs1(8, 8, true)] }, { name: "Act 1 - Town Transition South", border: true, levels: [makeMockDs1(8, 8, true)] }, { name: "Act 1 - Fallen Camp Bishibosh", border: false, levels: [makeMockDs1(8, 8, true)] }, { name: "Act 1 - Cairn Stones", border: false, levels: [makeMockDs1(8, 8, true)] }, { name: "Act 1 - Inifus", border: false, levels: [makeMockDs1(8, 8, true)] }, ] } describe("Wilderness Seam Alignment and Coordinate Interlocking (Issue #69)", () => { describe("getPerimeterOpenings unit tests", () => { test("uses forced gate.blockCoord directly", () => { const gates: PlannedGate[] = [ { label: "Cold Plains", side: "east", toLevelId: 3, blockCoord: 5 }, ] const openings = getPerimeterOpenings(2, 10, 10, gates) // East side is at x = 9, y = 5 expect(openings.has("9,5")).toBe(true) }) test("clamps gate.blockCoord away from corners [1, size - 2]", () => { const gates: PlannedGate[] = [ { label: "Stony Field", side: "north", toLevelId: 4, blockCoord: 0 }, // Out of bounds / corner -> clamp to 1 { label: "Dark Wood", side: "south", toLevelId: 5, blockCoord: 9 }, // Out of bounds / corner -> clamp to 8 ] const openings = getPerimeterOpenings(3, 10, 10, gates) // North opening at x = 1, y = 0 expect(openings.has("1,0")).toBe(true) // South opening at x = 8, y = 9 expect(openings.has("8,9")).toBe(true) }) test("matches seamConstraints by side and level ID", () => { const gates: PlannedGate[] = [ { label: "Blood Moor", side: "west", toLevelId: 2 }, ] const seamConstraints: SeamConstraint[] = [ { side: "west", fromLevelId: 2, toLevelId: 3, blockCoord: 4 }, ] const openings = getPerimeterOpenings(3, 10, 10, gates, seamConstraints) // West opening at x = 0, y = 4 expect(openings.has("0,4")).toBe(true) }) test("converts tileCoord to blockCoord when blockCoord is not specified", () => { const gates: PlannedGate[] = [ { label: "Stony Field", side: "north", toLevelId: 4, tileCoord: { x: 48, y: 0 } }, ] // tile x: 48 -> block x: 48 / 8 = 6 const openings = getPerimeterOpenings(3, 10, 10, gates) expect(openings.has("6,0")).toBe(true) }) test("handles extreme out-of-bounds coordinates safely", () => { const gates: PlannedGate[] = [ { label: "Blood Moor", side: "west", toLevelId: 2, blockCoord: -100 }, { label: "Cold Plains", side: "east", toLevelId: 3, blockCoord: 1000 }, ] const openings = getPerimeterOpenings(2, 10, 10, gates) expect(openings.has("0,1")).toBe(true) expect(openings.has("9,8")).toBe(true) }) }) describe("Sequential generation with seam interlocking (Blood Moor -> Cold Plains -> Stony Field)", () => { test("interlocks Blood Moor East exit with Cold Plains West entrance", () => { const pieces = makeTestPieces() // Step 1: Generate Blood Moor (level 2) const bloodMoorResult = generateWilderness({ levelId: 2, levelName: "Blood Moor", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 0x123456, pieces, substitutions: [], }) const bloodMoorOpenings = bloodMoorResult.stats.gateOpenings expect(bloodMoorOpenings).toBeDefined() expect(bloodMoorOpenings!.length).toBeGreaterThan(0) // Find East exit to Cold Plains (level 3) const eastGate = bloodMoorOpenings!.find(g => g.side === "east" && g.toLevelId === 3) expect(eastGate).toBeDefined() const sharedYBlock = eastGate!.blockCoord expect(sharedYBlock).toBeGreaterThanOrEqual(1) expect(sharedYBlock).toBeLessThanOrEqual(8) // Step 2: Generate Cold Plains (level 3) with seam constraint matching Blood Moor's East exit const coldPlainsResult = generateWilderness({ levelId: 3, levelName: "Cold Plains", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 0x654321, pieces, substitutions: [], seamConstraints: [ { side: "west", fromLevelId: 2, toLevelId: 3, blockCoord: sharedYBlock, ...(eastGate!.tileCoord ? { tileCoord: { x: 0, y: eastGate!.tileCoord.y } } : {}), }, ], }) const coldPlainsOpenings = coldPlainsResult.stats.gateOpenings expect(coldPlainsOpenings).toBeDefined() // Find West entrance from Blood Moor (level 2) const westGate = coldPlainsOpenings!.find(g => g.side === "west" && g.toLevelId === 2) expect(westGate).toBeDefined() expect(westGate!.blockCoord).toBe(sharedYBlock) // If tileCoord was provided, verify along-seam coordinate matches if (eastGate!.tileCoord && westGate!.tileCoord) { expect(westGate!.tileCoord.y).toBe(eastGate!.tileCoord.y) } // Step 3: Cold Plains has exit to Stony Field (level 4) on North const northGate = coldPlainsOpenings!.find(g => g.side === "north" && g.toLevelId === 4) expect(northGate).toBeDefined() const sharedXBlock = northGate!.blockCoord expect(sharedXBlock).toBeGreaterThanOrEqual(1) expect(sharedXBlock).toBeLessThanOrEqual(8) // Step 4: Generate Stony Field (level 4) with seam constraint on South matching Cold Plains North exit const stonyFieldResult = generateWilderness({ levelId: 4, levelName: "Stony Field", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 0xabcdef, pieces, substitutions: [], seamConstraints: [ { side: "south", fromLevelId: 3, toLevelId: 4, blockCoord: sharedXBlock, ...(northGate!.tileCoord ? { tileCoord: { x: northGate!.tileCoord.x, y: 79 } } : {}), }, ], }) const stonyFieldOpenings = stonyFieldResult.stats.gateOpenings expect(stonyFieldOpenings).toBeDefined() const southGate = stonyFieldOpenings!.find(g => g.side === "south" && g.toLevelId === 3) expect(southGate).toBeDefined() expect(southGate!.blockCoord).toBe(sharedXBlock) if (northGate!.tileCoord && southGate!.tileCoord) { expect(southGate!.tileCoord.x).toBe(northGate!.tileCoord.x) } }) }) describe("PlannedGate forced blockCoord and tileCoord in generateWilderness", () => { test("honors gates with explicit blockCoord", () => { const pieces = makeTestPieces() const result = generateWilderness({ levelId: 3, levelName: "Cold Plains", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 0x999999, pieces, substitutions: [], gates: [ { label: "Blood Moor", side: "west", toLevelId: 2, blockCoord: 3 }, { label: "Stony Field", side: "north", toLevelId: 4, blockCoord: 6 }, { label: "Underground", side: "east", toLevelId: 10, blockCoord: 7 }, ], }) const openings = result.stats.gateOpenings expect(openings).toBeDefined() const west = openings!.find(g => g.side === "west") expect(west?.blockCoord).toBe(3) const north = openings!.find(g => g.side === "north") expect(north?.blockCoord).toBe(6) const east = openings!.find(g => g.side === "east") expect(east?.blockCoord).toBe(7) }) test("honors gates with explicit tileCoord and positions road anchors", () => { const pieces = makeTestPieces() const targetTileY = 8 * 4 + 3 // block 4, tile 35 const result = generateWilderness({ levelId: 3, levelName: "Cold Plains", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 0x777777, pieces, substitutions: [], gates: [ { label: "Blood Moor", side: "west", toLevelId: 2, tileCoord: { x: 3, y: targetTileY } }, ], }) const openings = result.stats.gateOpenings expect(openings).toBeDefined() const west = openings!.find(g => g.side === "west") expect(west?.blockCoord).toBe(4) expect(west?.tileCoord).toEqual({ x: 3, y: targetTileY }) }) }) describe("Bidirectional level links & seam alignment consistency", () => { test("level A (East) and level B (West) have matching seam and opening block", () => { const pieces = makeTestPieces() const targetSeamY = 5 const levelA = generateWilderness({ levelId: 2, levelName: "Level A", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 0x111, pieces, substitutions: [], gates: [{ label: "Level B", side: "east", toLevelId: 3, blockCoord: targetSeamY }], }) const levelB = generateWilderness({ levelId: 3, levelName: "Level B", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 0x222, pieces, substitutions: [], seamConstraints: [{ side: "west", fromLevelId: 2, toLevelId: 3, blockCoord: targetSeamY }], }) const gateA = levelA.stats.gateOpenings!.find(g => g.side === "east")! const gateB = levelB.stats.gateOpenings!.find(g => g.side === "west")! expect(gateA.blockCoord).toBe(targetSeamY) expect(gateB.blockCoord).toBe(targetSeamY) // Along-seam tile coordinate: Y coordinate matches expect(gateA.tileCoord!.y).toBe(gateB.tileCoord!.y) }) test("level B (North) and level C (South) have matching seam and opening block", () => { const pieces = makeTestPieces() const targetSeamX = 6 const levelB = generateWilderness({ levelId: 3, levelName: "Level B", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 0x333, pieces, substitutions: [], gates: [{ label: "Level C", side: "north", toLevelId: 4, blockCoord: targetSeamX }], }) const levelC = generateWilderness({ levelId: 4, levelName: "Level C", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 0x444, pieces, substitutions: [], seamConstraints: [{ side: "south", fromLevelId: 3, toLevelId: 4, blockCoord: targetSeamX }], }) const gateB = levelB.stats.gateOpenings!.find(g => g.side === "north")! const gateC = levelC.stats.gateOpenings!.find(g => g.side === "south")! expect(gateB.blockCoord).toBe(targetSeamX) expect(gateC.blockCoord).toBe(targetSeamX) // Along-seam tile coordinate: X coordinate matches expect(gateB.tileCoord!.x).toBe(gateC.tileCoord!.x) }) }) describe("Out of bounds clamping gracefully preserves valid playable borders", () => { test("clamps negative blockCoord to 1 without crashing", () => { const pieces = makeTestPieces() const result = generateWilderness({ levelId: 3, levelName: "Cold Plains", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 0x555, pieces, substitutions: [], seamConstraints: [{ side: "west", fromLevelId: 2, toLevelId: 3, blockCoord: -10 }], }) const west = result.stats.gateOpenings!.find(g => g.side === "west") expect(west?.blockCoord).toBe(1) }) test("clamps excessive blockCoord to gridWidth - 2 without crashing", () => { const pieces = makeTestPieces() const result = generateWilderness({ levelId: 3, levelName: "Cold Plains", levelTypeName: "Act 1 - Wilderness", sizeX: 80, sizeY: 80, subType: 6, subTheme: 0, seed: 0x666, pieces, substitutions: [], seamConstraints: [{ side: "north", fromLevelId: 4, toLevelId: 3, blockCoord: 999 }], }) const north = result.stats.gateOpenings!.find(g => g.side === "north") expect(north?.blockCoord).toBe(8) // 80 / 8 = 10 blocks -> max is 10 - 2 = 8 }) }) })