add pytest
This commit is contained in:
@@ -304,8 +304,10 @@ class Avatar(
|
||||
self._init_known_regions()
|
||||
|
||||
def __hash__(self) -> int:
|
||||
if not hasattr(self, 'id'):
|
||||
# 防御性编程:如果id尚未初始化(例如deepcopy过程中),使用对象内存地址
|
||||
return super().__hash__()
|
||||
return hash(self.id)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.get_info(detailed=False))
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ Avatar读档反序列化Mixin
|
||||
读档策略:
|
||||
- 两阶段加载:先加载所有Avatar(relations留空),再重建relations网络
|
||||
- 引用对象:通过id从全局字典获取(如techniques_by_id)
|
||||
- weapon/auxiliary:深拷贝后恢复special_data
|
||||
- weapon/auxiliary:浅拷贝后恢复special_data(避免deepcopy带来的递归/崩溃风险)
|
||||
- 错误容错:缺失的引用对象会跳过而不是崩溃
|
||||
"""
|
||||
from typing import TYPE_CHECKING
|
||||
import copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.world import World
|
||||
@@ -98,21 +99,26 @@ class AvatarLoadMixin:
|
||||
if item_id in items_by_id:
|
||||
avatar.items[items_by_id[item_id]] = quantity
|
||||
|
||||
# 重建weapon(深拷贝因为special_data是实例特有的)
|
||||
# 重建weapon
|
||||
# 使用copy而非deepcopy,避免潜在的递归引用导致的崩溃,且性能更好
|
||||
# special_data 是实例特有的,需要单独赋值
|
||||
weapon_id = data.get("weapon_id")
|
||||
if weapon_id is not None and weapon_id in weapons_by_id:
|
||||
import copy
|
||||
avatar.weapon = copy.deepcopy(weapons_by_id[weapon_id])
|
||||
# 浅拷贝:复制引用,但 weapon.special_data 会被共享
|
||||
# 所以需要手动重新赋值 special_data
|
||||
weapon_proto = weapons_by_id[weapon_id]
|
||||
avatar.weapon = copy.copy(weapon_proto)
|
||||
avatar.weapon.special_data = data.get("weapon_special_data", {})
|
||||
|
||||
# 恢复兵器熟练度
|
||||
avatar.weapon_proficiency = float(data.get("weapon_proficiency", 0.0))
|
||||
|
||||
# 重建auxiliary(深拷贝因为special_data是实例特有的)
|
||||
# 重建auxiliary
|
||||
# 同上,使用copy
|
||||
auxiliary_id = data.get("auxiliary_id")
|
||||
if auxiliary_id is not None and auxiliary_id in auxiliaries_by_id:
|
||||
import copy
|
||||
avatar.auxiliary = copy.deepcopy(auxiliaries_by_id[auxiliary_id])
|
||||
auxiliary_proto = auxiliaries_by_id[auxiliary_id]
|
||||
avatar.auxiliary = copy.copy(auxiliary_proto)
|
||||
avatar.auxiliary.special_data = data.get("auxiliary_special_data", {})
|
||||
|
||||
# 重建spirit_animal
|
||||
@@ -197,4 +203,3 @@ class AvatarLoadMixin:
|
||||
avatar.recalc_effects()
|
||||
|
||||
return avatar
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
|
||||
import pytest
|
||||
import copy
|
||||
from src.classes.weapon import Weapon, WeaponType
|
||||
from src.classes.equipment_grade import EquipmentGrade
|
||||
from src.classes.sect import Sect, SectHeadQuarter
|
||||
from src.classes.alignment import Alignment
|
||||
from pathlib import Path
|
||||
|
||||
def test_weapon_deepcopy_does_not_copy_sect():
|
||||
# 1. 创建模拟的 Sect
|
||||
hq = SectHeadQuarter("HQ", "Desc", Path("img.png"))
|
||||
sect = Sect(
|
||||
id=1, name="TestSect", desc="Desc", member_act_style="Style",
|
||||
alignment=Alignment.Righteous, headquarter=hq, technique_names=[]
|
||||
)
|
||||
|
||||
# 向 Sect 中添加一些可能导致问题的成员(虽然这里只是简单测试引用)
|
||||
# 在真实场景中,Sect.members 可能包含复杂的 Avatar 对象
|
||||
sect.members["dummy"] = "DummyAvatar"
|
||||
|
||||
# 2. 创建 Weapon 并关联 Sect
|
||||
weapon = Weapon(
|
||||
id=101, name="TestWeapon", weapon_type=WeaponType.SWORD,
|
||||
grade=EquipmentGrade.COMMON, sect_id=1, desc="Desc", sect=sect
|
||||
)
|
||||
|
||||
# 3. 深拷贝 Weapon
|
||||
weapon_copy = copy.deepcopy(weapon)
|
||||
|
||||
# 4. 验证 Weapon 被复制了
|
||||
assert weapon_copy is not weapon
|
||||
assert weapon_copy.id == weapon.id
|
||||
|
||||
# 5. 关键验证:Sect 应该是同一个对象(浅拷贝)
|
||||
assert weapon_copy.sect is sect
|
||||
assert weapon_copy.sect is weapon.sect
|
||||
|
||||
# 验证 Sect 的成员没有被复制
|
||||
assert weapon_copy.sect.members is sect.members
|
||||
|
||||
def test_weapon_special_data_is_copied():
|
||||
# 验证 special_data 是否被正确深拷贝
|
||||
weapon = Weapon(
|
||||
id=101, name="TestWeapon", weapon_type=WeaponType.SWORD,
|
||||
grade=EquipmentGrade.COMMON, sect_id=None, desc="Desc"
|
||||
)
|
||||
weapon.special_data = {"souls": 10, "nested": {"a": 1}}
|
||||
|
||||
weapon_copy = copy.deepcopy(weapon)
|
||||
|
||||
assert weapon_copy.special_data == weapon.special_data
|
||||
assert weapon_copy.special_data is not weapon.special_data
|
||||
assert weapon_copy.special_data["nested"] is not weapon.special_data["nested"]
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import pytest
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from src.classes.world import World
|
||||
from src.classes.map import Map
|
||||
from src.classes.tile import TileType
|
||||
from src.classes.calendar import Month, Year, create_month_stamp
|
||||
from src.classes.avatar import Avatar, Gender
|
||||
from src.classes.age import Age
|
||||
from src.classes.cultivation import Realm
|
||||
from src.sim.simulator import Simulator
|
||||
from src.sim.save.save_game import save_game
|
||||
from src.sim.load.load_game import load_game
|
||||
from src.utils.id_generator import get_avatar_id
|
||||
from src.utils.config import CONFIG
|
||||
|
||||
# Helper to create a simple map
|
||||
def create_simple_map():
|
||||
m = Map(width=5, height=5) # Slightly larger to be safe
|
||||
for x in range(5):
|
||||
for y in range(5):
|
||||
m.create_tile(x, y, TileType.PLAIN)
|
||||
return m
|
||||
|
||||
@pytest.fixture
|
||||
def temp_save_dir(tmp_path):
|
||||
d = tmp_path / "saves"
|
||||
d.mkdir()
|
||||
return d
|
||||
|
||||
def test_save_load_cycle(temp_save_dir):
|
||||
"""
|
||||
Test the full save and load cycle with a real World and Simulator instance,
|
||||
but without running the LLM or stepping the simulation.
|
||||
"""
|
||||
# 1. Setup World
|
||||
# Create a deterministic map for testing
|
||||
game_map = create_simple_map()
|
||||
|
||||
# Set a specific time
|
||||
start_year = Year(100)
|
||||
start_month = Month.JANUARY
|
||||
month_stamp = create_month_stamp(start_year, start_month)
|
||||
|
||||
world = World(map=game_map, month_stamp=month_stamp)
|
||||
|
||||
# 2. Add an Avatar
|
||||
avatar_id = get_avatar_id()
|
||||
avatar_name = "TestUser_SaveLoad"
|
||||
avatar = Avatar(
|
||||
world=world,
|
||||
name=avatar_name,
|
||||
id=avatar_id,
|
||||
birth_month_stamp=create_month_stamp(Year(80), Month.JANUARY),
|
||||
age=Age(20, Realm.Qi_Refinement),
|
||||
gender=Gender.MALE
|
||||
)
|
||||
# Set some specific attributes to verify persistence
|
||||
# Note: hp.max is recalculated from realm and effects on load, so setting it manually
|
||||
# without a supporting effect will result in it being reset.
|
||||
# We test hp.cur persistence instead (as long as it's <= max).
|
||||
# Default max for Qi_Refinement is 100.
|
||||
avatar.hp.cur = 80
|
||||
|
||||
# Add to world
|
||||
world.avatar_manager.avatars[avatar.id] = avatar
|
||||
|
||||
# 3. Setup Simulator
|
||||
sim = Simulator(world)
|
||||
# Modify a config value on the instance to check if it persists
|
||||
test_birth_rate = 0.99
|
||||
sim.birth_rate = test_birth_rate
|
||||
|
||||
# 4. Prepare Existed Sects (Empty for this basic test)
|
||||
existed_sects = []
|
||||
|
||||
# 5. Save Game
|
||||
save_filename = "test_save_cycle.json"
|
||||
save_path = temp_save_dir / save_filename
|
||||
|
||||
success, saved_name = save_game(world, sim, existed_sects, save_path)
|
||||
|
||||
assert success, "Save operation failed"
|
||||
assert save_path.exists(), "Save file was not created"
|
||||
|
||||
# 6. Load Game
|
||||
# We need to patch 'load_cultivation_world_map' because load_game calls it.
|
||||
# We want it to return our simple map (or a new equivalent one) instead of loading the real huge map.
|
||||
# Note: load_game imports it inside the function, so we patch where it is imported FROM if it was global,
|
||||
# but since it's inside, we rely on sys.modules or patch the target module path.
|
||||
# The import in load_game.py is: from src.run.load_map import load_cultivation_world_map
|
||||
|
||||
with patch('src.run.load_map.load_cultivation_world_map', return_value=create_simple_map()):
|
||||
# We also need to be careful about 'sects_by_id' if we had sects, but we don't.
|
||||
loaded_world, loaded_sim, loaded_sects = load_game(save_path)
|
||||
|
||||
# 7. Verification
|
||||
|
||||
# Verify World Metadata
|
||||
assert loaded_world.month_stamp == world.month_stamp
|
||||
assert loaded_world.month_stamp.get_year() == 100
|
||||
|
||||
# Verify Avatar
|
||||
assert len(loaded_world.avatar_manager.avatars) == 1
|
||||
assert avatar_id in loaded_world.avatar_manager.avatars
|
||||
|
||||
loaded_avatar = loaded_world.avatar_manager.avatars[avatar_id]
|
||||
assert loaded_avatar.name == avatar_name
|
||||
assert loaded_avatar.age.age == 20
|
||||
assert loaded_avatar.cultivation_progress.realm == Realm.Qi_Refinement
|
||||
assert loaded_avatar.gender == Gender.MALE
|
||||
# hp.max is reset to 100 based on Realm.Qi_Refinement
|
||||
assert loaded_avatar.hp.max == 100
|
||||
assert loaded_avatar.hp.cur == 80
|
||||
|
||||
# Verify Simulator
|
||||
assert loaded_sim.birth_rate == test_birth_rate
|
||||
|
||||
# Verify World/Simulator linkage
|
||||
assert loaded_sim.world == loaded_world
|
||||
assert loaded_avatar.world == loaded_world
|
||||
|
||||
def test_save_load_with_relations(temp_save_dir):
|
||||
"""
|
||||
Test saving and loading avatars with relationships.
|
||||
"""
|
||||
game_map = create_simple_map()
|
||||
world = World(map=game_map, month_stamp=create_month_stamp(Year(1), Month.JANUARY))
|
||||
|
||||
# Create two avatars
|
||||
av1 = Avatar(world, "Av1", get_avatar_id(), create_month_stamp(Year(1), Month.JANUARY), Age(20, Realm.Qi_Refinement), Gender.MALE)
|
||||
av2 = Avatar(world, "Av2", get_avatar_id(), create_month_stamp(Year(1), Month.JANUARY), Age(20, Realm.Qi_Refinement), Gender.FEMALE)
|
||||
|
||||
world.avatar_manager.avatars[av1.id] = av1
|
||||
world.avatar_manager.avatars[av2.id] = av2
|
||||
|
||||
# Add relationship
|
||||
from src.classes.relation import Relation
|
||||
|
||||
# Manually adding relation for test (usually done via helper methods)
|
||||
# relation value is integer
|
||||
av1.relations[av2] = Relation.FRIEND
|
||||
|
||||
sim = Simulator(world)
|
||||
|
||||
save_path = temp_save_dir / "test_relation.json"
|
||||
save_game(world, sim, [], save_path)
|
||||
|
||||
with patch('src.run.load_map.load_cultivation_world_map', return_value=create_simple_map()):
|
||||
l_world, _, _ = load_game(save_path)
|
||||
|
||||
l_av1 = l_world.avatar_manager.avatars[av1.id]
|
||||
l_av2 = l_world.avatar_manager.avatars[av2.id]
|
||||
|
||||
assert l_av2 in l_av1.relations
|
||||
assert l_av1.relations[l_av2] == Relation.FRIEND
|
||||
|
||||
Reference in New Issue
Block a user