/** * Milestone 2 Empirical Stress & Statistical Verification Suite * * Requirements: * 1. Player Scaling Statistical Monte Carlo: * - Compare drop yields on regular monsters (Act 1 H2H A) under /players 1 vs 3 vs 5 vs 7 vs 8 (solo & partied). * - Verify NoDrop rate dampening matches theoretical expectations within statistical variance (Z < 3.5, delta < 1%). * 2. Multi-Seed PRNG Determinism: * - Verify that setting /players N maintains identical PRNG stream determinism across 50 seeds in lockstep simulation. * 3. Execution Robustness: * - Run 50,000 drops across all difficulties with various player counts and equipment configurations. * - Assert 0 exceptions, 0 NaNs, 100% valid bases, qualities, ilvls, and boss TC non-upgrades. * * Usage: npx tsx scripts/verify-challenger-m2-stress.ts */ import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts' import { executeDropPipeline } from '../src/game/drop-pipeline.ts' import { expandTreasureClass, computeEffectivePlayers, computeScaledNoDrop, } from '../src/game/treasure-engine.ts' import { D2Rng } from '../src/game/d2-rng.ts' import { Rng } from '../src/game/rng.ts' import { GameEngine, type WorldMapProvider, type GameEngineOptions } from '../src/game/engine.ts' import { createWorld, spawnMonsters, tickCombat, damageMonster, monsterStatsFromTable } from '../src/game/combat.ts' import { parseTable } from '../src/game/tables.ts' import { Inventory, goldItem, ItemQuality, type Item, type PlacedItem } from '../src/game/items.ts' import { QuestLog } from '../src/game/quests.ts' import { LockstepSession, type InputFrame, type LockstepSimulation } from '../src/net/lockstep.ts' import { getMonsterTreasureClass, monsterStatsOf, applyEliteModifiers, type MonsterRank, type Difficulty, } from '../src/game/monsters.ts' import { handleActSceneChatCommand } from '../src/scene/act-scene.ts' const dropTables = getEmbeddedDropTables() const dummyTerrain: WorldMapProvider = { widthPx: 2000, heightPx: 2000, overlap: () => 0, } // 50 diverse seeds spanning primes, bit patterns, boundaries, and pseudo-random ranges const TEST_SEEDS = [ 1, 2, 3, 7, 13, 42, 100, 255, 256, 1000, 10007, 104729, 1299709, 0x1234, 0x5678, 0xabcd, 0xbeef, 0x10000000, 0x55555555, 0xAAAAAAAA, 0x7FFFFFFF, 0xFFFFFFFF, 123456789, 987654321, 314159265, 271828182, 161803398, ...Array.from({ length: 23 }, (_, i) => ((i * 1664525 + 1013904223) >>> 0) & 0x7FFFFFFF), ] interface MonteCarloResult { players: number partyPlayers: number effectivePlayers: number trials: number emptyDrops: number itemDrops: number observedNoDropRate: number expectedNoDropRate: number observedDropYield: number expectedDropYield: number zScore: number delta: number passed: boolean } function runMonteCarlo( tcName: string, players: number, partyPlayers: number, trials = 100000, seed = 0x12345678, ): MonteCarloResult { const tcNode = dropTables.tcTable.get(tcName) if (!tcNode) { throw new Error(`TreasureClass "${tcName}" not found`) } const baseNoDrop = tcNode.noDrop ?? 0 const totalProb = tcNode.totalProbExpansion ?? 0 const effPlayers = computeEffectivePlayers(players, partyPlayers) const scaledNoDrop = computeScaledNoDrop(baseNoDrop, totalProb, effPlayers) const totalWeight = scaledNoDrop + totalProb const expectedNoDropRate = totalWeight > 0 ? scaledNoDrop / totalWeight : 0 const expectedDropYield = totalWeight > 0 ? totalProb / totalWeight : 0 const rng = new D2Rng(seed) let emptyDrops = 0 let itemDrops = 0 for (let i = 0; i < trials; i++) { const drops = expandTreasureClass(tcName, { rng, tcTable: dropTables.tcTable, autoTcTable: dropTables.autoTcTable, gamePlayers: players, partyPlayers, isExpansion: true, }) if (drops.length === 0) { emptyDrops++ } else { itemDrops++ } } const observedNoDropRate = emptyDrops / trials const observedDropYield = itemDrops / trials const standardError = Math.sqrt((expectedNoDropRate * (1 - expectedNoDropRate)) / trials) const zScore = standardError > 0 ? Math.abs(observedNoDropRate - expectedNoDropRate) / standardError : 0 const delta = Math.abs(observedNoDropRate - expectedNoDropRate) // Pass if within 3.5 sigma and absolute delta < 1.0% const passed = zScore < 3.5 && delta < 0.01 return { players, partyPlayers, effectivePlayers: effPlayers, trials, emptyDrops, itemDrops, observedNoDropRate, expectedNoDropRate, observedDropYield, expectedDropYield, zScore, delta, passed, } } async function main() { console.log('======================================================================') console.log(' Milestone 2 Empirical Stress & Statistical Verification (Challenger 2)') console.log('======================================================================\n') let allPassed = true const failureReasons: string[] = [] // ========================================================================== // Section 1: Player Scaling Statistical Monte Carlo (Act 1 H2H A) // ========================================================================== console.log('----------------------------------------------------------------------') console.log('1. Player Scaling Statistical Monte Carlo: Act 1 H2H A (100,000 trials each)') console.log('----------------------------------------------------------------------') const configurations = [ { players: 1, party: 1, label: '/players 1 (solo)' }, { players: 3, party: 1, label: '/players 3 (solo)' }, { players: 5, party: 1, label: '/players 5 (solo)' }, { players: 7, party: 1, label: '/players 7 (solo)' }, { players: 8, party: 1, label: '/players 8 (solo)' }, { players: 8, party: 8, label: '/players 8 (partied)' }, ] const mcResults: MonteCarloResult[] = [] for (const cfg of configurations) { const res = runMonteCarlo('Act 1 H2H A', cfg.players, cfg.party, 100000, 0x5a17e0 + cfg.players * 100 + cfg.party) mcResults.push(res) console.log( `[${res.passed ? 'PASS' : 'FAIL'}] ${cfg.label}: n_eff=${res.effectivePlayers} | ` + `NoDrop Obs: ${(res.observedNoDropRate * 100).toFixed(3)}% (Exp: ${(res.expectedNoDropRate * 100).toFixed(3)}%) | ` + `Yield Obs: ${(res.observedDropYield * 100).toFixed(3)}% (Exp: ${(res.expectedDropYield * 100).toFixed(3)}%) | ` + `Z: ${res.zScore.toFixed(2)} | Delta: ${(res.delta * 100).toFixed(3)}%` ) if (!res.passed) { allPassed = false failureReasons.push(`Monte Carlo failed for ${cfg.label}: delta=${res.delta}, Z=${res.zScore}`) } } // Verify Monotonic Dampening Invariant // P1 < P3 < P5 < P7 == P8_solo < P8_partied const [p1, p3, p5, p7, p8s, p8p] = mcResults const yieldsMonotonic = p1!.observedDropYield < p3!.observedDropYield && p3!.observedDropYield < p5!.observedDropYield && p5!.observedDropYield < p7!.observedDropYield && Math.abs(p7!.observedDropYield - p8s!.observedDropYield) < 0.01 && p8s!.observedDropYield < p8p!.observedDropYield console.log(`\nDrop Yield Monotonicity (P1 < P3 < P5 < P7 ≈ P8_solo < P8_party): ${yieldsMonotonic ? 'VERIFIED' : 'FAILED'}`) if (!yieldsMonotonic) { allPassed = false failureReasons.push('Drop yields do not follow monotonic dampening sequence') } // Also test Monte Carlo through GameEngine with setPlayers() console.log('\n--- Verifying GameEngine.setPlayers() End-to-End Drop Yields ---') const engineP1Trials = 10000 let engineP1Drops = 0 const engineP1 = new GameEngine(dummyTerrain, { spawn: { x: 100, y: 100 }, stats: [], dropTables, difficulty: 'normal', gamePlayers: 1, partyPlayers: 1, npcDefs: [], questDefs: [], inventoryCols: 10, inventoryRows: 4, xpTable: [0, 0, 1000], itemBases: [], prefixAffixes: [], suffixAffixes: [], skills: [], combatOptions: { playerSpeed: 180, playerReach: 48, playerCooldownTicks: 12, playerDamage: 6, playerManaPerAttack: 2, respawnTicks: 40, }, talkRadius: 80, pickupRadius: 60, }) for (let i = 0; i < engineP1Trials; i++) { const drops = executeDropPipeline(dropTables, { tcName: 'Act 1 H2H A', nLevel: 1, monsterType: 1, difficulty: 'normal', gamePlayers: engineP1.gamePlayers, partyPlayers: engineP1.partyPlayers, monsterRng: new D2Rng(100000 + i), }) if (drops.length > 0) engineP1Drops++ } const engineP1Yield = engineP1Drops / engineP1Trials engineP1.setPlayers(8) let engineP8Drops = 0 for (let i = 0; i < engineP1Trials; i++) { const drops = executeDropPipeline(dropTables, { tcName: 'Act 1 H2H A', nLevel: 1, monsterType: 1, difficulty: 'normal', gamePlayers: engineP1.gamePlayers, partyPlayers: engineP1.partyPlayers, monsterRng: new D2Rng(100000 + i), }) if (drops.length > 0) engineP8Drops++ } const engineP8Yield = engineP8Drops / engineP1Trials console.log(`GameEngine /players 1 yield: ${(engineP1Yield * 100).toFixed(2)}% (expected ~37.5%)`) console.log(`GameEngine /players 8 yield: ${(engineP8Yield * 100).toFixed(2)}% (expected ~85.7%)`) const engineScalingOk = Math.abs(engineP1Yield - 0.375) < 0.02 && Math.abs(engineP8Yield - 0.857) < 0.02 console.log(`GameEngine setPlayers() scaling check: ${engineScalingOk ? 'PASS' : 'FAIL'}`) if (!engineScalingOk) { allPassed = false failureReasons.push('GameEngine setPlayers() drop yields out of tolerance') } // ========================================================================== // Section 2: Multi-Seed PRNG Determinism Across 50 Seeds // ========================================================================== console.log('\n----------------------------------------------------------------------') console.log(`2. Multi-Seed PRNG Stream Determinism Across ${TEST_SEEDS.length} Seeds`) console.log('----------------------------------------------------------------------') let streamDeterminismPassCount = 0 let lockstepSessionPassCount = 0 const dropSequenceTcs = [ 'Act 1 H2H A', 'Act 1 Champ A', 'Act 1 Unique A', 'Act 1 Boss A', 'Countess', 'Andariel', 'Act 2 H2H A', 'Duriel', 'Act 3 H2H A', 'Mephisto', 'Act 4 H2H A', 'Diablo', 'Act 5 H2H A', 'Baal', ] for (let sIdx = 0; sIdx < TEST_SEEDS.length; sIdx++) { const seed = TEST_SEEDS[sIdx]! const testPlayers = 1 + (sIdx % 8) // 2a. Sequential Drop Stream Determinism // Run 1: const rng1 = new D2Rng(seed) const run1Drops: any[] = [] for (const tc of dropSequenceTcs) { const drops = executeDropPipeline(dropTables, { tcName: tc, nLevel: 50, monsterType: tc.includes('Boss') || ['Andariel', 'Duriel', 'Mephisto', 'Diablo', 'Baal'].includes(tc) ? 4 : tc.includes('Unique') ? 3 : tc.includes('Champ') ? 2 : 1, difficulty: 'nightmare', gamePlayers: testPlayers, partyPlayers: 1, playerMf: 150, playerGf: 75, monsterRng: rng1, }) run1Drops.push(drops.map(d => ({ code: d.code, quality: d.quality, rarity: d.rarity, uniqueId: d.uniqueId, durability: d.durability, stats: d.stats, }))) } const seedEnd1 = rng1.getSeed() // Run 2 (fresh instance, exact same initial seed): const rng2 = new D2Rng(seed) const run2Drops: any[] = [] for (const tc of dropSequenceTcs) { const drops = executeDropPipeline(dropTables, { tcName: tc, nLevel: 50, monsterType: tc.includes('Boss') || ['Andariel', 'Duriel', 'Mephisto', 'Diablo', 'Baal'].includes(tc) ? 4 : tc.includes('Unique') ? 3 : tc.includes('Champ') ? 2 : 1, difficulty: 'nightmare', gamePlayers: testPlayers, partyPlayers: 1, playerMf: 150, playerGf: 75, monsterRng: rng2, }) run2Drops.push(drops.map(d => ({ code: d.code, quality: d.quality, rarity: d.rarity, uniqueId: d.uniqueId, durability: d.durability, stats: d.stats, }))) } const seedEnd2 = rng2.getSeed() const streamMatches = JSON.stringify(run1Drops) === JSON.stringify(run2Drops) && seedEnd1.lo === seedEnd2.lo && seedEnd1.hi === seedEnd2.hi if (streamMatches) { streamDeterminismPassCount++ } else { console.error(`Drop stream desync for seed ${seed} at players=${testPlayers}`) } // 2b. Lockstep Multi-Session Determinism with Players Setting (200 ticks) const combatStats = monsterStatsFromTable(parseTable([ 'Id\tName\tHP\tDamage\tCooldownTicks\tReach\tAggroRadius\tSpeed\tXP', 'fallen\tFallen\t12\t3\t24\t36\t400\t80\t8', 'zombie\tZombie\t30\t6\t32\t40\t300\t50\t15', ].join('\n'))) function createLockstepSim(simSeed: number, pCount: number): LockstepSimulation { const world = createWorld(0, 0) spawnMonsters(world, combatStats, 6, { x: 200, y: 0 }, 200, { overlap: () => 0 }) const simRng = new Rng(simSeed) const bag = new Inventory(10, 4) const questLog = new QuestLog([]) let simDropsCount = 0 return { advance: (inputs) => { const frame = inputs[0]! tickCombat( world, { movement: frame.movement, attack: frame.attack }, { playerSpeed: 180, playerReach: 48, playerCooldownTicks: 12, playerDamage: 6, playerManaPerAttack: 2, respawnTicks: 40 }, { overlap: () => 0 }, [0, 0, 1000], ) for (const ev of world.events) { if (ev.kind === 'kill') { const drops = executeDropPipeline(dropTables, { tcName: 'Act 1 H2H A', nLevel: 1, monsterType: 1, difficulty: 'normal', gamePlayers: pCount, partyPlayers: 1, monsterRng: new D2Rng(simRng.int(0, 0x7FFFFFFF)), }) simDropsCount += drops.length } } if (frame.attack && world.player.cooldown === 0) { world.monsters.forEach((monster, index) => { if (monster.state === 'dead') return if (Math.hypot(monster.x - world.player.x, monster.y - world.player.y) > 48) return damageMonster(world, index, 6) }) } }, hash: () => { const stateStr = `${world.tick}:${world.kills}:${simRng.seed}:${world.player.hp}:${simDropsCount}` return LockstepSession.digest(stateStr) }, } } const sessionA = new LockstepSession({ peers: 1, inputDelayTicks: 2 }, createLockstepSim(seed, testPlayers)) const sessionB = new LockstepSession({ peers: 1, inputDelayTicks: 2 }, createLockstepSim(seed, testPlayers)) let sessionsInSync = true for (let tick = 0; tick < 200; tick++) { const phase = Math.floor(tick / 25) % 4 const movement = phase === 0 ? { x: 1, y: 0 } : phase === 1 ? { x: 0, y: 1 } : phase === 2 ? { x: -1, y: 0 } : { x: 0, y: -1 } const frame: InputFrame = { tick, movement, attack: tick % 4 === 0, pickup: false, talk: false, skill: 0, } sessionA.submit(0, frame) sessionB.submit(0, frame) const resA = sessionA.step() const resB = sessionB.step() if (resA.kind === 'stepped' && resB.kind === 'stepped') { if (resA.hash !== resB.hash) { sessionsInSync = false break } } } if (sessionsInSync) { lockstepSessionPassCount++ } else { console.error(`Lockstep session desync for seed ${seed}`) } } console.log(`Drop Pipeline Sequential Determinism: ${streamDeterminismPassCount}/${TEST_SEEDS.length} passed`) console.log(`Multiplayer Lockstep Determinism with /players N: ${lockstepSessionPassCount}/${TEST_SEEDS.length} passed`) if (streamDeterminismPassCount !== TEST_SEEDS.length || lockstepSessionPassCount !== TEST_SEEDS.length) { allPassed = false failureReasons.push(`Determinism failures: Stream=${streamDeterminismPassCount}/${TEST_SEEDS.length}, Lockstep=${lockstepSessionPassCount}/${TEST_SEEDS.length}`) } // ========================================================================== // Section 3: Execution Robustness Under Heavy Load (50,000 Drops) // ========================================================================== console.log('\n----------------------------------------------------------------------') console.log('3. Execution Robustness Under Heavy Load (50,000 Drops Stress Test)') console.log('----------------------------------------------------------------------') const difficulties: Difficulty[] = ['normal', 'nightmare', 'hell'] const ranks: MonsterRank[] = ['normal', 'champion', 'unique', 'minion', 'boss'] const equipmentSetups = [ { name: 'Naked (0 MF, 0 GF)', mf: 0, gf: 0 }, { name: 'Early (65 MF, 40 GF)', mf: 65, gf: 40 }, { name: 'Standard MF (350 MF, 150 GF)', mf: 350, gf: 150 }, { name: 'Extreme MF (1000 MF, 500 GF)', mf: 1000, gf: 500 }, { name: 'Negative MF (-50 MF, 0 GF)', mf: -50, gf: 0 }, { name: 'Boundary Suppression (-100 MF, 0 GF)', mf: -100, gf: 0 }, { name: 'Extreme Negative (-250 MF, 0 GF)', mf: -250, gf: 0 }, ] const bossList = ['Andariel', 'Duriel', 'Mephisto', 'Diablo', 'Baal'] const superUniques = ['Bishibosh', 'Rakanishu', 'Griswold', 'Radament', 'The Summoner', 'Izual', 'Nihlathak'] const sampleMonsterKinds = Array.from(dropTables.monsterKinds.keys()).slice(0, 40) let totalStressDrops = 0 let totalItemsEmitted = 0 let exceptionsCaught = 0 let corruptItemsCount = 0 let bossTcUpgradeViolations = 0 let eliteMlvlViolations = 0 let negativeMfViolations = 0 const qualityDistribution: Record = { normal: 0, superior: 0, magic: 0, rare: 0, set: 0, unique: 0, low: 0, gold: 0, } const STRESS_TARGET = 50000 const stressRng = new D2Rng(0xc0ffee42) const startTime = Date.now() for (let i = 0; i < STRESS_TARGET; i++) { const diff = difficulties[i % difficulties.length]! const rank = ranks[i % ranks.length]! const setup = equipmentSetups[i % equipmentSetups.length]! const pCount = 1 + (i % 8) const partyCount = (i % 2 === 0) ? 1 : Math.min(pCount, 1 + (i % 4)) let tcToRoll = 'Act 1 H2H A' let nLevel = 10 + (i % 85) const isActBoss = rank === 'boss' || i % 10 === 0 const isSuper = !isActBoss && i % 7 === 0 if (isActBoss) { tcToRoll = bossList[i % bossList.length]! nLevel = 30 + (i % 65) } else if (isSuper) { tcToRoll = superUniques[i % superUniques.length]! nLevel = 15 + (i % 75) } else { const mk = dropTables.monsterKinds.get(sampleMonsterKinds[i % sampleMonsterKinds.length]!) if (mk) { const resolvedTc = getMonsterTreasureClass(mk, diff, 1) if (resolvedTc) tcToRoll = resolvedTc } } const monsterType = isActBoss ? 4 : (rank === 'unique' || rank === 'minion' ? 3 : (rank === 'champion' ? 2 : 1)) // Elite mlvl calculation check let expectedMlvl = nLevel if (rank === 'champion') expectedMlvl = nLevel + 2 else if (rank === 'unique' || rank === 'minion') expectedMlvl = nLevel + 3 try { const items = executeDropPipeline(dropTables, { tcName: tcToRoll, nLevel: expectedMlvl, monsterType, difficulty: diff, gamePlayers: pCount, partyPlayers: partyCount, playerMf: setup.mf, playerGf: setup.gf, isBoss: isActBoss, monsterRng: stressRng, }) totalStressDrops++ totalItemsEmitted += items.length for (const item of items) { const isGold = item.base?.id === 'gold' || item.code?.trim() === 'gld' if (isGold) { qualityDistribution.gold++ const amt = item.stack ?? item.value ?? 0 if (amt <= 0 || isNaN(amt)) corruptItemsCount++ continue } const rarity = item.rarity ?? 'normal' qualityDistribution[rarity] = (qualityDistribution[rarity] ?? 0) + 1 // 1. Base exists if (!item.base || !dropTables.getBase(item.base.id)) { corruptItemsCount++ } // 2. ilvl within bounds [1, 99] if (!item.ilvl || item.ilvl < 1 || item.ilvl > 99) { corruptItemsCount++ } // 3. Stats have no NaNs if (item.stats) { for (const [k, v] of Object.entries(item.stats)) { if (typeof v === 'number' && isNaN(v)) { corruptItemsCount++ } } } // 4. Durability consistency if (item.durability !== undefined && item.maxDurability !== undefined) { if (item.durability <= 0 || item.durability > item.maxDurability || isNaN(item.durability)) { corruptItemsCount++ } } // 5. Negative MF suppression check: if MF <= -100: // - Weapons and armor must NEVER roll magic, rare, set, or unique // - Always-magic items (rings, charms, amulets) can only be magic, never rare/set/unique (unless forced) if (setup.mf <= -100) { if (item.base.kind === 'weapon' || item.base.kind === 'armor') { if (['magic', 'rare', 'set', 'unique'].includes(rarity)) { negativeMfViolations++ } } else { if (['rare', 'set', 'unique'].includes(rarity)) { negativeMfViolations++ } } } } } catch (err: any) { exceptionsCaught++ console.error(`Exception during stress drop ${i}:`, err.message) } } const durationMs = Date.now() - startTime console.log(`Completed ${totalStressDrops} drop executions in ${(durationMs / 1000).toFixed(2)}s (${(totalStressDrops / (durationMs / 1000)).toFixed(0)} drops/s)`) console.log(`Total ground items emitted: ${totalItemsEmitted}`) console.log(`Quality Breakdown:`, JSON.stringify(qualityDistribution, null, 2)) console.log(`Exceptions caught: ${exceptionsCaught}`) console.log(`Corrupt items detected: ${corruptItemsCount}`) console.log(`Negative MF violations (<= -100 rolled magic+): ${negativeMfViolations}`) if (exceptionsCaught > 0 || corruptItemsCount > 0 || negativeMfViolations > 0) { allPassed = false failureReasons.push(`Stress test failures: Exceptions=${exceptionsCaught}, CorruptItems=${corruptItemsCount}, NegMfViolations=${negativeMfViolations}`) } // ========================================================================== // Summary & Verdict // ========================================================================== console.log('\n======================================================================') console.log(`FINAL EMPIRICAL VERDICT: ${allPassed ? 'APPROVE' : 'REQUEST_CHANGES'}`) console.log('======================================================================') if (!allPassed) { console.log('Failure details:') failureReasons.forEach(r => console.log(' - ' + r)) process.exit(1) } else { console.log('All statistical, PRNG determinism, and heavy load criteria passed with 100% success.') } } main().catch(err => { console.error('Fatal test runner error:', err) process.exit(1) })