Files
Code_of_Conquest/api/tests/test_stats.py
Phillip Tarrant a38906b445 feat(api): integrate equipment stats into combat damage system
Equipment-Combat Integration:
- Update Stats damage formula from STR//2 to int(STR*0.75) for better scaling
- Add spell_power system for magical weapons (staves, wands)
- Add spell_power_bonus field to Stats model with spell_power property
- Add spell_power field to Item model with is_magical_weapon() method
- Update Character.get_effective_stats() to populate spell_power_bonus

Combatant Model Updates:
- Add weapon property fields (crit_chance, crit_multiplier, damage_type)
- Add elemental weapon support (elemental_damage_type, physical_ratio, elemental_ratio)
- Update serialization to handle new weapon properties

DamageCalculator Refactoring:
- Remove weapon_damage parameter from calculate_physical_damage()
- Use attacker_stats.damage directly (includes weapon bonus)
- Use attacker_stats.spell_power for magical damage calculations

Combat Service Updates:
- Extract weapon properties in _create_combatant_from_character()
- Use stats.damage_bonus for enemy combatants from templates
- Remove hardcoded _get_weapon_damage() method
- Handle elemental weapons with split damage in _execute_attack()

Item Generation Updates:
- Add base_spell_power to BaseItemTemplate dataclass
- Add ARCANE damage type to DamageType enum
- Add magical weapon templates (wizard_staff, arcane_staff, wand, crystal_wand)

Test Updates:
- Update test_stats.py for new damage formula (0.75 scaling)
- Update test_character.py for equipment bonus calculations
- Update test_damage_calculator.py for new API signatures
- Update test_combat_service.py mock fixture for equipped attribute

Tests: 174 passing
2025-11-26 19:54:58 -06:00

