/** * Ground-truth tests for the 1.13c room-seam and doorway rules in `stamp`. * * Both behaviours are transcribed from Diablo II v1.13c `D2Common.dll` * (MD5 `ee1238806ef6d6d9801d12a09d128fe1`), via D2MOO: * * - `DRLGPRESET_InitPresetRoomGrids` (`DrlgPreset.cpp:1105`) derives * `bKillEdgeX`/`bKillEdgeY` from `LvlPrest.KillEdge` and the room's position * relative to the map's outer edge, and `DRLGROOMTILE_LoadInitRoomTiles` * (`DrlgRoomTile.cpp:449`) turns them into * `nTileCountX = nTileWidth + (bKillEdgeX == 0)`. * - `DRLGROOMTILE_LoadInitRoomTiles` drops hidden door and exit walls * (`DrlgRoomTile.cpp:466`, `479`) and `DRLGROOMTILE_LoadFloorWarpTiles` * (`DrlgRoomTile.cpp:706`) backfills a 2×2 threshold patch at `(nX-1, nY-1)` * using `gWarpTileOffsets_6FDD1320 = {0,0},{1,0},{0,1},{1,1}`. */ import { describe, expect, test } from 'vitest' import type { Ds1, Ds1Cell, Ds1Floor, Ds1Wall } from '../src/formats/ds1.ts' import { generateMaze, type MazePiece, type MazePieceKind, type MazeRequest } from '../src/game/maze.ts' /** DS1 wall types 1.13c treats as markers rather than artwork. */ const TYPE_LEFT_DOOR = 8 const TYPE_LEFT_EXIT = 10 /** A floor slot that draws something. */ function solidFloor(): Ds1Floor { return { prop1: 1, sequence: 0, style: 5, unknown1: 0, unknown2: 0, hidden: false } } /** The decoder's placeholder floor — what `floorIsBlank` looks for. */ function blankFloor(): Ds1Floor { return { prop1: 0, sequence: 0, style: 0, unknown1: 0, unknown2: 0, hidden: false } } /** A plain wall slot. */ function plainWall(): Ds1Wall { return { prop1: 0, sequence: 0, style: 0, type: 0, unknown1: 0, unknown2: 0, hidden: false } } /** Where a marker wall goes inside every piece, and what it looks like. */ interface MarkerSpec { readonly x: number readonly y: number readonly type: number readonly style: number readonly hidden: boolean /** Blank the 2×2 threshold the backfill is expected to repair. */ readonly blankThreshold?: boolean } /** * Build a piece DS1 of the given size, optionally carrying one marker wall. * * @param size - width and height in cells. * @param marker - the marker wall to embed, if any. * @returns the map. */ function pieceDs1(size: number, marker?: MarkerSpec): Ds1 { const blanks = new Set() if (marker?.blankThreshold === true) { for (const [dx, dy] of [[0, 0], [1, 0], [0, 1], [1, 1]] as const) { blanks.add(`${String(marker.x - 1 + dx)},${String(marker.y - 1 + dy)}`) } } const cells: Ds1Cell[][] = [] for (let y = 0; y < size; y += 1) { const row: Ds1Cell[] = [] for (let x = 0; x < size; x += 1) { const isMarker = marker !== undefined && marker.x === x && marker.y === y row.push({ walls: [isMarker ? { prop1: 0, sequence: 0, style: marker.style, type: marker.type, unknown1: 0, unknown2: 0, hidden: marker.hidden } : plainWall()], floors: [blanks.has(`${String(x)},${String(y)}`) ? blankFloor() : solidFloor()], shadows: [blankFloor()], substitutions: [], }) } cells.push(row) } return { version: 18, width: size, height: size, act: 1, substitutionType: 0, wallLayers: 1, floorLayers: 1, cells, objects: [], npcPathOffset: null, } } /** * The 15 side shapes plus the entrance and warp pieces `generateMaze` needs. * * Every piece is the same size and carries the same marker, so whichever piece * the generator picks the assertion below still has something to find. * * @param size - piece size in cells; one larger than the section stride. * @param killEdge - the `LvlPrest.KillEdge` value to report. * @param marker - the marker wall to embed in every piece. * @returns the piece set. */ function pieceSet(size: number, killEdge: boolean, marker?: MarkerSpec): MazePiece[] { const sides = ['N', 'S', 'E', 'W', 'NE', 'NW', 'SE', 'SW', 'NS', 'EW', 'NSE', 'NSW', 'NEW', 'SEW', 'NSEW'] const make = (name: string, kind: MazePieceKind, side: string): MazePiece => ({ name, kind, sides: side, levels: [pieceDs1(size, marker)], killEdge }) return [ make('Act 1 - Cave Entrance', 'entrance', ''), ...sides.map(side => make(`Act 1 - Cave ${side}`, 'room', side)), ...['N', 'S', 'E', 'W'].flatMap(side => [ make(`Act 1 - Cave Prev ${side}`, 'prev', side), make(`Act 1 - Cave Next ${side}`, 'next', side), make(`Act 1 - Cave Down ${side}`, 'down', side), make(`Act 1 - Cave Theme ${side}`, 'theme', side), ]), ] } /** A small deterministic Act 1 Cave request. */ function request(pieces: readonly MazePiece[], overrides: Partial = {}): MazeRequest { return { levelId: 2, levelName: 'Act 1 - Cave 1', levelTypeName: 'Act 1 - Cave', sectionSize: 7, minRooms: 5, merge: 0, seed: 0x1337, pieces, ...overrides, } } /** Every wall slot in a generated map, flattened. */ function allWalls(level: Ds1): Ds1Wall[] { return level.cells.flatMap(row => row.flatMap(cell => [...cell.walls])) } describe('1.13c bKillEdge room-seam suppression', () => { test('KillEdge trims exactly one column and one row off the composite map', () => { // A piece is one cell wider than the 7-cell stride, so without KillEdge the // outer room contributes an overhang that has no neighbour to overwrite it. const off = generateMaze(request(pieceSet(8, false))) const on = generateMaze(request(pieceSet(8, true))) expect(on.level.width).toBe(off.level.width - 1) expect(on.level.height).toBe(off.level.height - 1) }) test('KillEdge is read off the pieces when the request does not override it', () => { const derived = generateMaze(request(pieceSet(8, true))) const overridden = generateMaze(request(pieceSet(8, true), { killEdge: false })) const plain = generateMaze(request(pieceSet(8, false))) // The override wins over the piece rows, and matches the un-trimmed map. expect(overridden.level.width).toBe(plain.level.width) expect(derived.level.width).toBe(plain.level.width - 1) const notes = derived.stats.notes as string[] expect(notes.some(note => note.startsWith('killEdge trimmed'))).toBe(true) expect((plain.stats.notes as string[]).some(note => note.startsWith('killEdge trimmed'))).toBe(false) }) test('trimming never drops interior seams, only the outer edge', () => { // With KillEdge on, the map is exactly `(span of origins) + stride`, which // is what `pDrlgCoord.nWidth` is in 1.13c. Every interior room therefore // still stamps its full 8 cells and keeps overlapping its neighbour. const on = generateMaze(request(pieceSet(8, true), { minRooms: 9 })) const stride = 7 expect((on.level.width - stride) % stride).toBe(0) expect((on.level.height - stride) % stride).toBe(0) }) }) describe('1.13c hidden door and exit walls', () => { test('a hidden door wall is never emitted as artwork', () => { const hidden = generateMaze(request(pieceSet(8, false, { x: 3, y: 3, type: TYPE_LEFT_DOOR, style: 0, hidden: true, }))) const shown = generateMaze(request(pieceSet(8, false, { x: 3, y: 3, type: TYPE_LEFT_DOOR, style: 0, hidden: false, }))) expect(allWalls(hidden.level).some(wall => wall.type === TYPE_LEFT_DOOR)).toBe(false) // The identical non-hidden door is ordinary artwork and must survive. expect(allWalls(shown.level).some(wall => wall.type === TYPE_LEFT_DOOR)).toBe(true) }) test('an exit tile above the Warp0..7 slot range is dropped outright', () => { // `nTileStyle >= 8` is not a warp slot, so 1.13c skips the cell even though // the tile is not flagged hidden. const tooHigh = generateMaze(request(pieceSet(8, false, { x: 3, y: 3, type: TYPE_LEFT_EXIT, style: 8, hidden: false, }))) const inRange = generateMaze(request(pieceSet(8, false, { x: 3, y: 3, type: TYPE_LEFT_EXIT, style: 7, hidden: false, }))) expect(allWalls(tooHigh.level).some(wall => wall.type === TYPE_LEFT_EXIT)).toBe(false) expect(allWalls(inRange.level).some(wall => wall.type === TYPE_LEFT_EXIT)).toBe(true) }) test('a hidden exit backfills the 2x2 threshold floor patch', () => { const marker: MarkerSpec = { x: 3, y: 3, type: TYPE_LEFT_EXIT, style: 2, hidden: true, blankThreshold: true, } const patched = generateMaze(request(pieceSet(8, false, marker))) const notes = patched.stats.notes as string[] const note = notes.find(entry => entry.startsWith('hid ')) expect(note).toBeDefined() const filled = Number(/backfilled (\d+) threshold floor cells/.exec(note ?? '')?.[1] ?? '0') expect(filled).toBeGreaterThan(0) // Every backfilled cell must draw a floor and be flagged hidden, the way // `DRLGROOMTILE_LoadFloorWarpTiles` writes them. let hiddenFloors = 0 for (const row of patched.level.cells) { for (const cell of row) { const floor = cell.floors[0] if (floor !== undefined && floor.hidden && floor.style === 5) hiddenFloors += 1 } } expect(hiddenFloors).toBe(filled) }) test('a hidden door suppresses its wall but gets no warp threshold patch', () => { // `LoadFloorWarpTiles` is reached only from the exit branch; the door branch // above it spawns a unit and `continue`s without touching the floor. const door = generateMaze(request(pieceSet(8, false, { x: 3, y: 3, type: TYPE_LEFT_DOOR, style: 0, hidden: true, blankThreshold: true, }))) const exit = generateMaze(request(pieceSet(8, false, { x: 3, y: 3, type: TYPE_LEFT_EXIT, style: 2, hidden: true, blankThreshold: true, }))) const backfilled = (result: typeof door): number => { const note = (result.stats.notes as string[]).find(entry => entry.startsWith('hid ')) return Number(/backfilled (\d+) threshold floor cells/.exec(note ?? '')?.[1] ?? '0') } expect(backfilled(door)).toBe(0) expect(backfilled(exit)).toBeGreaterThan(0) }) test('a map with no markers reports no hidden walls and no backfill', () => { const plain = generateMaze(request(pieceSet(8, false))) expect((plain.stats.notes as string[]).some(note => note.startsWith('hid '))).toBe(false) }) })