""" Tests for the Item Generator and Affix System. Tests cover: - Affix loading from YAML - Base item template loading - Item generation with affixes - Name generation - Stat combination """ import pytest from unittest.mock import patch, MagicMock from app.models.affixes import Affix, BaseItemTemplate from app.models.enums import AffixType, AffixTier, ItemRarity, ItemType, DamageType from app.services.affix_loader import AffixLoader, get_affix_loader from app.services.base_item_loader import BaseItemLoader, get_base_item_loader from app.services.item_generator import ItemGenerator, get_item_generator class TestAffixModel: """Tests for the Affix dataclass.""" def test_affix_creation(self): """Test creating an Affix instance.""" affix = Affix( affix_id="flaming", name="Flaming", affix_type=AffixType.PREFIX, tier=AffixTier.MINOR, description="Fire damage", damage_type=DamageType.FIRE, elemental_ratio=0.25, damage_bonus=3, ) assert affix.affix_id == "flaming" assert affix.name == "Flaming" assert affix.affix_type == AffixType.PREFIX assert affix.tier == AffixTier.MINOR assert affix.applies_elemental_damage() def test_affix_can_apply_to(self): """Test affix eligibility checking.""" # Weapon-only affix weapon_affix = Affix( affix_id="sharp", name="Sharp", affix_type=AffixType.PREFIX, tier=AffixTier.MINOR, allowed_item_types=["weapon"], ) assert weapon_affix.can_apply_to("weapon", "rare") assert not weapon_affix.can_apply_to("armor", "rare") def test_affix_legendary_only(self): """Test legendary-only affix restriction.""" legendary_affix = Affix( affix_id="vorpal", name="Vorpal", affix_type=AffixType.PREFIX, tier=AffixTier.LEGENDARY, required_rarity="legendary", ) assert legendary_affix.is_legendary_only() assert legendary_affix.can_apply_to("weapon", "legendary") assert not legendary_affix.can_apply_to("weapon", "epic") def test_affix_serialization(self): """Test affix to_dict and from_dict.""" affix = Affix( affix_id="of_strength", name="of Strength", affix_type=AffixType.SUFFIX, tier=AffixTier.MINOR, stat_bonuses={"strength": 2}, ) data = affix.to_dict() restored = Affix.from_dict(data) assert restored.affix_id == affix.affix_id assert restored.name == affix.name assert restored.stat_bonuses == affix.stat_bonuses class TestBaseItemTemplate: """Tests for the BaseItemTemplate dataclass.""" def test_template_creation(self): """Test creating a BaseItemTemplate instance.""" template = BaseItemTemplate( template_id="dagger", name="Dagger", item_type="weapon", base_damage=6, base_value=15, crit_chance=0.08, required_level=1, ) assert template.template_id == "dagger" assert template.base_damage == 6 assert template.crit_chance == 0.08 def test_template_rarity_eligibility(self): """Test template rarity checking.""" template = BaseItemTemplate( template_id="plate_armor", name="Plate Armor", item_type="armor", min_rarity="rare", ) assert template.can_generate_at_rarity("rare") assert template.can_generate_at_rarity("epic") assert template.can_generate_at_rarity("legendary") assert not template.can_generate_at_rarity("common") assert not template.can_generate_at_rarity("uncommon") def test_template_level_eligibility(self): """Test template level checking.""" template = BaseItemTemplate( template_id="greatsword", name="Greatsword", item_type="weapon", required_level=5, ) assert template.can_drop_for_level(5) assert template.can_drop_for_level(10) assert not template.can_drop_for_level(4) class TestAffixLoader: """Tests for the AffixLoader service.""" def test_loader_initialization(self): """Test AffixLoader initializes correctly.""" loader = get_affix_loader() assert loader is not None def test_load_prefixes(self): """Test loading prefixes from YAML.""" loader = get_affix_loader() loader.load_all() prefixes = loader.get_all_prefixes() assert len(prefixes) > 0 # Check for known prefix flaming = loader.get_affix("flaming") assert flaming is not None assert flaming.affix_type == AffixType.PREFIX assert flaming.name == "Flaming" def test_load_suffixes(self): """Test loading suffixes from YAML.""" loader = get_affix_loader() loader.load_all() suffixes = loader.get_all_suffixes() assert len(suffixes) > 0 # Check for known suffix of_strength = loader.get_affix("of_strength") assert of_strength is not None assert of_strength.affix_type == AffixType.SUFFIX assert of_strength.name == "of Strength" def test_get_eligible_prefixes(self): """Test filtering eligible prefixes.""" loader = get_affix_loader() # Get weapon prefixes for rare items eligible = loader.get_eligible_prefixes("weapon", "rare") assert len(eligible) > 0 # All should be applicable to weapons for prefix in eligible: assert prefix.can_apply_to("weapon", "rare") def test_get_random_prefix(self): """Test random prefix selection.""" loader = get_affix_loader() prefix = loader.get_random_prefix("weapon", "rare") assert prefix is not None assert prefix.affix_type == AffixType.PREFIX class TestBaseItemLoader: """Tests for the BaseItemLoader service.""" def test_loader_initialization(self): """Test BaseItemLoader initializes correctly.""" loader = get_base_item_loader() assert loader is not None def test_load_weapons(self): """Test loading weapon templates from YAML.""" loader = get_base_item_loader() loader.load_all() weapons = loader.get_all_weapons() assert len(weapons) > 0 # Check for known weapon dagger = loader.get_template("dagger") assert dagger is not None assert dagger.item_type == "weapon" assert dagger.base_damage > 0 def test_load_armor(self): """Test loading armor templates from YAML.""" loader = get_base_item_loader() loader.load_all() armor = loader.get_all_armor() assert len(armor) > 0 # Check for known armor chainmail = loader.get_template("chainmail") assert chainmail is not None assert chainmail.item_type == "armor" assert chainmail.base_defense > 0 def test_get_eligible_templates(self): """Test filtering eligible templates.""" loader = get_base_item_loader() # Get weapons for level 1, common rarity eligible = loader.get_eligible_templates("weapon", "common", 1) assert len(eligible) > 0 # All should be eligible for template in eligible: assert template.can_drop_for_level(1) assert template.can_generate_at_rarity("common") def test_get_random_template(self): """Test random template selection.""" loader = get_base_item_loader() template = loader.get_random_template("weapon", "common", 1) assert template is not None assert template.item_type == "weapon" class TestItemGenerator: """Tests for the ItemGenerator service.""" def test_generator_initialization(self): """Test ItemGenerator initializes correctly.""" generator = get_item_generator() assert generator is not None def test_generate_common_item(self): """Test generating a common item (no affixes).""" generator = get_item_generator() item = generator.generate_item("weapon", ItemRarity.COMMON, 1) assert item is not None assert item.rarity == ItemRarity.COMMON assert item.is_generated assert len(item.applied_affixes) == 0 # Common items have no generated name assert item.generated_name == item.name def test_generate_rare_item(self): """Test generating a rare item (1 affix).""" generator = get_item_generator() item = generator.generate_item("weapon", ItemRarity.RARE, 1) assert item is not None assert item.rarity == ItemRarity.RARE assert item.is_generated assert len(item.applied_affixes) == 1 assert item.generated_name != item.name def test_generate_epic_item(self): """Test generating an epic item (2 affixes).""" generator = get_item_generator() item = generator.generate_item("weapon", ItemRarity.EPIC, 1) assert item is not None assert item.rarity == ItemRarity.EPIC assert item.is_generated assert len(item.applied_affixes) == 2 def test_generate_legendary_item(self): """Test generating a legendary item (3 affixes).""" generator = get_item_generator() item = generator.generate_item("weapon", ItemRarity.LEGENDARY, 5) assert item is not None assert item.rarity == ItemRarity.LEGENDARY assert item.is_generated assert len(item.applied_affixes) == 3 def test_generated_name_format(self): """Test that generated names follow the expected format.""" generator = get_item_generator() # Generate multiple items and check name patterns for _ in range(10): item = generator.generate_item("weapon", ItemRarity.EPIC, 1) if item: name = item.get_display_name() # EPIC should have both prefix and suffix (typically) # Name should contain the base item name assert item.name in name or item.base_template_id in name.lower() def test_stat_combination(self): """Test that affix stats are properly combined.""" generator = get_item_generator() # Generate items and verify stat bonuses are present for _ in range(5): item = generator.generate_item("weapon", ItemRarity.RARE, 1) if item and item.applied_affixes: # Item should have some stat modifications # Either stat_bonuses, damage_bonus, or elemental properties has_stats = ( bool(item.stat_bonuses) or item.damage > 0 or item.elemental_ratio > 0 ) assert has_stats def test_generate_armor(self): """Test generating armor items.""" generator = get_item_generator() item = generator.generate_item("armor", ItemRarity.RARE, 1) assert item is not None assert item.item_type == ItemType.ARMOR assert item.defense > 0 or item.resistance > 0 def test_generate_loot_drop(self): """Test random loot drop generation.""" generator = get_item_generator() # Generate multiple drops to test randomness rarities_seen = set() for _ in range(50): item = generator.generate_loot_drop(5, luck_stat=8) if item: rarities_seen.add(item.rarity) # Should see at least common and uncommon assert ItemRarity.COMMON in rarities_seen or ItemRarity.UNCOMMON in rarities_seen def test_luck_affects_rarity(self): """Test that higher luck increases rare drops.""" generator = get_item_generator() # This is a statistical test - higher luck should trend toward better rarity low_luck_rares = 0 high_luck_rares = 0 for _ in range(100): low_luck_item = generator.generate_loot_drop(5, luck_stat=1) high_luck_item = generator.generate_loot_drop(5, luck_stat=20) if low_luck_item and low_luck_item.rarity in [ItemRarity.RARE, ItemRarity.EPIC, ItemRarity.LEGENDARY]: low_luck_rares += 1 if high_luck_item and high_luck_item.rarity in [ItemRarity.RARE, ItemRarity.EPIC, ItemRarity.LEGENDARY]: high_luck_rares += 1 # High luck should generally produce more rare+ items # (This may occasionally fail due to randomness, but should pass most of the time) # We're just checking the trend, not a strict guarantee # logger.info(f"Low luck rares: {low_luck_rares}, High luck rares: {high_luck_rares}") class TestNameGeneration: """Tests specifically for item name generation.""" def test_prefix_only_name(self): """Test name with only a prefix.""" generator = get_item_generator() # Create mock affixes prefix = Affix( affix_id="flaming", name="Flaming", affix_type=AffixType.PREFIX, tier=AffixTier.MINOR, ) name = generator._build_name("Dagger", [prefix], []) assert name == "Flaming Dagger" def test_suffix_only_name(self): """Test name with only a suffix.""" generator = get_item_generator() suffix = Affix( affix_id="of_strength", name="of Strength", affix_type=AffixType.SUFFIX, tier=AffixTier.MINOR, ) name = generator._build_name("Dagger", [], [suffix]) assert name == "Dagger of Strength" def test_full_name(self): """Test name with prefix and suffix.""" generator = get_item_generator() prefix = Affix( affix_id="flaming", name="Flaming", affix_type=AffixType.PREFIX, tier=AffixTier.MINOR, ) suffix = Affix( affix_id="of_strength", name="of Strength", affix_type=AffixType.SUFFIX, tier=AffixTier.MINOR, ) name = generator._build_name("Dagger", [prefix], [suffix]) assert name == "Flaming Dagger of Strength" def test_multiple_prefixes(self): """Test name with multiple prefixes.""" generator = get_item_generator() prefix1 = Affix( affix_id="flaming", name="Flaming", affix_type=AffixType.PREFIX, tier=AffixTier.MINOR, ) prefix2 = Affix( affix_id="sharp", name="Sharp", affix_type=AffixType.PREFIX, tier=AffixTier.MINOR, ) name = generator._build_name("Dagger", [prefix1, prefix2], []) assert name == "Flaming Sharp Dagger" class TestStatCombination: """Tests for combining affix stats.""" def test_combine_stat_bonuses(self): """Test combining stat bonuses from multiple affixes.""" generator = get_item_generator() affix1 = Affix( affix_id="test1", name="Test1", affix_type=AffixType.PREFIX, tier=AffixTier.MINOR, stat_bonuses={"strength": 2, "constitution": 1}, ) affix2 = Affix( affix_id="test2", name="Test2", affix_type=AffixType.SUFFIX, tier=AffixTier.MINOR, stat_bonuses={"strength": 3, "dexterity": 2}, ) combined = generator._combine_affix_stats([affix1, affix2]) assert combined["stat_bonuses"]["strength"] == 5 assert combined["stat_bonuses"]["constitution"] == 1 assert combined["stat_bonuses"]["dexterity"] == 2 def test_combine_damage_bonuses(self): """Test combining damage bonuses.""" generator = get_item_generator() affix1 = Affix( affix_id="sharp", name="Sharp", affix_type=AffixType.PREFIX, tier=AffixTier.MINOR, damage_bonus=3, ) affix2 = Affix( affix_id="keen", name="Keen", affix_type=AffixType.PREFIX, tier=AffixTier.MAJOR, damage_bonus=5, ) combined = generator._combine_affix_stats([affix1, affix2]) assert combined["damage_bonus"] == 8 def test_combine_crit_bonuses(self): """Test combining crit chance and multiplier bonuses.""" generator = get_item_generator() affix1 = Affix( affix_id="sharp", name="Sharp", affix_type=AffixType.PREFIX, tier=AffixTier.MINOR, crit_chance_bonus=0.02, ) affix2 = Affix( affix_id="keen", name="Keen", affix_type=AffixType.PREFIX, tier=AffixTier.MAJOR, crit_chance_bonus=0.04, crit_multiplier_bonus=0.5, ) combined = generator._combine_affix_stats([affix1, affix2]) assert combined["crit_chance_bonus"] == pytest.approx(0.06) assert combined["crit_multiplier_bonus"] == pytest.approx(0.5)