382 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Unit tests for Stats dataclass.
Tests computed properties, serialization, and basic operations.
"""
import pytest
from app.models.stats import Stats
def test_stats_default_values():
"""Test that Stats initializes with default values."""
stats = Stats()
assert stats.strength == 10
assert stats.dexterity == 10
assert stats.constitution == 10
assert stats.intelligence == 10
assert stats.wisdom == 10
assert stats.charisma == 10
def test_stats_custom_values():
"""Test creating Stats with custom values."""
stats = Stats(
strength=15,
dexterity=12,
constitution=14,
intelligence=8,
wisdom=10,
charisma=11,
)
assert stats.strength == 15
assert stats.dexterity == 12
assert stats.constitution == 14
assert stats.intelligence == 8
assert stats.wisdom == 10
assert stats.charisma == 11
def test_hit_points_calculation():
"""Test HP calculation: 10 + (constitution × 2)."""
stats = Stats(constitution=10)
assert stats.hit_points == 30 # 10 + (10 × 2)
stats = Stats(constitution=15)
assert stats.hit_points == 40 # 10 + (15 × 2)
stats = Stats(constitution=20)
assert stats.hit_points == 50 # 10 + (20 × 2)
def test_mana_points_calculation():
"""Test MP calculation: 10 + (intelligence × 2)."""
stats = Stats(intelligence=10)
assert stats.mana_points == 30 # 10 + (10 × 2)
stats = Stats(intelligence=15)
assert stats.mana_points == 40 # 10 + (15 × 2)
stats = Stats(intelligence=8)
assert stats.mana_points == 26 # 10 + (8 × 2)
def test_defense_calculation():
"""Test defense calculation: constitution // 2."""
stats = Stats(constitution=10)
assert stats.defense == 5 # 10 // 2
stats = Stats(constitution=15)
assert stats.defense == 7 # 15 // 2
stats = Stats(constitution=21)
assert stats.defense == 10 # 21 // 2
def test_resistance_calculation():
"""Test resistance calculation: wisdom // 2."""
stats = Stats(wisdom=10)
assert stats.resistance == 5 # 10 // 2
stats = Stats(wisdom=14)
assert stats.resistance == 7 # 14 // 2
stats = Stats(wisdom=9)
assert stats.resistance == 4 # 9 // 2
def test_stats_serialization():
"""Test to_dict() serialization."""
stats = Stats(
strength=15,
dexterity=12,
constitution=14,
intelligence=10,
wisdom=11,
charisma=8,
)
data = stats.to_dict()
assert data["strength"] == 15
assert data["dexterity"] == 12
assert data["constitution"] == 14
assert data["intelligence"] == 10
assert data["wisdom"] == 11
assert data["charisma"] == 8
def test_stats_deserialization():
"""Test from_dict() deserialization."""
data = {
"strength": 18,
"dexterity": 14,
"constitution": 16,
"intelligence": 12,
"wisdom": 10,
"charisma": 9,
}
stats = Stats.from_dict(data)
assert stats.strength == 18
assert stats.dexterity == 14
assert stats.constitution == 16
assert stats.intelligence == 12
assert stats.wisdom == 10
assert stats.charisma == 9
def test_stats_deserialization_with_missing_values():
"""Test from_dict() with missing values (should use defaults)."""
data = {
"strength": 15,
# Missing other stats
}
stats = Stats.from_dict(data)
assert stats.strength == 15
assert stats.dexterity == 10 # Default
assert stats.constitution == 10 # Default
assert stats.intelligence == 10 # Default
assert stats.wisdom == 10 # Default
assert stats.charisma == 10 # Default
def test_stats_round_trip_serialization():
"""Test that serialization and deserialization preserve data."""
original = Stats(
strength=20,
dexterity=15,
constitution=18,
intelligence=10,
wisdom=12,
charisma=14,
)
# Serialize then deserialize
data = original.to_dict()
restored = Stats.from_dict(data)
assert restored.strength == original.strength
assert restored.dexterity == original.dexterity
assert restored.constitution == original.constitution
assert restored.intelligence == original.intelligence
assert restored.wisdom == original.wisdom
assert restored.charisma == original.charisma
def test_stats_copy():
"""Test that copy() creates an independent copy."""
original = Stats(strength=15, dexterity=12, constitution=14)
copy = original.copy()
assert copy.strength == original.strength
assert copy.dexterity == original.dexterity
assert copy.constitution == original.constitution
# Modify copy
copy.strength = 20
# Original should be unchanged
assert original.strength == 15
assert copy.strength == 20
def test_stats_repr():
"""Test string representation."""
stats = Stats(strength=15, constitution=12, intelligence=10)
repr_str = repr(stats)
assert "STR=15" in repr_str
assert "CON=12" in repr_str
assert "INT=10" in repr_str
assert "HP=" in repr_str
assert "MP=" in repr_str
# =============================================================================
# LUK Computed Properties (Combat System Integration)
# =============================================================================
def test_crit_bonus_calculation():
"""Test crit bonus calculation: luck * 0.5%."""
stats = Stats(luck=8)
assert stats.crit_bonus == pytest.approx(0.04, abs=0.001) # 4%
stats = Stats(luck=12)
assert stats.crit_bonus == pytest.approx(0.06, abs=0.001) # 6%
stats = Stats(luck=0)
assert stats.crit_bonus == pytest.approx(0.0, abs=0.001) # 0%
def test_hit_bonus_calculation():
"""Test hit bonus (miss reduction): luck * 0.5%."""
stats = Stats(luck=8)
assert stats.hit_bonus == pytest.approx(0.04, abs=0.001) # 4%
stats = Stats(luck=12)
assert stats.hit_bonus == pytest.approx(0.06, abs=0.001) # 6%
stats = Stats(luck=20)
assert stats.hit_bonus == pytest.approx(0.10, abs=0.001) # 10%
def test_lucky_roll_chance_calculation():
"""Test lucky roll chance: 5% + (luck * 0.25%)."""
stats = Stats(luck=8)
# 5% + (8 * 0.25%) = 5% + 2% = 7%
assert stats.lucky_roll_chance == pytest.approx(0.07, abs=0.001)
stats = Stats(luck=12)
# 5% + (12 * 0.25%) = 5% + 3% = 8%
assert stats.lucky_roll_chance == pytest.approx(0.08, abs=0.001)
stats = Stats(luck=0)
# 5% + (0 * 0.25%) = 5%
assert stats.lucky_roll_chance == pytest.approx(0.05, abs=0.001)
def test_repr_includes_combat_bonuses():
"""Test that repr includes LUK-based combat bonuses."""
stats = Stats(luck=10)
repr_str = repr(stats)
assert "CRIT_BONUS=" in repr_str
assert "HIT_BONUS=" in repr_str
# =============================================================================
# Equipment Bonus Fields (Task 2.5)
# =============================================================================
def test_bonus_fields_default_to_zero():
"""Test that equipment bonus fields default to zero."""
stats = Stats()
assert stats.damage_bonus == 0
assert stats.defense_bonus == 0
assert stats.resistance_bonus == 0
def test_damage_property_with_no_bonus():
"""Test damage calculation: int(strength * 0.75) + damage_bonus with no bonus."""
stats = Stats(strength=10)
# int(10 * 0.75) = 7, no bonus
assert stats.damage == 7
stats = Stats(strength=14)
# int(14 * 0.75) = 10, no bonus
assert stats.damage == 10
def test_damage_property_with_bonus():
"""Test damage calculation includes damage_bonus from weapons."""
stats = Stats(strength=10, damage_bonus=15)
# int(10 * 0.75) + 15 = 7 + 15 = 22
assert stats.damage == 22
stats = Stats(strength=14, damage_bonus=8)
# int(14 * 0.75) + 8 = 10 + 8 = 18
assert stats.damage == 18
def test_defense_property_with_bonus():
"""Test defense calculation includes defense_bonus from armor."""
stats = Stats(constitution=10, defense_bonus=10)
# (10 // 2) + 10 = 5 + 10 = 15
assert stats.defense == 15
stats = Stats(constitution=20, defense_bonus=5)
# (20 // 2) + 5 = 10 + 5 = 15
assert stats.defense == 15
def test_resistance_property_with_bonus():
"""Test resistance calculation includes resistance_bonus from armor."""
stats = Stats(wisdom=10, resistance_bonus=8)
# (10 // 2) + 8 = 5 + 8 = 13
assert stats.resistance == 13
stats = Stats(wisdom=14, resistance_bonus=3)
# (14 // 2) + 3 = 7 + 3 = 10
assert stats.resistance == 10
def test_bonus_fields_serialization():
"""Test that bonus fields are included in to_dict()."""
stats = Stats(
strength=15,
damage_bonus=12,
defense_bonus=8,
resistance_bonus=5,
)
data = stats.to_dict()
assert data["damage_bonus"] == 12
assert data["defense_bonus"] == 8
assert data["resistance_bonus"] == 5
def test_bonus_fields_deserialization():
"""Test that bonus fields are restored from from_dict()."""
data = {
"strength": 15,
"damage_bonus": 12,
"defense_bonus": 8,
"resistance_bonus": 5,
}
stats = Stats.from_dict(data)
assert stats.damage_bonus == 12
assert stats.defense_bonus == 8
assert stats.resistance_bonus == 5
def test_bonus_fields_deserialization_defaults():
"""Test that missing bonus fields default to zero on deserialization."""
data = {
"strength": 15,
# No bonus fields
}
stats = Stats.from_dict(data)
assert stats.damage_bonus == 0
assert stats.defense_bonus == 0
assert stats.resistance_bonus == 0
def test_copy_includes_bonus_fields():
"""Test that copy() preserves bonus fields."""
original = Stats(
strength=15,
damage_bonus=10,
defense_bonus=8,
resistance_bonus=5,
)
copy = original.copy()
assert copy.damage_bonus == 10
assert copy.defense_bonus == 8
assert copy.resistance_bonus == 5
# Verify independence
copy.damage_bonus = 20
assert original.damage_bonus == 10
assert copy.damage_bonus == 20
def test_repr_includes_damage():
"""Test that repr includes the damage computed property."""
stats = Stats(strength=10, damage_bonus=15)
repr_str = repr(stats)
assert "DMG=" in repr_str