diablo2-web/tests/maze-specials.test.ts

349 lines
12 KiB
TypeScript

import { describe, test, expect } from 'vitest'
import {
generateMaze,
unimplementedPassesFor,
UNIMPLEMENTED_PASSES,
MAZE_LEVEL_TYPE_PROFILES,
type MazePiece,
type MazePieceKind,
type MazeWarp,
type MazeWaypoint,
} from '../src/game/maze.ts'
import {
LVLPREST_ACT2_CORRUPT_HAREM_NE,
LVLPREST_ACT2_CORRUPT_HAREM_SE,
LVLPREST_ACT2_CORRUPT_HAREM_SW,
LVLPREST_ACT2_CORRUPT_HAREM_NW,
LVLPREST_ACT2_BASEMENT_NE,
LVLPREST_ACT2_BASEMENT_SE,
LVLPREST_ACT2_BASEMENT_SW,
LVLPREST_ACT2_BASEMENT_NW,
LVLPREST_ACT3_SPIDER_SW,
LVLPREST_ACT3_SPIDER_SE,
LVLPREST_ACT3_SPIDER_NW,
LVLPREST_ACT3_SPIDER_NE,
LVLPREST_ACT3_SPIDER_CHEST_NW,
LVLPREST_ACT3_SPIDER_CHEST_NE,
LVLPREST_ACT5_LAVA_N,
LVLPREST_ACT5_LAVA_S,
LVLPREST_ACT5_LAVA_E,
LVLPREST_ACT5_LAVA_W,
LVLPREST_ACT5_LAVA_EW,
LVLPREST_ACT5_LAVA_NS,
LVLPREST_MAZE_DEF_REGISTRY,
} from '../src/game/maze-special-passes.ts'
import type { Ds1, Ds1Cell, Ds1Object } from '../src/formats/ds1.ts'
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,
}
}
function mockPiece(
name: string,
kind: MazePieceKind,
sides = '',
objects: Ds1Object[] = [],
width = 10,
height = 10,
): MazePiece {
return {
name,
kind,
sides,
levels: [mockDs1(width, height, objects)],
}
}
const SIDES = ['W', 'E', 'EW', 'S', 'SW', 'SE', 'SEW', 'N', 'NW', 'NE', 'NEW', 'NS', 'NSW', 'NSE', 'NSEW']
function createPieceSetForType(
levelTypeName: string,
extraKinds: { kind: MazePieceKind; nameToken: string; objects?: Ds1Object[] }[] = [],
): MazePiece[] {
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} Waypoint ${s}`, 'waypoint', s, [{ type: 2, id: 156, x: 25, y: 25, flags: 0 }]),
),
...['N', 'S', 'E', 'W'].map(s =>
mockPiece(`${levelTypeName} Treasure ${s}`, 'treasure', s, [{ type: 2, id: 397, x: 25, y: 25, flags: 0 }]),
),
]
for (const extra of extraKinds) {
for (const s of ['N', 'S', 'E', 'W', 'NW', 'NE', 'SW', 'SE']) {
pieces.push(mockPiece(`${levelTypeName} ${extra.nameToken} ${s}`, extra.kind, s, extra.objects ?? []))
}
}
return pieces
}
describe('Maze Specials Profiles (Issue #84)', () => {
describe('Profile completeness and unimplementedPasses check', () => {
test('all 4 target level types report 0 unimplemented passes', () => {
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('UNIMPLEMENTED_PASSES global array is completely empty', () => {
expect(UNIMPLEMENTED_PASSES).toEqual([])
})
test('profile table contains correctly configured special tables for all four types', () => {
const harem = MAZE_LEVEL_TYPE_PROFILES.find(p => p.levelTypeName === 'Act 2 - Harem')!
expect(harem).toBeDefined()
expect(harem.ring).toBe(2)
expect(harem.grow).toBe(false)
expect(harem.specials.map(s => ({ kind: s.kind, levelIds: s.levelIds }))).toEqual([
{ kind: 'prev', levelIds: [51] },
{ kind: 'next', levelIds: [51] },
])
const basement = MAZE_LEVEL_TYPE_PROFILES.find(p => p.levelTypeName === 'Act 2 - Basement')!
expect(basement).toBeDefined()
expect(basement.ring).toBe(2)
expect(basement.grow).toBe(false)
expect(basement.specials.map(s => ({ kind: s.kind, levelIds: s.levelIds }))).toEqual([
{ kind: 'prev', levelIds: [52, 53, 54] },
{ kind: 'waypoint', levelIds: [52] },
{ kind: 'next', levelIds: [52, 53] },
])
const spider = MAZE_LEVEL_TYPE_PROFILES.find(p => p.levelTypeName === 'Act 3 - Spider')!
expect(spider).toBeDefined()
expect(spider.ring).toBe(2)
expect(spider.grow).toBe(false)
expect(spider.specials.map(s => ({ kind: s.kind, levelIds: s.levelIds }))).toEqual([
{ kind: 'prev', levelIds: [84, 85] },
{ kind: 'treasure', levelIds: [84, 85] },
])
const lava = MAZE_LEVEL_TYPE_PROFILES.find(p => p.levelTypeName === 'Act 5 - Lava')!
expect(lava).toBeDefined()
expect(lava.grow).toBe(false)
expect(lava.lavaPairs).toBe(true)
expect(lava.specials.map(s => ({ kind: s.kind, levelIds: s.levelIds }))).toEqual([
{ kind: 'prev', levelIds: [125, 126, 127] },
{ kind: 'treasure', levelIds: [125, 126, 127] },
])
})
})
describe('LvlPrest constants & def registry', () => {
test('defines canonical LvlPrest Def IDs', () => {
expect(LVLPREST_ACT2_CORRUPT_HAREM_NE).toBe(354)
expect(LVLPREST_ACT2_CORRUPT_HAREM_SE).toBe(355)
expect(LVLPREST_ACT2_CORRUPT_HAREM_SW).toBe(356)
expect(LVLPREST_ACT2_CORRUPT_HAREM_NW).toBe(357)
expect(LVLPREST_ACT2_BASEMENT_NE).toBe(358)
expect(LVLPREST_ACT2_BASEMENT_SE).toBe(359)
expect(LVLPREST_ACT2_BASEMENT_SW).toBe(360)
expect(LVLPREST_ACT2_BASEMENT_NW).toBe(361)
expect(LVLPREST_ACT3_SPIDER_SW).toBe(659)
expect(LVLPREST_ACT3_SPIDER_SE).toBe(660)
expect(LVLPREST_ACT3_SPIDER_NW).toBe(661)
expect(LVLPREST_ACT3_SPIDER_NE).toBe(662)
expect(LVLPREST_ACT3_SPIDER_CHEST_NW).toBe(663)
expect(LVLPREST_ACT3_SPIDER_CHEST_NE).toBe(664)
expect(LVLPREST_ACT5_LAVA_N).toBe(1053)
expect(LVLPREST_ACT5_LAVA_S).toBe(1054)
expect(LVLPREST_ACT5_LAVA_E).toBe(1055)
expect(LVLPREST_ACT5_LAVA_W).toBe(1056)
expect(LVLPREST_ACT5_LAVA_EW).toBe(1057)
expect(LVLPREST_ACT5_LAVA_NS).toBe(1058)
})
test('registers Spider chests in LVLPREST_MAZE_DEF_REGISTRY', () => {
expect(LVLPREST_MAZE_DEF_REGISTRY[LVLPREST_ACT3_SPIDER_CHEST_NW]).toEqual({
name: 'Act 3 - Spider Chest NW',
kind: 'treasure',
sides: 'NW',
})
expect(LVLPREST_MAZE_DEF_REGISTRY[LVLPREST_ACT3_SPIDER_CHEST_NE]).toEqual({
name: 'Act 3 - Spider Chest NE',
kind: 'treasure',
sides: 'NE',
})
})
})
describe('generateMaze execution with specials', () => {
test('Act 2 - Harem (Level 51) places prev and next specials', () => {
const pieces = createPieceSetForType('Act 2 - Harem')
const result = generateMaze({
levelId: 51,
levelName: 'Harem Level 2',
levelTypeName: 'Act 2 - Harem',
sectionSize: 10,
minRooms: 4,
merge: 0,
seed: 51001,
pieces,
})
expect(result.stats.unimplementedPasses).toEqual([])
const specials = result.stats.specialsApplied as { room: number; kind: string; sides: string }[]
const kinds = specials.map(s => s.kind)
expect(kinds).toContain('prev')
expect(kinds).toContain('next')
const warps = result.stats.warps as MazeWarp[]
expect(warps.length).toBeGreaterThanOrEqual(2)
expect(warps.some(w => w.direction === 'up')).toBe(true)
expect(warps.some(w => w.direction === 'down')).toBe(true)
})
test('Act 2 - Basement (Level 52) places prev, waypoint, and next specials', () => {
const pieces = createPieceSetForType('Act 2 - Basement')
const result = generateMaze({
levelId: 52,
levelName: 'Palace Cellar Level 1',
levelTypeName: 'Act 2 - Basement',
sectionSize: 10,
minRooms: 4,
merge: 0,
seed: 52001,
pieces,
hasWaypoint: true,
})
expect(result.stats.unimplementedPasses).toEqual([])
const specials = result.stats.specialsApplied as { room: number; kind: string; sides: string }[]
const kinds = specials.map(s => s.kind)
expect(kinds).toContain('prev')
expect(kinds).toContain('waypoint')
expect(kinds).toContain('next')
const waypoints = result.stats.waypoints as MazeWaypoint[]
expect(waypoints.length).toBe(1)
const warps = result.stats.warps as MazeWarp[]
expect(warps.length).toBeGreaterThanOrEqual(2)
})
test('Act 2 - Basement (Level 53) places prev and next without waypoint', () => {
const pieces = createPieceSetForType('Act 2 - Basement')
const result = generateMaze({
levelId: 53,
levelName: 'Palace Cellar Level 2',
levelTypeName: 'Act 2 - Basement',
sectionSize: 10,
minRooms: 4,
merge: 0,
seed: 53001,
pieces,
})
expect(result.stats.unimplementedPasses).toEqual([])
const specials = result.stats.specialsApplied as { room: number; kind: string; sides: string }[]
const kinds = specials.map(s => s.kind)
expect(kinds).toContain('prev')
expect(kinds).toContain('next')
expect(kinds).not.toContain('waypoint')
const waypoints = result.stats.waypoints as MazeWaypoint[]
expect(waypoints.length).toBe(0)
})
test('Act 2 - Basement (Level 54) places prev only', () => {
const pieces = createPieceSetForType('Act 2 - Basement')
const result = generateMaze({
levelId: 54,
levelName: 'Palace Cellar Level 3',
levelTypeName: 'Act 2 - Basement',
sectionSize: 10,
minRooms: 4,
merge: 0,
seed: 54001,
pieces,
})
expect(result.stats.unimplementedPasses).toEqual([])
const specials = result.stats.specialsApplied as { room: number; kind: string; sides: string }[]
const kinds = specials.map(s => s.kind)
expect(kinds).toContain('prev')
expect(kinds).not.toContain('next')
expect(kinds).not.toContain('waypoint')
})
test('Act 3 - Spider (Level 84 & 85) places prev and treasure', () => {
for (const levelId of [84, 85]) {
const pieces = createPieceSetForType('Act 3 - Spider')
const result = generateMaze({
levelId,
levelName: levelId === 84 ? 'Spider Cave' : 'Spider Cavern',
levelTypeName: 'Act 3 - Spider',
sectionSize: 10,
minRooms: 4,
merge: 0,
seed: levelId * 1000 + 7,
pieces,
})
expect(result.stats.unimplementedPasses).toEqual([])
const specials = result.stats.specialsApplied as { room: number; kind: string; sides: string }[]
const kinds = specials.map(s => s.kind)
expect(kinds).toContain('prev')
expect(kinds).toContain('treasure')
const warps = result.stats.warps as MazeWarp[]
expect(warps.some(w => w.direction === 'up')).toBe(true)
}
})
test('Act 5 - Lava (Levels 125, 126, 127) places prev and treasure', () => {
for (const levelId of [125, 126, 127]) {
const pieces = createPieceSetForType('Act 5 - Lava')
const result = generateMaze({
levelId,
levelName: `Lava Pit ${levelId}`,
levelTypeName: 'Act 5 - Lava',
sectionSize: 10,
minRooms: 4,
merge: 0,
seed: levelId * 1000 + 9,
pieces,
})
expect(result.stats.unimplementedPasses).toEqual([])
const specials = result.stats.specialsApplied as { room: number; kind: string; sides: string }[]
const kinds = specials.map(s => s.kind)
expect(kinds).toContain('prev')
expect(kinds).toContain('treasure')
const warps = result.stats.warps as MazeWarp[]
expect(warps.some(w => w.direction === 'up')).toBe(true)
}
})
})
})