322 lines
11 KiB
TypeScript
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(2700) // 27 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)
|
|
}
|
|
}
|
|
})
|
|
})
|