656 lines
23 KiB
TypeScript
656 lines
23 KiB
TypeScript
/**
|
|
* 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<string, number> = {
|
|
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)
|
|
})
|