250 lines
9.0 KiB
Python
250 lines
9.0 KiB
Python
import pytest
|
||
import copy
|
||
from unittest.mock import MagicMock, patch
|
||
from src.classes.world import World
|
||
from src.classes.circulation import CirculationManager
|
||
from src.classes.weapon import Weapon, WeaponType
|
||
from src.classes.auxiliary import Auxiliary
|
||
from src.classes.cultivation import Realm
|
||
from src.classes.avatar import Avatar, Gender
|
||
from src.classes.age import Age
|
||
from src.classes.calendar import Month, Year, create_month_stamp
|
||
from src.utils.id_generator import get_avatar_id
|
||
from src.sim.save.save_game import save_game
|
||
from src.sim.load.load_game import load_game
|
||
from src.sim.simulator import Simulator
|
||
from src.classes.map import Map
|
||
from src.classes.tile import TileType
|
||
|
||
# --- Helper Objects ---
|
||
|
||
def create_mock_weapon(w_id=1, name="MockSword"):
|
||
w = MagicMock(spec=Weapon)
|
||
w.id = w_id
|
||
w.name = name
|
||
w.realm = Realm.Qi_Refinement
|
||
w.special_data = {"test_val": 123}
|
||
# Mock to_save_dict behavior manually or rely on CirculationManager using id/special_data
|
||
return w
|
||
|
||
def create_mock_auxiliary(a_id=1, name="MockRing"):
|
||
a = MagicMock(spec=Auxiliary)
|
||
a.id = a_id
|
||
a.name = name
|
||
a.realm = Realm.Qi_Refinement
|
||
a.special_data = {"souls": 5}
|
||
return a
|
||
|
||
def create_test_map():
|
||
m = Map(width=10, height=10)
|
||
for x in range(10):
|
||
for y in range(10):
|
||
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
|
||
|
||
@pytest.fixture
|
||
def empty_world():
|
||
game_map = create_test_map()
|
||
return World(map=game_map, month_stamp=create_month_stamp(Year(1), Month.JANUARY))
|
||
|
||
# --- Tests ---
|
||
|
||
def test_circulation_manager_basic():
|
||
"""Test basic adding of items to CirculationManager"""
|
||
cm = CirculationManager()
|
||
|
||
# Test adding Weapon
|
||
w = create_mock_weapon(1, "Sword")
|
||
# CirculationManager uses deepcopy, so we need to ensure the mock supports it or use real objects if possible.
|
||
# MagicMock is hard to deepcopy properly in some contexts, let's use a simple object structure or patch copy.deepcopy
|
||
# But for robustness, let's try to make a real-ish object or a class that looks like Weapon
|
||
|
||
# Let's define a simple dummy class for testing to avoid importing all Weapon dependencies
|
||
class DummyItem:
|
||
def __init__(self, id, name, special_data=None):
|
||
self.id = id
|
||
self.name = name
|
||
self.special_data = special_data or {}
|
||
|
||
w1 = DummyItem(1, "Sword", {"kills": 10})
|
||
cm.add_weapon(w1)
|
||
|
||
assert len(cm.sold_weapons) == 1
|
||
assert cm.sold_weapons[0].name == "Sword"
|
||
# Ensure it's a copy
|
||
assert cm.sold_weapons[0] is not w1
|
||
assert cm.sold_weapons[0].special_data["kills"] == 10
|
||
|
||
# Test adding Auxiliary
|
||
a1 = DummyItem(2, "Ring", {"mana": 50})
|
||
cm.add_auxiliary(a1)
|
||
|
||
assert len(cm.sold_auxiliaries) == 1
|
||
assert cm.sold_auxiliaries[0].name == "Ring"
|
||
|
||
def test_circulation_serialization():
|
||
"""Test to_save_dict and load_from_dict"""
|
||
cm = CirculationManager()
|
||
|
||
# Prepare data using real-ish mocks that can be looked up by ID
|
||
# We need to patch weapons_by_id and auxiliaries_by_id during load
|
||
|
||
class DummyItem:
|
||
def __init__(self, id, name):
|
||
self.id = id
|
||
self.name = name
|
||
self.special_data = {}
|
||
|
||
w1 = DummyItem(101, "RareSword")
|
||
w1.special_data = {"stat": 1}
|
||
|
||
a1 = DummyItem(202, "RareRing")
|
||
a1.special_data = {"stat": 2}
|
||
|
||
cm.add_weapon(w1)
|
||
cm.add_auxiliary(a1)
|
||
|
||
saved_data = cm.to_save_dict()
|
||
|
||
# Verify saved structure
|
||
assert "weapons" in saved_data
|
||
assert "auxiliaries" in saved_data
|
||
assert len(saved_data["weapons"]) == 1
|
||
assert saved_data["weapons"][0]["id"] == 101
|
||
assert saved_data["weapons"][0]["special_data"] == {"stat": 1}
|
||
|
||
# Test Loading
|
||
new_cm = CirculationManager()
|
||
|
||
# We need to mock the global dictionaries used in load_from_dict
|
||
mock_weapons_db = {101: DummyItem(101, "RareSword_Proto")} # Proto doesn't have special_data usually
|
||
mock_aux_db = {202: DummyItem(202, "RareRing_Proto")}
|
||
|
||
with patch("src.classes.weapon.weapons_by_id", mock_weapons_db), \
|
||
patch("src.classes.auxiliary.auxiliaries_by_id", mock_aux_db):
|
||
|
||
new_cm.load_from_dict(saved_data)
|
||
|
||
assert len(new_cm.sold_weapons) == 1
|
||
assert new_cm.sold_weapons[0].id == 101
|
||
assert new_cm.sold_weapons[0].name == "RareSword_Proto" # Should come from prototype
|
||
assert new_cm.sold_weapons[0].special_data == {"stat": 1} # Should be restored from save
|
||
|
||
assert len(new_cm.sold_auxiliaries) == 1
|
||
assert new_cm.sold_auxiliaries[0].id == 202
|
||
|
||
def test_avatar_sell_integration(empty_world):
|
||
"""Test that selling an item via Avatar correctly adds it to World.circulation"""
|
||
|
||
# Setup Avatar
|
||
avatar = Avatar(
|
||
world=empty_world,
|
||
name="Seller",
|
||
id=get_avatar_id(),
|
||
birth_month_stamp=create_month_stamp(Year(1), Month.JANUARY),
|
||
age=Age(20, Realm.Qi_Refinement),
|
||
gender=Gender.MALE
|
||
)
|
||
empty_world.avatar_manager.avatars[avatar.id] = avatar
|
||
|
||
# Setup Prices mock to avoid complex price calculation dependencies
|
||
with patch("src.classes.prices.prices") as mock_prices:
|
||
mock_prices.get_weapon_price.return_value = 100
|
||
mock_prices.get_auxiliary_price.return_value = 200
|
||
# 这里的 sell_weapon 现在调用 get_selling_price,我们也 mock 它
|
||
# 假设这里没有额外加成,或者我们直接设定最终售价
|
||
mock_prices.get_selling_price.side_effect = lambda obj, seller: \
|
||
100 if getattr(obj, "id", 0) == 999 else \
|
||
(300 if getattr(obj, "id", 0) == 888 else 0)
|
||
# 上面的 side_effect 比较复杂,因为测试里先后卖了 weapon(id=999) 和 aux(id=888)
|
||
# 我们可以简单地根据类型返回,或者分段 mock
|
||
|
||
# 重新定义 mock 逻辑
|
||
def get_selling_price_mock(obj, seller):
|
||
if hasattr(obj, "id") and obj.id == 999: return 100
|
||
if hasattr(obj, "id") and obj.id == 888: return 200
|
||
return 0
|
||
mock_prices.get_selling_price.side_effect = get_selling_price_mock
|
||
|
||
# 1. Test Sell Weapon
|
||
# Create a dummy weapon that acts like the real one
|
||
weapon = MagicMock(spec=Weapon)
|
||
weapon.id = 999
|
||
weapon.name = "TestBlade"
|
||
weapon.realm = Realm.Qi_Refinement
|
||
|
||
# The mixin usually requires self.materials to have the material for sell_material,
|
||
# but sell_weapon/sell_auxiliary are for equipped items or passed items.
|
||
# Looking at inventory_mixin.py: sell_weapon(self, weapon) just calculates price and adds stones.
|
||
# It calls _get_sell_multiplier()
|
||
|
||
# Ensure avatar has magic stones initialized
|
||
avatar.magic_stone = 0
|
||
|
||
# Action
|
||
avatar.sell_weapon(weapon)
|
||
|
||
# Verify
|
||
assert avatar.magic_stone == 100
|
||
assert len(empty_world.circulation.sold_weapons) == 1
|
||
# Since we use MagicMock, deepcopy might be weird, but let's check basic attr
|
||
assert empty_world.circulation.sold_weapons[0].id == 999
|
||
|
||
# 2. Test Sell Auxiliary
|
||
aux = MagicMock(spec=Auxiliary)
|
||
aux.id = 888
|
||
aux.name = "TestAmulet"
|
||
|
||
# Action
|
||
avatar.sell_auxiliary(aux)
|
||
|
||
# Verify
|
||
assert avatar.magic_stone == 300 # 100 + 200
|
||
assert len(empty_world.circulation.sold_auxiliaries) == 1
|
||
assert empty_world.circulation.sold_auxiliaries[0].id == 888
|
||
|
||
def test_save_load_circulation(temp_save_dir, empty_world):
|
||
"""Test full save/load cycle with circulation data"""
|
||
|
||
# 1. Populate circulation
|
||
class SimpleItem:
|
||
def __init__(self, id, name):
|
||
self.id = id
|
||
self.name = name
|
||
self.special_data = {}
|
||
self.realm = Realm.Qi_Refinement # needed if deepcopy looks at it or for other checks
|
||
|
||
w1 = SimpleItem(10, "LostSword")
|
||
w1.special_data = {"kills": 99}
|
||
empty_world.circulation.add_weapon(w1)
|
||
|
||
# 2. Save
|
||
sim = Simulator(empty_world)
|
||
save_path = temp_save_dir / "circulation_test.json"
|
||
|
||
save_game(empty_world, sim, [], save_path)
|
||
|
||
# 3. Load
|
||
# We need to mock the DBs to recognize ID 10
|
||
mock_weapons_db = {10: SimpleItem(10, "LostSword_Proto")}
|
||
|
||
with patch("src.run.load_map.load_cultivation_world_map", return_value=create_test_map()), \
|
||
patch("src.classes.weapon.weapons_by_id", mock_weapons_db), \
|
||
patch("src.classes.auxiliary.auxiliaries_by_id", {}):
|
||
|
||
loaded_world, _, _ = load_game(save_path)
|
||
|
||
# 4. Verify
|
||
assert len(loaded_world.circulation.sold_weapons) == 1
|
||
loaded_w = loaded_world.circulation.sold_weapons[0]
|
||
assert loaded_w.id == 10
|
||
assert loaded_w.name == "LostSword_Proto" # Should be restored from proto name
|
||
assert loaded_w.special_data == {"kills": 99} # Should have restored data
|
||
|