diablo2-web/tests/adversarial-world-placement...

322 lines
11 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { SEAMLESS_ADJACENCY, PINNED_SIDES } from '../src/game/world-graph.ts'
import { resolveWorldOffsets, type WorldOffset, CANONICAL_LEVEL_ROWS } from '../src/game/world-placement.ts'
export function doRectanglesOverlap(a: WorldOffset, b: WorldOffset): boolean {
return (
a.offsetX < b.offsetX + b.sizeX &&
a.offsetX + a.sizeX > b.offsetX &&
a.offsetY < b.offsetY + b.sizeY &&
a.offsetY + a.sizeY > b.offsetY
)
}
export interface BoundaryCheckResult {
readonly touching: boolean
readonly type: 'vertical-boundary' | 'horizontal-boundary' | 'none'
readonly sharedLength: number
readonly gapX: number
readonly gapY: number
}
export function checkBoundarySharing(a: WorldOffset, b: WorldOffset): BoundaryCheckResult {
const touchLR = a.offsetX + a.sizeX === b.offsetX || b.offsetX + b.sizeX === a.offsetX
const overlapY = Math.max(
0,
Math.min(a.offsetY + a.sizeY, b.offsetY + b.sizeY) - Math.max(a.offsetY, b.offsetY),
)
if (touchLR && overlapY > 0) {
return {
touching: true,
type: 'vertical-boundary',
sharedLength: overlapY,
gapX: 0,
gapY: 0,
}
}
const touchTB = a.offsetY + a.sizeY === b.offsetY || b.offsetY + b.sizeY === a.offsetY
const overlapX = Math.max(
0,
Math.min(a.offsetX + a.sizeX, b.offsetX + b.sizeX) - Math.max(a.offsetX, b.offsetX),
)
if (touchTB && overlapX > 0) {
return {
touching: true,
type: 'horizontal-boundary',
sharedLength: overlapX,
gapX: 0,
gapY: 0,
}
}
const gapX = a.offsetX + a.sizeX <= b.offsetX
? b.offsetX - (a.offsetX + a.sizeX)
: b.offsetX + b.sizeX <= a.offsetX
? a.offsetX - (b.offsetX + b.sizeX)
: 0
const gapY = a.offsetY + a.sizeY <= b.offsetY
? b.offsetY - (a.offsetY + a.sizeY)
: b.offsetY + b.sizeY <= a.offsetY
? a.offsetY - (b.offsetY + b.sizeY)
: 0
return {
touching: false,
type: 'none',
sharedLength: 0,
gapX,
gapY,
}
}
// Generate 100 test seeds identical to standard suite
export function generateTestSeeds(): number[] {
const seedSet = new Set<number>([
0x301cd095, 0x416d61c5, 0xdeadbeef, 42, 123, 9999,
0, 1, 0x7fffffff, 0xffffffff, 241, 572, 919,
])
let state = 0x8546291a
while (seedSet.size < 100) {
state = (state * 1664525 + 1013904223) >>> 0
seedSet.add(state)
}
return Array.from(seedSet)
}
const TEST_SEEDS = generateTestSeeds()
describe('Adversarial Challenger M3 Gen2: World Placement & Graph Topology', () => {
it('Property 1: 100% boundary sharing (sharedLength > 0, zero gaps) across all 100 seeds for all contiguous pairs in SEAMLESS_ADJACENCY', () => {
let totalChecks = 0
let passedChecks = 0
const failureList: string[] = []
for (const seed of TEST_SEEDS) {
for (let act = 1; act <= 5; act++) {
const offsets = resolveWorldOffsets(act, seed)
const actSeamless = SEAMLESS_ADJACENCY.filter(([a, b]) => offsets.has(a) && offsets.has(b))
for (const [aId, bId] of actSeamless) {
totalChecks++
const a = offsets.get(aId)!
const b = offsets.get(bId)!
const res = checkBoundarySharing(a, b)
if (res.touching && res.sharedLength > 0 && res.gapX === 0 && res.gapY === 0) {
passedChecks++
} else {
failureList.push(`Act ${act}: pair [${aId}, ${bId}] seed=${seed} (touching=${res.touching}, len=${res.sharedLength}, gapX=${res.gapX}, gapY=${res.gapY})`)
}
}
}
}
expect(failureList).toEqual([])
expect(passedChecks).toBe(totalChecks)
expect(totalChecks).toBe(2600) // 26 seamless pairs * 100 seeds
})
it('Property 2a: 0 interior overlaps between any pair of seamless levels across all 100 seeds (9,900 checks)', () => {
let totalChecks = 0
let passedChecks = 0
for (const seed of TEST_SEEDS) {
for (let act = 1; act <= 5; act++) {
const offsets = resolveWorldOffsets(act, seed)
const actSeamless = SEAMLESS_ADJACENCY.filter(([a, b]) => offsets.has(a) && offsets.has(b))
const seamlessIds = Array.from(new Set(actSeamless.flat()))
for (let i = 0; i < seamlessIds.length; i++) {
for (let j = i + 1; j < seamlessIds.length; j++) {
totalChecks++
const a = offsets.get(seamlessIds[i]!)!
const b = offsets.get(seamlessIds[j]!)!
if (!doRectanglesOverlap(a, b)) {
passedChecks++
}
}
}
}
}
expect(passedChecks).toBe(totalChecks)
expect(totalChecks).toBe(9900)
})
it('Property 2b: 0 interior overlaps between ANY pair of placed levels across all 100 seeds (210,700 checks)', () => {
let totalChecks = 0
let passedChecks = 0
const overlaps: string[] = []
for (const seed of TEST_SEEDS) {
for (let act = 1; act <= 5; act++) {
const offsets = resolveWorldOffsets(act, seed)
const allPlaced = Array.from(offsets.values())
for (let i = 0; i < allPlaced.length; i++) {
for (let j = i + 1; j < allPlaced.length; j++) {
totalChecks++
const a = allPlaced[i]!
const b = allPlaced[j]!
if (!doRectanglesOverlap(a, b)) {
passedChecks++
} else {
overlaps.push(`Act ${act} Seed ${seed}: Level ${a.levelId} overlaps with Level ${b.levelId}`)
}
}
}
}
}
expect(overlaps).toEqual([])
expect(passedChecks).toBe(totalChecks)
expect(totalChecks).toBe(210700)
})
it('Property 3: Act 5 Level 111 West edge X = 702 alignment across all 100 seeds matching ka.ma0 Layer 79 automap cluster', () => {
let checkedCount = 0
for (const seed of TEST_SEEDS) {
const offsets = resolveWorldOffsets(5, seed)
const l110 = offsets.get(110)
const l111 = offsets.get(111)
const l112 = offsets.get(112)
expect(l110).toBeDefined()
expect(l111).toBeDefined()
expect(l112).toBeDefined()
// Level 110 at (760, 1000)
expect(l110!.offsetX).toBe(760)
expect(l110!.offsetY).toBe(1000)
// Level 111 width 58, offset X = 702
expect(l111!.sizeX).toBe(58)
expect(l111!.offsetX).toBe(702)
expect(l111!.offsetX + l111!.sizeX).toBe(l110!.offsetX) // Zero gap to Level 110 (702 + 58 = 760)
// Level 112 width 58, offset X = 702
expect(l112!.sizeX).toBe(58)
expect(l112!.offsetX).toBe(702)
expect(l112!.offsetY + l112!.sizeY).toBe(l111!.offsetY) // Zero gap North to Level 111
// Combined bounding box cluster check [702..759] x [717..1026]
const minX = Math.min(l111!.offsetX, l112!.offsetX)
const maxX = Math.max(l111!.offsetX + l111!.sizeX - 1, l112!.offsetX + l112!.sizeX - 1)
const minY = Math.min(l111!.offsetY, l112!.offsetY)
const maxY = Math.max(l111!.offsetY + l111!.sizeY, l112!.offsetY + l112!.sizeY)
expect(minX).toBe(702)
expect(maxX).toBe(759)
expect(minY).toBe(717)
expect(maxY).toBe(1026)
checkedCount++
}
expect(checkedCount).toBe(100)
})
it('Property 4: Determinism replay across 100 seeds across all 5 acts produces 100% bit-identical coordinate maps', () => {
let checkedCount = 0
for (const seed of TEST_SEEDS) {
for (let act = 1; act <= 5; act++) {
const run1 = resolveWorldOffsets(act, seed)
const run2 = resolveWorldOffsets(act, seed)
const entries1 = Array.from(run1.entries())
const entries2 = Array.from(run2.entries())
expect(entries1).toEqual(entries2)
checkedCount++
}
}
expect(checkedCount).toBe(500)
})
it('Requirement: Level 33 resolves relative to Level 32 at (3996, 966) with zero overlap and exact North boundary contact', () => {
for (const seed of TEST_SEEDS) {
const act1 = resolveWorldOffsets(1, seed)
const l32 = act1.get(32)
const l33 = act1.get(33)
expect(l32).toBeDefined()
expect(l33).toBeDefined()
// Level 32 is isolated parent at (4000, 1000)
expect(l32!.offsetX).toBe(4000)
expect(l32!.offsetY).toBe(1000)
// Level 33 has depend: 32, offset: (-4, -34), sizeX: 48, sizeY: 34
expect(l33!.offsetX).toBe(3996) // 4000 + (-4)
expect(l33!.offsetY).toBe(966) // 1000 + (-34)
expect(l33!.sizeX).toBe(28)
expect(l33!.sizeY).toBe(34)
// North boundary sharing: l33 bottom edge (966 + 34 = 1000) abuts l32 top edge (1000)
expect(l33!.offsetY + l33!.sizeY).toBe(l32!.offsetY)
expect(doRectanglesOverlap(l32!, l33!)).toBe(false)
const boundary = checkBoundarySharing(l32!, l33!)
expect(boundary.touching).toBe(true)
expect(boundary.type).toBe('horizontal-boundary')
expect(boundary.sharedLength).toBeGreaterThan(0)
}
})
it('Adversarial Extended Check: All 136 game levels placed with positive integer dimensions and valid non-negative coordinates', () => {
let totalLevelsPlaced = 0
for (let act = 1; act <= 5; act++) {
const offsets = resolveWorldOffsets(act, 42)
const expectedLevelsInAct = CANONICAL_LEVEL_ROWS.filter(r => r.act === act - 1)
expect(offsets.size).toBe(expectedLevelsInAct.length)
for (const [levelId, offset] of offsets.entries()) {
totalLevelsPlaced++
expect(offset.levelId).toBe(levelId)
expect(Number.isInteger(offset.offsetX)).toBe(true)
expect(offset.offsetX).toBeGreaterThanOrEqual(0)
expect(Number.isInteger(offset.offsetY)).toBe(true)
expect(offset.offsetY).toBeGreaterThanOrEqual(0)
expect(Number.isInteger(offset.sizeX)).toBe(true)
expect(offset.sizeX).toBeGreaterThan(0)
expect(Number.isInteger(offset.sizeY)).toBe(true)
expect(offset.sizeY).toBeGreaterThan(0)
}
}
expect(totalLevelsPlaced).toBe(136)
})
it('Adversarial Topology Check: SEAMLESS_ADJACENCY is planar and cycle-free across all acts', () => {
// Build adjacency list for seamless connections
const adj = new Map<number, Set<number>>()
for (const [a, b] of SEAMLESS_ADJACENCY) {
if (!adj.has(a)) adj.set(a, new Set())
if (!adj.has(b)) adj.set(b, new Set())
adj.get(a)!.add(b)
adj.get(b)!.add(a)
}
// Check each connected component is a tree / forest (no cycles)
const visited = new Set<number>()
const hasCycle = (node: number, parent: number | null): boolean => {
visited.add(node)
for (const neighbor of adj.get(node) ?? []) {
if (neighbor === parent) continue
if (visited.has(neighbor)) return true
if (hasCycle(neighbor, node)) return true
}
return false
}
for (const node of adj.keys()) {
if (!visited.has(node)) {
const cycleFound = hasCycle(node, null)
expect(cycleFound).toBe(false)
}
}
})
})