diablo2-web/tests/wilderness-d2moo-parity.tes...

729 lines
26 KiB
TypeScript

/**
* Wilderness D2MOO Ground Truth Parity Test Suite (Issue #455).
*
* Verifies that outdoor wilderness level generation adheres with 100% fidelity
* to Diablo II v1.13c ground truth as established by D2MOO C++ DRLG engine
* (`D2Common/src/Drlg/`) and native binary execution (`d2moo_mapgen`).
*
* Multi-Tier Parity Coverage:
* - Tier 1: Dynamic Level Dimensions (96x56 horizontal vs 56x96 vertical) & Topology (R1)
* - Tier 2: Outer River Placement, Dual Columns (x=80, 88), and Boundary Suppression (R2)
* - Tier 3: Town Transition (TownSTrans.ds1 56x8) Anchored to Eastern River Edge (R4)
* - Tier 4: Unconditional Landmark Presets & FarAway Anti-Overlap Metric (R4)
* - Tier 5: 4-Pass Secondary Border Evolution & Interior 2x3 Forest Closures (Bord9..12) (R3)
* - Tier 6: Orthogonal Polyline Dirt Paths & Width-2 Bresenham Rasterization (R5)
*/
import { describe, it, expect, beforeAll } from 'vitest'
import { existsSync, readFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { MountedArchives } from '../src/mpq/mount.ts'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { loadActTables, parseTable, cell, tileMemberPath, type D2Table } from '../src/game/acts.ts'
import { decodeDs1, type Ds1 } from '../src/formats/ds1.ts'
import {
generateWilderness,
type WildernessPiece,
type WildernessSubstitution,
type WildernessResult,
} from '../src/game/wilderness.ts'
/* ------------------------------------------------------------------------- *
* D2MOO Data Contracts & Types
* ------------------------------------------------------------------------- */
export interface OutdoorCellJson {
readonly x: number
readonly y: number
readonly fPacked: number
readonly fStyle: number
readonly fSeq: number
readonly wPacked: number
readonly wStyle: number
readonly wSeq: number
readonly wType: number
readonly dirt: number
}
export interface D2MooRoomJson {
readonly index: number
readonly type: number
readonly tileX: number
readonly tileY: number
readonly tileW: number
readonly tileH: number
readonly mapPtr: number
readonly mapX: number
readonly mapY: number
readonly mapW: number
readonly mapH: number
readonly lvlPrestId: number
readonly pickedFile: number
readonly ds1File: string
readonly ds1Width: number
readonly ds1Height: number
readonly doorMask: number
readonly orthCount: number
readonly outdoorCells: readonly OutdoorCellJson[]
}
export interface D2MooLevelJson {
readonly levelId: number
readonly levelName: string
readonly act: number
readonly drlgType: number
readonly levelType: number
readonly seed: number
readonly posX: number
readonly posY: number
readonly width: number
readonly height: number
readonly roomCount: number
readonly rooms: readonly D2MooRoomJson[]
}
/* ------------------------------------------------------------------------- *
* Dual-Mode Execution & Fixture Loader
* ------------------------------------------------------------------------- */
const D2MOO_BIN = process.env.D2MOO_BIN ?? '/usr/local/google/home/taodao/tmp/d2moo_runner/d2moo_mapgen'
const FIXTURES_DIR = 'tests/fixtures/d2moo'
const hasD2Archives = existsSync('samples/d2/d2data.mpq')
/**
* Loads a D2MOO level layout either by executing `d2moo_mapgen` dynamically
* or by reading the pre-recorded golden fixture from `tests/fixtures/d2moo/`.
*/
export function loadOrRunD2MooLevel(levelId: number, seed: number | string): D2MooLevelJson {
const seedNum = typeof seed === 'number' ? seed : (seed.startsWith('0x') ? parseInt(seed, 16) : parseInt(seed, 10))
const seedHex = `0x${seedNum.toString(16)}`
const seedDec = seedNum.toString(10)
// 1. If native binary is present and not explicitly bypassed, run dynamically
if (existsSync(D2MOO_BIN) && process.env.D2MOO_FORCE_FIXTURE !== '1') {
try {
const tmpOut = join(tmpdir(), `d2moo_parity_${levelId}_${seedNum.toString(16)}.json`)
execFileSync(D2MOO_BIN, [String(levelId), seedHex, '0', tmpOut], { stdio: 'ignore' })
if (existsSync(tmpOut)) {
return JSON.parse(readFileSync(tmpOut, 'utf8')) as D2MooLevelJson
}
} catch {
// Fallback to pre-recorded fixture
}
}
// 2. Fallback to pre-recorded golden JSON fixture
const candidateNames = [
`level${levelId}_seed_0x${seedNum.toString(16)}.json`,
`level${levelId}_seed_${seedDec}.json`,
`level${levelId}_seed_${seedNum.toString(16)}.json`,
`level_${levelId}_seed_${seedNum.toString(16)}.json`,
]
for (const name of candidateNames) {
const p = join(FIXTURES_DIR, name)
if (existsSync(p)) {
return JSON.parse(readFileSync(p, 'utf8')) as D2MooLevelJson
}
}
throw new Error(`Neither executable d2moo_mapgen nor golden fixture found for Level ${levelId} Seed ${seedHex}`)
}
/**
* Extracts distinct preset stamp instances (deduplicating multi-room presets sharing mapPtr).
*/
export function getDistinctPresetStamps(level: D2MooLevelJson): Array<{
file: string
x: number
y: number
w: number
h: number
}> {
const seenMapPtrs = new Set<number>()
const stamps: Array<{ file: string; x: number; y: number; w: number; h: number }> = []
for (const room of level.rooms) {
if (!room.ds1File) continue
if (seenMapPtrs.has(room.mapPtr)) continue
seenMapPtrs.add(room.mapPtr)
const file = room.ds1File.split('/').pop() ?? room.ds1File
stamps.push({
file,
x: room.mapX - level.posX,
y: room.mapY - level.posY,
w: room.mapW,
h: room.mapH,
})
}
return stamps
}
/**
* Extracts absolute world tile coordinates of all dirt road cells in the level.
*/
export function getDirtRoadCells(level: D2MooLevelJson): Array<{ x: number; y: number }> {
const cells: Array<{ x: number; y: number }> = []
for (const room of level.rooms) {
for (const cellRecord of room.outdoorCells) {
if (cellRecord.dirt !== 0) {
cells.push({
x: room.tileX - level.posX + cellRecord.x,
y: room.tileY - level.posY + cellRecord.y,
})
}
}
}
return cells
}
/**
* Analyzes connectivity of road cells via 8-way BFS flood-fill.
*/
export function evaluateRoadConnectivity(cells: Array<{ x: number; y: number }>): {
totalCells: number
componentCount: number
largestComponentSize: number
} {
const cellSet = new Set(cells.map(c => `${c.x},${c.y}`))
const visited = new Set<string>()
const components: string[][] = []
for (const key of cellSet) {
if (visited.has(key)) continue
const queue = [key]
visited.add(key)
const comp = [key]
while (queue.length > 0) {
const current = queue.shift()!
const [cx, cy] = current.split(',').map(Number)
for (const [dx, dy] of [
[1, 0], [-1, 0], [0, 1], [0, -1],
[1, 1], [1, -1], [-1, 1], [-1, -1],
]) {
const neighbor = `${cx + dx},${cy + dy}`
if (cellSet.has(neighbor) && !visited.has(neighbor)) {
visited.add(neighbor)
queue.push(neighbor)
comp.push(neighbor)
}
}
}
components.push(comp)
}
return {
totalCells: cells.length,
componentCount: components.length,
largestComponentSize: components[0]?.length ?? 0,
}
}
/* ========================================================================= *
* SUITE 1: D2MOO v1.13c Ground Truth Oracle & Fixture Invariants
* ========================================================================= */
describe('Part 1: D2MOO Ground Truth Fixture & Runner Verification (Issue #455)', () => {
// --- Tier 1: Dimensions & Orientations ---
describe('Tier 1: Dynamic Level Dimensions & Topology Invariants (R1)', () => {
it('verifies horizontal 96x56 Moor layout for Seed 0x12345678', () => {
const d2moo = loadOrRunD2MooLevel(2, 0x12345678)
expect(d2moo.width).toBe(96)
expect(d2moo.height).toBe(56)
expect(d2moo.width / 8).toBe(12)
expect(d2moo.height / 8).toBe(7)
expect(d2moo.roomCount).toBe(82)
})
it('verifies horizontal 96x56 Moor layout for Seed 12345', () => {
const d2moo = loadOrRunD2MooLevel(2, 12345)
expect(d2moo.width).toBe(96)
expect(d2moo.height).toBe(56)
expect(d2moo.width / 8).toBe(12)
expect(d2moo.height / 8).toBe(7)
expect(d2moo.roomCount).toBe(84)
})
it('verifies vertical 56x96 Moor layout for Seed 1', () => {
const d2moo = loadOrRunD2MooLevel(2, 1)
expect(d2moo.width).toBe(56)
expect(d2moo.height).toBe(96)
expect(d2moo.width / 8).toBe(7)
expect(d2moo.height / 8).toBe(12)
expect(d2moo.roomCount).toBe(79)
})
it('verifies vertical 56x96 Moor layout for Seed 42', () => {
const d2moo = loadOrRunD2MooLevel(2, 42)
expect(d2moo.width).toBe(56)
expect(d2moo.height).toBe(96)
expect(d2moo.width / 8).toBe(7)
expect(d2moo.height / 8).toBe(12)
expect(d2moo.roomCount).toBe(82)
})
it('verifies Cold Plains (Level 3) fixed 80x80 layout for Seed 1', () => {
const d2moo = loadOrRunD2MooLevel(3, 1)
expect(d2moo.width).toBe(80)
expect(d2moo.height).toBe(80)
expect(d2moo.roomCount).toBe(97)
})
it('verifies Stony Field (Level 4) fixed 80x80 layout for Seed 1', () => {
const d2moo = loadOrRunD2MooLevel(4, 1)
expect(d2moo.width).toBe(80)
expect(d2moo.height).toBe(80)
expect(d2moo.roomCount).toBe(91)
})
})
// --- Tier 2: Outer River Placement ---
describe('Tier 2: Outer River Placement & Boundary Suppression (R2)', () => {
it('spawns dual-column river on eastern boundary (x=80, x=88) for Seed 0x12345678', () => {
const d2moo = loadOrRunD2MooLevel(2, 0x12345678)
const riverRooms = d2moo.rooms.filter(r => r.ds1File && r.ds1File.includes('River'))
expect(riverRooms.length).toBeGreaterThanOrEqual(12)
const uRiverRooms = riverRooms.filter(r => r.ds1File.includes('URiverC'))
const lRiverRooms = riverRooms.filter(r => r.ds1File.includes('LRiverC'))
expect(uRiverRooms.length).toBeGreaterThanOrEqual(6)
expect(lRiverRooms.length).toBeGreaterThanOrEqual(6)
// Column x = 80 (block 10, nGridWidth - 2) for URiverC
for (const room of uRiverRooms) {
expect(room.mapX - d2moo.posX).toBe(80)
}
// Column x = 88 (block 11, nGridWidth - 1) for LRiverC
for (const room of lRiverRooms) {
expect(room.mapX - d2moo.posX).toBe(88)
}
// Outer boundary tree wall suppression: no Bord*.ds1 rooms exist at x = 88 where river flows
const eastBorders = d2moo.rooms.filter(r => {
const file = r.ds1File?.split('/').pop() ?? ''
const relX = r.mapX - d2moo.posX
return file.startsWith('Bord') && relX === 88
})
expect(eastBorders.length).toBe(0)
})
it('spawns dual-column river on eastern boundary (x=80, x=88) for Seed 12345', () => {
const d2moo = loadOrRunD2MooLevel(2, 12345)
const uRiverRooms = d2moo.rooms.filter(r => r.ds1File && r.ds1File.includes('URiverC'))
const lRiverRooms = d2moo.rooms.filter(r => r.ds1File && r.ds1File.includes('LRiverC'))
expect(uRiverRooms.length).toBeGreaterThanOrEqual(6)
expect(lRiverRooms.length).toBeGreaterThanOrEqual(6)
for (const room of uRiverRooms) expect(room.mapX - d2moo.posX).toBe(80)
for (const room of lRiverRooms) expect(room.mapX - d2moo.posX).toBe(88)
})
})
// --- Tier 3: Town Transition Alignment ---
describe('Tier 3: Town Transition Alignment & River Edge Anchoring (R4)', () => {
it('places TownSTrans.ds1 (56x8) anchored to East river at (40, 0) for Seed 0x12345678', () => {
const d2moo = loadOrRunD2MooLevel(2, 0x12345678)
const townRoom = d2moo.rooms.find(r => r.ds1File && r.ds1File.includes('TownSTrans'))
expect(townRoom).toBeDefined()
expect(townRoom!.mapX - d2moo.posX).toBe(40)
expect(townRoom!.mapY - d2moo.posY).toBe(0)
expect(townRoom!.mapW).toBe(56)
expect(townRoom!.mapH).toBe(8)
// 40 + 56 = 96: transition extends exactly to the eastern river edge (x = 80..96)
expect(townRoom!.mapX - d2moo.posX + townRoom!.mapW).toBe(d2moo.width)
})
})
// --- Tier 4: Landmark Presets & FarAway Anti-Overlap Metric ---
describe('Tier 4: Complete Landmark Presets & FarAway Distribution (R4)', () => {
it('unconditionally places all 5 canonical landmark categories in Blood Moor (Seed 0x12345678)', () => {
const d2moo = loadOrRunD2MooLevel(2, 0x12345678)
const uniquePresets = new Set(d2moo.rooms.map(r => r.ds1File.split('/').pop()).filter(Boolean))
expect(uniquePresets.has('DenEnt.ds1')).toBe(true)
expect(uniquePresets.has('Pond1.ds1')).toBe(true)
expect(uniquePresets.has('Cott1.ds1')).toBe(true)
expect(uniquePresets.has('Circle.ds1')).toBe(true)
expect(uniquePresets.has('Clover.ds1')).toBe(true)
})
it('verifies zero spatial overlap across all distinct preset stamps', () => {
for (const [lvl, s] of [
[2, 0x12345678],
[2, 12345],
[2, 1],
[2, 42],
[3, 1],
[4, 1],
] as const) {
const d2moo = loadOrRunD2MooLevel(lvl, s)
const stamps = getDistinctPresetStamps(d2moo)
for (let i = 0; i < stamps.length; i += 1) {
for (let j = i + 1; j < stamps.length; j += 1) {
const a = stamps[i]!
const b = stamps[j]!
const xOverlap = Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x))
const yOverlap = Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y))
expect(
xOverlap > 0 && yOverlap > 0,
`Overlap detected in Level ${lvl} Seed ${s} between ${a.file} and ${b.file}`,
).toBe(false)
}
}
}
})
it('verifies FarAway metric (|dx| + 2*|dy|) places Den of Evil distant from Town Gate', () => {
const d2moo = loadOrRunD2MooLevel(2, 0x12345678)
const townRoom = d2moo.rooms.find(r => r.ds1File.includes('TownSTrans'))!
const denRoom = d2moo.rooms.find(r => r.ds1File.includes('DenEnt'))!
const townX = townRoom.mapX - d2moo.posX
const townY = townRoom.mapY - d2moo.posY
const denX = denRoom.mapX - d2moo.posX
const denY = denRoom.mapY - d2moo.posY
// FarAway Manhattan metric: |dx| + 2*|dy|
const distance = Math.abs(denX - townX) + 2 * Math.abs(denY - townY)
expect(distance).toBeGreaterThanOrEqual(48)
})
it('verifies canonical presets in Cold Plains (Level 3) and Stony Field (Level 4)', () => {
const l3 = loadOrRunD2MooLevel(3, 1)
const l3Presets = new Set(l3.rooms.map(r => r.ds1File.split('/').pop()).filter(Boolean))
expect(l3Presets.has('CaveDr1.ds1')).toBe(true)
expect(l3Presets.has('FallCmpBish.ds1')).toBe(true)
expect(l3Presets.has('Cott1A.ds1')).toBe(true)
expect(l3Presets.has('Circle.ds1')).toBe(true)
expect(l3Presets.has('Clover.ds1')).toBe(true)
const l4 = loadOrRunD2MooLevel(4, 1)
const l4Presets = new Set(l4.rooms.map(r => r.ds1File.split('/').pop()).filter(Boolean))
expect(l4Presets.has('cairn2.ds1')).toBe(true)
expect(l4Presets.has('clfcave.ds1')).toBe(true)
expect(l4Presets.has('tome.ds1')).toBe(true)
expect(l4Presets.has('Corral1.ds1')).toBe(true)
expect(l4Presets.has('FallCmp.ds1')).toBe(true)
})
})
// --- Tier 5: 4-Pass Secondary Borders & 2x3 Forest Closures ---
describe('Tier 5: 4-Pass Secondary Border Evolution & 2x3 Closures (R3)', () => {
it('spawns Bord5..8 outer peninsulas and Bord9..12 interior 2x3 forest closures', () => {
for (const [lvl, s] of [
[2, 0x12345678],
[2, 12345],
[2, 1],
[2, 42],
[3, 1],
[4, 1],
] as const) {
const d2moo = loadOrRunD2MooLevel(lvl, s)
const files = d2moo.rooms.map(r => r.ds1File.split('/').pop()).filter(Boolean) as string[]
const peninsulas = files.filter(f => /^Bord[5678]\.ds1$/i.test(f))
const closures = files.filter(f => /^Bord(9|10|11|12)\.ds1$/i.test(f))
expect(peninsulas.length, `Level ${lvl} Seed ${s} missing Bord5..8 peninsulas`).toBeGreaterThan(0)
expect(closures.length, `Level ${lvl} Seed ${s} missing Bord9..12 closures`).toBeGreaterThanOrEqual(4)
}
})
})
// --- Tier 6: Dirt Path Connectivity ---
describe('Tier 6: Orthogonal Polyline Dirt Paths & Connectivity (R5)', () => {
it('generates 100% contiguous dirt road network connecting gate, den, and exits', () => {
for (const s of [0x12345678, 12345, 1, 42] as const) {
const d2moo = loadOrRunD2MooLevel(2, s)
const dirtCells = getDirtRoadCells(d2moo)
expect(dirtCells.length, `Level 2 Seed ${s} must have dirt road cells`).toBeGreaterThanOrEqual(100)
const connectivity = evaluateRoadConnectivity(dirtCells)
expect(
connectivity.componentCount,
`Level 2 Seed ${s} road cells should form a single connected component`,
).toBe(1)
expect(connectivity.largestComponentSize).toBe(dirtCells.length)
}
})
})
})
/* ========================================================================= *
* SUITE 2: Wilderness Generator Ground Truth Parity (Issue #455 Milestones)
* ========================================================================= */
describe.skipIf(!hasD2Archives)('Part 2: DRLG Wilderness Generator Parity vs D2MOO (Milestones M1..M6)', () => {
let act1Pieces: WildernessPiece[]
let act1Subs: WildernessSubstitution[]
const WILDERNESS_PIECE_FAMILIES = [
'Act 1 - Wild', 'Act 1 - Town 1 Transition', 'Act 1 - Cave Entrance',
'Act 1 - DOE Entrance', 'Act 1 - Corral Fill', 'Act 1 - Fence Fill',
'Act 1 - River', 'Act 1 - Bridge', 'Act 1 - Bivouac', 'Act 1 - Pond',
'Act 1 - Swamp Fill', 'Act 1 - Stone Fill', 'Act 1 - Cottages',
'Act 1 - Fallen Camp', 'Act 1 - Camp', 'Act 1 - Cairn Stones',
'Act 1 - Inifus', 'Act 1 - Tower', 'Act 1 - Ruin', 'Act 1 - Tree Fill',
'Act 1 - Graveyard',
]
beforeAll(async () => {
const archives = new MountedArchives()
for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
try {
archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`)))
} catch {
// Skip optional archive if missing
}
}
const actTables = await loadActTables(archives)
const lvlsubTable = parseTable(await archives.read('data\\global\\excel\\LvlSub.txt'))
const ds1Cache = new Map<string, Ds1>()
async function loadDs1(relative: string): Promise<Ds1> {
const member = tileMemberPath(relative)
const cached = ds1Cache.get(member)
if (cached !== undefined) return cached
const decoded = decodeDs1(await archives.read(member))
ds1Cache.set(member, decoded)
return decoded
}
async function rowDs1s(table: D2Table, row: readonly string[]): Promise<Ds1[]> {
const levels: Ds1[] = []
for (let slot = 1; slot <= 6; slot += 1) {
const val = cell(table, row, `File${String(slot)}`)
if (val === '' || val === '0') continue
levels.push(await loadDs1(val))
}
return levels
}
// Load Act 1 wilderness preset pieces
act1Pieces = []
for (const row of actTables.lvlprest.rows) {
const name = cell(actTables.lvlprest, row, 'Name')
if (!WILDERNESS_PIECE_FAMILIES.some(fam => name.startsWith(fam))) continue
const levels = await rowDs1s(actTables.lvlprest, row)
if (levels.length === 0) continue
act1Pieces.push({ name, levels, border: /\bBorder\b/i.test(name) })
}
// Load Act 1 substitutions (SubTypes 0, 1, 6)
act1Subs = []
for (const row of lvlsubTable.rows) {
const subType = Number(cell(lvlsubTable, row, 'Type'))
if (subType !== 0 && subType !== 1 && subType !== 6) continue
const file = cell(lvlsubTable, row, 'File')
if (file === '' || file === '0') continue
const levels = [await loadDs1(file)]
act1Subs.push({
name: cell(lvlsubTable, row, 'Name'),
type: subType,
gridSize: Number(cell(lvlsubTable, row, 'GridSize')) || 1,
bordType: Number(cell(lvlsubTable, row, 'BordType')),
dt1Mask: Number(cell(lvlsubTable, row, 'Dt1Mask')) || 0,
prob: [0, 1, 2, 3, 4].map(i => Number(cell(lvlsubTable, row, `Prob${String(i)}`)) || 0),
trials: [0, 1, 2, 3, 4].map(i => Number(cell(lvlsubTable, row, `Trials${String(i)}`)) || 0),
max: [0, 1, 2, 3, 4].map(i => Number(cell(lvlsubTable, row, `Max${String(i)}`)) || 0),
levels,
})
}
})
// --- Milestone M1: Dynamic Dimensions ---
describe('Milestone M1 Parity: Dynamic Level Dimensions (R1)', () => {
it('generates 96x56 horizontal Moor for Seed 0x12345678', () => {
const d2moo = loadOrRunD2MooLevel(2, 0x12345678)
const result: WildernessResult = generateWilderness({
levelId: 2,
levelName: 'Blood Moor',
levelTypeName: 'Act 1 - Wilderness',
sizeX: 80,
sizeY: 80,
subType: 0,
subTheme: 0,
seed: 0x12345678,
pieces: act1Pieces,
substitutions: act1Subs,
})
expect(result.level.width).toBe(d2moo.width)
expect(result.level.height).toBe(d2moo.height)
})
it('generates 56x96 vertical Moor for Seed 1', () => {
const d2moo = loadOrRunD2MooLevel(2, 1)
const result: WildernessResult = generateWilderness({
levelId: 2,
levelName: 'Blood Moor',
levelTypeName: 'Act 1 - Wilderness',
sizeX: 80,
sizeY: 80,
subType: 0,
subTheme: 0,
seed: 1,
pieces: act1Pieces,
substitutions: act1Subs,
})
expect(result.level.width).toBe(d2moo.width)
expect(result.level.height).toBe(d2moo.height)
})
})
// --- Milestone M2: Outer River Placement ---
describe('Milestone M2 Parity: Outer River Placement & Boundary Replacement (R2)', () => {
it('spawns river along eastern boundary replacing outer tree border for Seed 0x12345678', () => {
const result: WildernessResult = generateWilderness({
levelId: 2,
levelName: 'Blood Moor',
levelTypeName: 'Act 1 - Wilderness',
sizeX: 80,
sizeY: 80,
subType: 0,
subTheme: 0,
seed: 0x12345678,
pieces: act1Pieces,
substitutions: act1Subs,
})
// River tiles on east edge: x in [80..95] contains water floors
let hasWaterTile = false
const maxX = result.level.width
const maxY = result.level.height
for (let y = 8; y < maxY - 8; y += 1) {
for (let x = Math.max(0, maxX - 16); x < maxX; x += 1) {
const row = result.level.cells[y]
if (row?.[x]?.floors.some(f => f.style === 2)) {
hasWaterTile = true
break
}
}
if (hasWaterTile) break
}
expect(hasWaterTile).toBe(true)
})
})
// --- Milestone M4: Town Transition Alignment ---
describe('Milestone M4 Parity: Town Transition Alignment (R4)', () => {
it('aligns TownSTrans gate to river edge for Seed 0x12345678', () => {
const result: WildernessResult = generateWilderness({
levelId: 2,
levelName: 'Blood Moor',
levelTypeName: 'Act 1 - Wilderness',
sizeX: 80,
sizeY: 80,
subType: 0,
subTheme: 0,
seed: 0x12345678,
pieces: act1Pieces,
substitutions: act1Subs,
})
const notes = result.stats.notes ?? []
const hasTownTrans = notes.some(n => /Town.*Trans/i.test(n))
expect(hasTownTrans).toBe(true)
})
})
// --- Milestone M4: Unconditional Landmarks ---
describe('Milestone M4 Parity: Unconditional Landmark Presets & FarAway (R4)', () => {
it('places all 5 canonical landmark categories in Blood Moor (Seed 0x12345678)', () => {
const result: WildernessResult = generateWilderness({
levelId: 2,
levelName: 'Blood Moor',
levelTypeName: 'Act 1 - Wilderness',
sizeX: 80,
sizeY: 80,
subType: 0,
subTheme: 0,
seed: 0x12345678,
pieces: act1Pieces,
substitutions: act1Subs,
})
const notes = (result.stats.notes ?? []).join('\n')
expect(/DOE|Den/i.test(notes)).toBe(true)
expect(/Pond/i.test(notes)).toBe(true)
expect(/Cottage|Bivouac/i.test(notes)).toBe(true)
expect(/Circle|Stone/i.test(notes)).toBe(true)
expect(/Clover/i.test(notes)).toBe(true)
})
})
// --- Milestone M3: Secondary Borders & 2x3 Forest Closures ---
describe('Milestone M3 Parity: Secondary Borders & 2x3 Forest Closures (R3)', () => {
it('spawns Bord5..8 peninsulas and Bord9..12 interior 2x3 closures for Seed 0x12345678', () => {
const result: WildernessResult = generateWilderness({
levelId: 2,
levelName: 'Blood Moor',
levelTypeName: 'Act 1 - Wilderness',
sizeX: 80,
sizeY: 80,
subType: 0,
subTheme: 0,
seed: 0x12345678,
pieces: act1Pieces,
substitutions: act1Subs,
})
const notes = (result.stats.notes ?? []).join('\n')
// Must record secondary border peninsulas and interior 2x3 closures
expect(/Bord[5678]|peninsula/i.test(notes)).toBe(true)
expect(/Bord(9|10|11|12)|2x3|closure/i.test(notes)).toBe(true)
})
})
// --- Milestone M5: Orthogonal Dirt Road Rasterization ---
describe('Milestone M5 Parity: Orthogonal Dirt Paths & Width-2 Bresenham (R5)', () => {
it('generates orthogonal width-2 dirt road without spline loop deviations', () => {
const result: WildernessResult = generateWilderness({
levelId: 2,
levelName: 'Blood Moor',
levelTypeName: 'Act 1 - Wilderness',
sizeX: 80,
sizeY: 80,
subType: 0,
subTheme: 0,
seed: 0x12345678,
pieces: act1Pieces,
substitutions: act1Subs,
})
// Dirt road floor tiles in Act 1 use style 0 with sequence 1..46 (byte_6FDCF958 LUT)
let dirtTileCount = 0
for (const row of result.level.cells) {
for (const cellRecord of row) {
if (cellRecord.floors.some(f => (f.style === 0 && f.sequence >= 1 && f.sequence <= 46) || (f.style === 1 && f.prop1 === 194))) {
dirtTileCount += 1
}
}
}
// Parity expectation: road connects town gate to den and exit with width-2 footprint (>= 100 tiles)
expect(dirtTileCount).toBeGreaterThanOrEqual(100)
})
})
})