import { describe, expect, it, vi } from 'vitest' import { getInventoryGoldLimit, getGoldFlippyRect, resolveGroundItemSpriteRect, } from '../src/ui/inventory.ts' import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts' import { goldItem } from '../src/game/items.ts' import { GameEngine } from '../src/game/engine.ts' import type { Monster } from '../src/game/combat.ts' import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts' import { GroundItemManager, findSafeDropPosition, getIsometricRingCandidates, resolveGroundItemMetadata, triggerFlippyBounce, } from '../src/game/ground-items.ts' import { COLLIDE_NONE, COLLIDE_WALL, COLLIDE_BLANK, COLLIDE_MASK_INVALID, COLLIDE_WATER, type CollisionGrid, } from '../src/game/d2map.ts' const dropTables = getEmbeddedDropTables() function createTestEngine(playerLevel = 1, initialGold = 0, overrides?: Record): GameEngine { const engine = new GameEngine( { widthPx: 2000, heightPx: 2000, overlap: () => 0 }, { spawn: { x: 500, y: 500 }, stats: [], xpTable: [0, 500, 1500, 3750, 7875, 14175], skills: [], npcDefs: [], questDefs: [], combatOptions: { playerSpeed: 4, playerReach: 50, playerCooldownTicks: 10, playerDamage: 100, playerManaPerAttack: 1, respawnTicks: 100, }, talkRadius: 50, pickupRadius: 30, inventoryCols: 10, inventoryRows: 4, lootSeed: 1337, dropTables, ...overrides, }, ) engine.setDropTables(dropTables) engine.world.player.level = playerLevel engine.bag.gold = initialGold return engine } function createTestMonster( index: number, id: string, x: number, y: number, hp = 20, rank: 'normal' | 'champion' | 'unique' | 'minion' | 'boss' = 'normal', level = 5, ): Monster { return { index, stats: { id, name: id, hp, damage: 1, cooldownTicks: 10, reach: 20, aggroRadius: 100, speed: 10, xp: 10, level, rank, }, x, y, hp, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0, dropRolled: false, } } const emptyInput = { movement: { x: 0, y: 0 }, attacking: false, pickingUp: false, talking: false, digits: [], saving: false, loading: false, } describe('Challenger M3-1: Adversarial Fuzzing & Boundary Stress Suite', () => { describe('1. Gold Capacity Boundaries & Visual Tier Mutation Fuzzing', () => { it('1.1: fuzzes player.level across all valid levels (1..99) and degenerate boundaries (0, negative, float, NaN, >99)', () => { // Every valid D2 level 1..99 must strictly equal level * 10,000 for (let lvl = 1; lvl <= 99; lvl++) { const expected = lvl * 10_000 expect(getInventoryGoldLimit(lvl)).toBe(expected) const engine = createTestEngine(lvl, 0) expect(engine.maxGoldCapacity).toBe(expected) } // Degenerate lower bounds clamp safely to level 1 (10,000) expect(getInventoryGoldLimit(0)).toBe(10_000) expect(getInventoryGoldLimit(-1)).toBe(10_000) expect(getInventoryGoldLimit(-9999)).toBe(10_000) expect(getInventoryGoldLimit(Number.NaN)).toBe(10_000) // Fractional levels floor cleanly before multiplying by 10,000 expect(getInventoryGoldLimit(12.9)).toBe(120_000) expect(getInventoryGoldLimit(85.1)).toBe(850_000) // Upper bound clamping at D2 max character level 99 (990,000) expect(getInventoryGoldLimit(99)).toBe(990_000) expect(getInventoryGoldLimit(100)).toBe(990_000) expect(getInventoryGoldLimit(999)).toBe(990_000) }) it('1.2: tests small, exact, and oversized gold pickups at empty, half-full, 1-below-max, and at-max capacity', () => { const level = 5 // Max capacity = 50,000 const maxCap = 50_000 // Case A: Empty inventory (0 / 50,000) { const engine = createTestEngine(level, 0) // Small pickup const pSmall = engine.dropGold(45, 200, 200)! const rSmall = engine.pickupGold(pSmall.id) expect(rSmall).toEqual({ success: true, amount: 45, remaining: 0 }) expect(engine.gold).toBe(45) expect(engine.groundItems.get(pSmall.id)).toBeUndefined() // Reset to empty, exact max pickup engine.bag.gold = 0 const pExact = engine.dropGold(maxCap, 200, 200)! const rExact = engine.pickupGold(pExact.id) expect(rExact).toEqual({ success: true, amount: maxCap, remaining: 0 }) expect(engine.gold).toBe(maxCap) expect(engine.groundItems.get(pExact.id)).toBeUndefined() // Reset to empty, oversized pickup (60,000 -> picks 50,000, leaves 10,000) engine.bag.gold = 0 const pOver = engine.dropGold(60_000, 200, 200)! const rOver = engine.pickupGold(pOver.id) expect(rOver).toEqual({ success: true, amount: maxCap, remaining: 10_000 }) expect(engine.gold).toBe(maxCap) expect(engine.groundItems.get(pOver.id)?.amount).toBe(10_000) } // Case B: Half-full inventory (25,000 / 50,000) { const engine = createTestEngine(level, 25_000) // Small pickup (500) const pSmall = engine.dropGold(500, 200, 200)! expect(engine.pickupGold(pSmall.id)).toEqual({ success: true, amount: 500, remaining: 0 }) expect(engine.gold).toBe(25_500) // Exact remaining pickup (24,500) const pExact = engine.dropGold(24_500, 200, 200)! expect(engine.pickupGold(pExact.id)).toEqual({ success: true, amount: 24_500, remaining: 0 }) expect(engine.gold).toBe(maxCap) // Reset to half-full, oversized pickup (25,000 + 25,049 -> picks 25,000, leaves 49) engine.bag.gold = 25_000 const pOver = engine.dropGold(25_049, 200, 200)! expect(engine.pickupGold(pOver.id)).toEqual({ success: true, amount: 25_000, remaining: 49 }) expect(engine.gold).toBe(maxCap) expect(engine.groundItems.get(pOver.id)?.amount).toBe(49) } // Case C: 1 gold below max (49,999 / 50,000) { const engine = createTestEngine(level, maxCap - 1) // Exact 1 gold pickup const pOne = engine.dropGold(1, 200, 200)! expect(engine.pickupGold(pOne.id)).toEqual({ success: true, amount: 1, remaining: 0 }) expect(engine.gold).toBe(maxCap) expect(engine.groundItems.get(pOne.id)).toBeUndefined() // Reset to 1 below max, oversized 500 gold pile -> picks 1, leaves 499 (shifts Tier 2 -> Tier 1!) engine.bag.gold = maxCap - 1 const p500 = engine.dropGold(500, 200, 200)! expect(resolveGroundItemSpriteRect(p500)).toEqual(BAKED_UI_MANIFEST.flippyRects['flpgld_2']) const r500 = engine.pickupGold(p500.id) expect(r500).toEqual({ success: true, amount: 1, remaining: 499 }) expect(engine.gold).toBe(maxCap) expect(resolveGroundItemSpriteRect(p500)).toEqual(BAKED_UI_MANIFEST.flippyRects['flpgld_1']) } // Case D: Exactly at max capacity (50,000 / 50,000) { const engine = createTestEngine(level, maxCap) const pAny = engine.dropGold(1, 200, 200)! const rAny = engine.pickupGold(pAny.id) expect(rAny).toEqual({ success: false, amount: 0, remaining: 1 }) expect(engine.gold).toBe(maxCap) expect(engine.groundItems.get(pAny.id)?.amount).toBe(1) } }) it('1.3: verifies 3-tier visual transitions across exact tier boundaries (500 -> 499 -> 50 -> 49 -> 1)', () => { const flippy = BAKED_UI_MANIFEST.flippyRects const engine = createTestEngine(1, 9500) // 500 space left // Drop 1000 gold (Tier 2: >= 500) const pile = engine.dropGold(1000, 200, 200)! expect(resolveGroundItemSpriteRect(pile)).toEqual(flippy['flpgld_2']) // Pick up 500 -> 500 left (still Tier 2: exactly 500) engine.pickupGold(pile.id) expect(pile.amount).toBe(500) expect(resolveGroundItemSpriteRect(pile)).toEqual(flippy['flpgld_2']) // Free 1 gold capacity -> pick up 1 -> 499 left (Tier 1: 50..499) engine.bag.gold = 9999 engine.pickupGold(pile.id) expect(pile.amount).toBe(499) expect(resolveGroundItemSpriteRect(pile)).toEqual(flippy['flpgld_1']) // Free 449 gold capacity -> pick up 449 -> 50 left (still Tier 1: exactly 50) engine.bag.gold = 10_000 - 449 engine.pickupGold(pile.id) expect(pile.amount).toBe(50) expect(resolveGroundItemSpriteRect(pile)).toEqual(flippy['flpgld_1']) // Free 1 gold capacity -> pick up 1 -> 49 left (Tier 0: 1..49) engine.bag.gold = 9999 engine.pickupGold(pile.id) expect(pile.amount).toBe(49) expect(resolveGroundItemSpriteRect(pile)).toEqual(flippy['flpgld_0']) // Free 48 gold capacity -> pick up 48 -> 1 left (still Tier 0: 1) engine.bag.gold = 10_000 - 48 engine.pickupGold(pile.id) expect(pile.amount).toBe(1) expect(resolveGroundItemSpriteRect(pile)).toEqual(flippy['flpgld_0']) // Free 1 gold capacity -> pick up final 1 -> despawned engine.bag.gold = 9999 const finalRes = engine.pickupGold(pile.id) expect(finalRes).toEqual({ success: true, amount: 1, remaining: 0 }) expect(engine.groundItems.get(pile.id)).toBeUndefined() expect(engine.ground.length).toBe(0) }) it('1.4: walkover auto-pickup stops mid-loop when multiple piles exceed capacity', () => { const engine = createTestEngine(1, 9900, { pickupRadius: 150 }) // 100 capacity remaining, reach adjacent scattered tiles const p1 = engine.dropGold(60, 500, 500)! const p2 = engine.dropGold(80, 500, 500)! const p3 = engine.dropGold(200, 500, 500)! engine.tick(emptyInput) // Player must reach exactly 10,000 gold expect(engine.gold).toBe(10_000) // p1 (60) was fully picked up; p2 (80) had 40 picked up leaving 40; p3 (200) untouched expect(engine.groundItems.get(p1.id)).toBeUndefined() expect(engine.groundItems.get(p2.id)?.amount).toBe(40) expect(engine.groundItems.get(p3.id)?.amount).toBe(200) expect(engine.groundItems.count).toBe(2) }) }) describe('2. Death Animation Gating Fuzzing', () => { it('2.1: handles 0-frame death animation (deathTicks = 0, deathGated = false) by dropping immediately', () => { const engine = createTestEngine() const monster = createTestMonster(0, 'zombie1', 520, 500, 20) monster.deathTicks = 0 monster.deathGated = false engine.world.monsters.push(monster) engine.damageMonster(0, 999) engine.tick(emptyInput) expect(monster.dropRolled).toBe(true) expect(monster.animMode).toBe('dd') expect(monster.pendingDrop).toBeUndefined() expect(engine.metrics.dropsRolled).toBe(1) }) it('2.2: handles long-death animation (50 ticks) with zero premature spawns', () => { const engine = createTestEngine() const monster = createTestMonster(0, 'fallen1', 520, 500, 20) monster.deathTicks = 50 engine.world.monsters.push(monster) engine.damageMonster(0, 999) for (let t = 1; t < 50; t++) { engine.tick(emptyInput) expect(monster.dropRolled).toBe(false) expect(monster.animMode).toBe('dt') expect(monster.pendingDrop).toBeDefined() expect(engine.groundItems.count).toBe(0) } // 50th tick completes DT -> transitions to DD and rolls drop engine.tick(emptyInput) expect(monster.deathTicks).toBe(0) expect(monster.dropRolled).toBe(true) expect(monster.animMode).toBe('dd') expect(monster.pendingDrop).toBeUndefined() expect(engine.metrics.dropsRolled).toBe(1) expect(engine.groundItems.count).toBeGreaterThanOrEqual(1) }) it('2.3: stress-tests 20 simultaneous monster deaths with heterogeneous death durations and zero memory leaks', () => { const engine = createTestEngine(1, 0, { pickupRadius: 0 }) // disable auto-pickup so all gold stays on ground const monsterCount = 20 for (let i = 0; i < monsterCount; i++) { const m = createTestMonster(i, 'skeleton1', 600 + (i % 5) * 40, 600 + Math.floor(i / 5) * 40, 10, 'unique', 10) // Mix of instant (undefined), 0-tick, 1-tick, 5-tick, 12-tick, and explicit deathGated if (i % 5 === 0) { m.deathTicks = undefined } else if (i % 5 === 1) { m.deathTicks = 0 } else if (i % 5 === 2) { m.deathTicks = 1 } else if (i % 5 === 3) { m.deathTicks = 5 } else { m.deathGated = true } engine.world.monsters.push(m) } // Kill all 20 monsters in the same frame for (let i = 0; i < monsterCount; i++) { expect(engine.damageMonster(i, 999)).toBe(true) } // Tick 1: instant (i%5===0), 0-tick (i%5===1), and 1-tick (i%5===2) monsters finish (12 monsters) engine.tick(emptyInput) expect(engine.metrics.dropsRolled).toBe(12) // Ticks 2..4: 5-tick monsters still in DT for (let t = 2; t <= 4; t++) { engine.tick(emptyInput) expect(engine.metrics.dropsRolled).toBe(12) } // Tick 5: 5-tick monsters (i%5===3) finish (16 monsters total) engine.tick(emptyInput) expect(engine.metrics.dropsRolled).toBe(16) // Explicitly gated monsters (i%5===4) must still be waiting in DT const gatedMonsters = engine.world.monsters.filter((_, i) => i % 5 === 4) for (const gm of gatedMonsters) { expect(gm.dropRolled).toBe(false) expect(gm.pendingDrop).toBeDefined() engine.triggerMonsterDrop(gm) } // All 20 monsters must have completed drops with zero retained pendingDrop closures expect(engine.metrics.dropsRolled).toBe(20) for (const m of engine.world.monsters) { expect(m.dropRolled).toBe(true) expect(m.animMode).toBe('dd') expect(m.pendingDrop).toBeUndefined() } }) }) describe('3. Drop SFX & Bounce Audio Fuzzing', () => { it('3.1: handles undefined dropsound, empty string dropsound, and non-standard dropsfxframe (0, negative, huge, NaN)', () => { const manager = new GroundItemManager() const startTime = 1000 const played: string[] = [] const audioMock = { playSfx: vi.fn((s: string) => played.push(s)), } // 1. Undefined dropsound -> falls back to 'item_drop' const eUndefined = manager.add({ name: 'Mystery' }, 0, 0, 0, 0, { bounce: true, now: startTime, durationMs: 400 }) ;(eUndefined as any).dropSound = undefined // 2. Frame 0 -> triggers immediately at startTime const eFrame0 = manager.add( { name: 'ZeroFrame', base: { dropsound: 'sfx_zero', dropsfxframe: 0 } }, 1, 0, 16, 0, { bounce: true, now: startTime, durationMs: 400 }, ) ;(eFrame0 as any).dropSfxFrame = 0 // 3. Negative frame (-10) -> triggers immediately at startTime const eNeg = manager.add( { name: 'NegFrame', base: { dropsound: 'sfx_neg', dropsfxframe: -10 } }, 2, 0, 32, 0, { bounce: true, now: startTime, durationMs: 400 }, ) // 4. Huge frame (999) -> triggers on landing at startTime + 400 const eHuge = manager.add( { name: 'HugeFrame', base: { dropsound: 'sfx_huge', dropsfxframe: 999 } }, 3, 0, 48, 0, { bounce: true, now: startTime, durationMs: 400 }, ) // 5. NaN frame -> triggers on landing at startTime + 400 const eNaN = manager.add( { name: 'NaNFrame', base: { dropsound: 'sfx_nan', dropsfxframe: Number.NaN } }, 4, 0, 64, 0, { bounce: true, now: startTime, durationMs: 400 }, ) // Tick at elapsed = 0ms: frame 0 and negative frame trigger immediately manager.updateBounces(startTime, audioMock) expect(eFrame0.sfxPlayed).toBe(true) expect(eNeg.sfxPlayed).toBe(true) expect(eUndefined.sfxPlayed).toBe(false) expect(eHuge.sfxPlayed).toBe(false) expect(eNaN.sfxPlayed).toBe(false) // Rapid 1ms bounce ticks from startTime + 1 to startTime + 500 for (let t = 1; t <= 500; t += 5) { manager.updateBounces(startTime + t, audioMock) } // Every single item must have played its SFX exactly once expect(eUndefined.sfxPlayed).toBe(true) expect(eHuge.sfxPlayed).toBe(true) expect(eNaN.sfxPlayed).toBe(true) expect(audioMock.playSfx).toHaveBeenCalledTimes(5) expect(played).toContain('item_drop') expect(played).toContain('sfx_zero') expect(played).toContain('sfx_neg') expect(played).toContain('sfx_huge') expect(played).toContain('sfx_nan') }) it('3.2: survives broken audioManager objects and re-triggers SFX on partial gold pickup bounce', () => { const manager = new GroundItemManager() const startTime = 2000 const goldEntity = manager.add( { isGold: true, amount: 500, code: 'gld' }, 5, 5, 100, 100, { bounce: true, now: startTime, durationMs: 400 }, ) // Broken audioManager without playSfx function expect(() => manager.updateBounces(startTime + 480, {} as any)).not.toThrow() expect(goldEntity.sfxPlayed).toBe(true) // Trigger flippy bounce again (as happens during partial gold pickup) triggerFlippyBounce(goldEntity, startTime + 500, { durationMs: 350 }) expect(goldEntity.sfxPlayed).toBe(false) const validAudio = { playSfx: vi.fn() } manager.updateBounces(startTime + 500 + 350, validAudio) expect(validAudio.playSfx).toHaveBeenCalledTimes(1) expect(validAudio.playSfx).toHaveBeenCalledWith('item_gold') }) }) describe('4. Collision Grid Quadrant Spiral Stress', () => { it('4.1: 1-cell wide zigzag corridor surrounded by walls & void: 10 items drop with 0 blocked placements', () => { const width = 15 const height = 15 const masks = new Uint16Array(width * height).fill(COLLIDE_WALL | COLLIDE_BLANK) // Carve a narrow 1-cell wide L-shaped corridor through (7, 4..10) and (4..10, 7) for (let i = 4; i <= 10; i++) { masks[7 * width + i] = COLLIDE_NONE masks[i * width + 7] = COLLIDE_NONE } const grid = { width, height, collisionMasks: masks } const occupied: { cellX: number; cellY: number }[] = [] for (let i = 0; i < 10; i++) { const pos = findSafeDropPosition(grid, 7, 7, 4, occupied) occupied.push(pos) const cellMask = masks[pos.cellY * width + pos.cellX] expect(cellMask).toBe(COLLIDE_NONE) } // All 10 items must occupy distinct walkable cells along the cross corridor const uniqueCells = new Set(occupied.map(p => `${p.cellX},${p.cellY}`)) expect(uniqueCells.size).toBe(10) }) it('4.2: single-cell walkable island surrounded by COLLIDE_WALL / COLLIDE_MASK_INVALID: 20 burst drops never escape into walls', () => { const cellsX = 9 const cellsY = 9 const gridWidth = cellsX * 5 const blocked = new Uint8Array(gridWidth * cellsY * 5).fill(1) const collisionMasks = new Uint16Array(gridWidth * cellsY * 5).fill(COLLIDE_WALL | COLLIDE_MASK_INVALID) // Only cell (4, 4) center sub-tile is walkable floor const walkableSubIdx = (4 * 5 + 2) * gridWidth + (4 * 5 + 2) blocked[walkableSubIdx] = 0 collisionMasks[walkableSubIdx] = COLLIDE_NONE const collisionGrid: CollisionGrid = { cellsX, cellsY, gridWidth, originX: 0, originY: 0, blocked, collisionMasks, } const occupied: { cellX: number; cellY: number }[] = [] for (let i = 0; i < 20; i++) { // Test both dropping directly on (4, 4) and dropping from an adjacent wall (3, 4) const originX = i % 2 === 0 ? 4 : 3 const pos = findSafeDropPosition(collisionGrid, originX, 4, 3, occupied) occupied.push(pos) expect(pos.cellX).toBe(4) expect(pos.cellY).toBe(4) } }) it('4.3: massive 25-item burst drop in open room places all 25 items on unique walkable coordinates', () => { const width = 20 const height = 20 const masks = new Uint16Array(width * height).fill(COLLIDE_NONE) const grid = { width, height, collisionMasks: masks } const occupied: { cellX: number; cellY: number }[] = [] for (let i = 0; i < 25; i++) { const pos = findSafeDropPosition(grid, 10, 10, 4, occupied) occupied.push(pos) expect(masks[pos.cellY * width + pos.cellX]).toBe(COLLIDE_NONE) } const uniqueCells = new Set(occupied.map(p => `${p.cellX},${p.cellY}`)) expect(uniqueCells.size).toBe(25) }) it('4.4: 25-item burst drop in constrained 3x3 room fills all 9 walkable cells then safely stacks on walkable floor', () => { const width = 11 const height = 11 const masks = new Uint16Array(width * height).fill(COLLIDE_WALL) // 3x3 walkable room at x=4..6, y=4..6 for (let y = 4; y <= 6; y++) { for (let x = 4; x <= 6; x++) { masks[y * width + x] = COLLIDE_NONE } } const grid = { width, height, collisionMasks: masks } const occupied: { cellX: number; cellY: number }[] = [] for (let i = 0; i < 25; i++) { const pos = findSafeDropPosition(grid, 5, 5, 4, occupied) occupied.push(pos) // Every single item must be within [4..6, 4..6] walkable room expect(pos.cellX).toBeGreaterThanOrEqual(4) expect(pos.cellX).toBeLessThanOrEqual(6) expect(pos.cellY).toBeGreaterThanOrEqual(4) expect(pos.cellY).toBeLessThanOrEqual(6) expect(masks[pos.cellY * width + pos.cellX]).toBe(COLLIDE_NONE) } // All 9 walkable cells must have been utilized before stacking const uniqueCells = new Set(occupied.map(p => `${p.cellX},${p.cellY}`)) expect(uniqueCells.size).toBe(9) }) }) })