Files
Code_of_Conquest/api/tests/test_enemy_loader.py
Phillip Tarrant 03ab783eeb Combat Backend & Data Models
- Implement Combat Service
- Implement Damage Calculator
- Implement Effect Processor
- Implement Combat Actions
- Created Combat API Endpoints
2025-11-26 15:43:20 -06:00

400 lines
13 KiB
Python

"""
Unit tests for EnemyTemplate model and EnemyLoader service.
Tests enemy loading, serialization, and filtering functionality.
"""
import pytest
from pathlib import Path
from app.models.enemy import EnemyTemplate, EnemyDifficulty, LootEntry
from app.models.stats import Stats
from app.services.enemy_loader import EnemyLoader
# =============================================================================
# EnemyTemplate Model Tests
# =============================================================================
class TestEnemyTemplate:
"""Tests for EnemyTemplate dataclass."""
def test_create_basic_enemy(self):
"""Test creating an enemy with minimal attributes."""
enemy = EnemyTemplate(
enemy_id="test_enemy",
name="Test Enemy",
description="A test enemy",
base_stats=Stats(strength=10, constitution=8),
)
assert enemy.enemy_id == "test_enemy"
assert enemy.name == "Test Enemy"
assert enemy.base_stats.strength == 10
assert enemy.difficulty == EnemyDifficulty.EASY # Default
def test_enemy_with_full_attributes(self):
"""Test creating an enemy with all attributes."""
loot = [
LootEntry(item_id="sword", drop_chance=0.5),
LootEntry(item_id="gold", drop_chance=1.0, quantity_min=5, quantity_max=10),
]
enemy = EnemyTemplate(
enemy_id="goblin_boss",
name="Goblin Boss",
description="A fearsome goblin leader",
base_stats=Stats(strength=14, dexterity=12, constitution=12),
abilities=["basic_attack", "power_strike"],
loot_table=loot,
experience_reward=100,
gold_reward_min=20,
gold_reward_max=50,
difficulty=EnemyDifficulty.HARD,
tags=["humanoid", "goblinoid", "boss"],
base_damage=12,
crit_chance=0.15,
flee_chance=0.25,
)
assert enemy.enemy_id == "goblin_boss"
assert enemy.experience_reward == 100
assert enemy.difficulty == EnemyDifficulty.HARD
assert len(enemy.loot_table) == 2
assert len(enemy.abilities) == 2
assert "boss" in enemy.tags
def test_is_boss(self):
"""Test boss detection."""
easy_enemy = EnemyTemplate(
enemy_id="minion",
name="Minion",
description="",
base_stats=Stats(),
difficulty=EnemyDifficulty.EASY,
)
boss_enemy = EnemyTemplate(
enemy_id="boss",
name="Boss",
description="",
base_stats=Stats(),
difficulty=EnemyDifficulty.BOSS,
)
assert not easy_enemy.is_boss()
assert boss_enemy.is_boss()
def test_has_tag(self):
"""Test tag checking."""
enemy = EnemyTemplate(
enemy_id="zombie",
name="Zombie",
description="",
base_stats=Stats(),
tags=["undead", "slow", "Humanoid"], # Mixed case
)
assert enemy.has_tag("undead")
assert enemy.has_tag("UNDEAD") # Case insensitive
assert enemy.has_tag("humanoid")
assert not enemy.has_tag("beast")
def test_get_gold_reward(self):
"""Test gold reward generation."""
enemy = EnemyTemplate(
enemy_id="test",
name="Test",
description="",
base_stats=Stats(),
gold_reward_min=10,
gold_reward_max=20,
)
# Run multiple times to check range
for _ in range(50):
gold = enemy.get_gold_reward()
assert 10 <= gold <= 20
def test_roll_loot_empty_table(self):
"""Test loot rolling with empty table."""
enemy = EnemyTemplate(
enemy_id="test",
name="Test",
description="",
base_stats=Stats(),
loot_table=[],
)
drops = enemy.roll_loot()
assert drops == []
def test_roll_loot_guaranteed_drop(self):
"""Test loot rolling with guaranteed drop."""
enemy = EnemyTemplate(
enemy_id="test",
name="Test",
description="",
base_stats=Stats(),
loot_table=[
LootEntry(item_id="guaranteed_item", drop_chance=1.0),
],
)
drops = enemy.roll_loot()
assert len(drops) == 1
assert drops[0]["item_id"] == "guaranteed_item"
def test_serialization_round_trip(self):
"""Test that to_dict/from_dict preserves data."""
original = EnemyTemplate(
enemy_id="test_enemy",
name="Test Enemy",
description="A test description",
base_stats=Stats(strength=15, dexterity=12, luck=10),
abilities=["attack", "defend"],
loot_table=[
LootEntry(item_id="sword", drop_chance=0.5),
],
experience_reward=50,
gold_reward_min=10,
gold_reward_max=25,
difficulty=EnemyDifficulty.MEDIUM,
tags=["humanoid", "test"],
base_damage=8,
crit_chance=0.10,
flee_chance=0.40,
)
# Serialize and deserialize
data = original.to_dict()
restored = EnemyTemplate.from_dict(data)
# Verify all fields match
assert restored.enemy_id == original.enemy_id
assert restored.name == original.name
assert restored.description == original.description
assert restored.base_stats.strength == original.base_stats.strength
assert restored.base_stats.luck == original.base_stats.luck
assert restored.abilities == original.abilities
assert len(restored.loot_table) == len(original.loot_table)
assert restored.experience_reward == original.experience_reward
assert restored.gold_reward_min == original.gold_reward_min
assert restored.gold_reward_max == original.gold_reward_max
assert restored.difficulty == original.difficulty
assert restored.tags == original.tags
assert restored.base_damage == original.base_damage
assert restored.crit_chance == pytest.approx(original.crit_chance)
assert restored.flee_chance == pytest.approx(original.flee_chance)
class TestLootEntry:
"""Tests for LootEntry dataclass."""
def test_create_loot_entry(self):
"""Test creating a loot entry."""
entry = LootEntry(
item_id="gold_coin",
drop_chance=0.75,
quantity_min=5,
quantity_max=15,
)
assert entry.item_id == "gold_coin"
assert entry.drop_chance == 0.75
assert entry.quantity_min == 5
assert entry.quantity_max == 15
def test_loot_entry_defaults(self):
"""Test loot entry default values."""
entry = LootEntry(item_id="item")
assert entry.drop_chance == 0.1
assert entry.quantity_min == 1
assert entry.quantity_max == 1
# =============================================================================
# EnemyLoader Service Tests
# =============================================================================
class TestEnemyLoader:
"""Tests for EnemyLoader service."""
@pytest.fixture
def loader(self):
"""Create an enemy loader with the actual data directory."""
return EnemyLoader()
def test_load_goblin(self, loader):
"""Test loading the goblin enemy."""
enemy = loader.load_enemy("goblin")
assert enemy is not None
assert enemy.enemy_id == "goblin"
assert enemy.name == "Goblin Scout"
assert enemy.difficulty == EnemyDifficulty.EASY
assert "humanoid" in enemy.tags
assert "goblinoid" in enemy.tags
def test_load_goblin_shaman(self, loader):
"""Test loading the goblin shaman."""
enemy = loader.load_enemy("goblin_shaman")
assert enemy is not None
assert enemy.enemy_id == "goblin_shaman"
assert enemy.base_stats.intelligence == 12 # Caster stats
assert "caster" in enemy.tags
def test_load_dire_wolf(self, loader):
"""Test loading the dire wolf."""
enemy = loader.load_enemy("dire_wolf")
assert enemy is not None
assert enemy.difficulty == EnemyDifficulty.MEDIUM
assert "beast" in enemy.tags
assert enemy.base_stats.strength == 14
def test_load_bandit(self, loader):
"""Test loading the bandit."""
enemy = loader.load_enemy("bandit")
assert enemy is not None
assert enemy.difficulty == EnemyDifficulty.MEDIUM
assert "rogue" in enemy.tags
assert enemy.crit_chance == 0.12
def test_load_skeleton_warrior(self, loader):
"""Test loading the skeleton warrior."""
enemy = loader.load_enemy("skeleton_warrior")
assert enemy is not None
assert "undead" in enemy.tags
assert "fearless" in enemy.tags
def test_load_orc_berserker(self, loader):
"""Test loading the orc berserker."""
enemy = loader.load_enemy("orc_berserker")
assert enemy is not None
assert enemy.difficulty == EnemyDifficulty.HARD
assert enemy.base_stats.strength == 18
assert enemy.base_damage == 15
def test_load_nonexistent_enemy(self, loader):
"""Test loading an enemy that doesn't exist."""
enemy = loader.load_enemy("nonexistent_enemy_12345")
assert enemy is None
def test_load_all_enemies(self, loader):
"""Test loading all enemies."""
enemies = loader.load_all_enemies()
# Should have at least our 6 sample enemies
assert len(enemies) >= 6
assert "goblin" in enemies
assert "goblin_shaman" in enemies
assert "dire_wolf" in enemies
assert "bandit" in enemies
assert "skeleton_warrior" in enemies
assert "orc_berserker" in enemies
def test_get_enemies_by_difficulty(self, loader):
"""Test filtering enemies by difficulty."""
loader.load_all_enemies() # Ensure loaded
easy_enemies = loader.get_enemies_by_difficulty(EnemyDifficulty.EASY)
medium_enemies = loader.get_enemies_by_difficulty(EnemyDifficulty.MEDIUM)
hard_enemies = loader.get_enemies_by_difficulty(EnemyDifficulty.HARD)
# Check we got enemies in each category
assert len(easy_enemies) >= 2 # goblin, goblin_shaman
assert len(medium_enemies) >= 3 # dire_wolf, bandit, skeleton_warrior
assert len(hard_enemies) >= 1 # orc_berserker
# Verify difficulty is correct
for enemy in easy_enemies:
assert enemy.difficulty == EnemyDifficulty.EASY
def test_get_enemies_by_tag(self, loader):
"""Test filtering enemies by tag."""
loader.load_all_enemies()
humanoids = loader.get_enemies_by_tag("humanoid")
undead = loader.get_enemies_by_tag("undead")
beasts = loader.get_enemies_by_tag("beast")
# Verify results
assert len(humanoids) >= 3 # goblin, goblin_shaman, bandit, orc
assert len(undead) >= 1 # skeleton_warrior
assert len(beasts) >= 1 # dire_wolf
# Verify tags
for enemy in humanoids:
assert enemy.has_tag("humanoid")
def test_get_random_enemies(self, loader):
"""Test random enemy selection."""
loader.load_all_enemies()
# Get 3 random enemies
random_enemies = loader.get_random_enemies(count=3)
assert len(random_enemies) == 3
# All should be EnemyTemplate instances
for enemy in random_enemies:
assert isinstance(enemy, EnemyTemplate)
def test_get_random_enemies_with_filters(self, loader):
"""Test random selection with difficulty filter."""
loader.load_all_enemies()
# Get only easy enemies
easy_enemies = loader.get_random_enemies(
count=5,
difficulty=EnemyDifficulty.EASY,
)
# All returned enemies should be easy
for enemy in easy_enemies:
assert enemy.difficulty == EnemyDifficulty.EASY
def test_cache_behavior(self, loader):
"""Test that caching works correctly."""
# Load an enemy twice
enemy1 = loader.load_enemy("goblin")
enemy2 = loader.load_enemy("goblin")
# Should be the same object (cached)
assert enemy1 is enemy2
# Clear cache
loader.clear_cache()
# Load again
enemy3 = loader.load_enemy("goblin")
# Should be a new object
assert enemy3 is not enemy1
assert enemy3.enemy_id == enemy1.enemy_id
# =============================================================================
# EnemyDifficulty Enum Tests
# =============================================================================
class TestEnemyDifficulty:
"""Tests for EnemyDifficulty enum."""
def test_difficulty_values(self):
"""Test difficulty enum values."""
assert EnemyDifficulty.EASY.value == "easy"
assert EnemyDifficulty.MEDIUM.value == "medium"
assert EnemyDifficulty.HARD.value == "hard"
assert EnemyDifficulty.BOSS.value == "boss"
def test_difficulty_from_string(self):
"""Test creating difficulty from string."""
assert EnemyDifficulty("easy") == EnemyDifficulty.EASY
assert EnemyDifficulty("hard") == EnemyDifficulty.HARD