198 lines
5.3 KiB
TypeScript
198 lines
5.3 KiB
TypeScript
import { describe, expect, test } from 'vitest'
|
|
import type { Ds1, Ds1Cell } from '../src/formats/ds1'
|
|
import { generateMaze, type MazePiece, type MazePieceKind, type MazeRequest } from '../src/game/maze'
|
|
|
|
function makeMockDs1(width = 10, height = 10): Ds1 {
|
|
const cells: Ds1Cell[][] = []
|
|
for (let y = 0; y < height; y += 1) {
|
|
const row: Ds1Cell[] = []
|
|
for (let x = 0; x < width; x += 1) {
|
|
row.push({
|
|
walls: [{ prop1: 0, sequence: 0, style: 0, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
|
floors: [{ prop1: 0, 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 makeMockPiece(name: string, kind: MazePieceKind, sides: string): MazePiece {
|
|
return {
|
|
name,
|
|
kind,
|
|
sides,
|
|
levels: [makeMockDs1(10, 10)],
|
|
}
|
|
}
|
|
|
|
function makeCavePieceSet(): MazePiece[] {
|
|
const ds1 = makeMockDs1(10, 10)
|
|
const tokens = [
|
|
'NSEW',
|
|
'NSE', 'NSW', 'NEW', 'SEW',
|
|
'NS', 'EW', 'NE', 'NW', 'SE', 'SW',
|
|
'N', 'S', 'E', 'W',
|
|
]
|
|
const pieces: MazePiece[] = tokens.map(token => ({
|
|
name: `Act 1 - Cave ${token}`,
|
|
kind: 'room',
|
|
sides: token,
|
|
levels: [ds1],
|
|
}))
|
|
pieces.push({
|
|
name: 'Act 1 - Cave Prev NS',
|
|
kind: 'prev',
|
|
sides: 'NS',
|
|
levels: [ds1],
|
|
})
|
|
pieces.push({
|
|
name: 'Act 1 - Cave Prev EW',
|
|
kind: 'prev',
|
|
sides: 'EW',
|
|
levels: [ds1],
|
|
})
|
|
pieces.push({
|
|
name: 'Act 1 - Cave Down NS',
|
|
kind: 'down',
|
|
sides: 'NS',
|
|
levels: [ds1],
|
|
})
|
|
pieces.push({
|
|
name: 'Act 1 - Cave Down EW',
|
|
kind: 'down',
|
|
sides: 'EW',
|
|
levels: [ds1],
|
|
})
|
|
return pieces
|
|
}
|
|
|
|
describe('Maze Global Bounding Box Constraints (Issue #48)', () => {
|
|
test('generateMaze respects maxSectionsX and maxSectionsY limits', () => {
|
|
const pieces = makeCavePieceSet()
|
|
const request: MazeRequest = {
|
|
levelId: 2,
|
|
levelName: 'Act 1 - Cave 1',
|
|
levelTypeName: 'Act 1 - Cave',
|
|
sectionSize: 10,
|
|
sectionHeight: 10,
|
|
minRooms: 9,
|
|
merge: 0,
|
|
seed: 12345,
|
|
pieces,
|
|
maxSectionsX: 3,
|
|
maxSectionsY: 3,
|
|
}
|
|
|
|
const result = generateMaze(request)
|
|
expect(result.level.width).toBeLessThanOrEqual(30)
|
|
expect(result.level.height).toBeLessThanOrEqual(30)
|
|
expect(result.stats.maxCellsX).toBe(30)
|
|
expect(result.stats.maxCellsY).toBe(30)
|
|
expect(Number(result.stats.roomsPlaced)).toBeGreaterThanOrEqual(9)
|
|
})
|
|
|
|
test('generateMaze respects maxCellsX and maxCellsY directly', () => {
|
|
const pieces = makeCavePieceSet()
|
|
const request: MazeRequest = {
|
|
levelId: 2,
|
|
levelName: 'Act 1 - Cave 1',
|
|
levelTypeName: 'Act 1 - Cave',
|
|
sectionSize: 10,
|
|
sectionHeight: 10,
|
|
minRooms: 8,
|
|
merge: 500,
|
|
seed: 99999,
|
|
pieces,
|
|
maxCellsX: 30,
|
|
maxCellsY: 30,
|
|
}
|
|
|
|
const result = generateMaze(request)
|
|
expect(result.level.width).toBeLessThanOrEqual(30)
|
|
expect(result.level.height).toBeLessThanOrEqual(30)
|
|
expect(result.stats.maxCellsX).toBe(30)
|
|
expect(result.stats.maxCellsY).toBe(30)
|
|
})
|
|
|
|
test('forces maze to fold vertically when maxSectionsX is narrow (e.g. 2 sections wide)', () => {
|
|
const pieces = makeCavePieceSet()
|
|
const request: MazeRequest = {
|
|
levelId: 2,
|
|
levelName: 'Act 1 - Cave 1',
|
|
levelTypeName: 'Act 1 - Cave',
|
|
sectionSize: 10,
|
|
sectionHeight: 10,
|
|
minRooms: 8,
|
|
merge: 0,
|
|
seed: 42,
|
|
pieces,
|
|
maxSectionsX: 2,
|
|
maxSectionsY: 6,
|
|
}
|
|
|
|
const result = generateMaze(request)
|
|
expect(result.level.width).toBeLessThanOrEqual(20)
|
|
expect(result.level.height).toBeLessThanOrEqual(60)
|
|
expect(Number(result.stats.roomsPlaced)).toBeGreaterThanOrEqual(8)
|
|
expect(Number(result.stats.boundsRejected)).toBeGreaterThanOrEqual(1)
|
|
})
|
|
|
|
test('records boundsRejected when room growth attempts cross the configured boundary', () => {
|
|
const pieces = makeCavePieceSet()
|
|
const request: MazeRequest = {
|
|
levelId: 2,
|
|
levelName: 'Act 1 - Cave 1',
|
|
levelTypeName: 'Act 1 - Cave',
|
|
sectionSize: 10,
|
|
sectionHeight: 10,
|
|
minRooms: 9,
|
|
merge: 800,
|
|
seed: 54321,
|
|
pieces,
|
|
maxSectionsX: 3,
|
|
maxSectionsY: 3,
|
|
}
|
|
|
|
const result = generateMaze(request)
|
|
expect(result.stats.boundsRejected).toBeDefined()
|
|
expect(typeof result.stats.boundsRejected).toBe('number')
|
|
expect(result.level.width).toBeLessThanOrEqual(30)
|
|
expect(result.level.height).toBeLessThanOrEqual(30)
|
|
})
|
|
|
|
test('unconstrained maze allows wide sprawl when no bounds are specified', () => {
|
|
const pieces = makeCavePieceSet()
|
|
const request: MazeRequest = {
|
|
levelId: 2,
|
|
levelName: 'Act 1 - Cave 1',
|
|
levelTypeName: 'Act 1 - Cave',
|
|
sectionSize: 10,
|
|
sectionHeight: 10,
|
|
minRooms: 9,
|
|
merge: 0,
|
|
seed: 12345,
|
|
pieces,
|
|
}
|
|
|
|
const result = generateMaze(request)
|
|
expect(result.stats.maxCellsX).toBeUndefined()
|
|
expect(result.stats.maxCellsY).toBeUndefined()
|
|
expect(result.stats.boundsRejected).toBe(0)
|
|
expect(Number(result.stats.roomsPlaced)).toBeGreaterThanOrEqual(9)
|
|
})
|
|
})
|