""" 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