diablo2-web/tests/challenger-m4-adversarial.t...

244 lines
10 KiB
TypeScript

/**
* 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 { ActorAnimator } from '../src/client/render/actor-animator.ts'
import {
BeltHud,
BELT_COLS,
BELT_ROWS,
BELT_POTION_CATALOG,
areBeltPotionsCompatible,
isAutoBeltablePotion,
itemToBeltPotion,
} from '../src/client/ui/belt.ts'
import { resolveGroundItemSpriteRect, itemToUiInventoryItem } from '../src/client/ui/inventory.ts'
import { BAKED_UI_MANIFEST } from '../src/common/items/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/common/items/ground-items.ts'
import { GameEngine } from '../src/server/engine/engine.ts'
import { getEmbeddedDropTables } from '../src/common/items/embedded-drop-tables.ts'
import { DEMO_EXPERIENCE, DEMO_SKILLS, DEMO_QUESTS } from '../src/common/data/demo-data.ts'
import type { SpriteRenderer } from '../src/client/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('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')
})
})
// ============================================================================
// 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)
})
})
})