Files
Code_of_Conquest/api/tests/test_combat_simulation.py
2025-11-24 23:10:55 -06:00

510 lines
14 KiB
Python
Raw 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.
"""
Integration tests for combat system.
Tests the complete combat flow including damage calculation, effects, and turn order.
"""
import pytest
from app.models.stats import Stats
from app.models.items import Item
from app.models.effects import Effect
from app.models.abilities import Ability
from app.models.combat import Combatant, CombatEncounter
from app.models.character import Character
from app.models.skills import PlayerClass
from app.models.enums import (
ItemType,
DamageType,
EffectType,
StatType,
AbilityType,
CombatStatus,
)
@pytest.fixture
def warrior_combatant():
"""Create a warrior combatant for testing."""
stats = Stats(strength=15, dexterity=10, constitution=14, intelligence=8, wisdom=10, charisma=11)
return Combatant(
combatant_id="warrior_1",
name="Test Warrior",
is_player=True,
current_hp=stats.hit_points,
max_hp=stats.hit_points,
current_mp=stats.mana_points,
max_mp=stats.mana_points,
stats=stats,
abilities=["basic_attack", "power_strike"],
)
@pytest.fixture
def goblin_combatant():
"""Create a goblin enemy for testing."""
stats = Stats(strength=8, dexterity=12, constitution=10, intelligence=6, wisdom=8, charisma=6)
return Combatant(
combatant_id="goblin_1",
name="Goblin",
is_player=False,
current_hp=stats.hit_points,
max_hp=stats.hit_points,
current_mp=stats.mana_points,
max_mp=stats.mana_points,
stats=stats,
abilities=["basic_attack"],
)
def test_combatant_creation(warrior_combatant):
"""Test creating a Combatant."""
assert warrior_combatant.combatant_id == "warrior_1"
assert warrior_combatant.name == "Test Warrior"
assert warrior_combatant.is_player == True
assert warrior_combatant.is_alive() == True
assert warrior_combatant.is_stunned() == False
def test_combatant_take_damage(warrior_combatant):
"""Test taking damage."""
initial_hp = warrior_combatant.current_hp
damage_dealt = warrior_combatant.take_damage(10)
assert damage_dealt == 10
assert warrior_combatant.current_hp == initial_hp - 10
def test_combatant_take_damage_with_shield(warrior_combatant):
"""Test taking damage with shield absorption."""
# Add a shield effect
shield = Effect(
effect_id="shield_1",
name="Shield",
effect_type=EffectType.SHIELD,
duration=3,
power=15,
)
warrior_combatant.add_effect(shield)
initial_hp = warrior_combatant.current_hp
# Deal 10 damage - should be fully absorbed by shield
damage_dealt = warrior_combatant.take_damage(10)
assert damage_dealt == 0 # No HP damage
assert warrior_combatant.current_hp == initial_hp
def test_combatant_death(warrior_combatant):
"""Test combatant death."""
assert warrior_combatant.is_alive() == True
# Deal massive damage
warrior_combatant.take_damage(1000)
assert warrior_combatant.is_alive() == False
assert warrior_combatant.is_dead() == True
def test_combatant_healing(warrior_combatant):
"""Test healing."""
# Take some damage first
warrior_combatant.take_damage(20)
damaged_hp = warrior_combatant.current_hp
# Heal
healed = warrior_combatant.heal(10)
assert healed == 10
assert warrior_combatant.current_hp == damaged_hp + 10
def test_combatant_healing_capped_at_max(warrior_combatant):
"""Test that healing cannot exceed max HP."""
max_hp = warrior_combatant.max_hp
# Try to heal beyond max
healed = warrior_combatant.heal(1000)
assert warrior_combatant.current_hp == max_hp
def test_combatant_stun_effect(warrior_combatant):
"""Test stun effect prevents actions."""
assert warrior_combatant.is_stunned() == False
# Add stun effect
stun = Effect(
effect_id="stun_1",
name="Stunned",
effect_type=EffectType.STUN,
duration=1,
power=0,
)
warrior_combatant.add_effect(stun)
assert warrior_combatant.is_stunned() == True
def test_combatant_tick_effects(warrior_combatant):
"""Test that ticking effects deals damage/healing."""
# Add a DOT effect
poison = Effect(
effect_id="poison_1",
name="Poison",
effect_type=EffectType.DOT,
duration=3,
power=5,
)
warrior_combatant.add_effect(poison)
initial_hp = warrior_combatant.current_hp
# Tick effects
results = warrior_combatant.tick_effects()
# Should have taken 5 poison damage
assert len(results) == 1
assert results[0]["effect_type"] == "dot"
assert results[0]["value"] == 5
assert warrior_combatant.current_hp == initial_hp - 5
def test_combatant_effect_expiration(warrior_combatant):
"""Test that expired effects are removed."""
# Add effect with 1 turn duration
dot = Effect(
effect_id="burn_1",
name="Burning",
effect_type=EffectType.DOT,
duration=1,
power=5,
)
warrior_combatant.add_effect(dot)
assert len(warrior_combatant.active_effects) == 1
# Tick - effect should expire
results = warrior_combatant.tick_effects()
assert results[0]["expired"] == True
assert len(warrior_combatant.active_effects) == 0 # Removed
def test_ability_mana_cost(warrior_combatant):
"""Test ability mana cost and usage."""
ability = Ability(
ability_id="fireball",
name="Fireball",
description="Fiery explosion",
ability_type=AbilityType.SPELL,
base_power=30,
damage_type=DamageType.FIRE,
mana_cost=15,
)
initial_mp = warrior_combatant.current_mp
# Check if can use
assert warrior_combatant.can_use_ability("fireball", ability) == False # Not in ability list
warrior_combatant.abilities.append("fireball")
assert warrior_combatant.can_use_ability("fireball", ability) == True
# Use ability
warrior_combatant.use_ability_cost(ability, "fireball")
assert warrior_combatant.current_mp == initial_mp - 15
def test_ability_cooldown(warrior_combatant):
"""Test ability cooldowns."""
ability = Ability(
ability_id="power_strike",
name="Power Strike",
description="Powerful attack",
ability_type=AbilityType.SKILL,
base_power=20,
cooldown=3,
)
warrior_combatant.abilities.append("power_strike")
# Can use initially
assert warrior_combatant.can_use_ability("power_strike", ability) == True
# Use ability
warrior_combatant.use_ability_cost(ability, "power_strike")
# Now on cooldown
assert "power_strike" in warrior_combatant.cooldowns
assert warrior_combatant.cooldowns["power_strike"] == 3
assert warrior_combatant.can_use_ability("power_strike", ability) == False
# Tick cooldown
warrior_combatant.tick_cooldowns()
assert warrior_combatant.cooldowns["power_strike"] == 2
# Tick more
warrior_combatant.tick_cooldowns()
warrior_combatant.tick_cooldowns()
# Should be available again
assert "power_strike" not in warrior_combatant.cooldowns
assert warrior_combatant.can_use_ability("power_strike", ability) == True
def test_combat_encounter_initialization(warrior_combatant, goblin_combatant):
"""Test initializing a combat encounter."""
encounter = CombatEncounter(
encounter_id="combat_001",
combatants=[warrior_combatant, goblin_combatant],
)
encounter.initialize_combat()
# Should have turn order
assert len(encounter.turn_order) == 2
assert encounter.round_number == 1
assert encounter.status == CombatStatus.ACTIVE
# Both combatants should have initiative
assert warrior_combatant.initiative > 0
assert goblin_combatant.initiative > 0
def test_combat_turn_advancement(warrior_combatant, goblin_combatant):
"""Test advancing turns in combat."""
encounter = CombatEncounter(
encounter_id="combat_001",
combatants=[warrior_combatant, goblin_combatant],
)
encounter.initialize_combat()
# Get first combatant
first = encounter.get_current_combatant()
assert first is not None
# Advance turn
encounter.advance_turn()
# Should be second combatant now
second = encounter.get_current_combatant()
assert second is not None
assert second.combatant_id != first.combatant_id
# Advance again - should cycle back to first and increment round
encounter.advance_turn()
assert encounter.round_number == 2
third = encounter.get_current_combatant()
assert third.combatant_id == first.combatant_id
def test_combat_victory_condition(warrior_combatant, goblin_combatant):
"""Test victory condition detection."""
encounter = CombatEncounter(
encounter_id="combat_001",
combatants=[warrior_combatant, goblin_combatant],
)
encounter.initialize_combat()
# Kill the goblin
goblin_combatant.current_hp = 0
# Check end condition
status = encounter.check_end_condition()
assert status == CombatStatus.VICTORY
assert encounter.status == CombatStatus.VICTORY
def test_combat_defeat_condition(warrior_combatant, goblin_combatant):
"""Test defeat condition detection."""
encounter = CombatEncounter(
encounter_id="combat_001",
combatants=[warrior_combatant, goblin_combatant],
)
encounter.initialize_combat()
# Kill the warrior
warrior_combatant.current_hp = 0
# Check end condition
status = encounter.check_end_condition()
assert status == CombatStatus.DEFEAT
assert encounter.status == CombatStatus.DEFEAT
def test_combat_start_turn_processing(warrior_combatant):
"""Test start_turn() processes effects and cooldowns."""
encounter = CombatEncounter(
encounter_id="combat_001",
combatants=[warrior_combatant],
)
# Initialize combat to set turn order
encounter.initialize_combat()
# Add a DOT effect
poison = Effect(
effect_id="poison_1",
name="Poison",
effect_type=EffectType.DOT,
duration=3,
power=5,
)
warrior_combatant.add_effect(poison)
# Add a cooldown
warrior_combatant.cooldowns["power_strike"] = 2
initial_hp = warrior_combatant.current_hp
# Start turn
results = encounter.start_turn()
# Effects should have ticked
assert len(results) == 1
assert warrior_combatant.current_hp == initial_hp - 5
# Cooldown should have decreased
assert warrior_combatant.cooldowns["power_strike"] == 1
def test_combat_logging(warrior_combatant, goblin_combatant):
"""Test combat log entries."""
encounter = CombatEncounter(
encounter_id="combat_001",
combatants=[warrior_combatant, goblin_combatant],
)
encounter.log_action("attack", "warrior_1", "Warrior attacks Goblin for 10 damage")
assert len(encounter.combat_log) == 1
assert encounter.combat_log[0]["action_type"] == "attack"
assert encounter.combat_log[0]["combatant_id"] == "warrior_1"
assert "Warrior attacks Goblin" in encounter.combat_log[0]["message"]
def test_ability_damage_calculation():
"""Test ability power calculation with stat scaling."""
stats = Stats(strength=20, intelligence=16)
# Physical ability scaling with strength
physical = Ability(
ability_id="cleave",
name="Cleave",
description="Powerful strike",
ability_type=AbilityType.SKILL,
base_power=15,
scaling_stat=StatType.STRENGTH,
scaling_factor=0.5,
)
power = physical.calculate_power(stats)
# 15 (base) + (20 strength × 0.5) = 15 + 10 = 25
assert power == 25
# Magical ability scaling with intelligence
magical = Ability(
ability_id="fireball",
name="Fireball",
description="Fire spell",
ability_type=AbilityType.SPELL,
base_power=20,
scaling_stat=StatType.INTELLIGENCE,
scaling_factor=0.5,
)
power = magical.calculate_power(stats)
# 20 (base) + (16 intelligence × 0.5) = 20 + 8 = 28
assert power == 28
def test_full_combat_simulation():
"""Integration test: Full combat simulation with all systems."""
# Create warrior
warrior_stats = Stats(strength=15, constitution=14)
warrior = Combatant(
combatant_id="hero",
name="Hero",
is_player=True,
current_hp=warrior_stats.hit_points,
max_hp=warrior_stats.hit_points,
current_mp=warrior_stats.mana_points,
max_mp=warrior_stats.mana_points,
stats=warrior_stats,
)
# Create goblin
goblin_stats = Stats(strength=8, constitution=10)
goblin = Combatant(
combatant_id="goblin",
name="Goblin",
is_player=False,
current_hp=goblin_stats.hit_points,
max_hp=goblin_stats.hit_points,
current_mp=goblin_stats.mana_points,
max_mp=goblin_stats.mana_points,
stats=goblin_stats,
)
# Create encounter
encounter = CombatEncounter(
encounter_id="test_combat",
combatants=[warrior, goblin],
)
encounter.initialize_combat()
# Verify setup
assert encounter.status == CombatStatus.ACTIVE
assert len(encounter.turn_order) == 2
assert warrior.is_alive() and goblin.is_alive()
# Simulate turns until combat ends
max_turns = 50 # Increased to ensure combat completes
turn_count = 0
while encounter.status == CombatStatus.ACTIVE and turn_count < max_turns:
# Get current combatant
current = encounter.get_current_combatant()
# Start turn (tick effects)
encounter.start_turn()
if current and current.is_alive() and not current.is_stunned():
# Simple AI: deal damage to opponent
if current.combatant_id == "hero":
target = goblin
else:
target = warrior
# Calculate simple attack damage: strength / 2 - target defense
damage = max(1, (current.stats.strength // 2) - target.stats.defense)
target.take_damage(damage)
encounter.log_action(
"attack",
current.combatant_id,
f"{current.name} attacks {target.name} for {damage} damage",
)
# Check for combat end
encounter.check_end_condition()
# Advance turn
encounter.advance_turn()
turn_count += 1
# Combat should have ended
assert encounter.status in [CombatStatus.VICTORY, CombatStatus.DEFEAT]
assert len(encounter.combat_log) > 0