528 lines
20 KiB
TypeScript
528 lines
20 KiB
TypeScript
/**
|
|
* Adversarial Empirical Stress Testing Suite for Milestone 4:
|
|
* Sorceress Fire Tree — Enchant (Skill 52) & Fire Mastery (Skill 61) Math & Mechanics
|
|
*
|
|
* 1.13c Ground Truth Sources:
|
|
* - Skills.txt (row 52 Enchant, row 61 Fire Mastery, row 37 Warmth)
|
|
* - D2Common.dll (HitShift 7 256-fixed-point, 5-band damage progression, ln12 duration)
|
|
* - D2Game.dll (Enchant buff attachment, ranged 33% penalty, Sorceress melee double-dip)
|
|
*
|
|
* Test Dimensions:
|
|
* 1. Monotonicity sweeps across slvl 1..99 for Enchant damage, AR bonus %, duration, and Fire Mastery %
|
|
* 2. Synergy scaling: Warmth synergy (+9%/lvl) linear scaling without overflow/underflow at syn 0, 20, 50, 99
|
|
* 3. Fire Mastery cast multiplier: (1 + calculateFireMasteryBonus(fmSlvl) / 100) base buff scaling & melee double-dip
|
|
* 4. Discrete values verification: exact 1.13c values at slvl 1, 10, 20
|
|
* 5. Extreme inputs: slvl 0, negative, and NaN inputs safely clamped
|
|
* 6. Differential testing against independent brute-force oracle (1000+ randomized iterations)
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
calculateEnchantDamage,
|
|
calculateEnchantAttackRating,
|
|
calculateEnchantDuration,
|
|
calculateFireMasteryBonus,
|
|
calculateEnchantAttackDamage,
|
|
} from '../../../src/game/skills.ts'
|
|
|
|
// ============================================================================
|
|
// Independent Brute-Force Oracles for Differential Testing
|
|
// ============================================================================
|
|
|
|
/** Independent Oracle for raw 5-band minimum fire damage */
|
|
function oracleRawMin(lvl: number): number {
|
|
const l = Number.isFinite(lvl) ? Math.max(1, Math.floor(lvl)) : 1
|
|
let min = 16
|
|
for (let i = 2; i <= l; i++) {
|
|
if (i <= 8) min += 3
|
|
else if (i <= 16) min += 7
|
|
else if (i <= 22) min += 11
|
|
else if (i <= 28) min += 15
|
|
else min += 19
|
|
}
|
|
return min
|
|
}
|
|
|
|
/** Independent Oracle for raw 5-band maximum fire damage */
|
|
function oracleRawMax(lvl: number): number {
|
|
const l = Number.isFinite(lvl) ? Math.max(1, Math.floor(lvl)) : 1
|
|
let max = 20
|
|
for (let i = 2; i <= l; i++) {
|
|
if (i <= 8) max += 5
|
|
else if (i <= 16) max += 9
|
|
else if (i <= 22) max += 13
|
|
else if (i <= 28) max += 17
|
|
else max += 21
|
|
}
|
|
return max
|
|
}
|
|
|
|
/** Independent Oracle for Attack Rating bonus % */
|
|
function oracleEnchantAR(slvl: number): number {
|
|
if (!Number.isFinite(slvl) || slvl <= 0) return 0
|
|
const lvl = Math.floor(slvl)
|
|
return 20 + (lvl - 1) * 9
|
|
}
|
|
|
|
/** Independent Oracle for Duration in frames */
|
|
function oracleEnchantDuration(slvl: number): number {
|
|
if (!Number.isFinite(slvl) || slvl <= 0) return 0
|
|
const lvl = Math.floor(slvl)
|
|
return 3600 + (lvl - 1) * 600
|
|
}
|
|
|
|
/** Independent Oracle for Fire Mastery bonus % */
|
|
function oracleFireMasteryBonus(slvl: number): number {
|
|
if (!Number.isFinite(slvl) || slvl <= 0) return 0
|
|
const lvl = Math.floor(slvl)
|
|
return 30 + (lvl - 1) * 7
|
|
}
|
|
|
|
/** Independent Oracle for complete Enchant damage */
|
|
function oracleEnchantDamage(slvl: number, warmth: number = 0, fireMastery: number = 0) {
|
|
const rawMin = oracleRawMin(slvl)
|
|
const rawMax = oracleRawMax(slvl)
|
|
const wPts = Number.isFinite(warmth) ? Math.max(0, warmth) : 0
|
|
const fmPts = Number.isFinite(fireMastery) ? Math.max(0, fireMastery) : 0
|
|
|
|
const warmthMul = 1.0 + (wPts * 9) / 100
|
|
const fmMul = 1.0 + oracleFireMasteryBonus(fmPts) / 100
|
|
const totalMul = warmthMul * fmMul
|
|
|
|
const min256 = Math.floor(rawMin * 128 * totalMul)
|
|
const max256 = Math.floor(rawMax * 128 * totalMul)
|
|
const min = Math.round((rawMin * 0.5 * totalMul) * 10) / 10
|
|
const max = Math.round((rawMax * 0.5 * totalMul) * 10) / 10
|
|
|
|
return { min, max, min256, max256 }
|
|
}
|
|
|
|
describe('Milestone 4 Math Adversarial Stress Suite (Enchant & Fire Mastery)', () => {
|
|
// ==========================================================================
|
|
// Suite 1: Monotonicity Sweeps (slvl 1..99)
|
|
// ==========================================================================
|
|
describe('Suite 1: Monotonicity Sweeps across slvl 1..99', () => {
|
|
it('Enchant fire damage strictly increases monotonically across slvl 1..99', () => {
|
|
let prev = calculateEnchantDamage(1)
|
|
expect(prev.min).toBe(8.0)
|
|
expect(prev.max).toBe(10.0)
|
|
expect(prev.min256).toBe(2048)
|
|
expect(prev.max256).toBe(2560)
|
|
|
|
for (let slvl = 2; slvl <= 99; slvl++) {
|
|
const curr = calculateEnchantDamage(slvl)
|
|
|
|
// Strict monotonicity
|
|
expect(curr.min).toBeGreaterThan(prev.min)
|
|
expect(curr.max).toBeGreaterThan(prev.max)
|
|
expect(curr.min256).toBeGreaterThan(prev.min256)
|
|
expect(curr.max256).toBeGreaterThan(prev.max256)
|
|
|
|
// Invariant: min <= max
|
|
expect(curr.min).toBeLessThanOrEqual(curr.max)
|
|
expect(curr.min256).toBeLessThanOrEqual(curr.max256)
|
|
|
|
prev = curr
|
|
}
|
|
})
|
|
|
|
it('Enchant Attack Rating bonus % strictly increases monotonically across slvl 1..99', () => {
|
|
let prev = calculateEnchantAttackRating(1)
|
|
expect(prev).toBe(20)
|
|
|
|
for (let slvl = 2; slvl <= 99; slvl++) {
|
|
const curr = calculateEnchantAttackRating(slvl)
|
|
// Strict linear increase of exactly +9% per level
|
|
expect(curr).toBe(prev + 9)
|
|
expect(curr).toBe(20 + (slvl - 1) * 9)
|
|
prev = curr
|
|
}
|
|
|
|
expect(calculateEnchantAttackRating(99)).toBe(902)
|
|
})
|
|
|
|
it('Enchant duration in frames strictly increases monotonically across slvl 1..99', () => {
|
|
let prev = calculateEnchantDuration(1)
|
|
expect(prev).toBe(3600) // 144 seconds
|
|
|
|
for (let slvl = 2; slvl <= 99; slvl++) {
|
|
const curr = calculateEnchantDuration(slvl)
|
|
// Strict linear increase of exactly +600 frames (24 seconds) per level
|
|
expect(curr).toBe(prev + 600)
|
|
expect(curr).toBe(3600 + (slvl - 1) * 600)
|
|
prev = curr
|
|
}
|
|
|
|
expect(calculateEnchantDuration(99)).toBe(62400) // 2496s = 41.6 minutes
|
|
})
|
|
|
|
it('Fire Mastery fire damage bonus % strictly increases monotonically across slvl 1..99', () => {
|
|
let prev = calculateFireMasteryBonus(1)
|
|
expect(prev).toBe(30) // +30%
|
|
|
|
for (let slvl = 2; slvl <= 99; slvl++) {
|
|
const curr = calculateFireMasteryBonus(slvl)
|
|
// Strict linear increase of exactly +7% per level
|
|
expect(curr).toBe(prev + 7)
|
|
expect(curr).toBe(30 + (slvl - 1) * 7)
|
|
prev = curr
|
|
}
|
|
|
|
expect(calculateFireMasteryBonus(99)).toBe(716) // +716%
|
|
})
|
|
})
|
|
|
|
// ==========================================================================
|
|
// Suite 2: Synergy Scaling & Linear Progression (Warmth +9%/lvl)
|
|
// ==========================================================================
|
|
describe('Suite 2: Synergy Scaling & Precision (Warmth +9%/lvl)', () => {
|
|
it('Warmth synergy scales linearly at slvl 1, 10, 20, 50, 99 with syn 0, 20, 50, 99', () => {
|
|
const testLevels = [1, 10, 20, 50, 99]
|
|
const testSynergies = [0, 20, 50, 99]
|
|
|
|
for (const slvl of testLevels) {
|
|
const base = calculateEnchantDamage(slvl, 0)
|
|
|
|
for (const syn of testSynergies) {
|
|
const withSyn = calculateEnchantDamage(slvl, syn)
|
|
const expectedMultiplier = 1.0 + (syn * 9) / 100
|
|
|
|
// Check finiteness and positivity
|
|
expect(Number.isFinite(withSyn.min)).toBe(true)
|
|
expect(Number.isFinite(withSyn.max)).toBe(true)
|
|
expect(Number.isFinite(withSyn.min256)).toBe(true)
|
|
expect(Number.isFinite(withSyn.max256)).toBe(true)
|
|
expect(withSyn.min).toBeGreaterThan(0)
|
|
expect(withSyn.max).toBeGreaterThanOrEqual(withSyn.min)
|
|
|
|
// Exact 256-fixed-point integer parity
|
|
const rawMin = oracleRawMin(slvl)
|
|
const rawMax = oracleRawMax(slvl)
|
|
expect(withSyn.min256).toBe(Math.floor(rawMin * 128 * expectedMultiplier))
|
|
expect(withSyn.max256).toBe(Math.floor(rawMax * 128 * expectedMultiplier))
|
|
|
|
// Floating point 0.5 step parity rounded to 1 decimal
|
|
expect(withSyn.min).toBe(Math.round((rawMin * 0.5 * expectedMultiplier) * 10) / 10)
|
|
expect(withSyn.max).toBe(Math.round((rawMax * 0.5 * expectedMultiplier) * 10) / 10)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('verifies exact step increments per Warmth hard point across syn 0..20', () => {
|
|
const slvl = 20
|
|
const rawMin = 137
|
|
const rawMax = 179
|
|
|
|
let prev = calculateEnchantDamage(slvl, 0)
|
|
for (let w = 1; w <= 20; w++) {
|
|
const curr = calculateEnchantDamage(slvl, w)
|
|
|
|
// Strict monotonicity per point of synergy
|
|
expect(curr.min256).toBeGreaterThan(prev.min256)
|
|
expect(curr.max256).toBeGreaterThan(prev.max256)
|
|
|
|
// Multiplier check: 1 + 0.09 * w
|
|
const mul = 1 + 0.09 * w
|
|
expect(curr.min256).toBe(Math.floor(rawMin * 128 * mul))
|
|
expect(curr.max256).toBe(Math.floor(rawMax * 128 * mul))
|
|
|
|
prev = curr
|
|
}
|
|
|
|
// At Warmth 20: +180% synergy bonus (2.8x base)
|
|
const maxWarmth = calculateEnchantDamage(20, 20)
|
|
expect(maxWarmth.min256).toBe(Math.floor(137 * 128 * 2.8)) // 49100
|
|
expect(maxWarmth.max256).toBe(Math.floor(179 * 128 * 2.8)) // 64153
|
|
})
|
|
|
|
it('handles negative or invalid synergy values by safe clamping to 0', () => {
|
|
const base = calculateEnchantDamage(20, 0)
|
|
const negSyn = calculateEnchantDamage(20, -10)
|
|
expect(negSyn).toEqual(base)
|
|
|
|
const nanSyn = calculateEnchantDamage(20, NaN)
|
|
expect(nanSyn).toEqual(base)
|
|
})
|
|
})
|
|
|
|
// ==========================================================================
|
|
// Suite 3: Fire Mastery Cast Multiplier & Interaction
|
|
// ==========================================================================
|
|
describe('Suite 3: Fire Mastery Cast Multiplier & Combat Integration', () => {
|
|
it('scales Enchant base buff damage by (1 + calculateFireMasteryBonus(fmSlvl) / 100) at cast time', () => {
|
|
const slvls = [1, 10, 20, 50, 99]
|
|
const fmLvls = [0, 1, 10, 20, 50, 99]
|
|
|
|
for (const slvl of slvls) {
|
|
const base = calculateEnchantDamage(slvl)
|
|
|
|
for (const fmSlvl of fmLvls) {
|
|
const fmBonus = calculateFireMasteryBonus(fmSlvl)
|
|
const fmMul = 1.0 + fmBonus / 100
|
|
|
|
const castDmg = calculateEnchantDamage(slvl, 0, fmSlvl)
|
|
const expectedMin = Math.round(base.min * fmMul * 10) / 10
|
|
const expectedMax = Math.round(base.max * fmMul * 10) / 10
|
|
|
|
expect(castDmg.min).toBe(expectedMin)
|
|
expect(castDmg.max).toBe(expectedMax)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('supports synergies object argument { warmth, fireMastery } and positional arguments equivalently', () => {
|
|
const slvl = 20
|
|
const warmth = 20
|
|
const fmSlvl = 20
|
|
|
|
const viaObj = calculateEnchantDamage(slvl, { warmth, fireMastery: fmSlvl })
|
|
const viaPos = calculateEnchantDamage(slvl, warmth, fmSlvl)
|
|
|
|
expect(viaObj).toEqual(viaPos)
|
|
expect(viaObj.min).toBe(504.4)
|
|
expect(viaObj.max).toBe(659.1)
|
|
})
|
|
|
|
it('verifies compound scaling: Warmth synergy combined with Fire Mastery multiplier', () => {
|
|
// slvl 20 Enchant: rawMin=137, rawMax=179
|
|
// Warmth 20 (+180% -> 2.80x)
|
|
// Fire Mastery 20 (+163% -> 2.63x)
|
|
// Total multiplier = 2.80 * 2.63 = 7.364x
|
|
const buffed = calculateEnchantDamage(20, { warmth: 20, fireMastery: 20 })
|
|
const expectedMin256 = Math.floor(137 * 128 * 2.8 * 2.63)
|
|
const expectedMax256 = Math.floor(179 * 128 * 2.8 * 2.63)
|
|
expect(buffed.min256).toBe(expectedMin256)
|
|
expect(buffed.max256).toBe(expectedMax256)
|
|
|
|
const expectedMin = Math.round((137 * 0.5 * 2.8 * 2.63) * 10) / 10
|
|
const expectedMax = Math.round((179 * 0.5 * 2.8 * 2.63) * 10) / 10
|
|
expect(buffed.min).toBe(expectedMin)
|
|
expect(buffed.max).toBe(expectedMax)
|
|
})
|
|
|
|
it('verifies Sorceress melee double-dip vs ranged 33% penalty via calculateEnchantAttackDamage', () => {
|
|
const castDmg = calculateEnchantDamage(20, 0, 20) // min: 180.2, max: 235.4
|
|
const fmBonus = calculateFireMasteryBonus(20) // 163% -> 2.63x
|
|
|
|
// 1. Sorceress Melee: Double-Dip multiplies fire damage again!
|
|
const meleeSorc = calculateEnchantAttackDamage({
|
|
enchantDamage: castDmg,
|
|
isRanged: false,
|
|
isSorceress: true,
|
|
fireMasterySlvl: 20,
|
|
})
|
|
const expectedMeleeMin = Math.round(castDmg.min * (1 + fmBonus / 100) * 10) / 10 // 473.9
|
|
const expectedMeleeMax = Math.round(castDmg.max * (1 + fmBonus / 100) * 10) / 10 // 619.1
|
|
expect(meleeSorc.min).toBe(expectedMeleeMin)
|
|
expect(meleeSorc.max).toBe(expectedMeleeMax)
|
|
|
|
// 2. Ranged attack: 33% penalty, zero second Fire Mastery multiplier
|
|
const rangedSorc = calculateEnchantAttackDamage({
|
|
enchantDamage: castDmg,
|
|
isRanged: true,
|
|
isSorceress: true,
|
|
fireMasterySlvl: 20,
|
|
})
|
|
const expectedRangedMin = Math.round(castDmg.min * 0.33 * 10) / 10 // 59.5
|
|
const expectedRangedMax = Math.round(castDmg.max * 0.33 * 10) / 10 // 77.7
|
|
expect(rangedSorc.min).toBe(expectedRangedMin)
|
|
expect(rangedSorc.max).toBe(expectedRangedMax)
|
|
|
|
// 3. Non-Sorceress ally in melee: 100% buff damage, no secondary multiplier
|
|
const allyMelee = calculateEnchantAttackDamage({
|
|
enchantDamage: castDmg,
|
|
isRanged: false,
|
|
isSorceress: false,
|
|
})
|
|
expect(allyMelee.min).toBe(castDmg.min)
|
|
expect(allyMelee.max).toBe(castDmg.max)
|
|
|
|
// 4. Fire immunity barrier: enemies with >= 100% resistance take 0 damage
|
|
const immune = calculateEnchantAttackDamage({
|
|
enchantDamage: castDmg,
|
|
isRanged: false,
|
|
isSorceress: true,
|
|
fireMasterySlvl: 20,
|
|
targetFireResist: 100,
|
|
})
|
|
expect(immune.min).toBe(0)
|
|
expect(immune.max).toBe(0)
|
|
expect(immune.average).toBe(0)
|
|
})
|
|
})
|
|
|
|
// ==========================================================================
|
|
// Suite 4: Exact 1.13c Discrete Values Verification (slvl 1, 10, 20)
|
|
// ==========================================================================
|
|
describe('Suite 4: Exact 1.13c Discrete Values Verification (slvl 1, 10, 20)', () => {
|
|
it('verifies exact discrete values at slvl 1', () => {
|
|
// slvl 1: 8.0-10.0 fire dmg, +20% AR, 3600 frames duration (144s), +30% Fire Mastery
|
|
const dmg = calculateEnchantDamage(1)
|
|
expect(dmg.min).toBe(8.0)
|
|
expect(dmg.max).toBe(10.0)
|
|
expect(dmg.min256).toBe(2048)
|
|
expect(dmg.max256).toBe(2560)
|
|
|
|
expect(calculateEnchantAttackRating(1)).toBe(20)
|
|
expect(calculateEnchantDuration(1)).toBe(3600)
|
|
expect(calculateFireMasteryBonus(1)).toBe(30)
|
|
})
|
|
|
|
it('verifies exact discrete values at slvl 10', () => {
|
|
// slvl 10: 25.5-36.5 fire dmg, +101% AR, 9000 frames duration (360s), +93% Fire Mastery
|
|
const dmg = calculateEnchantDamage(10)
|
|
expect(dmg.min).toBe(25.5)
|
|
expect(dmg.max).toBe(36.5)
|
|
expect(dmg.min256).toBe(6528)
|
|
expect(dmg.max256).toBe(9344)
|
|
|
|
expect(calculateEnchantAttackRating(10)).toBe(101)
|
|
expect(calculateEnchantDuration(10)).toBe(9000)
|
|
expect(calculateFireMasteryBonus(10)).toBe(93)
|
|
})
|
|
|
|
it('verifies exact discrete values at slvl 20', () => {
|
|
// slvl 20: 68.5-89.5 fire dmg, +191% AR, 15000 frames duration (600s), +163% Fire Mastery
|
|
const dmg = calculateEnchantDamage(20)
|
|
expect(dmg.min).toBe(68.5)
|
|
expect(dmg.max).toBe(89.5)
|
|
expect(dmg.min256).toBe(17536)
|
|
expect(dmg.max256).toBe(22912)
|
|
|
|
expect(calculateEnchantAttackRating(20)).toBe(191)
|
|
expect(calculateEnchantDuration(20)).toBe(15000)
|
|
expect(calculateFireMasteryBonus(20)).toBe(163)
|
|
})
|
|
|
|
it('verifies 5-band breakpoint transition step deltas across slvl 2..99', () => {
|
|
for (let lvl = 2; lvl <= 99; lvl++) {
|
|
const prev = calculateEnchantDamage(lvl - 1)
|
|
const curr = calculateEnchantDamage(lvl)
|
|
const dMin256 = curr.min256 - prev.min256
|
|
const dMax256 = curr.max256 - prev.max256
|
|
|
|
let expDMin = 3 * 128
|
|
let expDMax = 5 * 128
|
|
if (lvl >= 9 && lvl <= 16) {
|
|
expDMin = 7 * 128
|
|
expDMax = 9 * 128
|
|
} else if (lvl >= 17 && lvl <= 22) {
|
|
expDMin = 11 * 128
|
|
expDMax = 13 * 128
|
|
} else if (lvl >= 23 && lvl <= 28) {
|
|
expDMin = 15 * 128
|
|
expDMax = 17 * 128
|
|
} else if (lvl >= 29) {
|
|
expDMin = 19 * 128
|
|
expDMax = 21 * 128
|
|
}
|
|
|
|
expect(dMin256).toBe(expDMin)
|
|
expect(dMax256).toBe(expDMax)
|
|
}
|
|
})
|
|
})
|
|
|
|
// ==========================================================================
|
|
// Suite 5: Extreme Inputs & Boundary Values Clamping
|
|
// ==========================================================================
|
|
describe('Suite 5: Extreme Inputs & Boundary Values Clamping', () => {
|
|
it('safely clamps slvl 0 to valid baselines', () => {
|
|
// Enchant damage clamps slvl <= 0 to slvl 1
|
|
const dmg0 = calculateEnchantDamage(0)
|
|
const dmg1 = calculateEnchantDamage(1)
|
|
expect(dmg0).toEqual(dmg1)
|
|
|
|
// AR, Duration, and Fire Mastery clamp slvl <= 0 to 0
|
|
expect(calculateEnchantAttackRating(0)).toBe(0)
|
|
expect(calculateEnchantDuration(0)).toBe(0)
|
|
expect(calculateFireMasteryBonus(0)).toBe(0)
|
|
})
|
|
|
|
it('safely clamps negative slvl inputs (-1, -10, -99)', () => {
|
|
const dmgNeg = calculateEnchantDamage(-10)
|
|
const dmg1 = calculateEnchantDamage(1)
|
|
expect(dmgNeg).toEqual(dmg1)
|
|
|
|
expect(calculateEnchantAttackRating(-5)).toBe(0)
|
|
expect(calculateEnchantDuration(-50)).toBe(0)
|
|
expect(calculateFireMasteryBonus(-99)).toBe(0)
|
|
})
|
|
|
|
it('safely handles NaN and non-finite inputs', () => {
|
|
const dmgNaN = calculateEnchantDamage(NaN)
|
|
const dmg1 = calculateEnchantDamage(1)
|
|
expect(dmgNaN).toEqual(dmg1)
|
|
|
|
expect(calculateEnchantAttackRating(NaN)).toBe(0)
|
|
expect(calculateEnchantDuration(NaN)).toBe(0)
|
|
expect(calculateFireMasteryBonus(NaN)).toBe(0)
|
|
|
|
expect(calculateEnchantAttackRating(Infinity)).toBe(0)
|
|
expect(calculateEnchantDuration(-Infinity)).toBe(0)
|
|
expect(calculateFireMasteryBonus(Infinity)).toBe(0)
|
|
})
|
|
|
|
it('correctly truncates / floors floating-point skill levels', () => {
|
|
expect(calculateEnchantAttackRating(10.9)).toBe(calculateEnchantAttackRating(10))
|
|
expect(calculateEnchantDuration(15.7)).toBe(calculateEnchantDuration(15))
|
|
expect(calculateFireMasteryBonus(20.3)).toBe(calculateFireMasteryBonus(20))
|
|
expect(calculateEnchantDamage(20.8)).toEqual(calculateEnchantDamage(20))
|
|
})
|
|
})
|
|
|
|
// ==========================================================================
|
|
// Suite 6: Differential Testing against Brute-Force Oracle
|
|
// ==========================================================================
|
|
describe('Suite 6: Differential Testing against Brute-Force Oracle (1000+ Iterations)', () => {
|
|
it('matches independent brute-force oracle across 1000 randomized (slvl, warmth, fireMastery) inputs', () => {
|
|
// Deterministic pseudorandom generator (LCG)
|
|
let seed = 42
|
|
function rand() {
|
|
seed = (seed * 1664525 + 1013904223) >>> 0
|
|
return seed / 4294967296
|
|
}
|
|
|
|
const ITERATIONS = 1000
|
|
|
|
for (let i = 0; i < ITERATIONS; i++) {
|
|
// Biased input generation: mix of boundary values and uniform randoms
|
|
let slvl: number
|
|
let warmth: number
|
|
let fireMastery: number
|
|
|
|
if (i < 50) {
|
|
// Boundary testing phase
|
|
slvl = (i % 30) + 1
|
|
warmth = i % 25
|
|
fireMastery = i % 25
|
|
} else if (i < 100) {
|
|
// High level and extreme synergy phase
|
|
slvl = 80 + Math.floor(rand() * 20)
|
|
warmth = Math.floor(rand() * 100)
|
|
fireMastery = Math.floor(rand() * 100)
|
|
} else {
|
|
// General distribution 1..99
|
|
slvl = Math.floor(rand() * 99) + 1
|
|
warmth = Math.floor(rand() * 50)
|
|
fireMastery = Math.floor(rand() * 50)
|
|
}
|
|
|
|
const actual = calculateEnchantDamage(slvl, warmth, fireMastery)
|
|
const expected = oracleEnchantDamage(slvl, warmth, fireMastery)
|
|
|
|
// Strict differential checks against oracle
|
|
expect(actual.min).toBe(expected.min)
|
|
expect(actual.max).toBe(expected.max)
|
|
expect(actual.min256).toBe(expected.min256)
|
|
expect(actual.max256).toBe(expected.max256)
|
|
|
|
// AR and Duration oracle differential checks
|
|
expect(calculateEnchantAttackRating(slvl)).toBe(oracleEnchantAR(slvl))
|
|
expect(calculateEnchantDuration(slvl)).toBe(oracleEnchantDuration(slvl))
|
|
expect(calculateFireMasteryBonus(fireMastery)).toBe(oracleFireMasteryBonus(fireMastery))
|
|
}
|
|
})
|
|
})
|
|
})
|