diablo2-web/tests/tilesub-113c.test.ts

187 lines
7.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
applySubstitution,
applySubstitutions,
buildSubstitutionCandidatePermutation,
createCanvas,
stampSubstitutionPatch,
} from '../src/game/wilderness.ts'
import type { WildernessSubstitution } from '../src/game/wilderness.ts'
import { Rng } from '../src/game/rng.ts'
import type { Ds1 } from '../src/formats/ds1.ts'
function makePatchDs1(width: number, height: number, floorSeq: number, wallSeq = 10, roofSeq = 20, shadowSeq = 30): Ds1 {
const cells = Array.from({ length: height }, () =>
Array.from({ length: width }, () => ({
floors: [{ prop1: 1, style: 5, sequence: floorSeq, hidden: false, unknown1: 0, unknown2: 0 }],
walls: [
{ prop1: 1, style: 6, sequence: wallSeq, type: 1, hidden: false, unknown1: 0, unknown2: 0 },
{ prop1: 1, style: 7, sequence: roofSeq, type: 15, hidden: false, unknown1: 0, unknown2: 0 },
],
shadows: [{ prop1: 1, style: 8, sequence: shadowSeq, hidden: false, unknown1: 0, unknown2: 0 }],
substitutions: [{ value: 1 }],
})),
)
return {
version: 18,
width,
height,
act: 1,
substitutionType: 1,
wallLayers: 2,
floorLayers: 1,
cells,
objects: [{ type: 2, id: 15, x: 2, y: 2, flags: 0 }],
npcPathOffset: null,
}
}
describe('Blizzard D2 1.13c LvlSub.txt Trials & Exhaustive Permutation Substitution (Issue #76)', () => {
it('generates a complete, duplicate-free Fisher-Yates candidate permutation that varies by seed', () => {
const permA = buildSubstitutionCandidatePermutation(6, 6, 1, new Rng(1001))
const permB = buildSubstitutionCandidatePermutation(6, 6, 1, new Rng(2002))
expect(permA).toHaveLength(36)
expect(permB).toHaveLength(36)
// Verify all 36 grid coordinates are present with zero duplicates
const keysA = new Set(permA.map(p => `${p.ox},${p.oy}`))
expect(keysA.size).toBe(36)
// Verify permutation order differs between seeds (not fixed top-left scan)
const firstFiveA = permA.slice(0, 5).map(p => `${p.ox},${p.oy}`).join('|')
const firstFiveB = permB.slice(0, 5).map(p => `${p.ox},${p.oy}`).join('|')
expect(firstFiveA).not.toBe(firstFiveB)
})
it('finds the sole remaining free candidate slot when Trials == -1 (exhaustive scan guarantee)', () => {
const gridWidth = 8
const gridHeight = 8
const canvas = createCanvas(gridWidth * 8, gridHeight * 8, 2, 1, 1)
// Claim every interior block EXCEPT (bx = 5, by = 4)
const claimedBlocks = Array.from({ length: gridHeight }, () =>
Array.from({ length: gridWidth }, () => true),
)
claimedBlocks[4]![5] = false
const patch = makePatchDs1(8, 8, 42)
const subRow: WildernessSubstitution = {
name: 'Act 1 - Exhaustive Shrine',
type: 1,
bordType: -1,
gridSize: 1,
dt1Mask: 0,
prob: [100, 100, 100, 100, 100],
trials: [-1, -1, -1, -1, -1],
max: [1, 1, 1, 1, 1],
levels: [patch],
}
const stamped = applySubstitution(canvas, subRow, 0, gridWidth, gridHeight, new Rng(999), claimedBlocks)
expect(stamped).toBe(1)
// Verify the patch was stamped at exact block (bx = 5, by = 4) => tile (40, 32)
const targetCell = canvas.cells[32]![40]!
expect(targetCell.floors[0]?.sequence).toBe(42)
})
it('distributes Trials == -1 placements across varied interior locations rather than top-left (1,1)', () => {
const gridWidth = 10
const gridHeight = 10
const patch = makePatchDs1(8, 8, 55)
const subRow: WildernessSubstitution = {
name: 'Act 1 - Swamp Pool',
type: 1,
bordType: -1,
gridSize: 1,
dt1Mask: 0,
prob: [100, 100, 100, 100, 100],
trials: [-1, -1, -1, -1, -1],
max: [1, 1, 1, 1, 1],
levels: [patch],
}
const placedCoords = new Set<string>()
for (let seed = 1; seed <= 10; seed += 1) {
const canvas = createCanvas(gridWidth * 8, gridHeight * 8, 2, 1, 1)
const claimed = Array.from({ length: gridHeight }, () => Array.from({ length: gridWidth }, () => false))
applySubstitution(canvas, subRow, 0, gridWidth, gridHeight, new Rng(seed * 7919), claimed)
for (let by = 1; by < gridHeight - 1; by += 1) {
for (let bx = 1; bx < gridWidth - 1; bx += 1) {
if (claimed[by]![bx]) placedCoords.add(`${bx},${by}`)
}
}
}
// Across 10 seeds, Fisher-Yates permutation places the cluster in many different spots (not just 1,1)
expect(placedCoords.size).toBeGreaterThan(5)
})
it('atomically replaces Floor, Wall, Roof (type 15), and Shadow layers while clearing stale procedural clutter', () => {
const canvas = createCanvas(16, 16, 2, 1, 1)
// Pre-populate cell (4, 4) with procedural clutter on floor, wall, roof, and shadow layers
const staleCell = canvas.cells[4]![4]!
;(staleCell.floors as unknown[])[0] = { prop1: 1, style: 1, sequence: 1, hidden: false, unknown1: 0, unknown2: 0 }
;(staleCell.walls as unknown[])[0] = { prop1: 1, style: 99, sequence: 99, type: 1, hidden: false, unknown1: 0, unknown2: 0 }
;(staleCell.walls as unknown[])[1] = { prop1: 1, style: 99, sequence: 98, type: 15, hidden: false, unknown1: 0, unknown2: 0 }
;(staleCell.shadows as unknown[])[0] = { prop1: 1, style: 99, sequence: 97, hidden: false, unknown1: 0, unknown2: 0 }
const patch = makePatchDs1(2, 2, 11, 22, 33, 44)
const written = stampSubstitutionPatch(canvas, patch, 4, 4)
expect(written).toBe(4)
const updatedCell = canvas.cells[4]![4]!
expect(updatedCell.floors[0]?.sequence).toBe(11)
expect(updatedCell.walls[0]?.sequence).toBe(22)
expect(updatedCell.walls[0]?.type).toBe(1)
expect(updatedCell.walls[1]?.sequence).toBe(33)
expect(updatedCell.walls[1]?.type).toBe(15)
expect(updatedCell.shadows[0]?.sequence).toBe(44)
expect(updatedCell.substitutions[0]?.value).toBe(1)
expect(canvas.objects).toHaveLength(1)
})
it('applies theme-indexed Prob0..4, Trials0..4, and Max0..4 with multi-variant selection', () => {
const gridWidth = 10
const gridHeight = 10
const canvas = createCanvas(gridWidth * 8, gridHeight * 8, 2, 1, 1)
const claimed = Array.from({ length: gridHeight }, () => Array.from({ length: gridWidth }, () => false))
const variantA = makePatchDs1(8, 8, 101)
const variantB = makePatchDs1(8, 8, 102)
const rows: WildernessSubstitution[] = [
{
name: 'Theme 2 Multi-Variant Bivouac',
type: 1,
bordType: -1,
gridSize: 1,
dt1Mask: 0,
prob: [0, 0, 100, 0, 0],
trials: [0, 0, 20, 0, 0],
max: [0, 0, 4, 0, 0],
levels: [variantA, variantB],
},
]
const stats = {
levelId: 2,
seed: 12345,
subTheme: 2,
gridWidth,
gridHeight,
substitutions: [] as { name: string; role: string; enabled: boolean; clusters: number }[],
borderPieces: {},
unresolved: [],
notes: [],
}
const total = applySubstitutions(canvas, rows, 2, gridWidth, gridHeight, new Rng(12345), stats as never, claimed)
expect(total).toBe(4)
expect(stats.substitutions[0]?.enabled).toBe(true)
expect(stats.substitutions[0]?.clusters).toBe(4)
})
})