diablo2-web/tests/outdoor-waypoint-113c.test.ts

351 lines
11 KiB
TypeScript

import { describe, expect, test } from "vitest"
import {
type Canvas,
DRLG_GRID_FLAG_WAYPOINT,
DRLG_GRID_FLAG_WAYPOINT_COLDPLAINS,
DRLG_GRID_SPAWN_INVALID_MASK,
DRLGOUTDOORS_SpawnAct12Waypoint,
type PlacedPresetRect,
rollLimited,
type SeamConstraint,
TestGridCellSpawnValid,
type WildernessStats,
} from "../src/game/wilderness.ts"
import { Rng } from "../src/game/rng.ts"
import type { Ds1Cell } from "../src/formats/ds1.ts"
class TrackingRng extends Rng {
rollCount = 0
override next(): number {
this.rollCount += 1
return super.next()
}
}
function makeOutdoorCanvas(width: number, height: number): Canvas {
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: [],
floors: [{
prop1: 2,
sequence: 0,
style: 0,
unknown1: 0,
unknown2: 0,
hidden: false,
}],
shadows: [],
substitutions: [],
})
}
cells.push(row)
}
return {
width,
height,
cells,
wallLayers: 1,
floorLayers: 1,
substitutionLayers: 1,
objects: [],
}
}
function makeEmptyStats(): WildernessStats {
return {
substitutions: [],
borderPieces: {},
unresolved: [],
notes: [],
borderStamped: 0,
groundCells: 0,
groundTile: null,
sizeSource: "default",
roadCells: 0,
roadSegments: 0,
anchors: 0,
specialPresets: [],
entrances: [],
waypointsSpawned: 0,
shrinesSpawned: 0,
interactivesSpawned: 0,
}
}
describe("DRLGOUTDOORS_SpawnAct12Waypoint 1.13c Parity (Issue #80)", () => {
describe("TestGridCellSpawnValid and DRLG_GRID_SPAWN_INVALID_MASK", () => {
test("DRLG_GRID_SPAWN_INVALID_MASK matches 1.13c disassembly 0x1B81", () => {
expect(DRLG_GRID_SPAWN_INVALID_MASK).toBe(0x1b81)
})
test("cell flags with no 0x1B81 bits pass validation", () => {
const validFlags = [
0,
0x0002,
0x0004,
0x0008,
0x0010,
0x0020,
0x0040,
0x0400,
0x2000,
0x4000,
0x8000,
DRLG_GRID_FLAG_WAYPOINT, // 0x10000
DRLG_GRID_FLAG_WAYPOINT_COLDPLAINS, // 0x20000
0x0002 | 0x0010 | 0x20000,
]
for (const flag of validFlags) {
const grid2D = [[flag]]
expect(TestGridCellSpawnValid(grid2D, 0, 0)).toBe(true)
const grid1D = [flag]
expect(TestGridCellSpawnValid(grid1D, 0, 0, 1)).toBe(true)
const gridU32 = new Uint32Array([flag])
expect(TestGridCellSpawnValid(gridU32, 0, 0, 1)).toBe(true)
const gridObj = { width: 1, height: 1, flags: [flag] }
expect(TestGridCellSpawnValid(gridObj, 0, 0)).toBe(true)
}
})
test("cell flags with any 0x1B81 bits fail validation", () => {
const invalidFlags = [
0x0001, // preset occupied
0x0080, // border cell
0x0100, // collision bit
0x0200, // collision bit
0x0800, // collision bit
0x1000, // walls / impassable
0x1b81, // full mask
0x1000 | 0x0002, // mixed
0x0001 | DRLG_GRID_FLAG_WAYPOINT,
]
for (const flag of invalidFlags) {
const grid2D = [[flag]]
expect(TestGridCellSpawnValid(grid2D, 0, 0)).toBe(false)
const grid1D = [flag]
expect(TestGridCellSpawnValid(grid1D, 0, 0, 1)).toBe(false)
const gridU32 = new Uint32Array([flag])
expect(TestGridCellSpawnValid(gridU32, 0, 0, 1)).toBe(false)
}
})
test("out of bounds coordinates safely return false", () => {
const grid = [[0, 0], [0, 0]]
expect(TestGridCellSpawnValid(grid, -1, 0)).toBe(false)
expect(TestGridCellSpawnValid(grid, 0, -1)).toBe(false)
expect(TestGridCellSpawnValid(grid, 2, 0)).toBe(false)
expect(TestGridCellSpawnValid(grid, 0, 2)).toBe(false)
})
})
describe("rollLimited helper", () => {
test("rolls within [0, max - 1] and consumes exactly 1 roll per call", () => {
const rng = new TrackingRng(0x12345678)
const max = 36
for (let i = 0; i < 100; i += 1) {
const prevRolls = rng.rollCount
const val = rollLimited(rng, max)
expect(val).toBeGreaterThanOrEqual(0)
expect(val).toBeLessThan(max)
expect(rng.rollCount).toBe(prevRolls + 1)
}
})
test("handles edge case max <= 0 without consuming or erroring", () => {
const rng = new TrackingRng(0x12345678)
expect(rollLimited(rng, 0)).toBe(0)
expect(rollLimited(rng, -5)).toBe(0)
expect(rng.rollCount).toBe(0)
})
})
describe("Branch A: Cold Plains Special (levelId === 3)", () => {
test("consumes exactly 0 RNG rolls", () => {
const canvas = makeOutdoorCanvas(64, 64)
const stats = makeEmptyStats()
const rng = new TrackingRng(0xabcdef)
const seamConstraints: SeamConstraint[] = [
{ fromLevelId: 3, toLevelId: 2, side: "west", blockCoord: 0 },
]
DRLGOUTDOORS_SpawnAct12Waypoint(
canvas,
3,
1,
undefined,
[],
rng,
stats,
"Act 1 - Wilderness",
undefined,
seamConstraints,
)
expect(stats.waypointsSpawned).toBe(1)
expect(stats.waypointTile).toBeDefined()
// Exactly 0 RNG rolls consumed!
expect(rng.rollCount).toBe(0)
})
test("clamps connection to Blood Moor on all 4 boundary sides and sets 0x20000 flag", () => {
const testCases: { side: "west" | "east" | "north" | "south"; expectedBlockX: number; expectedBlockY: number }[] = [
{ side: "west", expectedBlockX: 1, expectedBlockY: 4 }, // j=0 clamped to 1, i=4
{ side: "east", expectedBlockX: 6, expectedBlockY: 4 }, // j=7 clamped to 6, i=4
{ side: "north", expectedBlockX: 4, expectedBlockY: 1 }, // i=0 clamped to 1, j=4
{ side: "south", expectedBlockX: 4, expectedBlockY: 6 }, // i=7 clamped to 6, j=4
]
for (const tc of testCases) {
const canvas = makeOutdoorCanvas(64, 64) // 8x8 blocks (0..7)
const stats = makeEmptyStats()
const rng = new TrackingRng(0x9999)
const seamConstraints: SeamConstraint[] = [
{ fromLevelId: 3, toLevelId: 2, side: tc.side, blockCoord: 4 },
]
DRLGOUTDOORS_SpawnAct12Waypoint(
canvas,
3,
1,
undefined,
[],
rng,
stats,
"Act 1 - Wilderness",
undefined,
seamConstraints,
)
expect(stats.waypointsSpawned).toBe(1)
expect(stats.waypointTile).toBeDefined()
const wpTile = stats.waypointTile!
const blockX = Math.floor(wpTile.x / 8)
const blockY = Math.floor(wpTile.y / 8)
expect(blockX).toBe(tc.expectedBlockX)
expect(blockY).toBe(tc.expectedBlockY)
const note = stats.notes.find(n => n.includes("Cold Plains special grid cell"))
expect(note).toBeDefined()
expect(note).toContain(`(${tc.expectedBlockX}, ${tc.expectedBlockY})`)
expect(rng.rollCount).toBe(0)
}
})
test("clamps extreme boundary corners (e.g. j=0 clamped to 1, i=0 clamped to 1)", () => {
const canvas = makeOutdoorCanvas(64, 64)
const stats = makeEmptyStats()
const rng = new TrackingRng(0x4242)
// West gate at extreme top (blockCoord 0)
const seamConstraints: SeamConstraint[] = [
{ fromLevelId: 3, toLevelId: 2, side: "west", blockCoord: 0 },
]
DRLGOUTDOORS_SpawnAct12Waypoint(
canvas,
3,
1,
undefined,
[],
rng,
stats,
"Act 1 - Wilderness",
undefined,
seamConstraints,
)
expect(stats.waypointTile).toBeDefined()
const blockX = Math.floor(stats.waypointTile!.x / 8)
const blockY = Math.floor(stats.waypointTile!.y / 8)
expect(blockX).toBe(1)
expect(blockY).toBe(1)
expect(rng.rollCount).toBe(0)
})
})
describe("Branch B: General Path (e.g. Stony Field, levelId === 4)", () => {
test("consumes exactly 2 * nArea RNG rolls for coordinate shuffle", () => {
// 64x64 tiles -> 8x8 blocks of 8x8 tiles
// nGridWidth = 8, nGridHeight = 8
// nWidth = 8 - 2 = 6, nHeight = 8 - 2 = 6
// nArea = 6 * 6 = 36
// Exactly 2 * nArea = 72 rolls for shuffle, and 0 rolls during placement search
const canvas = makeOutdoorCanvas(64, 64)
const stats = makeEmptyStats()
const rng = new TrackingRng(0x778899)
DRLGOUTDOORS_SpawnAct12Waypoint(canvas, 4, 1, undefined, [], rng, stats)
expect(stats.waypointsSpawned).toBe(1)
expect(stats.waypointTile).toBeDefined()
const expectedRolls = 2 * 36
expect(rng.rollCount).toBe(expectedRolls)
})
test("consumes exactly 2 * nArea rolls for larger map (80x80 tiles -> 10x10 blocks -> 8x8 area = 64 cells -> 128 rolls)", () => {
const canvas = makeOutdoorCanvas(80, 80)
const stats = makeEmptyStats()
const rng = new TrackingRng(0x112233)
DRLGOUTDOORS_SpawnAct12Waypoint(canvas, 4, 1, undefined, [], rng, stats)
expect(stats.waypointsSpawned).toBe(1)
expect(stats.waypointTile).toBeDefined()
// nWidth = 10 - 2 = 8, nHeight = 10 - 2 = 8, nArea = 64
// 2 * 64 = 128 rolls
expect(rng.rollCount).toBe(128)
})
test("skips cells occupied by preset rectangles (obstacle-free placement via TestGridCellSpawnValid)", () => {
const canvas = makeOutdoorCanvas(64, 64)
const stats = makeEmptyStats()
const rng = new TrackingRng(0x5555)
// Mark preset covering block (1, 1) -> tiles [8..15, 8..15]
const placedPresets: PlacedPresetRect[] = [
{ name: "PresetObstacle", x: 8, y: 8, w: 8, h: 8 },
]
DRLGOUTDOORS_SpawnAct12Waypoint(canvas, 4, 1, undefined, placedPresets, rng, stats)
expect(stats.waypointsSpawned).toBe(1)
expect(stats.waypointTile).toBeDefined()
const wp = stats.waypointTile!
const blockX = Math.floor(wp.x / 8)
const blockY = Math.floor(wp.y / 8)
// Block (1, 1) had an obstacle so waypoint must not be in (1, 1)
expect(blockX === 1 && blockY === 1).toBe(false)
// Must be within inner bounds [1..6, 1..6]
expect(blockX).toBeGreaterThanOrEqual(1)
expect(blockX).toBeLessThanOrEqual(6)
expect(blockY).toBeGreaterThanOrEqual(1)
expect(blockY).toBeLessThanOrEqual(6)
})
test("is deterministic: identical seeds produce identical waypoint coordinates", () => {
const canvas1 = makeOutdoorCanvas(64, 64)
const stats1 = makeEmptyStats()
const rng1 = new TrackingRng(0xfeedbeef)
DRLGOUTDOORS_SpawnAct12Waypoint(canvas1, 4, 1, undefined, [], rng1, stats1)
const canvas2 = makeOutdoorCanvas(64, 64)
const stats2 = makeEmptyStats()
const rng2 = new TrackingRng(0xfeedbeef)
DRLGOUTDOORS_SpawnAct12Waypoint(canvas2, 4, 1, undefined, [], rng2, stats2)
expect(stats1.waypointTile).toEqual(stats2.waypointTile)
expect(rng1.rollCount).toBe(rng2.rollCount)
})
})
})