- Implement Combat Service - Implement Damage Calculator - Implement Effect Processor - Implement Combat Actions - Created Combat API Endpoints
400 lines
13 KiB
Python
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
|