/** * Empirical verification harness for Milestone M1 (Issue #146: ControlBar XP bar alignment & 10-segment ticks). * * Runs headless verification of: * 1. Geometry of the 9 divider ticks: strictly at x = 256 + 12*i, height 4. * 2. Hit-testing boundary edge cases: (256, 560), (375, 563), (255, 560), (376, 560), (256, 557), (256, 558), (256, 566), (256, 567). * 3. All XP values from 0.0 to 1.0 (0%, 5%, 10%, 12.5%, 50%, 99.9%, 100%, negative, >1.0 overflow). */ import { ControlBarHud, EXP_BAR_BOUNDS, EXP_BAR_HOVER_BOUNDS, type ControlBarState, } from '../src/ui/control-bar.ts' import { D2FontRenderer } from '../src/ui/font.ts' interface DrawCall { fillStyle: string x: number y: number w: number h: number } function createMockCtx(): { ctx: CanvasRenderingContext2D; rects: DrawCall[] } { const rects: DrawCall[] = [] let currentFillStyle = '#000000' const ctx = { get fillStyle() { return currentFillStyle }, set fillStyle(val: any) { currentFillStyle = String(val) }, fillRect(x: number, y: number, w: number, h: number) { rects.push({ fillStyle: currentFillStyle, x, y, w, h }) }, createLinearGradient() { return { addColorStop: () => {} } }, drawImage() {}, strokeRect() {}, } as unknown as CanvasRenderingContext2D return { ctx, rects } } function createDummyState(xp: number, cur: number, next: number): ControlBarState { return { level: 85, xp, currentLevelXp: cur, nextLevelXp: next, stamina: 465, maxStamina: 465, isRunning: true, unspentStatPoints: 0, unspentSkillPoints: 0, } } async function runEmpiricalVerification() { console.log('=== [M1 EMPIRICAL VERIFICATION HARNESS: ISSUE #146] ===\n') let allPassed = true function assert(condition: boolean, msg: string) { if (condition) { console.log(` [PASS] ${msg}`) } else { console.error(` [FAIL] ${msg}`) allPassed = false } } // --- SECTION 1: GEOMETRY & BOUNDS --- console.log('1. Checking EXP_BAR_BOUNDS & EXP_BAR_HOVER_BOUNDS:') assert(EXP_BAR_BOUNDS.x === 256, `EXP_BAR_BOUNDS.x === 256 (actual: ${EXP_BAR_BOUNDS.x})`) assert(EXP_BAR_BOUNDS.y === 560, `EXP_BAR_BOUNDS.y === 560 (actual: ${EXP_BAR_BOUNDS.y})`) assert(EXP_BAR_BOUNDS.width === 120, `EXP_BAR_BOUNDS.width === 120 (actual: ${EXP_BAR_BOUNDS.width})`) assert(EXP_BAR_BOUNDS.height === 4, `EXP_BAR_BOUNDS.height === 4 (actual: ${EXP_BAR_BOUNDS.height})`) assert(EXP_BAR_HOVER_BOUNDS.x === 256, `EXP_BAR_HOVER_BOUNDS.x === 256 (actual: ${EXP_BAR_HOVER_BOUNDS.x})`) assert(EXP_BAR_HOVER_BOUNDS.y === 558, `EXP_BAR_HOVER_BOUNDS.y === 558 (actual: ${EXP_BAR_HOVER_BOUNDS.y})`) assert(EXP_BAR_HOVER_BOUNDS.width === 120, `EXP_BAR_HOVER_BOUNDS.width === 120 (actual: ${EXP_BAR_HOVER_BOUNDS.width})`) assert(EXP_BAR_HOVER_BOUNDS.height === 8, `EXP_BAR_HOVER_BOUNDS.height === 8 (actual: ${EXP_BAR_HOVER_BOUNDS.height})`) // --- SECTION 2: 9 DIVIDER TICKS GEOMETRY --- console.log('\n2. Checking 9 Divider Ticks Geometry (strictly at x = 256 + 12*i, height 4):') const bar = new ControlBarHud() const { ctx: ctxTicks, rects: rectsTicks } = createMockCtx() bar.draw(ctxTicks, createDummyState(500, 0, 1000), { ctrlPnlImg: null, runBtnImg: null, menuBtnImg: null, miniPanelImg: null, miniPanelBtnsImg: null, levelBtnImg: null, levelSocketImg: null, }, new D2FontRenderer()) const tickRects = rectsTicks.filter(r => r.fillStyle === '#1e1812' && r.y === 560 && r.w === 1 && r.h === 4) assert(tickRects.length === 9, `Rendered exactly 9 divider ticks (actual: ${tickRects.length})`) for (let i = 1; i <= 9; i++) { const expectedX = 256 + 12 * i const tick = tickRects[i - 1] const matches = tick && tick.x === expectedX && tick.y === 560 && tick.w === 1 && tick.h === 4 assert(!!matches, `Tick #${i}: x=${expectedX}, y=560, w=1, h=4 (actual: ${JSON.stringify(tick)})`) } // --- SECTION 3: HIT-TESTING BOUNDARY EDGE CASES --- console.log('\n3. Checking Hit-Testing Boundary Edge Cases:') const hitCases = [ { x: 256, y: 560, expected: 'exp', desc: '(256, 560) Top-left of inner groove' }, { x: 375, y: 563, expected: 'exp', desc: '(375, 563) Bottom-right interior of groove' }, { x: 255, y: 560, expected: null, desc: '(255, 560) 1px left of groove' }, { x: 376, y: 560, expected: 'exp', desc: '(376, 560) Right edge boundary (x = 256+120)' }, { x: 256, y: 557, expected: null, desc: '(256, 557) 1px above padded hover bounds' }, { x: 256, y: 558, expected: 'exp', desc: '(256, 558) Top edge of padded hover bounds' }, { x: 256, y: 566, expected: 'exp', desc: '(256, 566) Bottom edge of padded hover bounds' }, { x: 256, y: 567, expected: null, desc: '(256, 567) 1px below padded hover bounds' }, ] for (const hc of hitCases) { bar.handleMouseMove(hc.x, hc.y) const actual = bar.hoveredElement assert( actual === hc.expected, `${hc.desc} -> expected: ${hc.expected}, actual: ${actual}`, ) } // --- SECTION 4: XP RENDERING & VALUE SWEEPS --- console.log('\n4. Checking XP Fill Widths for Various XP Values (0.0 to 1.0, negative, overflow):') const xpCases = [ { label: '0% (0.0)', xp: 0, cur: 0, next: 1000, expectedW: 0 }, { label: '5% (0.05)', xp: 50, cur: 0, next: 1000, expectedW: 6 }, { label: '10% (0.10)', xp: 100, cur: 0, next: 1000, expectedW: 12 }, { label: '12.5% (0.125)', xp: 125, cur: 0, next: 1000, expectedW: 15 }, { label: '50% (0.50)', xp: 500, cur: 0, next: 1000, expectedW: 60 }, { label: '99.9% (0.999)', xp: 999, cur: 0, next: 1000, expectedW: 120 }, { label: '100% (1.0)', xp: 1000, cur: 0, next: 1000, expectedW: 120 }, { label: 'Negative XP (-500)', xp: -500, cur: 0, next: 1000, expectedW: 0 }, { label: 'XP below currentLevelXp', xp: 200, cur: 300, next: 1000, expectedW: 0 }, { label: 'XP overflow (>1.0: 1500)', xp: 1500, cur: 0, next: 1000, expectedW: 120 }, { label: 'Massive XP overflow (10000000)', xp: 10000000, cur: 0, next: 1000, expectedW: 120 }, ] for (const xc of xpCases) { const { ctx, rects } = createMockCtx() bar.draw(ctx, createDummyState(xc.xp, xc.cur, xc.next), { ctrlPnlImg: null, runBtnImg: null, menuBtnImg: null, miniPanelImg: null, miniPanelBtnsImg: null, levelBtnImg: null, levelSocketImg: null, }, new D2FontRenderer()) const fillRect = rects.find(r => r.fillStyle === '#d8d0a8' && r.y === 560) const actualW = fillRect ? fillRect.w : -1 assert( actualW === xc.expectedW, `${xc.label}: expected width ${xc.expectedW}, actual width ${actualW}`, ) } // --- SECTION 5: DIFFERENTIAL FUZZING --- console.log('\n5. Differential Fuzzing Across 1,000 Step Fractions:') let fuzzerFailures = 0 for (let i = 0; i <= 1000; i++) { const fraction = i / 1000 const xp = fraction * 50000 const { ctx, rects } = createMockCtx() bar.draw(ctx, createDummyState(xp, 0, 50000), { ctrlPnlImg: null, runBtnImg: null, menuBtnImg: null, miniPanelImg: null, miniPanelBtnsImg: null, levelBtnImg: null, levelSocketImg: null, }, new D2FontRenderer()) const fillRect = rects.find(r => r.fillStyle === '#d8d0a8' && r.y === 560) const expectedW = Math.round(120 * fraction) if (!fillRect || fillRect.w !== expectedW || fillRect.w < 0 || fillRect.w > 120) { fuzzerFailures++ } } assert(fuzzerFailures === 0, `1,000 differential fuzz iterations had 0 mismatches`) console.log(`\n=== FINAL VERDICT: ${allPassed ? 'ALL CHECKS PASSED (APPROVE)' : 'FAILED (REJECT)'} ===\n`) if (!allPassed) { process.exit(1) } } runEmpiricalVerification().catch(err => { console.error('Unhandled verification error:', err) process.exit(1) })