848 lines
34 KiB
TypeScript
848 lines
34 KiB
TypeScript
/**
|
|
* Unit tests for the 11 Diablo II DRLG Maze Special Passes.
|
|
*
|
|
* Covers:
|
|
* 1. DRLGMAZE_PlaceAct2TombStuff (Tal Rasha True/False Tombs, Kaa, Cube, Leatherarm, Waypoint)
|
|
* 2. DRLGMAZE_PlaceAct2LairStuff (Maggot Lair Coldworm Tight Spot & Staff of Kings Treasure)
|
|
* 3. DRLGMAZE_PlaceAct3DungeonStuff (Flayer Dungeon Brain chest, Kurast Temples Lam Esen's Tome)
|
|
* 4. DRLGMAZE_PlaceAct3SewerStuff (Kurast Sewers Drain lever descent & Chest)
|
|
* 5. DRLGMAZE_PlaceAct3MephistoStuff (Durance of Hate 3 Mephisto Complex, Durance 2 Waypoint)
|
|
* 6. DRLGMAZE_PlaceAct5TempleStuff (Halls of Vaught Nihlathak Final Room, Halls of Pain Waypoint)
|
|
* 7. DRLGMAZE_PlaceAct5BaalStuff (Throne of Destruction Baal Entrance/ThroneRoom, Worldstone Waypoint)
|
|
* 8. DRLGMAZE_PlaceAct5IceStuff (Frozen River Anya platform, Drifter Cavern / Icy Cellar Pools, Waypoints)
|
|
* 9. DRLGMAZE_PlaceAct1Barracks (Monastery Barracks Smith Forge & Court Connect)
|
|
* 10. DRLGMAZE_PlaceAct4Lava (River of Flame Hellforge, Bridges 1 & 2, Warp, Lava X fillers)
|
|
* 11. DRLGMAZE_ScanReplaceSpecialAct2SewersPresets (Lut Gholein Sewers Radament's Lair, Dual Entrances, Waypoint)
|
|
* 12. Full generateMaze integration tests verifying specialPassesApplied and the
|
|
* per-level-type `unimplementedPasses` gap list.
|
|
*/
|
|
|
|
import { describe, expect, test } from 'vitest'
|
|
import type { Ds1, Ds1Cell, Ds1Object } from '../src/formats/ds1.ts'
|
|
import {
|
|
generateMaze,
|
|
classifyMazePieceName,
|
|
MAZE_LEVEL_TYPE_PROFILES,
|
|
UNIMPLEMENTED_PASSES,
|
|
unimplementedPassesFor,
|
|
type MazePiece,
|
|
type MazePieceKind,
|
|
type MazeRequest,
|
|
} from '../src/game/maze.ts'
|
|
import {
|
|
runMazeSpecialPasses,
|
|
type SpecialPassName,
|
|
} from '../src/game/maze-special-passes.ts'
|
|
|
|
/**
|
|
* Construct a synthetic minimal Ds1 map.
|
|
*/
|
|
function mockDs1(width = 10, height = 10, objects: Ds1Object[] = []): Ds1 {
|
|
const cells: Ds1Cell[][] = []
|
|
for (let y = 0; y < height; y++) {
|
|
const row: Ds1Cell[] = []
|
|
for (let x = 0; x < width; x++) {
|
|
row.push({
|
|
walls: [{ prop1: 1, sequence: 0, style: 0, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
|
floors: [{ prop1: 1, sequence: 0, style: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
|
shadows: [{ prop1: 0, sequence: 0, style: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
|
substitutions: [],
|
|
})
|
|
}
|
|
cells.push(row)
|
|
}
|
|
return {
|
|
version: 18,
|
|
width,
|
|
height,
|
|
act: 1,
|
|
substitutionType: 0,
|
|
wallLayers: 1,
|
|
floorLayers: 1,
|
|
cells,
|
|
objects,
|
|
npcPathOffset: null,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Construct a synthetic MazePiece.
|
|
*/
|
|
function mockPiece(
|
|
name: string,
|
|
kind: MazePieceKind,
|
|
sides = '',
|
|
objects: Ds1Object[] = [],
|
|
width = 10,
|
|
height = 10,
|
|
): MazePiece {
|
|
return {
|
|
name,
|
|
kind,
|
|
sides,
|
|
levels: [mockDs1(width, height, objects)],
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a full standard piece set for a level type so maze generation has
|
|
* all 15 basic room shapes plus entrance and common directions.
|
|
*/
|
|
function createStandardPieceSet(levelTypeName: string, extraPieces: MazePiece[] = []): MazePiece[] {
|
|
const SIDES = [
|
|
'N', 'S', 'E', 'W',
|
|
'NE', 'NW', 'SE', 'SW',
|
|
'NS', 'EW',
|
|
'NSE', 'NSW', 'NEW', 'SEW',
|
|
'NSEW',
|
|
]
|
|
const pieces: MazePiece[] = [
|
|
mockPiece(`${levelTypeName} Entrance`, 'entrance', ''),
|
|
...SIDES.map(s => mockPiece(`${levelTypeName} ${s}`, 'room', s)),
|
|
...['N', 'S', 'E', 'W'].map(s => mockPiece(`${levelTypeName} Prev ${s}`, 'prev', s, [{ type: 2, id: 1, x: 25, y: 25, flags: 0 }])),
|
|
...['N', 'S', 'E', 'W'].map(s => mockPiece(`${levelTypeName} Next ${s}`, 'next', s, [{ type: 2, id: 2, x: 25, y: 25, flags: 0 }])),
|
|
...['N', 'S', 'E', 'W'].map(s => mockPiece(`${levelTypeName} Down ${s}`, 'down', s, [{ type: 2, id: 3, x: 25, y: 25, flags: 0 }])),
|
|
...['N', 'S', 'E', 'W'].map(s => mockPiece(`${levelTypeName} Theme ${s}`, 'theme', s)),
|
|
...extraPieces,
|
|
]
|
|
return pieces
|
|
}
|
|
|
|
describe('Diablo II DRLG Maze Special Passes', () => {
|
|
test('UNIMPLEMENTED_PASSES is empty when all maze profiles define specials', () => {
|
|
const emptySpecials = MAZE_LEVEL_TYPE_PROFILES
|
|
.filter(profile => profile.specials.length === 0 && profile.singleRoom === undefined && profile.specialPass === undefined)
|
|
.map(profile => profile.levelTypeName)
|
|
expect(emptySpecials).toEqual([])
|
|
expect(UNIMPLEMENTED_PASSES).toEqual([])
|
|
})
|
|
|
|
test('unimplementedPassesFor is empty for a fully transcribed level type', () => {
|
|
expect(unimplementedPassesFor('Act 1 - Cave')).toEqual([])
|
|
expect(unimplementedPassesFor('Act 2 - Tomb')).toEqual([])
|
|
expect(unimplementedPassesFor('Act 2 - Harem')).toEqual([])
|
|
expect(unimplementedPassesFor('Act 2 - Basement')).toEqual([])
|
|
expect(unimplementedPassesFor('Act 3 - Spider')).toEqual([])
|
|
expect(unimplementedPassesFor('Act 5 - Lava')).toEqual([])
|
|
})
|
|
|
|
test('unimplementedPassesFor reports a level type that has no profile at all', () => {
|
|
expect(unimplementedPassesFor('Act 9 - Nowhere'))
|
|
.toEqual(['DRLGMAZE_GenerateLevel profile (Act 9 - Nowhere)'])
|
|
})
|
|
|
|
test('generateMaze reports empty unimplementedPasses for transcribed levels', () => {
|
|
const spider = generateMaze({
|
|
levelId: 84,
|
|
levelName: 'Spider Cavern',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 8484,
|
|
pieces: createStandardPieceSet('Act 3 - Spider'),
|
|
})
|
|
expect(spider.stats.unimplementedPasses).toEqual([])
|
|
// The other two diagnostic channels must travel with it.
|
|
expect(Array.isArray(spider.stats.unresolvedRoles)).toBe(true)
|
|
expect(Array.isArray(spider.stats.notes)).toBe(true)
|
|
|
|
const cave = generateMaze({
|
|
levelId: 8,
|
|
levelName: 'Den of Evil',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 8484,
|
|
pieces: createStandardPieceSet('Act 1 - Cave'),
|
|
})
|
|
expect(cave.stats.unimplementedPasses).toEqual([])
|
|
})
|
|
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* 1. Act 2 - Tomb
|
|
* ----------------------------------------------------------------------- */
|
|
describe('Pass 1: DRLGMAZE_PlaceAct2TombStuff', () => {
|
|
const tombExtraPieces = [
|
|
mockPiece('Act 2 - Tomb Talrasha N', 'quest', 'N', [{ type: 2, id: 152, x: 25, y: 25, flags: 0 }]), // Horadric Staff Orifice
|
|
mockPiece('Act 2 - Tomb Chest N', 'treasure', 'N', [{ type: 2, id: 397, x: 25, y: 25, flags: 0 }]), // Golden chest
|
|
mockPiece('Act 2 - Tomb Kaa N', 'quest', 'N', [{ type: 1, id: 250, x: 25, y: 25, flags: 0 }]), // Ancient Kaa
|
|
mockPiece('Act 2 - Tomb Cube N', 'treasure', 'N', [{ type: 2, id: 354, x: 25, y: 25, flags: 0 }]), // Horadric Cube
|
|
mockPiece('Act 2 - Tomb Leatherarm N', 'quest', 'N'),
|
|
mockPiece('Act 2 - Tomb Treasure N', 'treasure', 'N'),
|
|
mockPiece('Act 2 - Tomb Waypoint N', 'waypoint', 'N', [{ type: 2, id: 156, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece('Act 2 - Tomb Prev NSW', 'prev', 'NSW'),
|
|
mockPiece('Act 2 - Tomb Prev NEW', 'prev', 'NEW'),
|
|
mockPiece('Act 2 - Tomb Prev NSE', 'prev', 'NSE'),
|
|
mockPiece('Act 2 - Tomb Prev SEW', 'prev', 'SEW'),
|
|
]
|
|
|
|
test('True Tomb (staffTombLevelId): places Talrasha Horadric Staff orifice', () => {
|
|
const pieces = createStandardPieceSet('Act 2 - Tomb', tombExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 66,
|
|
levelName: 'Tal Rashas Tomb 1',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 12345,
|
|
pieces,
|
|
staffTombLevelId: 66,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct2TombStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
const hasTalrasha = specials.some(s => s.kind === 'quest')
|
|
expect(hasTalrasha).toBe(true)
|
|
// Level size is tripled for staff tomb
|
|
expect(res.stats.targetRooms).toBe(15)
|
|
})
|
|
|
|
test('False Tombs (67..72): places golden chest room', () => {
|
|
const pieces = createStandardPieceSet('Act 2 - Tomb', tombExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 67,
|
|
levelName: 'Tal Rashas Tomb 2',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 54321,
|
|
pieces,
|
|
staffTombLevelId: 66,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct2TombStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'treasure')).toBe(true)
|
|
})
|
|
|
|
test('Halls of the Dead 2 (level 59): places Waypoint', () => {
|
|
const pieces = createStandardPieceSet('Act 2 - Tomb', tombExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 59,
|
|
levelName: 'Halls of the Dead 2',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 99999,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct2TombStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'waypoint')).toBe(true)
|
|
})
|
|
|
|
test('Halls of the Dead 3 (level 60): places Horadric Cube chest', () => {
|
|
const pieces = createStandardPieceSet('Act 2 - Tomb', tombExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 60,
|
|
levelName: 'Halls of the Dead 3',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 88888,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct2TombStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'quest' || s.kind === 'treasure')).toBe(true)
|
|
})
|
|
|
|
test('Boss Tomb: places Ancient Kaa the Soulless', () => {
|
|
const pieces = createStandardPieceSet('Act 2 - Tomb', tombExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 68,
|
|
levelName: 'Tal Rashas Tomb 3',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 77777,
|
|
pieces,
|
|
staffTombLevelId: 66,
|
|
bossTombLevelId: 68,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct2TombStuff')
|
|
expect(res.stats.targetRooms).toBe(10) // Doubled for boss tomb
|
|
})
|
|
})
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* 2. Act 2 - Lair
|
|
* ----------------------------------------------------------------------- */
|
|
describe('Pass 2: DRLGMAZE_PlaceAct2LairStuff', () => {
|
|
const lairExtraPieces = [
|
|
mockPiece('Act 2 - Lair Tight Spot S', 'quest', 'S', [{ type: 1, id: 284, x: 25, y: 25, flags: 0 }]), // Coldworm
|
|
mockPiece('Act 2 - Lair Treasure W', 'treasure', 'W', [{ type: 2, id: 356, x: 25, y: 25, flags: 0 }]), // Staff of Kings chest
|
|
]
|
|
|
|
test('Maggot Lair 3 (level 64): places Coldworm Tight Spot and Staff of Kings chest', () => {
|
|
const pieces = createStandardPieceSet('Act 2 - Lair', lairExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 64,
|
|
levelName: 'Maggot Lair 3',
|
|
sectionSize: 10,
|
|
minRooms: 6,
|
|
merge: 500,
|
|
seed: 42,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct2LairStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'quest')).toBe(true)
|
|
expect(specials.some(s => s.kind === 'treasure')).toBe(true)
|
|
expect(res.level.objects.some(o => o.type === 1 && o.id === 284)).toBe(true)
|
|
expect(res.level.objects.some(o => o.type === 2 && o.id === 356)).toBe(true)
|
|
})
|
|
|
|
test('Maggot Lair 1 (level 62): places regular next and prev', () => {
|
|
const pieces = createStandardPieceSet('Act 2 - Lair', lairExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 62,
|
|
levelName: 'Maggot Lair 1',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 101,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct2LairStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'next')).toBe(true)
|
|
})
|
|
})
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* 3. Act 3 - Dungeon
|
|
* ----------------------------------------------------------------------- */
|
|
describe('Pass 3: DRLGMAZE_PlaceAct3DungeonStuff', () => {
|
|
const dungeonExtraPieces = [
|
|
mockPiece('Act 3 - Dungeon Treasure 2', 'treasure', '', [{ type: 2, id: 406, x: 25, y: 25, flags: 0 }]), // Khalim's Brain chest
|
|
mockPiece('Act 3 - Dungeon Treasure 1', 'treasure', ''),
|
|
mockPiece('Act 3 - Temple 1', 'quest', '', [{ type: 2, id: 193, x: 25, y: 25, flags: 0 }]), // Lam Esen's Tome altar
|
|
]
|
|
|
|
test("Flayer Dungeon 3 (level 91): places Khalim's Brain chest (Treasure 2)", () => {
|
|
const pieces = createStandardPieceSet('Act 3 - Dungeon', dungeonExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 91,
|
|
levelName: 'Flayer Dungeon 3',
|
|
levelTypeName: 'Act 3 - Dungeon',
|
|
sectionSize: 10,
|
|
minRooms: 6,
|
|
merge: 500,
|
|
seed: 31415,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct3DungeonStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'treasure')).toBe(true)
|
|
expect(res.level.objects.some(o => o.type === 2 && o.id === 406)).toBe(true)
|
|
})
|
|
|
|
test("Ruined Temple (level 94): places Lam Esen's Tome altar (Temple 1)", () => {
|
|
const pieces = createStandardPieceSet('Act 3 - Dungeon', dungeonExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 94,
|
|
levelName: 'Ruined Temple',
|
|
levelTypeName: 'Act 3 - Dungeon',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 27182,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct3DungeonStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'quest')).toBe(true)
|
|
expect(res.level.objects.some(o => o.type === 2 && o.id === 193)).toBe(true)
|
|
})
|
|
})
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* 4. Act 3 - Sewer
|
|
* ----------------------------------------------------------------------- */
|
|
describe('Pass 4: DRLGMAZE_PlaceAct3SewerStuff', () => {
|
|
const sewerExtraPieces = [
|
|
mockPiece('Act 3 - Sewer Drain N', 'next', 'N', [{ type: 2, id: 366, x: 25, y: 25, flags: 0 }]), // Drain lever to Level 2
|
|
mockPiece('Act 3 - Sewer Chest S', 'treasure', 'S'),
|
|
mockPiece('Act 3 - Sewer Treasure X', 'treasure', ''),
|
|
mockPiece('Act 3 - Sewer NW', 'room', 'NW'),
|
|
mockPiece('Act 3 - Sewer NE', 'room', 'NE'),
|
|
mockPiece('Act 3 - Sewer SW', 'room', 'SW'),
|
|
mockPiece('Act 3 - Sewer SE', 'room', 'SE'),
|
|
mockPiece('Act 3 - Sewer Prev NW', 'prev', 'NW'),
|
|
mockPiece('Act 3 - Sewer Prev NE', 'prev', 'NE'),
|
|
mockPiece('Act 3 - Sewer Prev SW', 'prev', 'SW'),
|
|
mockPiece('Act 3 - Sewer Prev SE', 'prev', 'SE'),
|
|
]
|
|
|
|
test('Kurast Sewers 1 (level 92): places Drain lever and Chest', () => {
|
|
const pieces = createStandardPieceSet('Act 3 - Sewer', sewerExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 92,
|
|
levelName: 'Kurast Sewers 1',
|
|
sectionSize: 10,
|
|
minRooms: 18,
|
|
merge: 500,
|
|
seed: 777,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct3SewerStuff')
|
|
expect(res.level.objects.some(o => o.type === 2 && o.id === 366)).toBe(true)
|
|
})
|
|
})
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* 5. Act 3 - Mephisto
|
|
* ----------------------------------------------------------------------- */
|
|
describe('Pass 5: DRLGMAZE_PlaceAct3MephistoStuff', () => {
|
|
const mephistoExtraPieces = [
|
|
mockPiece('Act 3 - Mephisto Complex', 'quest', '', [{ type: 2, id: 251, x: 50, y: 50, flags: 0 }]), // Red portal to Act 4
|
|
mockPiece('Act 3 - Mephisto Waypoint N', 'waypoint', 'N', [{ type: 2, id: 237, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece('Act 3 - Mephisto Prev N', 'prev', 'N'),
|
|
mockPiece('Act 3 - Mephisto Next N', 'next', 'N'),
|
|
]
|
|
|
|
test('Durance of Hate 3 (level 102): places Mephisto Complex', () => {
|
|
const pieces = createStandardPieceSet('Act 3 - Kurast', mephistoExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 102,
|
|
levelName: 'Durance of Hate 3',
|
|
levelTypeName: 'Act 3 - Kurast',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 666,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct3MephistoStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'quest')).toBe(true)
|
|
expect(res.level.objects.some(o => o.type === 2 && o.id === 251)).toBe(true)
|
|
})
|
|
|
|
test('Durance of Hate 2 (level 101): places Waypoint', () => {
|
|
const pieces = createStandardPieceSet('Act 3 - Kurast', mephistoExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 101,
|
|
levelName: 'Durance of Hate 2',
|
|
levelTypeName: 'Act 3 - Kurast',
|
|
sectionSize: 10,
|
|
minRooms: 6,
|
|
merge: 500,
|
|
seed: 10101,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct3MephistoStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'waypoint')).toBe(true)
|
|
})
|
|
})
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* 6. Act 5 - Temple
|
|
* ----------------------------------------------------------------------- */
|
|
describe('Pass 6: DRLGMAZE_PlaceAct5TempleStuff', () => {
|
|
const templeExtraPieces = [
|
|
mockPiece('Act 5 - Temple Final Room', 'quest', '', [{ type: 1, id: 526, x: 25, y: 25, flags: 0 }]), // Nihlathak
|
|
mockPiece('Act 5 - Temple NE Waypoint', 'waypoint', 'NE'),
|
|
mockPiece('Act 5 - Temple NW Waypoint', 'waypoint', 'NW'),
|
|
mockPiece('Act 5 - Temple SW Waypoint', 'waypoint', 'SW'),
|
|
mockPiece('Act 5 - Temple SE Waypoint', 'waypoint', 'SE'),
|
|
mockPiece('Act 5 - Temple NE Down', 'down', 'NE'),
|
|
mockPiece('Act 5 - Temple NW Down', 'down', 'NW'),
|
|
mockPiece('Act 5 - Temple SW Down', 'down', 'SW'),
|
|
mockPiece('Act 5 - Temple SE up', 'prev', 'SE'),
|
|
mockPiece('Act 5 - Temple SE', 'room', 'SE'),
|
|
mockPiece('Act 5 - Temple NE', 'room', 'NE'),
|
|
mockPiece('Act 5 - Temple NW', 'room', 'NW'),
|
|
mockPiece('Act 5 - Temple SW', 'room', 'SW'),
|
|
]
|
|
|
|
test('Halls of Vaught (level 124): places Nihlathak Final Room altar', () => {
|
|
const pieces = createStandardPieceSet('Act 5 - Temple', templeExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 124,
|
|
levelName: 'Halls of Vaught',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 124124,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct5TempleStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'quest')).toBe(true)
|
|
expect(res.level.objects.some(o => o.type === 1 && o.id === 526)).toBe(true)
|
|
})
|
|
|
|
test('Halls of Pain (level 123): places Waypoint and stairs', () => {
|
|
const pieces = createStandardPieceSet('Act 5 - Temple', templeExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 123,
|
|
levelName: 'Halls of Pain',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 123123,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct5TempleStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'waypoint')).toBe(true)
|
|
})
|
|
|
|
test('Halls of Pain (level 123): generates all 4 waypoint directions including NE across seeds', () => {
|
|
const pieces = createStandardPieceSet('Act 5 - Temple', templeExtraPieces)
|
|
const observedDirs = new Set<string>()
|
|
|
|
for (let seed = 1; seed <= 50; seed++) {
|
|
const res = generateMaze({
|
|
levelId: 123,
|
|
levelName: 'Halls of Pain',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed,
|
|
pieces,
|
|
})
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
const wp = specials.find(s => s.kind === 'waypoint')
|
|
if (wp !== undefined) {
|
|
observedDirs.add(wp.sides)
|
|
}
|
|
}
|
|
|
|
expect(observedDirs).toContain('NE')
|
|
expect(observedDirs).toContain('NW')
|
|
expect(observedDirs).toContain('SW')
|
|
expect(observedDirs).toContain('SE')
|
|
})
|
|
})
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* 7. Act 5 - Baal
|
|
* ----------------------------------------------------------------------- */
|
|
describe('Pass 7: DRLGMAZE_PlaceAct5BaalStuff', () => {
|
|
const baalExtraPieces = [
|
|
mockPiece('Act 5 - ThroneRoom', 'quest', '', [{ type: 2, id: 563, x: 25, y: 25, flags: 0 }]), // Baal Throne
|
|
mockPiece('Act 5 - Baal Waypoint N', 'waypoint', 'N'),
|
|
mockPiece('Act 5 - Baal Prev NSW', 'prev', 'NSW'),
|
|
mockPiece('Act 5 - Baal Prev NEW', 'prev', 'NEW'),
|
|
mockPiece('Act 5 - Baal Prev NSE', 'prev', 'NSE'),
|
|
mockPiece('Act 5 - Baal Prev SEW', 'prev', 'SEW'),
|
|
]
|
|
|
|
test('Throne of Destruction (level 131): places ThroneRoom', () => {
|
|
const pieces = createStandardPieceSet('Act 5 - Baal', baalExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 131,
|
|
levelName: 'Throne of Destruction',
|
|
levelTypeName: 'Act 5 - Baal',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 131131,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct5BaalStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'quest')).toBe(true)
|
|
expect(res.level.objects.some(o => o.type === 2 && o.id === 563)).toBe(true)
|
|
})
|
|
|
|
test('Worldstone Keep 2 (level 129): places Waypoint', () => {
|
|
const pieces = createStandardPieceSet('Act 5 - Baal', baalExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 129,
|
|
levelName: 'Worldstone Keep 2',
|
|
levelTypeName: 'Act 5 - Baal',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 129129,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct5BaalStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'waypoint')).toBe(true)
|
|
})
|
|
})
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* 8. Act 5 - Ice
|
|
* ----------------------------------------------------------------------- */
|
|
describe('Pass 8: DRLGMAZE_PlaceAct5IceStuff', () => {
|
|
const iceExtraPieces = [
|
|
mockPiece('Act 5 - Ice River A', 'quest', '', [{ type: 2, id: 460, x: 25, y: 25, flags: 0 }]), // Frozen Anya
|
|
mockPiece('Act 5 - Ice River B', 'quest', '', [{ type: 2, id: 460, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece('Act 5 - Ice Pool A', 'quest', ''),
|
|
mockPiece('Act 5 - Ice Pool B', 'quest', ''),
|
|
mockPiece('Act 5 - Ice waypoint N', 'waypoint', 'N'),
|
|
]
|
|
|
|
test('Frozen River (level 114): places Anya platform', () => {
|
|
const pieces = createStandardPieceSet('Act 5 - Ice Caves', iceExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 114,
|
|
levelName: 'Frozen River',
|
|
levelTypeName: 'Act 5 - Ice Caves',
|
|
sectionSize: 10,
|
|
minRooms: 1, // Single-room preset in profile
|
|
merge: 500,
|
|
seed: 114114,
|
|
pieces,
|
|
})
|
|
|
|
// Level 114 resolves single room preset Act 5 - Ice River A
|
|
expect(res.level.objects.some(o => o.type === 2 && o.id === 460)).toBe(true)
|
|
})
|
|
|
|
test('Glacial Trail (level 115): places Waypoint', () => {
|
|
const pieces = createStandardPieceSet('Act 5 - Ice Caves', iceExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 115,
|
|
levelName: 'Glacial Trail',
|
|
levelTypeName: 'Act 5 - Ice Caves',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 115115,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct5IceStuff')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'waypoint')).toBe(true)
|
|
})
|
|
})
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* 9. Act 1 - Barracks
|
|
* ----------------------------------------------------------------------- */
|
|
describe('Pass 9: DRLGMAZE_PlaceAct1Barracks', () => {
|
|
const barracksExtraPieces = [
|
|
mockPiece('Act 1 - Barracks Forge N', 'quest', 'N', [{ type: 2, id: 108, x: 25, y: 25, flags: 0 }]), // Charsi's Malus forge
|
|
mockPiece('Act 1 - Barracks Forge S', 'quest', 'S', [{ type: 2, id: 108, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece('Act 1 - Barracks Forge E', 'quest', 'E', [{ type: 2, id: 108, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece('Act 1 - Barracks Forge W', 'quest', 'W', [{ type: 2, id: 108, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece('Act 1 - Barracks Court Connect', 'prev', '', [{ type: 2, id: 1, x: 10, y: 10, flags: 0 }]), // Entrance from Cloister
|
|
]
|
|
|
|
test('Monastery Barracks (level 28): places Court Connect and Smith Forge', () => {
|
|
const pieces = createStandardPieceSet('Act 1 - Barracks', barracksExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 28,
|
|
levelName: 'Barracks',
|
|
sectionSize: 10,
|
|
sectionHeight: 14,
|
|
minRooms: 6,
|
|
merge: 500,
|
|
seed: 2828,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct1Barracks')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'quest')).toBe(true)
|
|
expect(specials.some(s => s.kind === 'prev')).toBe(true)
|
|
expect(res.level.objects.some(o => o.type === 2 && o.id === 108)).toBe(true)
|
|
})
|
|
})
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* 10. Act 4 - Lava
|
|
* ----------------------------------------------------------------------- */
|
|
describe('Pass 10: DRLGMAZE_PlaceAct4Lava', () => {
|
|
const lavaExtraPieces = [
|
|
mockPiece('Act 4 - Lava Warp N', 'prev', 'N', [{ type: 2, id: 255, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece('Act 4 - Bridge 1', 'next', '', [{ type: 2, id: 10, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece('Act 4 - Bridge 2', 'next', '', [{ type: 2, id: 11, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece('Act 4 - Lava Forge W', 'quest', 'W', [{ type: 2, id: 376, x: 25, y: 25, flags: 0 }]), // Hellforge
|
|
mockPiece('Act 4 - Lava Forge E', 'quest', 'E', [{ type: 2, id: 376, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece('Act 4 - Lava X', 'room', ''), // Filler
|
|
]
|
|
|
|
test('River of Flame (level 107): places Hellforge, Bridges, Warp and Lava X filler', () => {
|
|
const pieces = createStandardPieceSet('Act 4 - Lava', lavaExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 107,
|
|
levelName: 'River of Flame',
|
|
levelTypeName: 'Act 4 - Lava',
|
|
sectionSize: 10,
|
|
minRooms: 6,
|
|
merge: 500,
|
|
seed: 107107,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_PlaceAct4Lava')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'quest')).toBe(true)
|
|
expect(res.level.objects.some(o => o.type === 2 && o.id === 376)).toBe(true)
|
|
// Exactly 1 Warp N (never duplicate)
|
|
expect(specials.filter(s => s.kind === 'prev').length).toBe(1)
|
|
// 3 Bridge rooms: 1 of Bridge 1, 2 of Bridge 2
|
|
expect(res.stats.notes).toContain('piece "Act 4 - Bridge 1" used 1 time(s)')
|
|
expect(res.stats.notes).toContain('piece "Act 4 - Bridge 2" used 2 time(s)')
|
|
// Confirm Lava X filler spaces were placed
|
|
expect(specials.some(s => s.sides === 'filler')).toBe(true)
|
|
})
|
|
})
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* 11. Act 2 - Sewers
|
|
* ----------------------------------------------------------------------- */
|
|
describe('Pass 11: DRLGMAZE_ScanReplaceSpecialAct2SewersPresets', () => {
|
|
const sewer2ExtraPieces = [
|
|
mockPiece("Act 2 - Sewer Radament's Lair N", 'quest', 'N', [{ type: 1, id: 229, x: 25, y: 25, flags: 0 }]), // Radament
|
|
mockPiece("Act 2 - Sewer Radament's Lair S", 'quest', 'S', [{ type: 1, id: 229, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece("Act 2 - Sewer Radament's Lair E", 'quest', 'E', [{ type: 1, id: 229, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece("Act 2 - Sewer Radament's Lair W", 'quest', 'W', [{ type: 1, id: 229, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece('Act 2 - Sewer Prev E', 'prev', 'E'), // City entrance
|
|
mockPiece('Act 2 - Sewer Prev NS', 'prev', 'NS'), // Dock entrance
|
|
mockPiece('Act 2 - Sewer Waypoint N', 'waypoint', 'N', [{ type: 2, id: 156, x: 25, y: 25, flags: 0 }]),
|
|
mockPiece('Act 2 - Sewer Chest N', 'treasure', 'N', [{ type: 2, id: 355, x: 25, y: 25, flags: 0 }]),
|
|
]
|
|
|
|
test("Sewers Level 3 (level 49): places Radament's Lair", () => {
|
|
const pieces = createStandardPieceSet('Act 2 - Sewer', sewer2ExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 49,
|
|
levelName: 'Sewers Level 3',
|
|
sectionSize: 10,
|
|
minRooms: 6,
|
|
merge: 500,
|
|
seed: 4949,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_ScanReplaceSpecialAct2SewersPresets')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'quest')).toBe(true)
|
|
expect(res.level.objects.some(o => o.type === 1 && o.id === 229)).toBe(true)
|
|
})
|
|
|
|
test('Sewers Level 1 (level 47): places dock and city entrances', () => {
|
|
const pieces = createStandardPieceSet('Act 2 - Sewer', sewer2ExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 47,
|
|
levelName: 'Sewers Level 1',
|
|
sectionSize: 10,
|
|
minRooms: 6,
|
|
merge: 500,
|
|
seed: 4747,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_ScanReplaceSpecialAct2SewersPresets')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
const prevs = specials.filter(s => s.kind === 'prev')
|
|
expect(prevs.length).toBeGreaterThanOrEqual(1)
|
|
})
|
|
|
|
test('Sewers Level 2 (level 48): places Waypoint', () => {
|
|
const pieces = createStandardPieceSet('Act 2 - Sewer', sewer2ExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 48,
|
|
levelName: 'Sewers Level 2',
|
|
sectionSize: 10,
|
|
minRooms: 6,
|
|
merge: 500,
|
|
seed: 4848,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_ScanReplaceSpecialAct2SewersPresets')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'waypoint')).toBe(true)
|
|
})
|
|
|
|
test('Ancient Tunnels (level 50): places golden chest', () => {
|
|
const pieces = createStandardPieceSet('Act 2 - Sewer', sewer2ExtraPieces)
|
|
const res = generateMaze({
|
|
levelId: 50,
|
|
levelName: 'Ancient Tunnels',
|
|
sectionSize: 10,
|
|
minRooms: 5,
|
|
merge: 500,
|
|
seed: 5050,
|
|
pieces,
|
|
})
|
|
|
|
expect(res.stats.specialPassesApplied).toContain('DRLGMAZE_ScanReplaceSpecialAct2SewersPresets')
|
|
const specials = res.stats.specialsApplied as { room: number; kind: string; sides: string }[]
|
|
expect(specials.some(s => s.kind === 'treasure')).toBe(true)
|
|
})
|
|
})
|
|
|
|
/* ----------------------------------------------------------------------- *
|
|
* Classification tests
|
|
* ----------------------------------------------------------------------- */
|
|
describe('Piece Classification for Special Presets', () => {
|
|
test('classifies waypoint pieces', () => {
|
|
expect(classifyMazePieceName('Act 2 - Tomb Waypoint N', 'Act 2 - Tomb')).toEqual({
|
|
kind: 'waypoint',
|
|
sides: 'N',
|
|
})
|
|
expect(classifyMazePieceName('Act 5 - Temple NE Waypoint', 'Act 5 - Temple')).toEqual({
|
|
kind: 'waypoint',
|
|
sides: 'NE',
|
|
})
|
|
expect(classifyMazePieceName('Act 5 - Ice waypoint N', 'Act 5 - Ice Caves')).toEqual({
|
|
kind: 'waypoint',
|
|
sides: 'N',
|
|
})
|
|
})
|
|
|
|
test('classifies quest and special pieces', () => {
|
|
expect(classifyMazePieceName('Act 2 - Tomb Talrasha N', 'Act 2 - Tomb')).toEqual({
|
|
kind: 'quest',
|
|
sides: 'N',
|
|
})
|
|
expect(classifyMazePieceName("Act 2 - Sewer Radament's Lair S", 'Act 2 - Sewer')).toEqual({
|
|
kind: 'quest',
|
|
sides: 'S',
|
|
})
|
|
expect(classifyMazePieceName('Act 2 - Lair Tight Spot S', 'Act 2 - Lair')).toEqual({
|
|
kind: 'quest',
|
|
sides: 'S',
|
|
})
|
|
expect(classifyMazePieceName('Act 1 - Barracks Forge W', 'Act 1 - Barracks')).toEqual({
|
|
kind: 'quest',
|
|
sides: 'W',
|
|
})
|
|
expect(classifyMazePieceName('Act 4 - Bridge 1', 'Act 4 - Lava')).toEqual({
|
|
kind: 'next',
|
|
sides: '',
|
|
})
|
|
expect(classifyMazePieceName('Act 3 - Mephisto Complex', 'Act 3 - Kurast')).toEqual({
|
|
kind: 'quest',
|
|
sides: '',
|
|
})
|
|
expect(classifyMazePieceName('Act 5 - Temple Final Room', 'Act 5 - Temple')).toEqual({
|
|
kind: 'quest',
|
|
sides: '',
|
|
})
|
|
expect(classifyMazePieceName('Act 5 - ThroneRoom Entrance', 'Act 5 - Baal')).toEqual({
|
|
kind: 'quest',
|
|
sides: '',
|
|
})
|
|
})
|
|
})
|
|
})
|