/** * Challenger M4-1 — Adversarial & Boundary Stress Verification Suite * Covers: * - Issue #423: Flippy DC6 Coverage (all 506 base items + gold tiers), Fail-Fast Throw, Frozen Dying Monster Animation * - Issue #424: Ground Label Color Cartesian Permutations & UI Pickup -> Re-Drop Round-Trip * - Issue #425: 64px Sub-Tile Edge Pickup Boundary Fuzzing & 2-Pass Auto-Belt Quick-Slotting Stress * - Issue #427: Multiplayer Lockstep dwInitSeed Determinism & Hash Parity */ import { describe, it, expect, vi } from 'vitest' import { drawGroundItem, GROUND_ITEM_QUALITY_COLORS } from '../src/scene/act-scene.ts' import { ActorAnimator } from '../src/game/actor-animator.ts' import { GROUND_LABEL_QUALITY_COLORS, getGroundItemQualityColor, isGroundItemEthereal, isGroundItemSocketed, isGroundItemEtherealOrSocketed, computeInitialGroundLabelLayouts, } from '../src/ui/ground-labels.ts' import { BeltHud, BELT_COLS, BELT_ROWS, BELT_POTION_CATALOG, areBeltPotionsCompatible, isAutoBeltablePotion, itemToBeltPotion, } from '../src/ui/belt.ts' import { resolveGroundItemSpriteRect, itemToUiInventoryItem } from '../src/ui/inventory.ts' import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts' import { GroundItemManager, PLAYER_COLLISION_EXTENT_PX, ITEM_COLLISION_EXTENT_PX, D2_PICKUP_SUBTILE_REACH_PX, getPickupEdgeDistance, isWithinPickupBounds, type GroundItemEntity, } from '../src/game/ground-items.ts' import { GameEngine } from '../src/game/engine.ts' import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts' import { DEMO_EXPERIENCE, DEMO_SKILLS, DEMO_QUESTS } from '../src/game/demo-data.ts' import type { SpriteRenderer } from '../src/render/renderer.ts' function createAdversarialEngine(pickupRadius = 64, belt?: BeltHud) { const terrain = { widthPx: 2000, heightPx: 2000, overlap: () => 0 } return new GameEngine(terrain, { spawn: { x: 500, y: 500 }, stats: [], xpTable: DEMO_EXPERIENCE, skills: DEMO_SKILLS, questDefs: DEMO_QUESTS, combatOptions: { playerSpeed: 4, playerReach: 50, playerCooldownTicks: 10, playerDamage: 50, playerManaPerAttack: 1, respawnTicks: 100, }, talkRadius: 48, pickupRadius, inventoryCols: 10, inventoryRows: 4, lootSeed: 0x13572468, npcDefs: [], dropTables: getEmbeddedDropTables(), belt, }) } describe('Challenger M4-1 — Adversarial & Boundary Stress Verification', () => { // ============================================================================ // 1. Issue #423: Flippy DC6 Coverage, Fail-Fast Throw & Frozen Death Anim // ============================================================================ describe('Issue #423 — Exhaustive Flippy DC6 Coverage & Fail-Fast Rendering', () => { it('exhaustively resolves and renders every weapon, armor, and misc base item plus all gold tiers without procedural color boxes', () => { const tables = getEmbeddedDropTables() const allBases = [ ...tables.weapons.all, ...tables.armor.all, ...tables.misc.all, ] expect(allBases.length).toBe(659) const mgr = new GroundItemManager() for (const base of allBases) { const sr = resolveGroundItemSpriteRect( { code: base.code, name: base.name, flippyFile: (base as any).flippyfile, invFile: (base as any).invfile }, BAKED_UI_MANIFEST.flippyRects, BAKED_UI_MANIFEST.codeToFlippyFile, ) expect(sr, `Missing flippy rect for ${base.code} (${base.name})`).not.toBeNull() expect(sr!.w).toBeGreaterThan(0) expect(sr!.h).toBeGreaterThan(0) const quads: { x: number; y: number; w: number; h: number; color: readonly [number, number, number, number] }[] = [] const sprites: { frame: any; x: number; y: number; options: any }[] = [] const renderer = { drawSolid(x: number, y: number, w: number, h: number, color: readonly [number, number, number, number]) { quads.push({ x, y, w, h, color }) }, draw(frame: any, x: number, y: number, options: any) { sprites.push({ frame, x, y, options }) }, } as unknown as SpriteRenderer const entity = mgr.add( { code: base.code, name: base.name, flippyFile: (base as any).flippyfile, invFile: (base as any).invfile }, 0, 0, 300, 300, ) drawGroundItem(renderer, entity, 1000, undefined) expect(sprites.length).toBe(1) expect(sprites[0]!.frame.width).toBeGreaterThan(0) expect(sprites[0]!.frame.height).toBeGreaterThan(0) // Outside sparkle window (t=1000, cellX=0, cellY=0), only 1 ground shadow quad is drawn expect(quads.length).toBe(1) expect(quads[0]!.color[0]).toBe(0) expect(quads[0]!.color[1]).toBe(0) expect(quads[0]!.color[2]).toBe(0) } // Gold boundary tiers: 1, 49 (small: 24x7), 50, 499 (medium: 26x13), 500, 10000 (large: 32x20) const goldCases = [ { amount: 1, w: 24, h: 7 }, { amount: 49, w: 24, h: 7 }, { amount: 50, w: 26, h: 13 }, { amount: 499, w: 26, h: 13 }, { amount: 500, w: 32, h: 20 }, { amount: 10000, w: 32, h: 20 }, ] for (const gc of goldCases) { const sr = resolveGroundItemSpriteRect( { isGold: true, amount: gc.amount, code: 'gld' }, BAKED_UI_MANIFEST.flippyRects, BAKED_UI_MANIFEST.codeToFlippyFile, ) expect(sr).not.toBeNull() expect(sr!.w).toBe(gc.w) expect(sr!.h).toBe(gc.h) const sprites: { frame: any; x: number; y: number; options: any }[] = [] const renderer = { drawSolid() {}, draw(frame: any, x: number, y: number, options: any) { sprites.push({ frame, x, y, options }) }, } as unknown as SpriteRenderer const goldEntity = mgr.add({ isGold: true, amount: gc.amount, code: 'gld' }, 0, 0, 200, 200, { amount: gc.amount }) drawGroundItem(renderer, goldEntity, 1000, undefined) expect(sprites.length).toBe(1) expect(sprites[0]!.options.width).toBe(gc.w) expect(sprites[0]!.options.height).toBe(gc.h) } }) it('fails fast with an explicit Error in drawGroundItem when flippy sprite rect resolves to null', () => { // Temporarily empty flippyRects and itemRects to simulate a corrupt unmapped item with null sprite rect const savedFlippy = { ...BAKED_UI_MANIFEST.flippyRects } const savedItems = { ...BAKED_UI_MANIFEST.itemRects } try { for (const k of Object.keys(BAKED_UI_MANIFEST.flippyRects)) { delete (BAKED_UI_MANIFEST.flippyRects as any)[k] } for (const k of Object.keys(BAKED_UI_MANIFEST.itemRects)) { delete (BAKED_UI_MANIFEST.itemRects as any)[k] } const renderer = { drawSolid() {}, draw() {}, } as unknown as SpriteRenderer const corruptEntity: GroundItemEntity = { id: 'corrupt_999', item: { code: 'zzz', name: 'Corrupt Unknown Item' }, name: 'Corrupt Unknown Item', nameZh: '损坏未知物品', quality: 'normal', isGold: false, amount: 1, invWidth: 9, invHeight: 9, dropTime: 0, x: 100, y: 100, cellX: 0, cellY: 0, sparklePhase: 0, } expect(() => drawGroundItem(renderer, corruptEntity, 1000, undefined)).toThrow( /GroundItem render failure: missing flippy sprite rect/, ) } finally { Object.assign(BAKED_UI_MANIFEST.flippyRects, savedFlippy) Object.assign(BAKED_UI_MANIFEST.itemRects, savedItems) } }) it('advances death animation (DT -> DD) even when monster has frozenTicks > 0', () => { const animator = new ActorAnimator() const dtClip = { group: 0, frames: 4, directions: 8, speed: 256, loop: false, events: [] } const ddClip = { group: 8, frames: 1, directions: 8, speed: 256, loop: true, events: [] } const monster = { state: 'dead' as const, animMode: 'dt' as 'dt' | 'dd', frozenTicks: 100, deathGated: true, dropRolled: false, } animator.play('dt', dtClip) let dropTriggered = false for (let step = 0; step < 10; step++) { if (monster.state === 'dead' || monster.animMode === 'dt') { if (animator.mode === 'dt' && animator.finished) { animator.play('dd', ddClip) monster.animMode = 'dd' dropTriggered = true } } // Exact condition from src/scene/act-scene.ts:7236 if (monster.state === 'dead' || monster.animMode === 'dt' || !monster.frozenTicks || monster.frozenTicks <= 0) { animator.tick() } } expect(dropTriggered).toBe(true) expect(monster.animMode).toBe('dd') expect(animator.mode).toBe('dd') }) }) // ============================================================================ // 2. Issue #424: Ground Label Color Permutations & UI Round-Trip // ============================================================================ describe('Issue #424 — Ground Label Color Full Cartesian Permutations & UI Round-Trip', () => { it('fuzzes all combinations of quality, ethereal, sockets, isRuneword, and isGold across getGroundItemQualityColor and computeInitialGroundLabelLayouts', () => { const qualities = ['low', 'normal', 'superior', 'magic', 'rare', 'set', 'unique', 'craft', 'rune'] as const const ethereals = [false, true] as const const socketCounts = [0, 1, 6] as const const runewords = [false, true] as const const goldFlags = [false, true] as const const mgr = new GroundItemManager() let totalPermutations = 0 for (const quality of qualities) { for (const ethereal of ethereals) { for (const sockets of socketCounts) { for (const isRuneword of runewords) { for (const isGold of goldFlags) { totalPermutations += 1 const entity: GroundItemEntity = { id: `perm_${totalPermutations}`, item: { code: isGold ? 'gld' : 'bsd', name: isGold ? '100 Gold' : 'Broad Sword', quality, ethereal, sockets, isRuneword, isGold, }, name: isGold ? '100 Gold' : 'Broad Sword', nameZh: isGold ? '100 金币' : '阔剑', quality, isGold, amount: isGold ? 100 : 1, invWidth: 2, invHeight: 3, dropTime: 0, x: 200, y: 200, cellX: 5, cellY: 5, sparklePhase: 0, ...(ethereal ? { ethereal: true } : {}), ...(sockets > 0 ? { sockets } : {}), } // Independent 1.13c ground truth oracle let expectedColor: string if (isGold) { expectedColor = GROUND_LABEL_QUALITY_COLORS.gold! } else if (quality === 'low') { expectedColor = GROUND_LABEL_QUALITY_COLORS.low! } else if (isRuneword) { expectedColor = GROUND_LABEL_QUALITY_COLORS.unique! } else if ((quality === 'normal' || quality === 'superior') && (ethereal || sockets > 0)) { expectedColor = '#808080' } else { expectedColor = GROUND_LABEL_QUALITY_COLORS[quality]! } const actualColor = getGroundItemQualityColor(entity) expect( actualColor, `Mismatch for quality=${quality}, eth=${ethereal}, sock=${sockets}, rw=${isRuneword}, gold=${isGold}`, ).toBe(expectedColor) const layouts = computeInitialGroundLabelLayouts([entity], (wx, wy) => ({ x: wx, y: wy })) expect(layouts.length).toBe(1) expect(layouts[0]!.color).toBe(expectedColor) expect(layouts[0]!.borderColor).toBe(expectedColor) } } } } } // 9 * 2 * 3 * 2 * 2 = 216 exhaustive permutations verified expect(totalPermutations).toBe(216) expect(mgr.count).toBe(0) }) it('preserves ethereal, sockets, and #808080 ground label color across ground -> UI inventory -> ground re-drop round-trip', () => { const tables = getEmbeddedDropTables() const mgr = new GroundItemManager() // 1. Spawn ethereal socketed normal item on ground const droppedEthSocketed = mgr.add( { id: 'roundtrip_monarch', code: 'uit', name: 'Monarch', quality: 'normal', ethereal: true, sockets: 4, invWidth: 2, invHeight: 3, }, 10, 10, 320, 320, ) expect(isGroundItemEthereal(droppedEthSocketed)).toBe(true) expect(isGroundItemSocketed(droppedEthSocketed)).toBe(true) expect(getGroundItemQualityColor(droppedEthSocketed)).toBe('#808080') // 2. Pick up into UI inventory via itemToUiInventoryItem const uiItem = itemToUiInventoryItem(droppedEthSocketed.item, tables) expect(uiItem.ethereal).toBe(true) expect(uiItem.sockets).toBe(4) // 3. Re-drop UI inventory item back onto ground const redropped = mgr.add(uiItem, 11, 11, 340, 340) expect(redropped.ethereal).toBe(true) expect(redropped.sockets).toBe(4) expect(isGroundItemEthereal(redropped)).toBe(true) expect(isGroundItemSocketed(redropped)).toBe(true) expect(getGroundItemQualityColor(redropped)).toBe('#808080') const layouts = computeInitialGroundLabelLayouts([redropped], (wx, wy) => ({ x: wx, y: wy })) expect(layouts[0]!.color).toBe('#808080') }) }) // ============================================================================ // 3. Issue #425: Pickup Bounds Fuzzing & 2-Pass Auto-Belt Stress // ============================================================================ describe('Issue #425 — 64px Edge-to-Edge Pickup Boundary Fuzzing & 2-Pass Auto-Belt Stress', () => { it('accurately discriminates boundary distances around edgeDist = 63.99, 64.0, 64.01 (centerDist = 95.99, 96.0, 96.01) across 360 angles', () => { expect(PLAYER_COLLISION_EXTENT_PX + ITEM_COLLISION_EXTENT_PX).toBe(32) expect(D2_PICKUP_SUBTILE_REACH_PX).toBe(64) // Exact axis-aligned boundary checks expect(getPickupEdgeDistance(100, 100, 195.99, 100)).toBeCloseTo(63.99, 5) expect(isWithinPickupBounds(100, 100, 195.99, 100, 64)).toBe(true) expect(getPickupEdgeDistance(100, 100, 196.0, 100)).toBeCloseTo(64.0, 5) expect(isWithinPickupBounds(100, 100, 196.0, 100, 64)).toBe(true) expect(getPickupEdgeDistance(100, 100, 196.01, 100)).toBeCloseTo(64.01, 5) expect(isWithinPickupBounds(100, 100, 196.01, 100, 64)).toBe(false) // Overlapping boxes clamp edge distance to 0 expect(getPickupEdgeDistance(100, 100, 110, 100)).toBe(0) expect(isWithinPickupBounds(100, 100, 110, 100, 64)).toBe(true) // Radial fuzzing across 360 degrees const px = 500 const py = 500 for (let deg = 0; deg < 360; deg += 5) { const rad = (deg * Math.PI) / 180 const inX = px + Math.cos(rad) * 95.99 const inY = py + Math.sin(rad) * 95.99 expect(isWithinPickupBounds(px, py, inX, inY, 64)).toBe(true) const outX = px + Math.cos(rad) * 96.02 const outY = py + Math.sin(rad) * 96.02 expect(isWithinPickupBounds(px, py, outX, outY, 64)).toBe(false) } }) it('stress-tests 2-pass BeltHud.autoPlacePotion across all 15 potion codes, mixed-tier stacking, utility potion isolation, non-beltable rejection, partially drained columns, and full-belt fallback', () => { const allPotionCodes = [ 'hp1', 'hp2', 'hp3', 'hp4', 'hp5', 'mp1', 'mp2', 'mp3', 'mp4', 'mp5', 'rvs', 'rvl', 'vps', 'yps', 'wms', ] expect(allPotionCodes.length).toBe(15) for (const code of allPotionCodes) { expect(BELT_POTION_CATALOG[code]).toBeDefined() expect(isAutoBeltablePotion({ code })).toBe(true) } // Reject non-beltable scrolls, tomes, weapons, and throwing potions for (const nonBeltCode of ['tsc', 'isc', 'tbk', 'ibk', 'gpm', 'opm', 'gpl', 'opl', 'gps', 'ops', 'ssd', 'cap']) { expect(isAutoBeltablePotion({ code: nonBeltCode })).toBe(false) } const belt = new BeltHud() belt.clear() // Seed base row (row 0): col 0 = hp1, col 1 = mp1, col 2 = rvs, col 3 = vps (stamina) expect(belt.autoPlacePotionSlot({ code: 'hp1', id: 'c0-r0' })).toEqual( expect.objectContaining({ row: 0, col: 0 }), ) expect(belt.autoPlacePotionSlot({ code: 'mp1', id: 'c1-r0' })).toEqual( expect.objectContaining({ row: 0, col: 1 }), ) expect(belt.autoPlacePotionSlot({ code: 'rvs', id: 'c2-r0' })).toEqual( expect.objectContaining({ row: 0, col: 2 }), ) expect(belt.autoPlacePotionSlot({ code: 'vps', id: 'c3-r0' })).toEqual( expect.objectContaining({ row: 0, col: 3 }), ) // Mixed-tier stacking: hp2, hp4, hp5 stack into col 0 (rows 1, 2, 3) expect(belt.autoPlacePotionSlot({ code: 'hp2', id: 'c0-r1' })).toEqual( expect.objectContaining({ row: 1, col: 0 }), ) expect(belt.autoPlacePotionSlot({ code: 'hp4', id: 'c0-r2' })).toEqual( expect.objectContaining({ row: 2, col: 0 }), ) expect(belt.autoPlacePotionSlot({ code: 'hp5', id: 'c0-r3' })).toEqual( expect.objectContaining({ row: 3, col: 0 }), ) // Mixed-tier mana: mp3, mp5 stack into col 1 (rows 1, 2) expect(belt.autoPlacePotionSlot({ code: 'mp3', id: 'c1-r1' })).toEqual( expect.objectContaining({ row: 1, col: 1 }), ) expect(belt.autoPlacePotionSlot({ code: 'mp5', id: 'c1-r2' })).toEqual( expect.objectContaining({ row: 2, col: 1 }), ) // Mixed-tier rejuv: rvl stacks onto rvs in col 2 (row 1) expect(belt.autoPlacePotionSlot({ code: 'rvl', id: 'c2-r1' })).toEqual( expect.objectContaining({ row: 1, col: 2 }), ) // Utility potion isolation: yps (antidote) and wms (thawing) do NOT stack onto vps (stamina) in col 3, // and because all 4 base columns (row 0) are occupied, they return null (fallback to inventory)! expect(belt.autoPlacePotionSlot({ code: 'yps', id: 'anti-1' })).toBeNull() expect(belt.autoPlacePotionSlot({ code: 'wms', id: 'thaw-1' })).toBeNull() // Same utility potion (vps) DOES stack into col 3 (row 1) expect(belt.autoPlacePotionSlot({ code: 'vps', id: 'c3-r1' })).toEqual( expect.objectContaining({ row: 1, col: 3 }), ) // Partially drained column: drink bottom potion in col 0 (shifts rows 1..3 down to 0..2, leaving row 3 empty) const consumed = belt.useSlot(0) expect(consumed?.code).toBe('hp1') expect(belt.grid[0]![0]?.code).toBe('hp2') expect(belt.grid[3]![0]).toBeNull() // Next hp3 refills row 3 of col 0 expect(belt.autoPlacePotionSlot({ code: 'hp3', id: 'c0-refill' })).toEqual( expect.objectContaining({ row: 3, col: 0 }), ) // Fill remaining slots in cols 1, 2, 3 and verify full belt fallback to engine.bag expect(belt.autoPlacePotion({ code: 'mp2' })).toBe(true) // col 1 row 3 expect(belt.autoPlacePotion({ code: 'rvs' })).toBe(true) // col 2 row 2 expect(belt.autoPlacePotion({ code: 'rvl' })).toBe(true) // col 2 row 3 expect(belt.autoPlacePotion({ code: 'vps' })).toBe(true) // col 3 row 2 expect(belt.autoPlacePotion({ code: 'vps' })).toBe(true) // col 3 row 3 expect(belt.countTotalPotions()).toBe(BELT_COLS * BELT_ROWS) const engine = createAdversarialEngine(64, belt) const overflowGrounded = engine.dropItem( { id: 'overflow_hp5', code: 'hp5', name: 'Super Healing Potion', invWidth: 1, invHeight: 1 }, 500, 500, ) const pickupRes = engine.pickupItem(overflowGrounded.id) expect(pickupRes.success).toBe(true) expect(pickupRes.destination).toBe('inventory') expect(engine.bag.contents.length).toBe(1) }) }) })