728 lines
24 KiB
TypeScript
728 lines
24 KiB
TypeScript
/**
|
|
* Challenger 1 Empirical Stress Verification Suite for Milestone 1 (R1).
|
|
*
|
|
* Requirements:
|
|
* - 场景物件与窗户透光加色混合支持 (Additive Blending for Environment Objects & Window Light Rays)
|
|
* - Diablo II v1.13c Ground Truth Invariants:
|
|
* - objects.txt Trans = 7 (directional light beams R1..R5, 1R..4R)
|
|
* - objects.txt Trans = 1 (arcane orifice HA)
|
|
* - COF drawEffect = 3 / ADDITIVE_OBJECT_TOKENS (window light rays 11, 12)
|
|
* - Backward compatibility for baked and legacy scene packs
|
|
*
|
|
* Verification Areas:
|
|
* 1. All Token Variations: '11', '12', 'R1'..'R5', '1R'..'4R', 'HA' (upper, lower, whitespace)
|
|
* 2. Non-additive objects: torches ('TO'), pillars ('PL'), doors ('D1'), chests ('C1','C5'), normal trans=0
|
|
* 3. Trans flag overrides: trans=7 and trans=1 with arbitrary tokens, trans=0..8 discrimination
|
|
* 4. Boundary & malformed conditions: null row, undefined token, empty token, whitespace token
|
|
* 5. Differential fuzzing against independent Ground Truth Oracle (1,000 randomized iterations)
|
|
* 6. Level 108 Scene Pack Full Sweep: 119 objects, exactly 34 additive, all 34 tokens 11/12
|
|
* 7. Live 1.13c MPQ Ground Truth Table Verification: 574 Objects.txt rows, exactly 12 additive
|
|
*/
|
|
|
|
import * as fs from 'node:fs'
|
|
import * as path from 'node:path'
|
|
import { ADDITIVE_OBJECT_TOKENS, loadObjectsTable, resolveDs1Object, type ObjectsTable, type ObjectsRow } from '../src/game/objects.ts'
|
|
import { MpqArchive } from '../src/mpq/archive.ts'
|
|
import { fileSource } from '../src/mpq/file-source.ts'
|
|
import { MountedArchives } from '../src/mpq/mount.ts'
|
|
|
|
interface TestCaseResult {
|
|
suite: string
|
|
name: string
|
|
passed: boolean
|
|
input: any
|
|
expected: any
|
|
actual: any
|
|
error?: string | undefined
|
|
}
|
|
|
|
const testResults: TestCaseResult[] = []
|
|
|
|
function recordTest(suite: string, name: string, condition: boolean, input: any, expected: any, actual: any, extraMsg?: string) {
|
|
testResults.push({
|
|
suite,
|
|
name,
|
|
passed: condition,
|
|
input,
|
|
expected,
|
|
actual,
|
|
error: condition ? undefined : (extraMsg ?? `Assertion failed: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`),
|
|
})
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Runtime Helper mirroring act-scene.ts buildPackRuntime resolution logic
|
|
// ----------------------------------------------------------------------------
|
|
function resolvePackObjectBlendMode(obj: {
|
|
token?: string | null | undefined
|
|
trans?: number | null | undefined
|
|
blendMode?: 'normal' | 'additive' | null | undefined
|
|
}): 'normal' | 'additive' {
|
|
const tokenUpper = obj.token ? obj.token.trim().toUpperCase() : ''
|
|
const isAdditive =
|
|
obj.blendMode === 'additive' ||
|
|
obj.trans === 7 ||
|
|
obj.trans === 1 ||
|
|
ADDITIVE_OBJECT_TOKENS.has(tokenUpper)
|
|
return isAdditive ? 'additive' : (obj.blendMode ?? 'normal')
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Mock ObjectsTable builder for testing resolveDs1Object & toObjectsRow
|
|
// ----------------------------------------------------------------------------
|
|
function createMockObjectsTable(customRows: Partial<ObjectsRow>[] = []): ObjectsTable {
|
|
const rows: ObjectsRow[] = customRows.map((r, idx) => ({
|
|
id: r.id ?? idx,
|
|
name: r.name ?? `Obj_${idx}`,
|
|
token: r.token ?? '',
|
|
subClass: r.subClass ?? 0,
|
|
act: r.act ?? 0,
|
|
sizeX: r.sizeX ?? 1,
|
|
sizeY: r.sizeY ?? 1,
|
|
xOffset: r.xOffset ?? 0,
|
|
yOffset: r.yOffset ?? 0,
|
|
isDoor: r.isDoor ?? false,
|
|
trans: r.trans ?? 0,
|
|
blendMode: r.blendMode ?? (r.trans === 7 || r.trans === 1 || ADDITIVE_OBJECT_TOKENS.has((r.token ?? '').trim().toUpperCase()) ? 'additive' : 'normal'),
|
|
draw: 0,
|
|
totalPieces: 1,
|
|
autoMap: 0,
|
|
mode: [1, 0, 0, 0, 0, 0, 0, 0],
|
|
selectable: [1, 0, 0, 0, 0, 0, 0, 0],
|
|
frameCnt: [1, 0, 0, 0, 0, 0, 0, 0],
|
|
frameDelta: [256, 0, 0, 0, 0, 0, 0, 0],
|
|
start: [0, 0, 0, 0, 0, 0, 0, 0],
|
|
cycleAnim: [1, 0, 0, 0, 0, 0, 0, 0],
|
|
lit: [1, 0, 0, 0, 0, 0, 0, 0],
|
|
sync: 0,
|
|
components: ['hd'],
|
|
}))
|
|
|
|
const byId = new Map<number, ObjectsRow>()
|
|
for (const row of rows) {
|
|
byId.set(row.id, row)
|
|
}
|
|
|
|
return {
|
|
table: { header: [], rows: [] },
|
|
rows,
|
|
byId,
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// PART 1: ALL TOKEN VARIATIONS (CANONICAL, LOWERCASE, WHITESPACE, PADDED)
|
|
// ============================================================================
|
|
function testTokenVariations() {
|
|
const canonicalTokens = [
|
|
'11', '12',
|
|
'R1', 'R2', 'R3', 'R4', 'R5',
|
|
'1R', '2R', '3R', '4R',
|
|
'HA',
|
|
]
|
|
|
|
// Verify constant definition
|
|
recordTest(
|
|
'Part 1: Token Variations',
|
|
'ADDITIVE_OBJECT_TOKENS contains exactly 12 canonical tokens',
|
|
ADDITIVE_OBJECT_TOKENS.size === 12,
|
|
{ size: ADDITIVE_OBJECT_TOKENS.size },
|
|
12,
|
|
ADDITIVE_OBJECT_TOKENS.size,
|
|
)
|
|
|
|
for (const token of canonicalTokens) {
|
|
const hasCanonical = ADDITIVE_OBJECT_TOKENS.has(token)
|
|
recordTest(
|
|
'Part 1: Token Variations',
|
|
`ADDITIVE_OBJECT_TOKENS has canonical '${token}'`,
|
|
hasCanonical,
|
|
token,
|
|
true,
|
|
hasCanonical,
|
|
)
|
|
|
|
// Test variations: uppercase, lowercase, whitespace-padded, mixed
|
|
const variations = [
|
|
token,
|
|
token.toLowerCase(),
|
|
` ${token} `,
|
|
`\t${token}\n`,
|
|
` ${token.toLowerCase()} `,
|
|
` ${token} `,
|
|
]
|
|
|
|
for (const v of variations) {
|
|
// Test pack resolution
|
|
const packRes = resolvePackObjectBlendMode({ token: v, trans: 0 })
|
|
recordTest(
|
|
'Part 1: Token Variations',
|
|
`resolvePackObjectBlendMode handles variation '${v}' -> additive`,
|
|
packRes === 'additive',
|
|
{ token: v, trans: 0 },
|
|
'additive',
|
|
packRes,
|
|
)
|
|
|
|
// Test resolveDs1Object with token variation in mock table
|
|
const mockTable = createMockObjectsTable([{ id: 999, token: v, trans: 0 }])
|
|
// Act 4 id 44 is token '11', id 43 is token '12' in standard lookup
|
|
if (token === '11') {
|
|
const resolved = resolveDs1Object(mockTable, 4, 2, 44)
|
|
recordTest(
|
|
'Part 1: Token Variations',
|
|
`resolveDs1Object Act 4 ID 44 (token 11) -> additive`,
|
|
resolved.blendMode === 'additive',
|
|
{ act: 4, id: 44, token: resolved.token },
|
|
'additive',
|
|
resolved.blendMode,
|
|
)
|
|
} else if (token === '12') {
|
|
const resolved = resolveDs1Object(mockTable, 4, 2, 43)
|
|
recordTest(
|
|
'Part 1: Token Variations',
|
|
`resolveDs1Object Act 4 ID 43 (token 12) -> additive`,
|
|
resolved.blendMode === 'additive',
|
|
{ act: 4, id: 43, token: resolved.token },
|
|
'additive',
|
|
resolved.blendMode,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// PART 2: NON-ADDITIVE OBJECTS DISCRIMINATION
|
|
// ============================================================================
|
|
function testNonAdditiveObjects() {
|
|
const nonAdditiveTokens = [
|
|
'TO', 'to', ' TO ', // Torches
|
|
'PL', 'pl', ' PL ', // Pillars
|
|
'D1', 'd1', 'D2', // Doors
|
|
'C1', 'c1', 'C5', // Chests / Caskets
|
|
'B1', 'b1', // Barrels
|
|
'FL', 'A3', // Flags / Altars
|
|
'30', '31', '32', '33', '34', '35', // Level 108 ambient objects
|
|
'E1', 'E2', 'E3', 'E4', 'E5', // Level 108 ambient entities
|
|
'NORMAL_OBJ', 'STONE', 'WALL', '',
|
|
]
|
|
|
|
for (const token of nonAdditiveTokens) {
|
|
const packRes = resolvePackObjectBlendMode({ token, trans: 0 })
|
|
recordTest(
|
|
'Part 2: Non-Additive Objects',
|
|
`Non-additive token '${token}' with trans=0 -> normal`,
|
|
packRes === 'normal',
|
|
{ token, trans: 0 },
|
|
'normal',
|
|
packRes,
|
|
)
|
|
}
|
|
|
|
// Real Act 1 DS1 objects: Torch (id 1, token 'TO') and Chest (id 5, token 'L1')
|
|
const mockTable = createMockObjectsTable([
|
|
{ id: 1, token: 'TO', trans: 0 },
|
|
{ id: 5, token: 'L1', trans: 0 },
|
|
])
|
|
const resolvedTorch = resolveDs1Object(mockTable, 1, 2, 1)
|
|
recordTest(
|
|
'Part 2: Non-Additive Objects',
|
|
`resolveDs1Object Act 1 ID 1 (Torch 'TO') -> normal`,
|
|
resolvedTorch.blendMode === 'normal',
|
|
{ act: 1, id: 1, token: resolvedTorch.token },
|
|
'normal',
|
|
resolvedTorch.blendMode,
|
|
)
|
|
|
|
const resolvedChest = resolveDs1Object(mockTable, 1, 2, 5)
|
|
recordTest(
|
|
'Part 2: Non-Additive Objects',
|
|
`resolveDs1Object Act 1 ID 5 (Chest 'L1') -> normal`,
|
|
resolvedChest.blendMode === 'normal',
|
|
{ act: 1, id: 5, token: resolvedChest.token },
|
|
'normal',
|
|
resolvedChest.blendMode,
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// PART 3: TRANS FLAG OVERRIDES (TRANS=7 AND TRANS=1 GROUND TRUTH)
|
|
// ============================================================================
|
|
function testTransOverrides() {
|
|
const arbitraryTokens = ['RANDOM_OBJ', 'TO', 'D1', 'PL', 'BARREL', '', 'xyz']
|
|
|
|
for (const token of arbitraryTokens) {
|
|
// trans = 7 MUST be additive
|
|
const res7 = resolvePackObjectBlendMode({ token, trans: 7 })
|
|
recordTest(
|
|
'Part 3: Trans Overrides',
|
|
`Trans=7 with token '${token}' -> additive`,
|
|
res7 === 'additive',
|
|
{ token, trans: 7 },
|
|
'additive',
|
|
res7,
|
|
)
|
|
|
|
// trans = 1 MUST be additive
|
|
const res1 = resolvePackObjectBlendMode({ token, trans: 1 })
|
|
recordTest(
|
|
'Part 3: Trans Overrides',
|
|
`Trans=1 with token '${token}' -> additive`,
|
|
res1 === 'additive',
|
|
{ token, trans: 1 },
|
|
'additive',
|
|
res1,
|
|
)
|
|
}
|
|
|
|
// Non-additive trans values (0, 2, 3, 4, 5, 6, 8, -1) with arbitrary non-additive token
|
|
const nonAdditiveTransValues = [0, 2, 3, 4, 5, 6, 8, -1, 100]
|
|
for (const trans of nonAdditiveTransValues) {
|
|
const res = resolvePackObjectBlendMode({ token: 'CHEST', trans })
|
|
recordTest(
|
|
'Part 3: Trans Overrides',
|
|
`Trans=${trans} with non-additive token 'CHEST' -> normal`,
|
|
res === 'normal',
|
|
{ token: 'CHEST', trans },
|
|
'normal',
|
|
res,
|
|
)
|
|
}
|
|
|
|
// Trans values with additive token '11': token match MUST guarantee additive regardless of trans
|
|
for (const trans of nonAdditiveTransValues) {
|
|
const res = resolvePackObjectBlendMode({ token: '11', trans })
|
|
recordTest(
|
|
'Part 3: Trans Overrides',
|
|
`Trans=${trans} with additive token '11' -> additive`,
|
|
res === 'additive',
|
|
{ token: '11', trans },
|
|
'additive',
|
|
res,
|
|
)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// PART 4: BOUNDARY & MALFORMED CONDITIONS
|
|
// ============================================================================
|
|
function testBoundaryConditions() {
|
|
// 1. undefined token
|
|
const resUndefToken = resolvePackObjectBlendMode({ token: undefined, trans: 0 })
|
|
recordTest(
|
|
'Part 4: Boundary Conditions',
|
|
'undefined token with trans=0 -> normal',
|
|
resUndefToken === 'normal',
|
|
{ token: undefined, trans: 0 },
|
|
'normal',
|
|
resUndefToken,
|
|
)
|
|
|
|
// 2. undefined token with trans=7
|
|
const resUndefTrans7 = resolvePackObjectBlendMode({ token: undefined, trans: 7 })
|
|
recordTest(
|
|
'Part 4: Boundary Conditions',
|
|
'undefined token with trans=7 -> additive',
|
|
resUndefTrans7 === 'additive',
|
|
{ token: undefined, trans: 7 },
|
|
'additive',
|
|
resUndefTrans7,
|
|
)
|
|
|
|
// 3. empty string token
|
|
const resEmptyToken = resolvePackObjectBlendMode({ token: '', trans: 0 })
|
|
recordTest(
|
|
'Part 4: Boundary Conditions',
|
|
'empty string token with trans=0 -> normal',
|
|
resEmptyToken === 'normal',
|
|
{ token: '', trans: 0 },
|
|
'normal',
|
|
resEmptyToken,
|
|
)
|
|
|
|
// 4. whitespace-only token
|
|
const resWhitespaceToken = resolvePackObjectBlendMode({ token: ' ', trans: 0 })
|
|
recordTest(
|
|
'Part 4: Boundary Conditions',
|
|
'whitespace-only token with trans=0 -> normal',
|
|
resWhitespaceToken === 'normal',
|
|
{ token: ' ', trans: 0 },
|
|
'normal',
|
|
resWhitespaceToken,
|
|
)
|
|
|
|
// 5. null token (type cast check)
|
|
const resNullToken = resolvePackObjectBlendMode({ token: null as any, trans: 0 })
|
|
recordTest(
|
|
'Part 4: Boundary Conditions',
|
|
'null token with trans=0 -> normal',
|
|
resNullToken === 'normal',
|
|
{ token: null, trans: 0 },
|
|
'normal',
|
|
resNullToken,
|
|
)
|
|
|
|
// 6. null trans
|
|
const resNullTrans = resolvePackObjectBlendMode({ token: '11', trans: null as any })
|
|
recordTest(
|
|
'Part 4: Boundary Conditions',
|
|
'token 11 with null trans -> additive',
|
|
resNullTrans === 'additive',
|
|
{ token: '11', trans: null },
|
|
'additive',
|
|
resNullTrans,
|
|
)
|
|
|
|
// 7. Explicit blendMode = 'additive'
|
|
const resExplicitAdd = resolvePackObjectBlendMode({ token: 'CHEST', trans: 0, blendMode: 'additive' })
|
|
recordTest(
|
|
'Part 4: Boundary Conditions',
|
|
'Explicit blendMode=additive on non-additive token -> additive',
|
|
resExplicitAdd === 'additive',
|
|
{ token: 'CHEST', trans: 0, blendMode: 'additive' },
|
|
'additive',
|
|
resExplicitAdd,
|
|
)
|
|
|
|
// 8. Explicit blendMode = 'normal' conflicting with trans=7
|
|
// Ground truth takes precedence: trans=7 MUST override blendMode='normal'
|
|
const resConflictTrans7 = resolvePackObjectBlendMode({ token: 'CHEST', trans: 7, blendMode: 'normal' })
|
|
recordTest(
|
|
'Part 4: Boundary Conditions',
|
|
'Trans=7 overrides explicit blendMode=normal -> additive',
|
|
resConflictTrans7 === 'additive',
|
|
{ token: 'CHEST', trans: 7, blendMode: 'normal' },
|
|
'additive',
|
|
resConflictTrans7,
|
|
)
|
|
|
|
// 9. Explicit blendMode = 'normal' conflicting with token='11'
|
|
// Ground truth takes precedence: token='11' MUST override blendMode='normal'
|
|
const resConflictToken11 = resolvePackObjectBlendMode({ token: '11', trans: 0, blendMode: 'normal' })
|
|
recordTest(
|
|
'Part 4: Boundary Conditions',
|
|
'Token 11 overrides explicit blendMode=normal -> additive',
|
|
resConflictToken11 === 'additive',
|
|
{ token: '11', trans: 0, blendMode: 'normal' },
|
|
'additive',
|
|
resConflictToken11,
|
|
)
|
|
|
|
// 10. resolveDs1Object when row is null (empty table)
|
|
const emptyTable = createMockObjectsTable([])
|
|
// Act 4 ID 44 is window light ray (token 11). Even if table has no row, token is '11'
|
|
const resDs1NullRow11 = resolveDs1Object(emptyTable, 4, 2, 44)
|
|
recordTest(
|
|
'Part 4: Boundary Conditions',
|
|
'resolveDs1Object with null row but token 11 -> additive',
|
|
resDs1NullRow11.blendMode === 'additive' && resDs1NullRow11.row === null,
|
|
{ act: 4, id: 44, hasRow: resDs1NullRow11.row !== null },
|
|
'additive',
|
|
resDs1NullRow11.blendMode,
|
|
)
|
|
|
|
// Act 1 ID 1 is Torch (token 'TO'). With null row, it should resolve to 'normal'
|
|
const resDs1NullRowTorch = resolveDs1Object(emptyTable, 1, 2, 1)
|
|
recordTest(
|
|
'Part 4: Boundary Conditions',
|
|
'resolveDs1Object with null row and torch token -> normal',
|
|
resDs1NullRowTorch.blendMode === 'normal' && resDs1NullRowTorch.row === null,
|
|
{ act: 1, id: 1, hasRow: resDs1NullRowTorch.row !== null },
|
|
'normal',
|
|
resDs1NullRowTorch.blendMode,
|
|
)
|
|
|
|
// Monster / NPC spawn path in resolveDs1Object
|
|
const resDs1Monster = resolveDs1Object(emptyTable, 1, 1, 0)
|
|
recordTest(
|
|
'Part 4: Boundary Conditions',
|
|
'resolveDs1Object monster spawn (type=1) -> normal & trans=0',
|
|
resDs1Monster.blendMode === 'normal' && resDs1Monster.trans === 0 && resDs1Monster.kind === 'monster',
|
|
{ act: 1, type: 1, id: 0 },
|
|
'normal',
|
|
resDs1Monster.blendMode,
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// PART 5: DIFFERENTIAL FUZZING (1,000 ITERATIONS AGAINST INDEPENDENT ORACLE)
|
|
// ============================================================================
|
|
function testDifferentialFuzzing() {
|
|
// Independent Ground Truth Oracle
|
|
function groundTruthOracle(
|
|
token: string | undefined | null,
|
|
trans: number | undefined | null,
|
|
explicitBlendMode?: 'normal' | 'additive' | null,
|
|
): 'normal' | 'additive' {
|
|
if (explicitBlendMode === 'additive') return 'additive'
|
|
if (trans === 7 || trans === 1) return 'additive'
|
|
const norm = (token ?? '').trim().toUpperCase()
|
|
const additiveSet = new Set(['11', '12', 'R1', 'R2', 'R3', 'R4', 'R5', '1R', '2R', '3R', '4R', 'HA'])
|
|
if (additiveSet.has(norm)) return 'additive'
|
|
return 'normal'
|
|
}
|
|
|
|
const sampleTokens = [
|
|
undefined, null, '', ' ',
|
|
'11', '12', 'r1', 'R2', 'R3', 'r4', 'R5', '1r', '2R', '3r', '4R', 'ha', 'HA',
|
|
' 11 ', '\t12\n', ' r1 ', ' ha ',
|
|
'TO', 'to', ' D1 ', 'C5', 'PL', 'CHEST', 'BARREL', 'FL', '30', 'E4', 'XYZ999',
|
|
]
|
|
const sampleTrans = [undefined, null, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, 100]
|
|
const sampleBlendModes: (undefined | null | 'normal' | 'additive')[] = [undefined, null, 'normal', 'additive']
|
|
|
|
let mismatches = 0
|
|
const iterations = 1000
|
|
|
|
// Pseudo-random deterministic generator using SplitMix32
|
|
let seed = 1337420
|
|
function random(): number {
|
|
seed = (seed + 0x9e3779b9) | 0
|
|
let z = seed
|
|
z = Math.imul(z ^ (z >>> 16), 0x21f0aaad)
|
|
z = Math.imul(z ^ (z >>> 15), 0x735a2d97)
|
|
return ((z ^ (z >>> 15)) >>> 0) / 4294967296
|
|
}
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
const token = sampleTokens[Math.floor(random() * sampleTokens.length)]
|
|
const trans = sampleTrans[Math.floor(random() * sampleTrans.length)]
|
|
const explicitMode = sampleBlendModes[Math.floor(random() * sampleBlendModes.length)]
|
|
|
|
const expected = groundTruthOracle(token, trans, explicitMode)
|
|
const actual = resolvePackObjectBlendMode({ token: token as any, trans: trans as any, blendMode: explicitMode as any })
|
|
|
|
if (expected !== actual) {
|
|
mismatches++
|
|
recordTest(
|
|
'Part 5: Differential Fuzzing',
|
|
`Fuzz case ${i} mismatch for token=${token}, trans=${trans}, mode=${explicitMode}`,
|
|
false,
|
|
{ token, trans, explicitMode },
|
|
expected,
|
|
actual,
|
|
)
|
|
}
|
|
}
|
|
|
|
recordTest(
|
|
'Part 5: Differential Fuzzing',
|
|
`Passed 1,000 differential fuzzing iterations with zero mismatches`,
|
|
mismatches === 0,
|
|
{ iterations, mismatches },
|
|
0,
|
|
mismatches,
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// PART 6: LEVEL 108 SCENE PACK FULL SWEEP
|
|
// ============================================================================
|
|
function testLevel108ScenePack() {
|
|
const scenePath = 'samples/d2-packs/act4/108-act-4-diablo-1-var1/scene.json'
|
|
if (!fs.existsSync(scenePath)) {
|
|
throw new Error(`Scene pack not found: ${scenePath}`)
|
|
}
|
|
|
|
const rawJson = fs.readFileSync(scenePath, 'utf8')
|
|
const scene = JSON.parse(rawJson)
|
|
|
|
recordTest(
|
|
'Part 6: Level 108 Scene Pack',
|
|
'Scene has exactly 119 objects',
|
|
scene.objects.length === 119,
|
|
scene.objects.length,
|
|
119,
|
|
scene.objects.length,
|
|
)
|
|
|
|
let additiveCount = 0
|
|
let normalCount = 0
|
|
const additiveTokensFound: string[] = []
|
|
const additiveIds: number[] = []
|
|
|
|
for (let i = 0; i < scene.objects.length; i++) {
|
|
const obj = scene.objects[i]
|
|
const mode = resolvePackObjectBlendMode(obj)
|
|
|
|
if (mode === 'additive') {
|
|
additiveCount++
|
|
additiveTokensFound.push(obj.token)
|
|
additiveIds.push(obj.id)
|
|
} else {
|
|
normalCount++
|
|
}
|
|
}
|
|
|
|
recordTest(
|
|
'Part 6: Level 108 Scene Pack',
|
|
'Exactly 34 objects resolve to blendMode: additive',
|
|
additiveCount === 34,
|
|
{ total: scene.objects.length, additiveCount },
|
|
34,
|
|
additiveCount,
|
|
)
|
|
|
|
recordTest(
|
|
'Part 6: Level 108 Scene Pack',
|
|
'Exactly 85 objects resolve to blendMode: normal',
|
|
normalCount === 85,
|
|
{ total: scene.objects.length, normalCount },
|
|
85,
|
|
normalCount,
|
|
)
|
|
|
|
const nonLightRayAdditive = additiveTokensFound.filter(t => t !== '11' && t !== '12')
|
|
recordTest(
|
|
'Part 6: Level 108 Scene Pack',
|
|
'All 34 additive objects are tokens 11 or 12',
|
|
nonLightRayAdditive.length === 0,
|
|
{ nonLightRayAdditive },
|
|
[],
|
|
nonLightRayAdditive,
|
|
)
|
|
|
|
const token11Count = additiveTokensFound.filter(t => t === '11').length
|
|
const token12Count = additiveTokensFound.filter(t => t === '12').length
|
|
recordTest(
|
|
'Part 6: Level 108 Scene Pack',
|
|
'Additive tokens breakdown: exactly 19 of token 11 and 15 of token 12',
|
|
token11Count === 19 && token12Count === 15,
|
|
{ token11Count, token12Count },
|
|
{ token11Count: 19, token12Count: 15 },
|
|
{ token11Count, token12Count },
|
|
)
|
|
|
|
const uniqueIds = Array.from(new Set(additiveIds)).sort((a, b) => a - b)
|
|
recordTest(
|
|
'Part 6: Level 108 Scene Pack',
|
|
'All additive light rays originate from DS1 object IDs [43, 44]',
|
|
uniqueIds.length === 2 && uniqueIds[0] === 43 && uniqueIds[1] === 44,
|
|
uniqueIds,
|
|
[43, 44],
|
|
uniqueIds,
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// PART 7: LIVE 1.13c MPQ GROUND TRUTH CROSS-VERIFICATION
|
|
// ============================================================================
|
|
async function testMpqGroundTruth() {
|
|
if (!fs.existsSync('samples/d2')) {
|
|
console.log('Skipping live MPQ test: samples/d2 directory not found.')
|
|
return
|
|
}
|
|
|
|
const archives = new MountedArchives()
|
|
for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
|
|
archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`)))
|
|
}
|
|
|
|
const table = await loadObjectsTable(archives)
|
|
recordTest(
|
|
'Part 7: MPQ Ground Truth',
|
|
'Objects.txt parsed exactly 574 rows',
|
|
table.rows.length === 574,
|
|
table.rows.length,
|
|
574,
|
|
table.rows.length,
|
|
)
|
|
|
|
const additiveRows = table.rows.filter(r => r.blendMode === 'additive')
|
|
recordTest(
|
|
'Part 7: MPQ Ground Truth',
|
|
'Objects.txt contains exactly 12 additive rows',
|
|
additiveRows.length === 12,
|
|
additiveRows.length,
|
|
12,
|
|
additiveRows.length,
|
|
)
|
|
|
|
const expectedAdditiveTokens = ['R1', 'R2', 'R3', 'R4', 'R5', '1R', '2R', '3R', '4R', 'HA', '11', '12']
|
|
const foundTokens = additiveRows.map(r => r.token).sort()
|
|
const sortedExpected = [...expectedAdditiveTokens].sort()
|
|
|
|
recordTest(
|
|
'Part 7: MPQ Ground Truth',
|
|
'12 additive rows match exact tokens: R1..R5, 1R..4R, HA, 11, 12',
|
|
JSON.stringify(foundTokens) === JSON.stringify(sortedExpected),
|
|
foundTokens,
|
|
sortedExpected,
|
|
foundTokens,
|
|
)
|
|
|
|
// Verify trans flags on these 12 rows
|
|
for (const row of additiveRows) {
|
|
if (['R1', 'R2', 'R3', 'R4', 'R5', '1R', '2R', '3R', '4R'].includes(row.token)) {
|
|
recordTest(
|
|
'Part 7: MPQ Ground Truth',
|
|
`Row id=${row.id} (${row.token}) has Trans=7`,
|
|
row.trans === 7,
|
|
{ id: row.id, token: row.token, trans: row.trans },
|
|
7,
|
|
row.trans,
|
|
)
|
|
} else if (row.token === 'HA') {
|
|
recordTest(
|
|
'Part 7: MPQ Ground Truth',
|
|
`Row id=${row.id} (HA) has Trans=1`,
|
|
row.trans === 1,
|
|
{ id: row.id, token: row.token, trans: row.trans },
|
|
1,
|
|
row.trans,
|
|
)
|
|
} else if (['11', '12'].includes(row.token)) {
|
|
recordTest(
|
|
'Part 7: MPQ Ground Truth',
|
|
`Row id=${row.id} (${row.token}) has Trans=0 and blendMode=additive`,
|
|
row.trans === 0 && row.blendMode === 'additive',
|
|
{ id: row.id, token: row.token, trans: row.trans, blendMode: row.blendMode },
|
|
{ trans: 0, blendMode: 'additive' },
|
|
{ trans: row.trans, blendMode: row.blendMode },
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAIN RUNNER
|
|
// ============================================================================
|
|
async function runAll() {
|
|
console.log('======================================================================')
|
|
console.log('Challenger 1 Empirical Stress Verification Suite — Milestone 1 (R1)')
|
|
console.log('======================================================================\n')
|
|
|
|
testTokenVariations()
|
|
testNonAdditiveObjects()
|
|
testTransOverrides()
|
|
testBoundaryConditions()
|
|
testDifferentialFuzzing()
|
|
testLevel108ScenePack()
|
|
await testMpqGroundTruth()
|
|
|
|
const total = testResults.length
|
|
const passed = testResults.filter(r => r.passed).length
|
|
const failed = testResults.filter(r => !r.passed).length
|
|
|
|
console.log('\n----------------------------------------------------------------------')
|
|
console.log(`Execution Summary: Total Assertions=${total} | Passed=${passed} | Failed=${failed}`)
|
|
console.log('----------------------------------------------------------------------')
|
|
|
|
if (failed > 0) {
|
|
console.error(`\nFAILED TESTS (${failed}):`)
|
|
for (const r of testResults.filter(r => !r.passed)) {
|
|
console.error(` [${r.suite}] ${r.name}: ${r.error}`)
|
|
}
|
|
process.exit(1)
|
|
} else {
|
|
console.log('\nALL EMPIRICAL TESTS PASSED WITH ZERO REGRESSIONS.')
|
|
process.exit(0)
|
|
}
|
|
}
|
|
|
|
runAll().catch(err => {
|
|
console.error('Fatal execution error:', err)
|
|
process.exit(1)
|
|
})
|