Skip to main content
Glama

Path of Exile 2 Build Optimizer MCP

test_ehp_calculator.py25.9 kB
""" Unit tests for EHP Calculator Tests cover: 1. Basic EHP calculations for all damage types 2. PoE2-specific mechanics (chaos vs ES, armor before res, 50% block cap) 3. Hit size analysis and armor breakpoints 4. Defense gap identification 5. Upgrade comparisons 6. Edge cases and validation """ import pytest import logging from src.calculator.ehp_calculator import ( EHPCalculator, DefensiveStats, ThreatProfile, DamageType, DefenseGap, quick_physical_ehp, quick_elemental_ehp ) from src.calculator.defense_calculator import DefenseConstants class TestDefensiveStats: """Test DefensiveStats dataclass.""" def test_valid_stats(self): """Test creating valid defensive stats.""" stats = DefensiveStats( life=5000, energy_shield=2000, armor=10000, fire_res=75 ) assert stats.life == 5000 assert stats.energy_shield == 2000 assert stats.armor == 10000 assert stats.fire_res == 75 def test_minimal_stats(self): """Test creating stats with minimal values.""" stats = DefensiveStats(life=1) assert stats.life == 1 assert stats.energy_shield == 0 def test_negative_life(self): """Test that negative life raises error.""" with pytest.raises(ValueError, match="Life cannot be negative"): DefensiveStats(life=-100) def test_negative_es(self): """Test that negative ES raises error.""" with pytest.raises(ValueError, match="Energy Shield cannot be negative"): DefensiveStats(life=1000, energy_shield=-500) def test_no_hp_pool(self): """Test that zero life and ES raises error.""" with pytest.raises(ValueError, match="Must have some life or energy shield"): DefensiveStats(life=0, energy_shield=0) class TestThreatProfile: """Test ThreatProfile dataclass.""" def test_default_values(self): """Test default threat profile.""" threat = ThreatProfile() assert threat.expected_hit_size == 1000.0 assert threat.attacker_accuracy == 2000.0 def test_custom_values(self): """Test custom threat profile.""" threat = ThreatProfile(expected_hit_size=5000, attacker_accuracy=3000) assert threat.expected_hit_size == 5000 assert threat.attacker_accuracy == 3000 class TestBasicEHP: """Test basic EHP calculations.""" def setup_method(self): """Set up test fixtures.""" self.calc = EHPCalculator() self.threat = ThreatProfile(expected_hit_size=1000, attacker_accuracy=2000) def test_life_only_no_mitigation(self): """Test EHP with only life and no mitigation.""" stats = DefensiveStats(life=5000) result = self.calc.calculate_ehp(stats, DamageType.PHYSICAL, self.threat) # With no mitigation, EHP should equal raw HP assert result.raw_hp == 5000 assert result.effective_hp == pytest.approx(5000, rel=1e-2) assert result.total_mitigation == pytest.approx(0, abs=1e-2) def test_life_with_resistance(self): """Test EHP with life and capped resistance.""" stats = DefensiveStats(life=5000, fire_res=75) result = self.calc.calculate_ehp(stats, DamageType.FIRE, self.threat) # 75% res = 4× EHP multiplier # EHP = 5000 / (1 - 0.75) = 20000 assert result.raw_hp == 5000 assert result.effective_hp == pytest.approx(20000, rel=1e-2) assert result.total_mitigation == pytest.approx(0.75, abs=1e-2) def test_life_es_combined(self): """Test EHP with combined life and ES.""" stats = DefensiveStats(life=4000, energy_shield=2000, fire_res=75) result = self.calc.calculate_ehp(stats, DamageType.FIRE, self.threat) # Total pool: 6000 # 75% res = 4× multiplier # EHP = 6000 / 0.25 = 24000 assert result.raw_hp == 6000 assert result.effective_hp == pytest.approx(24000, rel=1e-2) def test_physical_with_armor(self): """Test physical EHP with armor.""" stats = DefensiveStats(life=5000, armor=10000) result = self.calc.calculate_ehp(stats, DamageType.PHYSICAL, self.threat) # Armor DR vs 1000 damage: 10000 / (10000 + 10*1000) = 50% # EHP = 5000 / 0.5 = 10000 assert result.armor_dr == pytest.approx(0.5, rel=1e-2) assert result.effective_hp == pytest.approx(10000, rel=1e-2) def test_block_chance(self): """Test EHP with block chance.""" stats = DefensiveStats(life=5000, block_chance=40, fire_res=75) result = self.calc.calculate_ehp(stats, DamageType.FIRE, self.threat) # Block: 40% avoidance = 1 / 0.6 = 1.667× multiplier # Resistance: 75% = 4× multiplier # Combined: 1.667 × 4 = 6.667× total # EHP = 5000 × 6.667 = 33333 expected_ehp = 5000 / ((1 - 0.40) * (1 - 0.75)) assert result.effective_hp == pytest.approx(expected_ehp, rel=1e-2) def test_evasion_chance(self): """Test EHP with evasion.""" stats = DefensiveStats(life=5000, evasion=5000, fire_res=75) result = self.calc.calculate_ehp(stats, DamageType.FIRE, self.threat) # Evasion provides some avoidance assert result.evade_mitigation > 0 # EHP should be higher than resistance alone assert result.effective_hp > 20000 class TestPoE2Mechanics: """Test PoE2-specific mechanics.""" def setup_method(self): """Set up test fixtures.""" self.calc = EHPCalculator() self.threat = ThreatProfile() def test_chaos_es_double_damage(self): """Test PoE2 mechanic: chaos removes 2× ES.""" stats = DefensiveStats(life=3000, energy_shield=2000, chaos_res=0) # Vs non-chaos: full ES fire_result = self.calc.calculate_ehp(stats, DamageType.FIRE, self.threat) assert fire_result.raw_hp == 5000 # Vs chaos: ES counts as half chaos_result = self.calc.calculate_ehp(stats, DamageType.CHAOS, self.threat) assert chaos_result.raw_hp == 4000 # 3000 life + 2000/2 ES def test_block_cap_50_percent(self): """Test PoE2 block cap of 50%.""" # Block over cap stats_over = DefensiveStats(life=5000, block_chance=75, fire_res=75) result_over = self.calc.calculate_ehp(stats_over, DamageType.FIRE, self.threat) # Block at cap stats_cap = DefensiveStats(life=5000, block_chance=50, fire_res=75) result_cap = self.calc.calculate_ehp(stats_cap, DamageType.FIRE, self.threat) # Both should have same EHP (capped at 50%) assert result_over.block_mitigation == pytest.approx(0.5, rel=1e-2) assert result_cap.block_mitigation == pytest.approx(0.5, rel=1e-2) assert result_over.effective_hp == pytest.approx(result_cap.effective_hp, rel=1e-2) def test_armor_before_resistance(self): """Test PoE2 mechanic: armor applies before resistance for physical.""" stats = DefensiveStats(life=5000, armor=10000, fire_res=75) # Physical: armor reduces BEFORE resistance (but phys has no res) phys_result = self.calc.calculate_ehp(stats, DamageType.PHYSICAL, self.threat) # Fire: only resistance applies fire_result = self.calc.calculate_ehp(stats, DamageType.FIRE, self.threat) # Fire should have higher EHP (75% res vs ~50% armor) assert fire_result.effective_hp > phys_result.effective_hp def test_armor_scales_with_hit_size(self): """Test that armor effectiveness decreases with hit size.""" stats = DefensiveStats(life=5000, armor=10000) # Small hit small_threat = ThreatProfile(expected_hit_size=500) small_result = self.calc.calculate_ehp(stats, DamageType.PHYSICAL, small_threat) # Large hit large_threat = ThreatProfile(expected_hit_size=5000) large_result = self.calc.calculate_ehp(stats, DamageType.PHYSICAL, large_threat) # Armor should be more effective vs small hits assert small_result.armor_dr > large_result.armor_dr assert small_result.effective_hp > large_result.effective_hp def test_resistance_90_hard_cap(self): """Test that resistance is capped at 90%.""" stats = DefensiveStats(life=5000, fire_res=95) # Over cap result = self.calc.calculate_ehp(stats, DamageType.FIRE, self.threat) # Should be capped at 90% assert result.resistance_dr <= 0.90 class TestLayeredDefenses: """Test multiple defense layers working together.""" def setup_method(self): """Set up test fixtures.""" self.calc = EHPCalculator() self.threat = ThreatProfile(expected_hit_size=1000) def test_all_layers_physical(self): """Test all defense layers for physical damage.""" stats = DefensiveStats( life=5000, armor=10000, evasion=5000, block_chance=40 ) result = self.calc.calculate_ehp(stats, DamageType.PHYSICAL, self.threat) # All layers should contribute assert result.evade_mitigation > 0 assert result.block_mitigation > 0 assert result.armor_dr > 0 assert result.total_mitigation > 0.5 # Should have good total mitigation def test_all_layers_elemental(self): """Test all defense layers for elemental damage.""" stats = DefensiveStats( life=5000, energy_shield=2000, evasion=5000, block_chance=40, fire_res=75 ) result = self.calc.calculate_ehp(stats, DamageType.FIRE, self.threat) # Evasion, block, and resistance should all contribute assert result.evade_mitigation > 0 assert result.block_mitigation > 0 assert result.resistance_dr == pytest.approx(0.75) def test_multiplicative_layering(self): """Test that defense layers multiply (not add).""" stats = DefensiveStats(life=5000, block_chance=40, fire_res=50) result = self.calc.calculate_ehp(stats, DamageType.FIRE, self.threat) # 40% block + 50% res ≠ 90% mitigation # Correct: 1 - (0.6 × 0.5) = 70% mitigation expected_mitigation = 1 - (0.6 * 0.5) assert result.total_mitigation == pytest.approx(expected_mitigation, rel=1e-2) class TestHitSizeAnalysis: """Test hit size analysis features.""" def setup_method(self): """Set up test fixtures.""" self.calc = EHPCalculator() self.stats = DefensiveStats(life=5000, armor=20000) def test_analyze_armor_vs_hit_sizes(self): """Test armor analysis against multiple hit sizes.""" analysis = self.calc.analyze_armor_vs_hit_sizes(self.stats) # Should have multiple hit sizes assert len(analysis) > 0 # DR should decrease as hit size increases hit_sizes = sorted(analysis.keys()) drs = [analysis[h]['dr_percent'] for h in hit_sizes] for i in range(len(drs) - 1): assert drs[i] >= drs[i + 1] # DR decreases or stays same def test_custom_hit_sizes(self): """Test armor analysis with custom hit sizes.""" custom_sizes = [100, 500, 2000] analysis = self.calc.analyze_armor_vs_hit_sizes(self.stats, custom_sizes) assert len(analysis) == 3 assert 100 in analysis assert 500 in analysis assert 2000 in analysis def test_find_armor_breakpoints(self): """Test finding armor breakpoints.""" breakpoints = self.calc.find_armor_breakpoints(self.stats) # Should have multiple breakpoints assert len(breakpoints) > 0 # Required armor should increase with target DR drs = sorted(breakpoints.keys()) armors = [breakpoints[dr] for dr in drs] for i in range(len(armors) - 1): # Each higher DR should need more armor (or be infinite) if armors[i] != float('inf') and armors[i + 1] != float('inf'): assert armors[i] <= armors[i + 1] def test_armor_breakpoint_90_percent_unreachable(self): """Test that 90% DR breakpoint is recognized as cap.""" breakpoints = self.calc.find_armor_breakpoints(self.stats, [90.0]) # 90% is at the cap, should be possible but very high assert 90.0 in breakpoints class TestDefenseGaps: """Test defense gap identification.""" def setup_method(self): """Set up test fixtures.""" self.calc = EHPCalculator() self.threat = ThreatProfile() def test_uncapped_resistance_gap(self): """Test identification of uncapped resistances.""" stats = DefensiveStats( life=5000, fire_res=50, # 25% below cap cold_res=75, lightning_res=75, chaos_res=0 ) gaps = self.calc.identify_defense_gaps(stats, self.threat) # Should identify fire res gap fire_gaps = [g for g in gaps if 'fire' in g.gap_type] assert len(fire_gaps) > 0 fire_gap = fire_gaps[0] assert fire_gap.current_value == 50 assert fire_gap.recommended_value == 75 def test_low_hp_pool_gap(self): """Test identification of low HP pool.""" stats = DefensiveStats( life=2000, # Very low fire_res=75, cold_res=75, lightning_res=75 ) gaps = self.calc.identify_defense_gaps(stats, self.threat) # Should identify low HP hp_gaps = [g for g in gaps if 'low_hp' in g.gap_type] assert len(hp_gaps) > 0 def test_no_layered_defenses_gap(self): """Test identification of missing defense layers.""" stats = DefensiveStats( life=5000, armor=0, evasion=0, block_chance=0, energy_shield=0, fire_res=75, cold_res=75, lightning_res=75 ) gaps = self.calc.identify_defense_gaps(stats, self.threat) # Should identify lack of layers layer_gaps = [g for g in gaps if 'layered' in g.gap_type or 'layer' in g.gap_type] assert len(layer_gaps) > 0 def test_overcapped_block_gap(self): """Test identification of wasted block chance.""" stats = DefensiveStats( life=5000, block_chance=70, # 20% over cap fire_res=75, cold_res=75, lightning_res=75 ) gaps = self.calc.identify_defense_gaps(stats, self.threat) # Should identify overcapped block block_gaps = [g for g in gaps if 'block' in g.gap_type] assert len(block_gaps) > 0 block_gap = block_gaps[0] assert block_gap.current_value == 70 assert block_gap.recommended_value == 50 def test_negative_chaos_res_gap(self): """Test identification of negative chaos resistance.""" stats = DefensiveStats( life=5000, fire_res=75, cold_res=75, lightning_res=75, chaos_res=-60 # Very negative ) gaps = self.calc.identify_defense_gaps(stats, self.threat) # Should identify negative chaos res chaos_gaps = [g for g in gaps if 'chaos' in g.gap_type] assert len(chaos_gaps) > 0 def test_gaps_sorted_by_severity(self): """Test that gaps are sorted by severity.""" stats = DefensiveStats( life=2000, # Low HP (high severity) fire_res=70, # Slightly low (lower severity) cold_res=75, lightning_res=75, chaos_res=-20 ) gaps = self.calc.identify_defense_gaps(stats, self.threat) # Gaps should be sorted by severity (descending) for i in range(len(gaps) - 1): assert gaps[i].severity >= gaps[i + 1].severity def test_good_build_minimal_gaps(self): """Test that a good build has minimal gaps.""" stats = DefensiveStats( life=5500, energy_shield=1500, armor=12000, block_chance=45, fire_res=75, cold_res=75, lightning_res=75, chaos_res=20 ) gaps = self.calc.identify_defense_gaps(stats, self.threat) # Should have few or no high-severity gaps critical_gaps = [g for g in gaps if g.severity >= 7] assert len(critical_gaps) == 0 class TestComparisons: """Test upgrade comparison features.""" def setup_method(self): """Set up test fixtures.""" self.calc = EHPCalculator() self.threat = ThreatProfile() def test_compare_armor_upgrade(self): """Test comparing armor upgrade.""" current = DefensiveStats(life=5000, armor=10000) upgraded = DefensiveStats(life=5000, armor=15000) comparison = self.calc.compare_upgrade(current, upgraded, self.threat) # Physical EHP should increase assert comparison['physical']['absolute_gain'] > 0 assert comparison['physical']['percent_gain'] > 0 def test_compare_resistance_upgrade(self): """Test comparing resistance upgrade.""" current = DefensiveStats(life=5000, fire_res=70) upgraded = DefensiveStats(life=5000, fire_res=75) comparison = self.calc.compare_upgrade(current, upgraded, self.threat) # Fire EHP should increase significantly (capping res is powerful) assert comparison['fire']['absolute_gain'] > 0 assert comparison['fire']['percent_gain'] > 10 # Should be meaningful gain def test_compare_life_upgrade(self): """Test comparing life upgrade.""" current = DefensiveStats(life=4500, fire_res=75) upgraded = DefensiveStats(life=5000, fire_res=75) comparison = self.calc.compare_upgrade(current, upgraded, self.threat) # All damage types should benefit equally from life phys_gain = comparison['physical']['percent_gain'] fire_gain = comparison['fire']['percent_gain'] # Gains should be similar (within 1% due to rounding) assert abs(phys_gain - fire_gain) < 1 def test_calculate_defense_value(self): """Test calculating value of defense increase.""" stats = DefensiveStats(life=5000, armor=10000, fire_res=75) # Test armor value armor_value = self.calc.calculate_defense_value( stats, 'armor', 5000, self.threat ) assert 'physical_ehp_gain' in armor_value assert armor_value['physical_ehp_gain'] > 0 # Test resistance value res_value = self.calc.calculate_defense_value( stats, 'fire_res', 5, self.threat ) assert 'fire_ehp_gain' in res_value def test_invalid_defense_type(self): """Test that invalid defense type raises error.""" stats = DefensiveStats(life=5000) with pytest.raises(ValueError, match="Unknown defense type"): self.calc.calculate_defense_value( stats, 'invalid_type', 100, self.threat ) class TestAllEHP: """Test calculating EHP for all damage types.""" def setup_method(self): """Set up test fixtures.""" self.calc = EHPCalculator() self.threat = ThreatProfile() def test_calculate_all_ehp(self): """Test calculating EHP for all damage types.""" stats = DefensiveStats( life=5000, armor=10000, fire_res=75, cold_res=75, lightning_res=75, chaos_res=0 ) results = self.calc.calculate_all_ehp(stats, self.threat) # Should have result for each damage type assert len(results) == len(DamageType) for damage_type in DamageType: assert damage_type in results def test_elemental_ehp_similar(self): """Test that elemental EHP is similar when resistances are equal.""" stats = DefensiveStats( life=5000, fire_res=75, cold_res=75, lightning_res=75 ) results = self.calc.calculate_all_ehp(stats, self.threat) fire_ehp = results[DamageType.FIRE].effective_hp cold_ehp = results[DamageType.COLD].effective_hp lightning_ehp = results[DamageType.LIGHTNING].effective_hp # All three should be very similar assert fire_ehp == pytest.approx(cold_ehp, rel=1e-2) assert fire_ehp == pytest.approx(lightning_ehp, rel=1e-2) def test_chaos_lower_with_es(self): """Test that chaos EHP is lower when relying on ES.""" stats = DefensiveStats( life=3000, energy_shield=2000, fire_res=75, chaos_res=0 ) results = self.calc.calculate_all_ehp(stats, self.threat) # Chaos should have lower raw HP due to 2× ES damage chaos_result = results[DamageType.CHAOS] fire_result = results[DamageType.FIRE] assert chaos_result.raw_hp < fire_result.raw_hp class TestQuickFunctions: """Test convenience quick calculation functions.""" def test_quick_physical_ehp(self): """Test quick physical EHP calculation.""" ehp = quick_physical_ehp(life=5000, armor=10000, block_chance=40, hit_size=1000) assert ehp > 0 assert ehp > 5000 # Should be higher than raw HP def test_quick_elemental_ehp(self): """Test quick elemental EHP calculation.""" ehp = quick_elemental_ehp(life=5000, resistance=75, block_chance=40) assert ehp > 0 assert ehp > 20000 # 75% res = ~4× multiplier, block adds more def test_quick_functions_match_full_calc(self): """Test that quick functions match full calculation.""" calc = EHPCalculator() # Test physical stats = DefensiveStats(life=5000, armor=10000, block_chance=40) threat = ThreatProfile(expected_hit_size=1000) full_result = calc.calculate_ehp(stats, DamageType.PHYSICAL, threat) quick_result = quick_physical_ehp(5000, 10000, 40, 1000) assert full_result.effective_hp == pytest.approx(quick_result, rel=1e-2) class TestEdgeCases: """Test edge cases and boundary conditions.""" def setup_method(self): """Set up test fixtures.""" self.calc = EHPCalculator() self.threat = ThreatProfile() def test_zero_armor_vs_physical(self): """Test physical EHP with zero armor.""" stats = DefensiveStats(life=5000, armor=0) result = self.calc.calculate_ehp(stats, DamageType.PHYSICAL, self.threat) assert result.armor_dr == 0 assert result.effective_hp == 5000 def test_zero_evasion(self): """Test EHP with zero evasion.""" stats = DefensiveStats(life=5000, evasion=0) result = self.calc.calculate_ehp(stats, DamageType.PHYSICAL, self.threat) assert result.evade_mitigation == 0 def test_maximum_mitigation(self): """Test near-maximum mitigation doesn't cause overflow.""" stats = DefensiveStats( life=5000, armor=100000, # Very high armor block_chance=50, fire_res=90 # At hard cap ) threat = ThreatProfile(expected_hit_size=100) # Small hit (high armor DR) result = self.calc.calculate_ehp(stats, DamageType.FIRE, threat) # Should have very high EHP but not infinite assert result.effective_hp > 100000 assert result.effective_hp != float('inf') def test_negative_resistance(self): """Test EHP with negative resistance.""" stats = DefensiveStats(life=5000, fire_res=-60) result = self.calc.calculate_ehp(stats, DamageType.FIRE, self.threat) # Negative res means taking more damage, so EHP < raw HP assert result.effective_hp < result.raw_hp def test_very_large_hit(self): """Test armor effectiveness against very large hits.""" stats = DefensiveStats(life=5000, armor=50000) threat = ThreatProfile(expected_hit_size=50000) # Enormous hit result = self.calc.calculate_ehp(stats, DamageType.PHYSICAL, threat) # Armor should provide minimal DR vs huge hits assert result.armor_dr < 0.6 # Less than 60% DR class TestLayersBreakdown: """Test detailed breakdown in EHP results.""" def setup_method(self): """Set up test fixtures.""" self.calc = EHPCalculator() self.threat = ThreatProfile() def test_breakdown_structure(self): """Test that breakdown has correct structure.""" stats = DefensiveStats( life=5000, armor=10000, evasion=5000, block_chance=40, fire_res=75 ) result = self.calc.calculate_ehp(stats, DamageType.PHYSICAL, self.threat) # Check breakdown exists assert 'raw_hp' in result.layers_breakdown assert 'evasion' in result.layers_breakdown assert 'block' in result.layers_breakdown assert 'armor' in result.layers_breakdown assert 'resistance' in result.layers_breakdown assert 'combined' in result.layers_breakdown def test_breakdown_multipliers(self): """Test that breakdown multipliers are calculated correctly.""" stats = DefensiveStats(life=5000, fire_res=75) result = self.calc.calculate_ehp(stats, DamageType.FIRE, self.threat) # 75% res should give 4× multiplier res_multiplier = result.layers_breakdown['resistance']['multiplier'] assert res_multiplier == pytest.approx(4.0, rel=1e-2) if __name__ == '__main__': # Run tests with pytest pytest.main([__file__, '-v', '--tb=short'])

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/HivemindOverlord/poe2-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server