566 lines
21 KiB
TypeScript
566 lines
21 KiB
TypeScript
/**
|
|
* tests/challenger-m4-stress.test.ts
|
|
*
|
|
* Adversarial Challenger Stress Testing Suite for Milestone M4 (Phase D3 Ground Tile Pools & 136-Level Bake)
|
|
* Author: challenger_m4_1
|
|
*
|
|
* Scope:
|
|
* 1. Ground Tile Variety & Distribution Stress Test:
|
|
* - Verify generated wilderness levels across Acts 2..5 exhibit multi-sequence ground variation (not just sequence 0).
|
|
* - Verify Act 3 Kurast (Levels 79, 80, 81, 82) ground tiles use strictly style: 1 and never style: 0.
|
|
* - Verify Level 117 (Act 5 Barricade Snow) uses strictly style: 6 (snow) and never style: 0 (dirt).
|
|
* 2. Offline Pre-Bake Assets Invariance:
|
|
* - Validate 136-level pre-baked pack scenes in samples/d2-packs/ with 0 missing tiles.
|
|
*
|
|
* Act I outdoor levels come from the DRLG port (src/game/drlg); their bit-exact parity with D2 is
|
|
* tests/drlg-act1-oracle.test.ts, which replaced the old Act 1 baseline-hash invariant.
|
|
*/
|
|
|
|
import { describe, expect, it, beforeAll } from 'vitest'
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
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 } from '../src/game/acts.ts'
|
|
import type { D2Table } from '../src/game/acts.ts'
|
|
import { decodeDs1 } from '../src/formats/ds1.ts'
|
|
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
|
import {
|
|
generateWilderness,
|
|
createCanvas,
|
|
fillGround,
|
|
CANONICAL_GROUND_TILES,
|
|
CANONICAL_GROUND_POOLS,
|
|
wildernessDt1Mask,
|
|
type Canvas,
|
|
type WildernessPiece,
|
|
type WildernessSubstitution,
|
|
type WeightedGroundTile,
|
|
} from '../src/game/wilderness.ts'
|
|
|
|
const TEST_SEEDS = [0x301cd095, 0x416d61c5, 0xdeadbeef] as const
|
|
|
|
/* ------------------------------------------------------------------------- *
|
|
* Mock Helpers for Hermetic Ground Stress Testing
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
function makeMockBorderPiece(name: string): WildernessPiece {
|
|
const cells: Ds1Cell[][] = []
|
|
for (let y = 0; y < 8; y += 1) {
|
|
const row: Ds1Cell[] = []
|
|
for (let x = 0; x < 8; x += 1) {
|
|
row.push({
|
|
walls: [],
|
|
floors: [],
|
|
shadows: [],
|
|
substitutions: [],
|
|
})
|
|
}
|
|
cells.push(row)
|
|
}
|
|
const ds1: Ds1 = {
|
|
version: 18,
|
|
width: 8,
|
|
height: 8,
|
|
act: 1,
|
|
substitutionType: 0,
|
|
wallLayers: 1,
|
|
floorLayers: 1,
|
|
cells,
|
|
objects: [],
|
|
npcPathOffset: null,
|
|
}
|
|
return { name, border: true, levels: [ds1] }
|
|
}
|
|
|
|
function makeMockBorderSet(actPrefix: string): WildernessPiece[] {
|
|
return [
|
|
makeMockBorderPiece(`${actPrefix} Border 1`),
|
|
makeMockBorderPiece(`${actPrefix} Border 2`),
|
|
makeMockBorderPiece(`${actPrefix} Border 3`),
|
|
makeMockBorderPiece(`${actPrefix} Border 4`),
|
|
makeMockBorderPiece(`${actPrefix} Border 5`),
|
|
makeMockBorderPiece(`${actPrefix} Border 6`),
|
|
]
|
|
}
|
|
|
|
describe('M4 Adversarial Challenge: Canonical Ground Pools Contract', () => {
|
|
it('CANONICAL_GROUND_POOLS defines multi-sequence pools for all Acts 2..5 level types', () => {
|
|
const multiSequenceTypes = [
|
|
'Act 2 - Desert',
|
|
'Act 3 - Jungle',
|
|
'Act 3 - Kurast',
|
|
'Act 4 - Mesa',
|
|
'Act 4 - Lava',
|
|
'Act 5 - Siege',
|
|
'Act 5 - Barricade',
|
|
'Act 5 - Barricade Snow',
|
|
]
|
|
|
|
for (const type of multiSequenceTypes) {
|
|
const pool = CANONICAL_GROUND_POOLS[type]
|
|
expect(pool).toBeDefined()
|
|
expect(pool!.length).toBeGreaterThan(1)
|
|
|
|
const sequences = new Set(pool!.map(t => t.sequence))
|
|
// Must contain at least 2 distinct sequences
|
|
expect(sequences.size).toBeGreaterThanOrEqual(2)
|
|
|
|
// Must NOT be just sequence 0
|
|
expect(pool!.some(t => t.sequence !== 0)).toBe(true)
|
|
|
|
// Weights must be positive integers
|
|
for (const t of pool!) {
|
|
expect(t.weight).toBeGreaterThan(0)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('Act 3 Kurast pool uses strictly style 1 and NEVER style 0 (DarkGrass.dt1 parity)', () => {
|
|
expect(CANONICAL_GROUND_TILES['Act 3 - Kurast'].style).toBe(1)
|
|
const pool = CANONICAL_GROUND_POOLS['Act 3 - Kurast']!
|
|
expect(pool).toBeDefined()
|
|
expect(pool.length).toBeGreaterThanOrEqual(4)
|
|
|
|
for (const t of pool) {
|
|
expect(t.style).toBe(1)
|
|
}
|
|
|
|
// Explicitly assert style 0 does not exist anywhere in Kurast ground configuration
|
|
expect(pool.filter(t => t.style === 0).length).toBe(0)
|
|
expect(CANONICAL_GROUND_TILES['Act 3 - Kurast'].style).not.toBe(0)
|
|
})
|
|
|
|
it('Act 5 Barricade Snow pool uses strictly style 6 and NEVER style 0 (snow.dt1 parity)', () => {
|
|
expect(CANONICAL_GROUND_TILES['Act 5 - Barricade Snow'].style).toBe(6)
|
|
const pool = CANONICAL_GROUND_POOLS['Act 5 - Barricade Snow']!
|
|
expect(pool).toBeDefined()
|
|
expect(pool.length).toBeGreaterThanOrEqual(5)
|
|
|
|
for (const t of pool) {
|
|
expect(t.style).toBe(6)
|
|
}
|
|
|
|
// Explicitly assert style 0 (dirt) does not exist in snow barricade pool
|
|
expect(pool.filter(t => t.style === 0).length).toBe(0)
|
|
expect(CANONICAL_GROUND_TILES['Act 5 - Barricade Snow'].style).not.toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('M4 Adversarial Challenge: fillGround Sampling & Statistical Variety', () => {
|
|
it('fillGround samples multiple sequences across a 64x64 canvas for all Acts 2..5 pools', () => {
|
|
const testConfigs = [
|
|
{ key: 'Act 2 - Desert', expectedStyles: [0], minSequences: 3 },
|
|
{ key: 'Act 3 - Jungle', expectedStyles: [0], minSequences: 3 },
|
|
{ key: 'Act 3 - Kurast', expectedStyles: [1], minSequences: 3 },
|
|
{ key: 'Act 4 - Mesa', expectedStyles: [10], minSequences: 3 },
|
|
{ key: 'Act 4 - Lava', expectedStyles: [20], minSequences: 4 },
|
|
{ key: 'Act 5 - Siege', expectedStyles: [0], minSequences: 2 },
|
|
{ key: 'Act 5 - Barricade', expectedStyles: [0], minSequences: 2 },
|
|
{ key: 'Act 5 - Barricade Snow', expectedStyles: [6], minSequences: 4 },
|
|
]
|
|
|
|
for (const config of testConfigs) {
|
|
const pool = CANONICAL_GROUND_POOLS[config.key]!
|
|
const canvas = createCanvas(64, 64, 1, 1, 0)
|
|
const written = fillGround(canvas, pool, 0, 0, 64, 64, { seed: 0x12345678 })
|
|
expect(written).toBe(64 * 64)
|
|
|
|
const observedSequences = new Set<number>()
|
|
const observedStyles = new Set<number>()
|
|
|
|
for (let y = 0; y < 64; y += 1) {
|
|
for (let x = 0; x < 64; x += 1) {
|
|
const floor = canvas.cells[y]![x]!.floors[0]!
|
|
observedStyles.add(floor.style)
|
|
observedSequences.add(floor.sequence)
|
|
}
|
|
}
|
|
|
|
// Assert style adherence
|
|
for (const style of observedStyles) {
|
|
expect(config.expectedStyles).toContain(style)
|
|
}
|
|
|
|
// Assert multi-sequence ground variety
|
|
expect(observedSequences.size).toBeGreaterThanOrEqual(config.minSequences)
|
|
expect(observedSequences.has(0) || observedSequences.has(1)).toBe(true)
|
|
expect(Array.from(observedSequences).some(s => s > 0)).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('M4 Adversarial Challenge: Hermetic Generation for Kurast (style 1) and Snow Barricade (style 6)', () => {
|
|
it('Hermetic Kurast generation (Levels 79, 80, 81, 82) strictly uses style 1 for ground and never style 0', () => {
|
|
const kurastLevels = [79, 80, 81, 82]
|
|
const pieces = makeMockBorderSet('Act 3 - Kurast')
|
|
|
|
for (const id of kurastLevels) {
|
|
for (const seed of TEST_SEEDS) {
|
|
const result = generateWilderness({
|
|
levelId: id,
|
|
levelName: `Kurast ${id}`,
|
|
levelTypeName: 'Act 3 - Kurast',
|
|
sizeX: 64,
|
|
sizeY: 64,
|
|
subType: 0,
|
|
subTheme: 0,
|
|
seed,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.groundTile?.style).toBe(1)
|
|
expect(result.stats.groundTile?.style).not.toBe(0)
|
|
|
|
const groundTiles = result.stats.groundTiles as readonly WeightedGroundTile[]
|
|
expect(groundTiles).toBeDefined()
|
|
expect(groundTiles.every(t => t.style === 1)).toBe(true)
|
|
|
|
// Across canvas, ground tiles must be style 1 (not style 0)
|
|
let groundCellCount = 0
|
|
const seenSequences = new Set<number>()
|
|
|
|
for (let y = 0; y < result.level.height; y += 1) {
|
|
for (let x = 0; x < result.level.width; x += 1) {
|
|
const floor = result.level.cells[y]![x]!.floors[0]
|
|
if (floor !== undefined && floor.style === 1) {
|
|
groundCellCount += 1
|
|
seenSequences.add(floor.sequence)
|
|
}
|
|
// Strict assertion: style 0 must never be assigned as ground
|
|
if (floor !== undefined && floor.style === 0) {
|
|
expect(floor.style).not.toBe(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ground cells with style 1 exist across canvas
|
|
const minExpectedCells = id === 82 ? 50 : 1000
|
|
expect(groundCellCount).toBeGreaterThan(minExpectedCells)
|
|
if (id !== 82) {
|
|
expect(seenSequences.size).toBeGreaterThanOrEqual(3)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
it('Hermetic Level 117 (Act 5 Barricade Snow) strictly uses style 6 for ground and never style 0', () => {
|
|
const pieces = makeMockBorderSet('Act 5 - Barricade')
|
|
|
|
for (const seed of TEST_SEEDS) {
|
|
const result = generateWilderness({
|
|
levelId: 117,
|
|
levelName: 'Frozen Tundra',
|
|
levelTypeName: 'Act 5 - Barricade',
|
|
sizeX: 64,
|
|
sizeY: 64,
|
|
subType: 11,
|
|
subTheme: 0,
|
|
seed,
|
|
pieces,
|
|
substitutions: [],
|
|
})
|
|
|
|
expect(result.stats.groundTile?.style).toBe(6)
|
|
expect(result.stats.groundTile?.style).not.toBe(0)
|
|
|
|
const groundTiles = result.stats.groundTiles as readonly WeightedGroundTile[]
|
|
expect(groundTiles).toBeDefined()
|
|
expect(groundTiles.every(t => t.style === 6)).toBe(true)
|
|
|
|
let snowCellCount = 0
|
|
const seenSequences = new Set<number>()
|
|
|
|
for (let y = 0; y < result.level.height; y += 1) {
|
|
for (let x = 0; x < result.level.width; x += 1) {
|
|
const floor = result.level.cells[y]![x]!.floors[0]
|
|
if (floor !== undefined && floor.style === 6) {
|
|
snowCellCount += 1
|
|
seenSequences.add(floor.sequence)
|
|
}
|
|
if (floor !== undefined && floor.style === 0) {
|
|
expect(floor.style).not.toBe(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(snowCellCount).toBeGreaterThan(1000)
|
|
expect(seenSequences.size).toBeGreaterThanOrEqual(3)
|
|
}
|
|
})
|
|
})
|
|
|
|
/* ------------------------------------------------------------------------- *
|
|
* MPQ Ground-Truth Full Procedural Verification
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
const hasMpq = existsSync('samples/d2/d2data.mpq')
|
|
|
|
describe.skipIf(!hasMpq)('M4 Adversarial Challenge: MPQ Procedural Ground Verification', () => {
|
|
let archives: MountedArchives
|
|
let tables: { levels: D2Table; lvltypes: D2Table; lvlprest: D2Table }
|
|
let lvlsub: D2Table
|
|
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 value = cell(table, row, `File${String(slot)}`)
|
|
if (value === '' || value === '0') continue
|
|
levels.push(await loadDs1(value))
|
|
}
|
|
return levels
|
|
}
|
|
|
|
const WILDERNESS_PIECE_FAMILIES: Readonly<Record<string, readonly string[]>> = {
|
|
'Act 2 - Desert': ['Act 2 - Desert'],
|
|
'Act 3 - Jungle': ['Act 3 - Jungle'],
|
|
'Act 3 - Kurast': ['Act 3 - Burst', 'Act 3 - Burbs', 'Act 3 - Clearing', 'Act 3 - Slums', 'Act 3 - Metro', 'Act 3 - Travincal', 'Act 3 - Bridge'],
|
|
'Act 4 - Mesa': ['Act 4 - Mesa', 'Act 4 - Fortress', 'Act 4 - Pits', 'Act 4 - Bridge'],
|
|
'Act 4 - Lava': ['Act 4 - Lava', 'Act 4 - Diablo'],
|
|
'Act 5 - Siege': ['Act 5 - Siege'],
|
|
'Act 5 - Barricade': ['Act 5 - Barricade'],
|
|
}
|
|
|
|
async function buildWildernessPieces(levelTypeName: string): Promise<WildernessPiece[]> {
|
|
const families = WILDERNESS_PIECE_FAMILIES[levelTypeName] ?? []
|
|
const pieces: WildernessPiece[] = []
|
|
for (const row of tables.lvlprest.rows) {
|
|
const name = cell(tables.lvlprest, row, 'Name')
|
|
if (!families.some(family => name.startsWith(family))) continue
|
|
if (levelTypeName === 'Act 5 - Barricade' && name.includes('Snow')) continue
|
|
const levels = await rowDs1s(tables.lvlprest, row)
|
|
if (levels.length === 0) continue
|
|
const isBorder = /border|cliff/i.test(name)
|
|
pieces.push({ name, levels, border: isBorder })
|
|
}
|
|
return pieces
|
|
}
|
|
|
|
async function buildSubstitutions(type: number): Promise<WildernessSubstitution[]> {
|
|
if (type < 0) return []
|
|
const rows: WildernessSubstitution[] = []
|
|
for (const row of lvlsub.rows) {
|
|
if (Number(cell(lvlsub, row, 'Type')) !== type) continue
|
|
const file = cell(lvlsub, row, 'File')
|
|
if (file === '' || file === '0') continue
|
|
const levels = [await loadDs1(file)]
|
|
rows.push({
|
|
name: cell(lvlsub, row, 'Name'),
|
|
type,
|
|
gridSize: Number(cell(lvlsub, row, 'GridSize')) || 1,
|
|
bordType: Number(cell(lvlsub, row, 'BordType')),
|
|
dt1Mask: Number(cell(lvlsub, row, 'Dt1Mask')) || 0,
|
|
prob: [0, 1, 2, 3, 4].map(i => Number(cell(lvlsub, row, `Prob${String(i)}`)) || 0),
|
|
trials: [0, 1, 2, 3, 4].map(i => Number(cell(lvlsub, row, `Trials${String(i)}`)) || 0),
|
|
max: [0, 1, 2, 3, 4].map(i => Number(cell(lvlsub, row, `Max${String(i)}`)) || 0),
|
|
levels,
|
|
})
|
|
}
|
|
return rows
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
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 missing optional archive
|
|
}
|
|
}
|
|
const actTables = await loadActTables(archives)
|
|
tables = {
|
|
levels: actTables.levels,
|
|
lvltypes: actTables.lvltypes,
|
|
lvlprest: actTables.lvlprest,
|
|
}
|
|
lvlsub = parseTable(await archives.read('data\\global\\excel\\LvlSub.txt'))
|
|
})
|
|
|
|
async function generateLevelForSeed(levelId: number, seed: number) {
|
|
const levelRow = tables.levels.rows.find(row => Number(cell(tables.levels, row, 'Id')) === levelId)!
|
|
const typeId = cell(tables.levels, levelRow, 'LevelType')
|
|
const typeRow = tables.lvltypes.rows.find(row => cell(tables.lvltypes, row, 'Id') === typeId)
|
|
const levelTypeName = typeRow ? cell(tables.lvltypes, typeRow, 'Name') : ''
|
|
const name = cell(tables.levels, levelRow, 'Name')
|
|
const sizeX = Number(cell(tables.levels, levelRow, 'SizeX'))
|
|
const sizeY = Number(cell(tables.levels, levelRow, 'SizeY'))
|
|
const subType = Number(cell(tables.levels, levelRow, 'SubType'))
|
|
const subShrine = Number(cell(tables.levels, levelRow, 'SubShrine'))
|
|
const subTheme = Number(cell(tables.levels, levelRow, 'SubTheme'))
|
|
|
|
const pieces = await buildWildernessPieces(levelTypeName)
|
|
const subs = await buildSubstitutions(subType)
|
|
const shrineSubs = await buildSubstitutions(subShrine)
|
|
|
|
return generateWilderness({
|
|
levelId,
|
|
levelName: name,
|
|
levelTypeName,
|
|
sizeX,
|
|
sizeY,
|
|
subType,
|
|
subTheme: Math.max(0, subTheme),
|
|
seed,
|
|
pieces,
|
|
substitutions: subs,
|
|
shrineSubstitutions: shrineSubs,
|
|
})
|
|
}
|
|
|
|
it('Act 3 Kurast (Levels 79, 80, 81, 82): ground tiles strictly use style 1 and NEVER style 0', async () => {
|
|
const kurastLevels = [79, 80, 81, 82]
|
|
|
|
for (const levelId of kurastLevels) {
|
|
for (const seed of TEST_SEEDS) {
|
|
const result = await generateLevelForSeed(levelId, seed)
|
|
|
|
// Verify stats reporting
|
|
expect(result.stats.groundTile?.style).toBe(1)
|
|
expect(result.stats.groundTile?.style).not.toBe(0)
|
|
|
|
const groundTiles = result.stats.groundTiles as readonly WeightedGroundTile[]
|
|
expect(groundTiles).toBeDefined()
|
|
expect(groundTiles.length).toBeGreaterThanOrEqual(1)
|
|
for (const t of groundTiles) {
|
|
expect(t.style).toBe(1)
|
|
}
|
|
|
|
// Verify level canvas cells: ground floor layer must have style 1 with multi-sequence variety
|
|
let kurastGroundCellsChecked = 0
|
|
const seenSequences = new Set<number>()
|
|
|
|
for (let y = 0; y < result.level.height; y += 1) {
|
|
const row = result.level.cells[y]!
|
|
for (let x = 0; x < result.level.width; x += 1) {
|
|
const cell = row[x]!
|
|
const floor = cell.floors[0]
|
|
if (floor !== undefined && floor.style === 1) {
|
|
kurastGroundCellsChecked += 1
|
|
seenSequences.add(floor.sequence)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ground cells with style 1 are present across the level
|
|
const minExpected = levelId === 82 ? 5 : 1000
|
|
expect(kurastGroundCellsChecked).toBeGreaterThan(minExpected)
|
|
if (levelId !== 82) {
|
|
expect(seenSequences.size).toBeGreaterThanOrEqual(2)
|
|
}
|
|
}
|
|
}
|
|
}, 60000)
|
|
|
|
it('Level 117 (Act 5 Barricade Snow): ground tiles strictly use style 6 (snow) and NEVER style 0 (dirt)', async () => {
|
|
for (const seed of TEST_SEEDS) {
|
|
const result = await generateLevelForSeed(117, seed)
|
|
|
|
// Verify stats reporting
|
|
expect(result.stats.groundTile?.style).toBe(6)
|
|
expect(result.stats.groundTile?.style).not.toBe(0)
|
|
|
|
const groundTiles = result.stats.groundTiles as readonly WeightedGroundTile[]
|
|
expect(groundTiles).toBeDefined()
|
|
expect(groundTiles.length).toBeGreaterThanOrEqual(1)
|
|
for (const t of groundTiles) {
|
|
expect(t.style).toBe(6)
|
|
}
|
|
|
|
// Verify level canvas cells: ground floor layer must have style 6 (snow) with multi-sequence variety
|
|
let snowCellsChecked = 0
|
|
const seenSequences = new Set<number>()
|
|
|
|
for (let y = 0; y < result.level.height; y += 1) {
|
|
const row = result.level.cells[y]!
|
|
for (let x = 0; x < result.level.width; x += 1) {
|
|
const cell = row[x]!
|
|
const floor = cell.floors[0]
|
|
if (floor !== undefined && floor.style === 6) {
|
|
snowCellsChecked += 1
|
|
seenSequences.add(floor.sequence)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Snow ground cells with style 6 are the vast majority of the map (> 3000 cells)
|
|
expect(snowCellsChecked).toBeGreaterThan(3000)
|
|
// Snow barricade has sequences 0, 1, 2, 3, 4
|
|
expect(seenSequences.size).toBeGreaterThanOrEqual(3)
|
|
}
|
|
}, 60000)
|
|
|
|
it('Acts 2..5 outdoor levels exhibit multi-sequence ground variation across generated levels', async () => {
|
|
const outdoorSampleLevels = [
|
|
{ id: 41, name: 'Rocky Waste', act: 2, minSeq: 3 },
|
|
{ id: 43, name: 'Far Oasis', act: 2, minSeq: 3 },
|
|
{ id: 76, name: 'Spider Forest', act: 3, minSeq: 3 },
|
|
{ id: 79, name: 'Kurast Bazaar', act: 3, minSeq: 2 },
|
|
{ id: 104, name: 'Outer Steppes', act: 4, minSeq: 3 },
|
|
{ id: 107, name: 'River of Flame', act: 4, minSeq: 4 },
|
|
{ id: 110, name: 'Bloody Foothills', act: 5, minSeq: 2 },
|
|
{ id: 111, name: 'Frigid Highlands', act: 5, minSeq: 2 },
|
|
{ id: 117, name: 'Frozen Tundra', act: 5, minSeq: 3 },
|
|
]
|
|
|
|
for (const target of outdoorSampleLevels) {
|
|
const seed = TEST_SEEDS[0]
|
|
const result = await generateLevelForSeed(target.id, seed)
|
|
|
|
const seenSequences = new Set<number>()
|
|
for (let y = 0; y < result.level.height; y += 1) {
|
|
const row = result.level.cells[y]!
|
|
for (let x = 0; x < result.level.width; x += 1) {
|
|
const floor = row[x]!.floors[0]
|
|
if (floor !== undefined && floor.prop1 !== 0) {
|
|
seenSequences.add(floor.sequence)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Assert multi-sequence variation
|
|
expect(seenSequences.size).toBeGreaterThanOrEqual(target.minSeq)
|
|
// Assert it is not just sequence 0
|
|
expect(Array.from(seenSequences).some(s => s !== 0)).toBe(true)
|
|
}
|
|
}, 60000)
|
|
})
|
|
|
|
describe('M4 Adversarial Challenge: Pre-baked Pack Directory Integrity', () => {
|
|
it('samples/d2-packs contains all 136 game levels with 0 missing tiles on Kurast and Frozen Tundra', () => {
|
|
expect(existsSync('samples/d2-packs')).toBe(true)
|
|
|
|
const criticalPackedLevels = [
|
|
'samples/d2-packs/act3/79-act-3-kurast-1-var1',
|
|
'samples/d2-packs/act3/80-act-3-kurast-2-var1',
|
|
'samples/d2-packs/act3/81-act-3-kurast-3-var1',
|
|
'samples/d2-packs/act3/82-act-3-kurast-4-var1',
|
|
'samples/d2-packs/act5/117-act-5-barricade-snow-var1',
|
|
]
|
|
|
|
for (const dir of criticalPackedLevels) {
|
|
expect(existsSync(dir)).toBe(true)
|
|
const scenePath = join(dir, 'scene.json')
|
|
const manifestPath = join(dir, 'manifest.json')
|
|
expect(existsSync(scenePath)).toBe(true)
|
|
expect(existsSync(manifestPath)).toBe(true)
|
|
|
|
const scene = JSON.parse(readFileSync(scenePath, 'utf8'))
|
|
expect(scene.stats.missingTiles).toBe(0)
|
|
expect(scene.stats.floors).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
})
|