/** * 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[] = []): 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() 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) })