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

455 lines
13 KiB
Python

"""
Unit tests for Character dataclass.
Tests the critical get_effective_stats() method which combines all stat modifiers,
as well as inventory, equipment, experience, and serialization.
"""
import pytest
from app.models.character import Character
from app.models.stats import Stats
from app.models.items import Item
from app.models.effects import Effect
from app.models.skills import PlayerClass, SkillTree, SkillNode
from app.models.origins import Origin, StartingLocation, StartingBonus
from app.models.enums import ItemType, EffectType, StatType, DamageType
@pytest.fixture
def basic_player_class():
"""Create a basic player class for testing."""
base_stats = Stats(strength=12, dexterity=10, constitution=14, intelligence=8, wisdom=10, charisma=11)
# Create a simple skill tree
skill_tree = SkillTree(
tree_id="warrior_offense",
name="Warrior Offense",
description="Offensive combat skills",
nodes=[
SkillNode(
skill_id="power_strike",
name="Power Strike",
description="+5 Strength",
tier=1,
effects={"strength": 5},
),
],
)
return PlayerClass(
class_id="warrior",
name="Warrior",
description="Strong melee fighter",
base_stats=base_stats,
skill_trees=[skill_tree],
starting_equipment=["basic_sword"],
starting_abilities=["basic_attack"],
)
@pytest.fixture
def basic_origin():
"""Create a basic origin for testing."""
starting_location = StartingLocation(
id="test_location",
name="Test Village",
region="Test Region",
description="A simple test location"
)
starting_bonus = StartingBonus(
trait="Test Trait",
description="A test trait for testing",
effect="+1 to all stats"
)
return Origin(
id="test_origin",
name="Test Origin",
description="A test origin for character testing",
starting_location=starting_location,
narrative_hooks=["Test hook 1", "Test hook 2"],
starting_bonus=starting_bonus
)
@pytest.fixture
def basic_character(basic_player_class, basic_origin):
"""Create a basic character for testing."""
return Character(
character_id="char_001",
user_id="user_001",
name="Test Hero",
player_class=basic_player_class,
origin=basic_origin,
level=1,
experience=0,
base_stats=basic_player_class.base_stats.copy(),
)
def test_character_creation(basic_character):
"""Test creating a Character instance."""
assert basic_character.character_id == "char_001"
assert basic_character.user_id == "user_001"
assert basic_character.name == "Test Hero"
assert basic_character.level == 1
assert basic_character.experience == 0
assert basic_character.gold == 0
def test_get_effective_stats_base_only(basic_character):
"""Test get_effective_stats() with only base stats (no modifiers)."""
effective = basic_character.get_effective_stats()
# Should match base stats exactly
assert effective.strength == 12
assert effective.dexterity == 10
assert effective.constitution == 14
assert effective.intelligence == 8
assert effective.wisdom == 10
assert effective.charisma == 11
def test_get_effective_stats_with_equipment(basic_character):
"""Test get_effective_stats() with equipped items."""
# Create a weapon with +5 strength
weapon = Item(
item_id="iron_sword",
name="Iron Sword",
item_type=ItemType.WEAPON,
description="A sturdy iron sword",
stat_bonuses={"strength": 5},
damage=10,
)
basic_character.equipped["weapon"] = weapon
effective = basic_character.get_effective_stats()
# Strength should be base (12) + weapon (5) = 17
assert effective.strength == 17
assert effective.dexterity == 10 # Unchanged
def test_get_effective_stats_with_skill_bonuses(basic_character):
"""Test get_effective_stats() with skill tree bonuses."""
# Unlock the "power_strike" skill which gives +5 strength
basic_character.unlocked_skills.append("power_strike")
effective = basic_character.get_effective_stats()
# Strength should be base (12) + skill (5) = 17
assert effective.strength == 17
def test_get_effective_stats_with_all_modifiers(basic_character):
"""Test get_effective_stats() with equipment + skills + active effects."""
# Add equipment: +5 strength
weapon = Item(
item_id="iron_sword",
name="Iron Sword",
item_type=ItemType.WEAPON,
description="A sturdy iron sword",
stat_bonuses={"strength": 5},
damage=10,
)
basic_character.equipped["weapon"] = weapon
# Unlock skill: +5 strength
basic_character.unlocked_skills.append("power_strike")
# Add buff effect: +3 strength
buff = Effect(
effect_id="str_buff",
name="Strength Boost",
effect_type=EffectType.BUFF,
duration=3,
power=3,
stat_affected=StatType.STRENGTH,
)
effective = basic_character.get_effective_stats([buff])
# Total strength: 12 (base) + 5 (weapon) + 5 (skill) + 3 (buff) = 25
assert effective.strength == 25
def test_get_effective_stats_with_debuff(basic_character):
"""Test get_effective_stats() with a debuff."""
debuff = Effect(
effect_id="weakened",
name="Weakened",
effect_type=EffectType.DEBUFF,
duration=2,
power=5,
stat_affected=StatType.STRENGTH,
)
effective = basic_character.get_effective_stats([debuff])
# Strength: 12 (base) - 5 (debuff) = 7
assert effective.strength == 7
def test_get_effective_stats_debuff_minimum(basic_character):
"""Test that debuffs cannot reduce stats below 1."""
# Massive debuff
debuff = Effect(
effect_id="weakened",
name="Weakened",
effect_type=EffectType.DEBUFF,
duration=2,
power=20, # More than base strength
stat_affected=StatType.STRENGTH,
)
effective = basic_character.get_effective_stats([debuff])
# Should be clamped at 1, not 0 or negative
assert effective.strength == 1
def test_inventory_management(basic_character):
"""Test adding and removing items from inventory."""
item = Item(
item_id="potion",
name="Health Potion",
item_type=ItemType.CONSUMABLE,
description="Restores 50 HP",
value=25,
)
# Add item
basic_character.add_item(item)
assert len(basic_character.inventory) == 1
assert basic_character.inventory[0].item_id == "potion"
# Remove item
removed = basic_character.remove_item("potion")
assert removed.item_id == "potion"
assert len(basic_character.inventory) == 0
def test_equip_and_unequip(basic_character):
"""Test equipping and unequipping items."""
weapon = Item(
item_id="iron_sword",
name="Iron Sword",
item_type=ItemType.WEAPON,
description="A sturdy sword",
stat_bonuses={"strength": 5},
damage=10,
)
# Add to inventory first
basic_character.add_item(weapon)
assert len(basic_character.inventory) == 1
# Equip weapon
basic_character.equip_item(weapon, "weapon")
assert "weapon" in basic_character.equipped
assert basic_character.equipped["weapon"].item_id == "iron_sword"
assert len(basic_character.inventory) == 0 # Removed from inventory
# Unequip weapon
unequipped = basic_character.unequip_item("weapon")
assert unequipped.item_id == "iron_sword"
assert "weapon" not in basic_character.equipped
assert len(basic_character.inventory) == 1 # Back in inventory
def test_equip_replaces_existing(basic_character):
"""Test that equipping a new item replaces the old one."""
weapon1 = Item(
item_id="iron_sword",
name="Iron Sword",
item_type=ItemType.WEAPON,
description="A sturdy sword",
damage=10,
)
weapon2 = Item(
item_id="steel_sword",
name="Steel Sword",
item_type=ItemType.WEAPON,
description="A better sword",
damage=15,
)
# Equip first weapon
basic_character.add_item(weapon1)
basic_character.equip_item(weapon1, "weapon")
# Equip second weapon
basic_character.add_item(weapon2)
previous = basic_character.equip_item(weapon2, "weapon")
assert previous.item_id == "iron_sword" # Old weapon returned
assert basic_character.equipped["weapon"].item_id == "steel_sword"
assert len(basic_character.inventory) == 1 # Old weapon back in inventory
def test_gold_management(basic_character):
"""Test adding and removing gold."""
assert basic_character.gold == 0
# Add gold
basic_character.add_gold(100)
assert basic_character.gold == 100
# Remove gold
success = basic_character.remove_gold(50)
assert success == True
assert basic_character.gold == 50
# Try to remove more than available
success = basic_character.remove_gold(100)
assert success == False
assert basic_character.gold == 50 # Unchanged
def test_can_afford(basic_character):
"""Test can_afford() method."""
basic_character.gold = 100
assert basic_character.can_afford(50) == True
assert basic_character.can_afford(100) == True
assert basic_character.can_afford(101) == False
def test_add_experience_no_level_up(basic_character):
"""Test adding experience without leveling up."""
leveled_up = basic_character.add_experience(50)
assert leveled_up == False
assert basic_character.level == 1
assert basic_character.experience == 50
def test_add_experience_with_level_up(basic_character):
"""Test adding enough experience to level up."""
# Level 1 requires 100 XP for level 2
leveled_up = basic_character.add_experience(100)
assert leveled_up == True
assert basic_character.level == 2
assert basic_character.experience == 0 # Reset
def test_add_experience_with_overflow(basic_character):
"""Test leveling up with overflow experience."""
# Level 1 requires 100 XP, give 150
leveled_up = basic_character.add_experience(150)
assert leveled_up == True
assert basic_character.level == 2
assert basic_character.experience == 50 # Overflow
def test_xp_calculation(basic_origin):
"""Test XP required for each level."""
char = Character(
character_id="test",
user_id="user",
name="Test",
player_class=PlayerClass(
class_id="test",
name="Test",
description="Test",
base_stats=Stats(),
),
origin=basic_origin,
)
# Formula: 100 * (level ^ 1.5)
assert char._calculate_xp_for_next_level() == 100 # Level 1→2
char.level = 2
assert char._calculate_xp_for_next_level() == 282 # Level 2→3
char.level = 3
assert char._calculate_xp_for_next_level() == 519 # Level 3→4
def test_get_unlocked_abilities(basic_character):
"""Test getting abilities from class + unlocked skills."""
# Should have starting abilities
abilities = basic_character.get_unlocked_abilities()
assert "basic_attack" in abilities
# TODO: When skills unlock abilities, test that here
def test_character_serialization(basic_character):
"""Test character to_dict() serialization."""
# Add some data
basic_character.gold = 500
basic_character.level = 3
basic_character.experience = 100
data = basic_character.to_dict()
assert data["character_id"] == "char_001"
assert data["user_id"] == "user_001"
assert data["name"] == "Test Hero"
assert data["level"] == 3
assert data["experience"] == 100
assert data["gold"] == 500
def test_character_deserialization(basic_player_class, basic_origin):
"""Test character from_dict() deserialization."""
data = {
"character_id": "char_002",
"user_id": "user_002",
"name": "Restored Hero",
"player_class": basic_player_class.to_dict(),
"origin": basic_origin.to_dict(),
"level": 5,
"experience": 200,
"base_stats": Stats(strength=15).to_dict(),
"unlocked_skills": ["power_strike"],
"inventory": [],
"equipped": {},
"gold": 1000,
"active_quests": ["quest_1"],
"discovered_locations": ["town_1"],
}
char = Character.from_dict(data)
assert char.character_id == "char_002"
assert char.name == "Restored Hero"
assert char.level == 5
assert char.gold == 1000
assert "power_strike" in char.unlocked_skills
def test_character_round_trip_serialization(basic_character):
"""Test that serialization and deserialization preserve all data."""
# Add complex state
basic_character.gold = 500
basic_character.level = 3
basic_character.unlocked_skills = ["power_strike"]
weapon = Item(
item_id="sword",
name="Sword",
item_type=ItemType.WEAPON,
description="A sword",
damage=10,
)
basic_character.equipped["weapon"] = weapon
# Serialize and deserialize
data = basic_character.to_dict()
restored = Character.from_dict(data)
assert restored.character_id == basic_character.character_id
assert restored.name == basic_character.name
assert restored.level == basic_character.level
assert restored.gold == basic_character.gold
assert restored.unlocked_skills == basic_character.unlocked_skills
assert "weapon" in restored.equipped
assert restored.equipped["weapon"].item_id == "sword"