245 lines
8.0 KiB
TypeScript
245 lines
8.0 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { SEAMLESS_ADJACENCY } from '../src/game/world-graph.ts'
|
|
import { resolveWorldOffsets, type WorldOffset } from '../src/game/world-placement.ts'
|
|
|
|
/**
|
|
* 100 distinct seeds for adversarial stress testing:
|
|
* - 6 canonical / regression seeds (0x301cd095, 0x416d61c5, 0xdeadbeef, 42, 123, 9999)
|
|
* - 7 boundary / extreme seeds (0, 1, 0x7fffffff, 0xffffffff, 241, 572, 919)
|
|
* - 87 pseudo-random seeds generated via standard LCG to ensure broad coverage
|
|
*/
|
|
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)
|
|
}
|
|
|
|
export const TEST_SEEDS = generateTestSeeds()
|
|
|
|
/** Check if two rectangles have overlapping interior areas. */
|
|
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
|
|
}
|
|
|
|
/**
|
|
* Check if two rectangular levels share an exact boundary edge without tears or gaps.
|
|
* Must share a non-zero length segment along their touching edge.
|
|
*/
|
|
export function checkBoundarySharing(a: WorldOffset, b: WorldOffset): BoundaryCheckResult {
|
|
// Check horizontal adjacency (A is West of B or B is West of A)
|
|
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,
|
|
}
|
|
}
|
|
|
|
// Check vertical adjacency (A is North of B or B is North of A)
|
|
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,
|
|
}
|
|
}
|
|
|
|
// Compute gaps / tears if not touching along an edge
|
|
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,
|
|
}
|
|
}
|
|
|
|
describe('Adversarial Stress Test: resolveWorldOffsets across 100 Seeds for Acts 1..5', () => {
|
|
it('generates exactly 100 distinct test seeds', () => {
|
|
expect(TEST_SEEDS).toHaveLength(100)
|
|
expect(new Set(TEST_SEEDS).size).toBe(100)
|
|
})
|
|
|
|
it('Act 5: Level 111 West edge consistently aligns to X = 702 when connected to Level 110 at (760, 1000)', () => {
|
|
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()
|
|
|
|
expect(l110!.offsetX).toBe(760)
|
|
expect(l110!.offsetY).toBe(1000)
|
|
|
|
expect(l111!.sizeX).toBe(58)
|
|
expect(l111!.offsetX).toBe(702)
|
|
expect(l111!.offsetX + l111!.sizeX).toBe(760) // Zero gap to Level 110
|
|
|
|
expect(l112!.sizeX).toBe(58)
|
|
expect(l112!.offsetX).toBe(702)
|
|
expect(l112!.offsetY + l112!.sizeY).toBe(l111!.offsetY) // Zero gap North to Level 111
|
|
|
|
checkedCount++
|
|
}
|
|
expect(checkedCount).toBe(100)
|
|
})
|
|
|
|
it('Determinism: identical seeds yield strictly identical WorldOffset 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)
|
|
expect(Array.from(run1.entries())).toEqual(Array.from(run2.entries()))
|
|
checkedCount++
|
|
}
|
|
}
|
|
expect(checkedCount).toBe(500)
|
|
})
|
|
|
|
it('Non-Overlapping Interiors: seamless outdoor levels have zero interior overlap', () => {
|
|
let pairsChecked = 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 placedSeamlessIds = Array.from(new Set(actSeamless.flat()))
|
|
|
|
for (let i = 0; i < placedSeamlessIds.length; i++) {
|
|
for (let j = i + 1; j < placedSeamlessIds.length; j++) {
|
|
const a = offsets.get(placedSeamlessIds[i]!)!
|
|
const b = offsets.get(placedSeamlessIds[j]!)!
|
|
expect(doRectanglesOverlap(a, b)).toBe(false)
|
|
pairsChecked++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
expect(pairsChecked).toBe(9900)
|
|
})
|
|
|
|
it('Acts 1, 2, 4, 5: contiguous pairs in SEAMLESS_ADJACENCY share exact boundary coordinates without tears or gaps', () => {
|
|
let pairsChecked = 0
|
|
const failures: string[] = []
|
|
|
|
for (const seed of TEST_SEEDS) {
|
|
for (const act of [1, 2, 4, 5]) {
|
|
const offsets = resolveWorldOffsets(act, seed)
|
|
const actSeamless = SEAMLESS_ADJACENCY.filter(([a, b]) => offsets.has(a) && offsets.has(b))
|
|
for (const [aId, bId] of actSeamless) {
|
|
const a = offsets.get(aId)!
|
|
const b = offsets.get(bId)!
|
|
const res = checkBoundarySharing(a, b)
|
|
if (!res.touching) {
|
|
failures.push(`Act ${act}: [${aId}, ${bId}] seed=${seed}`)
|
|
}
|
|
pairsChecked++
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(failures).toEqual([])
|
|
// 18 pairs (Act 1: 7, Act 2: 5, Act 4: 3, Act 5: 3) x 100 seeds.
|
|
expect(pairsChecked).toBe(1800)
|
|
})
|
|
|
|
it('Act 3 Seamless Adjacency: contiguous pairs in SEAMLESS_ADJACENCY share exact boundary coordinates without tears or gaps', () => {
|
|
let pairsChecked = 0
|
|
const failures: { seed: number; pair: string; reason: string }[] = []
|
|
|
|
for (const seed of TEST_SEEDS) {
|
|
const offsets = resolveWorldOffsets(3, seed)
|
|
const actSeamless = SEAMLESS_ADJACENCY.filter(([a, b]) => offsets.has(a) && offsets.has(b))
|
|
for (const [aId, bId] of actSeamless) {
|
|
const a = offsets.get(aId)!
|
|
const b = offsets.get(bId)!
|
|
const res = checkBoundarySharing(a, b)
|
|
if (!res.touching) {
|
|
failures.push({
|
|
seed,
|
|
pair: `[${aId}, ${bId}]`,
|
|
reason: `Touch at corner only, sharedLength=0, gapX=${res.gapX}, gapY=${res.gapY}`,
|
|
})
|
|
}
|
|
pairsChecked++
|
|
}
|
|
}
|
|
|
|
expect(pairsChecked).toBe(800)
|
|
expect(failures).toEqual([])
|
|
})
|
|
|
|
it('All-Level Interior Non-Overlap: all placed levels across all acts have zero interior overlap', () => {
|
|
let pairsChecked = 0
|
|
const overlaps: { seed: number; act: number; a: number; b: number }[] = []
|
|
|
|
for (const seed of TEST_SEEDS) {
|
|
for (let act = 1; act <= 5; act++) {
|
|
const offsets = resolveWorldOffsets(act, seed)
|
|
const placed = Array.from(offsets.values())
|
|
for (let i = 0; i < placed.length; i++) {
|
|
for (let j = i + 1; j < placed.length; j++) {
|
|
if (doRectanglesOverlap(placed[i]!, placed[j]!)) {
|
|
overlaps.push({ seed, act, a: placed[i]!.levelId, b: placed[j]!.levelId })
|
|
}
|
|
pairsChecked++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(pairsChecked).toBe(210700)
|
|
expect(overlaps).toEqual([])
|
|
})
|
|
})
|