619 lines
29 KiB
TypeScript
619 lines
29 KiB
TypeScript
/**
|
|
* Comprehensive Empirical Stress-Testing Harness for Milestone 1
|
|
* Ghost Interaction Decoupling & Combat Safety (Requirement R1)
|
|
*
|
|
* Playbook: solution_stress_testing (SKILL.md)
|
|
* Archetype: EMPIRICAL CHALLENGER
|
|
*/
|
|
|
|
import { KeyboardInput } from '../src/sim/input.ts'
|
|
import { HudManager } from '../src/ui/hud-manager.ts'
|
|
import { GameEngine, type GameEngineOptions, type WorldMapProvider } from '../src/game/engine.ts'
|
|
|
|
// Mock DOM Environment for Node
|
|
class MockWindow extends EventTarget {
|
|
innerWidth = 1920
|
|
innerHeight = 1080
|
|
devicePixelRatio = 1
|
|
}
|
|
|
|
class MockCanvas extends EventTarget {
|
|
clientWidth = 1920
|
|
clientHeight = 1080
|
|
width = 1920
|
|
height = 1080
|
|
style = { pointerEvents: 'none' }
|
|
getBoundingClientRect() {
|
|
return { left: 0, top: 0, width: 1920, height: 1080 }
|
|
}
|
|
getContext() {
|
|
return null
|
|
}
|
|
}
|
|
|
|
class SyntheticKeyboardEvent extends Event {
|
|
readonly code: string
|
|
readonly key: string
|
|
|
|
constructor(type: string, init: { code: string; key: string }) {
|
|
super(type, { cancelable: true })
|
|
this.code = init.code
|
|
this.key = init.key
|
|
}
|
|
}
|
|
|
|
// Global DOM shims for HudManager
|
|
const mockWin = new MockWindow()
|
|
// @ts-ignore
|
|
globalThis.window = mockWin
|
|
// @ts-ignore
|
|
globalThis.HTMLInputElement = class HTMLInputElement {}
|
|
// @ts-ignore
|
|
globalThis.HTMLSelectElement = class HTMLSelectElement {}
|
|
|
|
let totalAssertions = 0
|
|
let passedAssertions = 0
|
|
let failedAssertions = 0
|
|
|
|
function assert(condition: boolean, message: string): void {
|
|
totalAssertions++
|
|
if (condition) {
|
|
passedAssertions++
|
|
} else {
|
|
failedAssertions++
|
|
console.error(`❌ ASSERTION FAILED: ${message}`)
|
|
throw new Error(`Assertion failed: ${message}`)
|
|
}
|
|
}
|
|
|
|
async function runTestSuite(): Promise<void> {
|
|
console.log('=================================================================')
|
|
console.log('🚀 RUNNING EMPIRICAL CHALLENGER STRESS-TEST HARNESS (M1 / R1)')
|
|
console.log('=================================================================')
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// SUITE 1: Ghost Interaction Decoupling: KeyT & KeyB
|
|
// -----------------------------------------------------------------------------
|
|
console.log('\n--- [Suite 1] Ghost Interaction Decoupling: KeyT & KeyB ---')
|
|
{
|
|
const win = new MockWindow()
|
|
const input = new KeyboardInput(win as unknown as Window)
|
|
input.attach()
|
|
|
|
// 1.1 Initial state
|
|
assert(input.talking === false, 'Initial input.talking must be false')
|
|
assert(input.takeWaypoint() === false, 'Initial input.takeWaypoint() must be false')
|
|
|
|
// 1.2 KeyT dispatch (single, held, repeated)
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'KeyT', key: 't' }))
|
|
assert(input.talking === false, 'KeyT down must NOT set input.talking to true')
|
|
|
|
for (let tick = 0; tick < 100; tick++) {
|
|
assert(input.talking === false, `input.talking must remain false across 100 ticks of KeyT held (tick ${tick})`)
|
|
}
|
|
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'KeyT', key: 't' })) // repeat
|
|
assert(input.talking === false, 'Repeated KeyT keydown must NOT set input.talking')
|
|
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', { code: 'KeyT', key: 't' }))
|
|
assert(input.talking === false, 'KeyT keyup must leave input.talking false')
|
|
|
|
// 1.3 KeyB dispatch (single, held, repeated)
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'KeyB', key: 'b' }))
|
|
assert(input.takeWaypoint() === false, 'KeyB down must NOT trigger input.takeWaypoint()')
|
|
|
|
for (let tick = 0; tick < 100; tick++) {
|
|
assert(input.takeWaypoint() === false, `input.takeWaypoint() must remain false across 100 ticks of KeyB held (tick ${tick})`)
|
|
}
|
|
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'KeyB', key: 'b' }))
|
|
assert(input.takeWaypoint() === false, 'Repeated KeyB keydown must NOT trigger input.takeWaypoint()')
|
|
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', { code: 'KeyB', key: 'b' }))
|
|
assert(input.takeWaypoint() === false, 'KeyB keyup must leave input.takeWaypoint() false')
|
|
|
|
// 1.4 Test every printable key on the keyboard to ensure TALK_KEYS and WAYPOINT_KEYS are 100% empty
|
|
const testCodes = [
|
|
'KeyA', 'KeyB', 'KeyC', 'KeyD', 'KeyE', 'KeyF', 'KeyG', 'KeyH', 'KeyI', 'KeyJ',
|
|
'KeyK', 'KeyL', 'KeyM', 'KeyN', 'KeyO', 'KeyP', 'KeyQ', 'KeyR', 'KeyS', 'KeyT',
|
|
'KeyU', 'KeyV', 'KeyW', 'KeyX', 'KeyY', 'KeyZ', 'Digit1', 'Digit2', 'Digit3', 'Digit4',
|
|
'Space', 'Enter', 'Tab', 'Escape', 'Backspace'
|
|
]
|
|
for (const code of testCodes) {
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code, key: code.toLowerCase() }))
|
|
assert(input.talking === false, `Key ${code} must NOT set input.talking to true`)
|
|
assert(input.takeWaypoint() === false, `Key ${code} must NOT trigger input.takeWaypoint()`)
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', { code, key: code.toLowerCase() }))
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// SUITE 2: Combat Safety: Space vs Combat Swings & Panel Dismissal
|
|
// -----------------------------------------------------------------------------
|
|
console.log('\n--- [Suite 2] Combat Safety: Space Removal from ATTACK_KEYS & Panel Dismissal ---')
|
|
{
|
|
const win = new MockWindow()
|
|
const input = new KeyboardInput(win as unknown as Window)
|
|
input.attach()
|
|
|
|
// 2.1 Space does not set input.attacking
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Space', key: ' ' }))
|
|
assert(input.attacking === false, 'Space down must NOT set input.attacking to true')
|
|
|
|
for (let tick = 0; tick < 100; tick++) {
|
|
assert(input.attacking === false, `Space held must remain attacking=false across 100 ticks (tick ${tick})`)
|
|
}
|
|
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', { code: 'Space', key: ' ' }))
|
|
assert(input.attacking === false, 'Space up must remain attacking=false')
|
|
|
|
// 2.2 Legitimate attack keys DO set input.attacking (Positive Control)
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'KeyJ', key: 'j' }))
|
|
assert(input.attacking === true, 'KeyJ down MUST set input.attacking to true')
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', { code: 'KeyJ', key: 'j' }))
|
|
assert(input.attacking === false, 'KeyJ up must clear input.attacking')
|
|
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Enter', key: 'Enter' }))
|
|
assert(input.attacking === true, 'Enter down MUST set input.attacking to true')
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', { code: 'Enter', key: 'Enter' }))
|
|
assert(input.attacking === false, 'Enter up must clear input.attacking')
|
|
|
|
// 2.3 GameEngine combat verification: Space does not swing or damage monsters
|
|
const dummyTerrain: WorldMapProvider = {
|
|
widthPx: 1000,
|
|
heightPx: 1000,
|
|
overlap: () => 0,
|
|
}
|
|
const dummyOpts: GameEngineOptions = {
|
|
spawn: { x: 100, y: 100 },
|
|
stats: [
|
|
{ id: 'mon1', name: 'Demon', hp: 100, damage: 0, cooldownTicks: 100, reach: 20, aggroRadius: 100, speed: 0, xp: 5 }
|
|
],
|
|
xpTable: [0, 0, 10, 30, 60],
|
|
itemBases: [
|
|
{ id: 'sword', name: 'Sword', kind: 'weapon', invWidth: 1, invHeight: 3, maxStack: 1, value: 10, damage: 10, defense: 0, tags: ['weap'], level: 1 }
|
|
],
|
|
prefixAffixes: [],
|
|
suffixAffixes: [],
|
|
skills: [
|
|
{ id: 'attack', name: 'Attack', manaCost: 0, cooldownTicks: 1, range: 48, projectile: false, speed: 0, baseMinDamage: 10, baseMaxDamage: 15, damagePerLevel: 1, radius: 20 },
|
|
],
|
|
npcDefs: [
|
|
{ id: 'akara', name: 'Akara', questId: null, offerLines: [], progressLines: [], doneLines: [] }
|
|
],
|
|
questDefs: [],
|
|
combatOptions: {
|
|
playerSpeed: 100,
|
|
playerReach: 50,
|
|
playerCooldownTicks: 1,
|
|
playerDamage: 10,
|
|
playerManaPerAttack: 0,
|
|
respawnTicks: 100,
|
|
},
|
|
lootSeed: 12345,
|
|
talkRadius: 50,
|
|
pickupRadius: 50,
|
|
inventoryCols: 10,
|
|
inventoryRows: 4,
|
|
}
|
|
|
|
const engine = new GameEngine(dummyTerrain, dummyOpts)
|
|
// Place monster right next to player (player at 100, 100; monster at 110, 100)
|
|
assert(engine.world.monsters.length > 0, 'Engine must have spawned monsters')
|
|
engine.world.monsters[0]!.x = 110
|
|
engine.world.monsters[0]!.y = 100
|
|
const initialMonsterHp = engine.world.monsters[0]!.hp
|
|
const initialHits = engine.metrics.playerHits
|
|
|
|
// Tick engine with Space held via KeyboardInput
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Space', key: ' ' }))
|
|
for (let tick = 0; tick < 50; tick++) {
|
|
engine.tick({
|
|
movement: input.movement(),
|
|
attacking: input.attacking,
|
|
pickingUp: input.pickingUp,
|
|
talking: input.talking,
|
|
digits: input.digits,
|
|
saving: input.saving,
|
|
loading: input.loading,
|
|
})
|
|
}
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', { code: 'Space', key: ' ' }))
|
|
|
|
assert(engine.world.monsters[0]!.hp === initialMonsterHp, `Space key MUST NOT cause damage to monster (HP was ${initialMonsterHp}, now ${engine.world.monsters[0]!.hp})`)
|
|
assert(engine.metrics.playerHits === initialHits, `Space key MUST NOT register combat hits (hits was ${initialHits}, now ${engine.metrics.playerHits})`)
|
|
|
|
// Compare with KeyJ: ticking with KeyJ MUST damage monster
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'KeyJ', key: 'j' }))
|
|
for (let tick = 0; tick < 10; tick++) {
|
|
engine.tick({
|
|
movement: input.movement(),
|
|
attacking: input.attacking,
|
|
pickingUp: input.pickingUp,
|
|
talking: input.talking,
|
|
digits: input.digits,
|
|
saving: input.saving,
|
|
loading: input.loading,
|
|
})
|
|
}
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', { code: 'KeyJ', key: 'j' }))
|
|
|
|
assert(engine.world.monsters[0]!.hp < initialMonsterHp, 'KeyJ attack MUST inflict damage on adjacent monster (Positive Control)')
|
|
assert(engine.metrics.playerHits > initialHits, 'KeyJ attack MUST register combat hits in metrics (Positive Control)')
|
|
|
|
// 2.4 Panel Dismissal in HudManager: Space and Escape
|
|
const hudCanvas = new MockCanvas() as unknown as HTMLCanvasElement
|
|
const hud = new HudManager(hudCanvas, {
|
|
onToggleAutomap: () => {},
|
|
onWaypointTeleport: () => {},
|
|
})
|
|
hud['bindEvents']()
|
|
|
|
// Test Space closing Character Sheet
|
|
hud.toggleLeftPanel('char')
|
|
assert(hud.leftPanel === 'char', 'Left panel should be char')
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Space', key: ' ' }))
|
|
assert(hud.leftPanel === 'none', 'Space MUST close Character Sheet')
|
|
|
|
// Test Space closing Inventory Panel
|
|
hud.toggleRightPanel('inv')
|
|
assert(hud.rightPanel === 'inv', 'Right panel should be inv')
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Space', key: ' ' }))
|
|
assert(hud.rightPanel === 'none', 'Space MUST close Inventory Panel')
|
|
|
|
// Test Space closing Skill Tree Panel
|
|
hud.toggleRightPanel('skill')
|
|
assert(hud.rightPanel === 'skill', 'Right panel should be skill')
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Space', key: ' ' }))
|
|
assert(hud.rightPanel === 'none', 'Space MUST close Skill Tree Panel')
|
|
|
|
// Test Space closing both panels + belt + popup concurrently
|
|
hud.toggleLeftPanel('quest')
|
|
hud.toggleRightPanel('skill')
|
|
hud.belt.lockedOpen = true
|
|
hud.hotkeys.openPopup = 'right'
|
|
assert(hud.leftPanel === 'quest' && hud.rightPanel === 'skill', 'Panels should be open')
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Space', key: ' ' }))
|
|
assert(hud.leftPanel === 'none', 'Space MUST close leftPanel')
|
|
assert(hud.rightPanel === 'none', 'Space MUST close rightPanel')
|
|
assert((hud.belt.lockedOpen as boolean) === false, 'Space MUST close lockedOpen belt')
|
|
assert(hud.hotkeys.openPopup === null, 'Space MUST close hotkey popup')
|
|
|
|
// Test Escape parity with Space
|
|
hud.toggleLeftPanel('waypoint')
|
|
hud.toggleRightPanel('inv')
|
|
assert(hud.leftPanel === 'waypoint' && hud.rightPanel === 'inv', 'Panels open before Escape')
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Escape', key: 'Escape' }))
|
|
assert(hud.leftPanel === 'none', 'Escape MUST close leftPanel')
|
|
assert(hud.rightPanel === 'none', 'Escape MUST close rightPanel')
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// SUITE 3: Belt Potion Drinking & Skill Preservation (Keys 1..4)
|
|
// -----------------------------------------------------------------------------
|
|
console.log('\n--- [Suite 3] Belt Drinking & Skill Preservation (Keys 1..4) ---')
|
|
{
|
|
const win = new MockWindow()
|
|
const input = new KeyboardInput(win as unknown as Window)
|
|
input.attach()
|
|
|
|
// 3.1 Digit keys on KeyboardInput return []
|
|
for (const code of ['Digit1', 'Digit2', 'Digit3', 'Digit4']) {
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code, key: code.slice(-1) }))
|
|
assert(input.digits.length === 0, `input.digits MUST return [] when ${code} is pressed`)
|
|
}
|
|
assert(input.digits.length === 0, 'input.digits must be [] when all 1..4 are held')
|
|
for (const code of ['Digit1', 'Digit2', 'Digit3', 'Digit4']) {
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', { code, key: code.slice(-1) }))
|
|
}
|
|
|
|
// 3.2 GameEngine skill selection invariance
|
|
const dummyTerrain: WorldMapProvider = { widthPx: 1000, heightPx: 1000, overlap: () => 0 }
|
|
const dummyOpts: GameEngineOptions = {
|
|
spawn: { x: 100, y: 100 },
|
|
stats: [],
|
|
xpTable: [0, 0, 10, 30, 60],
|
|
itemBases: [],
|
|
prefixAffixes: [],
|
|
suffixAffixes: [],
|
|
skills: [
|
|
{ id: 'sk0', name: 'Skill 0', manaCost: 0, cooldownTicks: 1, range: 48, projectile: false, speed: 0, baseMinDamage: 1, baseMaxDamage: 2, damagePerLevel: 1, radius: 20 },
|
|
{ id: 'sk1', name: 'Skill 1', manaCost: 0, cooldownTicks: 1, range: 48, projectile: false, speed: 0, baseMinDamage: 1, baseMaxDamage: 2, damagePerLevel: 1, radius: 20 },
|
|
{ id: 'sk2', name: 'Skill 2', manaCost: 0, cooldownTicks: 1, range: 48, projectile: false, speed: 0, baseMinDamage: 1, baseMaxDamage: 2, damagePerLevel: 1, radius: 20 },
|
|
{ id: 'sk3', name: 'Skill 3', manaCost: 0, cooldownTicks: 1, range: 48, projectile: false, speed: 0, baseMinDamage: 1, baseMaxDamage: 2, damagePerLevel: 1, radius: 20 },
|
|
],
|
|
npcDefs: [],
|
|
questDefs: [],
|
|
combatOptions: { playerSpeed: 100, playerReach: 50, playerCooldownTicks: 1, playerDamage: 10, playerManaPerAttack: 0, respawnTicks: 100 },
|
|
lootSeed: 12345,
|
|
talkRadius: 50,
|
|
pickupRadius: 50,
|
|
inventoryCols: 10,
|
|
inventoryRows: 4,
|
|
}
|
|
const engine = new GameEngine(dummyTerrain, dummyOpts)
|
|
engine.selectedSkill = 2 // Choose Skill 2
|
|
assert(engine.selectedSkill === 2, 'Initial selectedSkill should be 2')
|
|
|
|
// Tick engine with Digit1..Digit4 pressed
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Digit1', key: '1' }))
|
|
engine.tick({
|
|
movement: input.movement(),
|
|
attacking: input.attacking,
|
|
pickingUp: input.pickingUp,
|
|
talking: input.talking,
|
|
digits: input.digits,
|
|
saving: input.saving,
|
|
loading: input.loading,
|
|
})
|
|
assert(engine.selectedSkill === 2, 'engine.selectedSkill MUST remain 2 after Digit1 tick')
|
|
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Digit4', key: '4' }))
|
|
engine.tick({
|
|
movement: input.movement(),
|
|
attacking: input.attacking,
|
|
pickingUp: input.pickingUp,
|
|
talking: input.talking,
|
|
digits: input.digits,
|
|
saving: input.saving,
|
|
loading: input.loading,
|
|
})
|
|
assert(engine.selectedSkill === 2, 'engine.selectedSkill MUST remain 2 after Digit4 tick')
|
|
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', { code: 'Digit1', key: '1' }))
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', { code: 'Digit4', key: '4' }))
|
|
|
|
// 3.3 HudManager Potion Drinking & Skill Preservation
|
|
const hudCanvas = new MockCanvas() as unknown as HTMLCanvasElement
|
|
const hud = new HudManager(hudCanvas, {
|
|
onToggleAutomap: () => {},
|
|
onWaypointTeleport: () => {},
|
|
})
|
|
hud['bindEvents']()
|
|
|
|
// Configure distinct active skills: Left = 47 (Fire Ball), Right = 64 (Frozen Orb)
|
|
hud.hotkeys.leftSkillId = 47
|
|
hud.hotkeys.rightSkillId = 64
|
|
hud.syncPublishedState()
|
|
assert(hud.state.leftSkillId === 47, 'Initial published leftSkillId should be 47')
|
|
assert(hud.state.rightSkillId === 64, 'Initial published rightSkillId should be 64')
|
|
|
|
// Lower HP and Mana to verify drinking effects
|
|
hud.hp = 300
|
|
hud.mana = 200
|
|
hud.syncPublishedState()
|
|
|
|
const initialTotalPotions = hud.belt.countTotalPotions()
|
|
assert(initialTotalPotions === 16, 'Full belt should have 16 potions (4 cols x 4 rows)')
|
|
|
|
// --- Drink Column 0 (Key '1') ---
|
|
const hpBefore1 = hud.hp
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Digit1', key: '1' }))
|
|
assert(hud.hp > hpBefore1, `Key 1 MUST consume health potion and heal HP (was ${hpBefore1}, now ${hud.hp})`)
|
|
assert(hud.hotkeys.leftSkillId === 47, 'Key 1 MUST NOT alter hotkeys.leftSkillId (remains 47)')
|
|
assert(hud.hotkeys.rightSkillId === 64, 'Key 1 MUST NOT alter hotkeys.rightSkillId (remains 64)')
|
|
assert(hud.state.leftSkillId === 47, 'Key 1 MUST NOT alter state.leftSkillId (remains 47)')
|
|
assert(hud.state.rightSkillId === 64, 'Key 1 MUST NOT alter state.rightSkillId (remains 64)')
|
|
assert(hud.belt.countTotalPotions() === initialTotalPotions - 1, 'Belt potion count must decrement by 1')
|
|
|
|
// --- Drink Column 1 (Key '2') ---
|
|
const hpBefore2 = hud.hp
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Digit2', key: '2' }))
|
|
assert(hud.hp >= hpBefore2, `Key 2 MUST consume column 1 potion (was ${hpBefore2}, now ${hud.hp})`)
|
|
assert(hud.hotkeys.leftSkillId === 47, 'Key 2 MUST NOT alter leftSkillId')
|
|
assert(hud.hotkeys.rightSkillId === 64, 'Key 2 MUST NOT alter rightSkillId')
|
|
assert(hud.state.leftSkillId === 47, 'Key 2 MUST NOT alter state leftSkillId')
|
|
assert(hud.state.rightSkillId === 64, 'Key 2 MUST NOT alter state rightSkillId')
|
|
assert(hud.belt.countTotalPotions() === initialTotalPotions - 2, 'Belt potion count must decrement by 2')
|
|
|
|
// --- Drink Column 2 (Key '3') ---
|
|
const manaBefore3 = hud.mana
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Digit3', key: '3' }))
|
|
assert(hud.mana > manaBefore3, `Key 3 MUST consume mana potion and restore Mana (was ${manaBefore3}, now ${hud.mana})`)
|
|
assert(hud.hotkeys.leftSkillId === 47, 'Key 3 MUST NOT alter leftSkillId')
|
|
assert(hud.hotkeys.rightSkillId === 64, 'Key 3 MUST NOT alter rightSkillId')
|
|
assert(hud.state.leftSkillId === 47, 'Key 3 MUST NOT alter state leftSkillId')
|
|
assert(hud.state.rightSkillId === 64, 'Key 3 MUST NOT alter state rightSkillId')
|
|
assert(hud.belt.countTotalPotions() === initialTotalPotions - 3, 'Belt potion count must decrement by 3')
|
|
|
|
// --- Drink Column 3 (Key '4') ---
|
|
hud.hp = 500
|
|
hud.mana = 200
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: 'Digit4', key: '4' }))
|
|
assert(hud.hp === 500 + 400 && hud.mana === 200 + 400, `Key 4 MUST consume rejuv potion and heal 400 HP & Mana (hp: ${hud.hp}, mana: ${hud.mana})`)
|
|
assert(hud.hotkeys.leftSkillId === 47, 'Key 4 MUST NOT alter leftSkillId')
|
|
assert(hud.hotkeys.rightSkillId === 64, 'Key 4 MUST NOT alter rightSkillId')
|
|
assert(hud.state.leftSkillId === 47, 'Key 4 MUST NOT alter state leftSkillId')
|
|
assert(hud.state.rightSkillId === 64, 'Key 4 MUST NOT alter state rightSkillId')
|
|
assert(hud.belt.countTotalPotions() === initialTotalPotions - 4, 'Belt potion count must decrement by 4')
|
|
|
|
// 3.4 Exhaust remaining belt slots (drink all 12 remaining potions)
|
|
for (let c = 0; c < 4; c++) {
|
|
for (let r = 0; r < 3; r++) {
|
|
hud.drinkBeltSlot(c)
|
|
}
|
|
}
|
|
assert(hud.belt.countTotalPotions() === 0, 'Belt should now be completely empty (0 potions)')
|
|
|
|
// Adversarial test: pressing 1..4 on empty belt
|
|
for (const key of ['1', '2', '3', '4']) {
|
|
const consumed = hud.drinkBeltSlot(Number(key) - 1)
|
|
assert(consumed === false, `Drinking from empty slot column ${Number(key) - 1} must return false`)
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keydown', { code: `Digit${key}`, key }))
|
|
assert(hud.hotkeys.leftSkillId === 47, `Empty belt press ${key} must NOT alter leftSkillId`)
|
|
assert(hud.hotkeys.rightSkillId === 64, `Empty belt press ${key} must NOT alter rightSkillId`)
|
|
assert(!Number.isNaN(hud.hp) && !Number.isNaN(hud.mana), `Empty belt press ${key} must NOT cause NaN`)
|
|
}
|
|
|
|
// Boundary column tests: out-of-range columns
|
|
assert(hud.drinkBeltSlot(-1) === false, 'drinkBeltSlot(-1) must return false')
|
|
assert(hud.drinkBeltSlot(4) === false, 'drinkBeltSlot(4) must return false')
|
|
assert(hud.drinkBeltSlot(100) === false, 'drinkBeltSlot(100) must return false')
|
|
assert(hud.hotkeys.leftSkillId === 47 && hud.hotkeys.rightSkillId === 64, 'Out-of-range drinkBeltSlot must not mutate skills')
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// SUITE 4: Adversarial Fuzzing & Differential Invariant Testing (5,000 Iterations)
|
|
// -----------------------------------------------------------------------------
|
|
console.log('\n--- [Suite 4] Adversarial Fuzzing & Differential Testing (5,000 Iterations) ---')
|
|
{
|
|
const win = new MockWindow()
|
|
const input = new KeyboardInput(win as unknown as Window)
|
|
input.attach()
|
|
|
|
const hudCanvas = new MockCanvas() as unknown as HTMLCanvasElement
|
|
const hud = new HudManager(hudCanvas, {
|
|
onToggleAutomap: () => {},
|
|
onWaypointTeleport: () => {},
|
|
})
|
|
hud['bindEvents']()
|
|
hud.hotkeys.leftSkillId = 47
|
|
hud.hotkeys.rightSkillId = 64
|
|
hud.syncPublishedState()
|
|
|
|
const candidateKeys = [
|
|
{ code: 'KeyT', key: 't' },
|
|
{ code: 'KeyB', key: 'b' },
|
|
{ code: 'Space', key: ' ' },
|
|
{ code: 'Digit1', key: '1' },
|
|
{ code: 'Digit2', key: '2' },
|
|
{ code: 'Digit3', key: '3' },
|
|
{ code: 'Digit4', key: '4' },
|
|
{ code: 'KeyW', key: 'w' },
|
|
{ code: 'KeyA', key: 'a' },
|
|
{ code: 'KeyS', key: 's' },
|
|
{ code: 'KeyD', key: 'd' },
|
|
{ code: 'KeyC', key: 'c' },
|
|
{ code: 'KeyJ', key: 'j' },
|
|
{ code: 'Enter', key: 'Enter' },
|
|
{ code: 'ShiftLeft', key: 'Shift' },
|
|
{ code: 'ShiftRight', key: 'Shift' },
|
|
{ code: 'ArrowUp', key: 'ArrowUp' },
|
|
{ code: 'ArrowDown', key: 'ArrowDown' },
|
|
{ code: 'ArrowLeft', key: 'ArrowLeft' },
|
|
{ code: 'ArrowRight', key: 'ArrowRight' },
|
|
{ code: 'Escape', key: 'Escape' },
|
|
]
|
|
|
|
const currentlyHeld = new Set<string>()
|
|
|
|
// Run 5,000 randomized state transitions
|
|
const NUM_ITERATIONS = 5000
|
|
for (let i = 0; i < NUM_ITERATIONS; i++) {
|
|
const candidate = candidateKeys[Math.floor(Math.random() * candidateKeys.length)]!
|
|
const isDown = Math.random() > 0.45 // 55% keydown, 45% keyup
|
|
|
|
if (isDown) {
|
|
currentlyHeld.add(candidate.code)
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', candidate))
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keydown', candidate))
|
|
} else {
|
|
currentlyHeld.delete(candidate.code)
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', candidate))
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keyup', candidate))
|
|
}
|
|
|
|
// --- INVARIANT ORACLE VERIFICATIONS ---
|
|
|
|
// Invariant 1: input.talking is ALWAYS false (KeyT decoupled)
|
|
assert(input.talking === false, `[Fuzz Step ${i}] input.talking MUST be false (held: ${Array.from(currentlyHeld).join(',')})`)
|
|
|
|
// Invariant 2: input.takeWaypoint() is ALWAYS false (KeyB decoupled)
|
|
assert(input.takeWaypoint() === false, `[Fuzz Step ${i}] input.takeWaypoint() MUST be false (held: ${Array.from(currentlyHeld).join(',')})`)
|
|
|
|
// Invariant 3: Space NEVER contributes to attacking
|
|
const expectedAttacking = currentlyHeld.has('KeyJ') || currentlyHeld.has('Enter')
|
|
assert(input.attacking === expectedAttacking, `[Fuzz Step ${i}] input.attacking mismatch: expected ${expectedAttacking}, got ${input.attacking}`)
|
|
|
|
// Invariant 4: input.digits is ALWAYS []
|
|
assert(input.digits.length === 0, `[Fuzz Step ${i}] input.digits MUST be empty array`)
|
|
|
|
// Invariant 5: WASD NEVER contributes to movement
|
|
const mov = input.movement()
|
|
if (!currentlyHeld.has('ArrowUp') && !currentlyHeld.has('ArrowDown') && !currentlyHeld.has('ArrowLeft') && !currentlyHeld.has('ArrowRight')) {
|
|
assert(mov.x === 0 && mov.y === 0, `[Fuzz Step ${i}] Movement must be {0,0} when no arrow keys held, even if WASD are held. Got {${mov.x}, ${mov.y}}`)
|
|
}
|
|
|
|
// Invariant 6: Shift tracking
|
|
const expectedShift = currentlyHeld.has('ShiftLeft') || currentlyHeld.has('ShiftRight')
|
|
assert(input.shiftHeld === expectedShift, `[Fuzz Step ${i}] ShiftHeld mismatch: expected ${expectedShift}, got ${input.shiftHeld}`)
|
|
|
|
// Invariant 7: Active skill IDs in HudManager remain strictly untouched
|
|
assert(hud.hotkeys.leftSkillId === 47, `[Fuzz Step ${i}] leftSkillId mutated! Got ${hud.hotkeys.leftSkillId}`)
|
|
assert(hud.hotkeys.rightSkillId === 64, `[Fuzz Step ${i}] rightSkillId mutated! Got ${hud.hotkeys.rightSkillId}`)
|
|
assert(hud.state.leftSkillId === 47, `[Fuzz Step ${i}] state.leftSkillId mutated! Got ${hud.state.leftSkillId}`)
|
|
assert(hud.state.rightSkillId === 64, `[Fuzz Step ${i}] state.rightSkillId mutated! Got ${hud.state.rightSkillId}`)
|
|
}
|
|
|
|
console.log(`✅ Passed 5,000 randomized fuzzing iterations with 7 live invariant checks per step (${NUM_ITERATIONS * 7} assertions)`)
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// SUITE 5: Simultaneous Chord Stress (All problematic keys pressed at once)
|
|
// -----------------------------------------------------------------------------
|
|
console.log('\n--- [Suite 5] Simultaneous Chord Stress (Space + KeyT + KeyB + 1..4 + WASD) ---')
|
|
{
|
|
const win = new MockWindow()
|
|
const input = new KeyboardInput(win as unknown as Window)
|
|
input.attach()
|
|
|
|
const hudCanvas = new MockCanvas() as unknown as HTMLCanvasElement
|
|
const hud = new HudManager(hudCanvas, {
|
|
onToggleAutomap: () => {},
|
|
onWaypointTeleport: () => {},
|
|
})
|
|
hud['bindEvents']()
|
|
hud.hotkeys.leftSkillId = 47
|
|
hud.hotkeys.rightSkillId = 64
|
|
hud.syncPublishedState()
|
|
|
|
const chord = [
|
|
{ code: 'KeyA', key: 'a' },
|
|
{ code: 'KeyT', key: 't' },
|
|
{ code: 'KeyB', key: 'b' },
|
|
{ code: 'Digit1', key: '1' },
|
|
{ code: 'Digit2', key: '2' },
|
|
{ code: 'Digit3', key: '3' },
|
|
{ code: 'Digit4', key: '4' },
|
|
{ code: 'KeyW', key: 'w' },
|
|
{ code: 'KeyS', key: 's' },
|
|
{ code: 'KeyD', key: 'd' },
|
|
{ code: 'Space', key: ' ' },
|
|
]
|
|
|
|
for (const evt of chord) {
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keydown', evt))
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keydown', evt))
|
|
}
|
|
|
|
assert(input.talking === false, 'Simultaneous chord: talking MUST be false')
|
|
assert(input.takeWaypoint() === false, 'Simultaneous chord: takeWaypoint MUST be false')
|
|
assert(input.attacking === false, 'Simultaneous chord: attacking MUST be false (Space does not attack)')
|
|
assert(input.digits.length === 0, 'Simultaneous chord: digits MUST be []')
|
|
assert(input.movement().x === 0 && input.movement().y === 0, 'Simultaneous chord: movement MUST be {0, 0}')
|
|
assert(input.active === false, 'Simultaneous chord: active MUST be false')
|
|
assert(hud.leftPanel === 'none', 'Simultaneous chord: Space closes leftPanel')
|
|
assert(hud.rightPanel === 'none', 'Simultaneous chord: Space closes rightPanel')
|
|
assert(hud.hotkeys.leftSkillId === 47, 'Simultaneous chord: leftSkillId is 47')
|
|
assert(hud.hotkeys.rightSkillId === 64, 'Simultaneous chord: rightSkillId is 64')
|
|
|
|
for (const evt of chord) {
|
|
win.dispatchEvent(new SyntheticKeyboardEvent('keyup', evt))
|
|
mockWin.dispatchEvent(new SyntheticKeyboardEvent('keyup', evt))
|
|
}
|
|
}
|
|
|
|
console.log('\n=================================================================')
|
|
console.log(`🎉 EMPIRICAL STRESS TEST RESULTS:`)
|
|
console.log(`Total Assertions Executed: ${totalAssertions}`)
|
|
console.log(`Assertions Passed: ${passedAssertions}`)
|
|
console.log(`Assertions Failed: ${failedAssertions}`)
|
|
console.log('Verdict: EMPIRICAL PROOF OF CORRECTNESS CONFIRMED')
|
|
console.log('=================================================================')
|
|
}
|
|
|
|
runTestSuite().catch((err) => {
|
|
console.error('Fatal error in stress test harness:', err)
|
|
process.exit(1)
|
|
})
|