From 0890fc18b211466e979954be7466cd1e5fb495f7 Mon Sep 17 00:00:00 2001 From: bridge Date: Thu, 18 Dec 2025 22:08:06 +0800 Subject: [PATCH] add pytest --- src/classes/cultivation.py | 6 ++- tests/conftest.py | 56 +++++++++++++++++++++++++ tests/test_basic.py | 39 +++++------------- tests/test_save_load.py | 19 +++++---- tests/test_sect_ranks.py | 43 +++++-------------- tests/test_simulator.py | 84 ++++++++++++++++++-------------------- 6 files changed, 130 insertions(+), 117 deletions(-) create mode 100644 tests/conftest.py diff --git a/src/classes/cultivation.py b/src/classes/cultivation.py index ac2b5fb..aa25dc7 100644 --- a/src/classes/cultivation.py +++ b/src/classes/cultivation.py @@ -124,8 +124,10 @@ class CultivationProgress: def get_move_step(self) -> int: """ 每月能够移动的距离, - 练气,筑基为1 - 金丹,元婴为2 + 练气: 2 + 筑基: 3 + 金丹: 4 + 元婴: 5 """ return REALM_TO_MOVE_STEP[self.realm] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8e09456 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,56 @@ +import pytest +from unittest.mock import MagicMock + +from src.classes.map import Map +from src.classes.tile import TileType +from src.classes.world import World +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.utils.id_generator import get_avatar_id +from src.classes.name import get_random_name + +@pytest.fixture +def base_map(): + """创建一个 10x10 的全平原地图""" + width, height = 10, 10 + game_map = Map(width=width, height=height) + for x in range(width): + for y in range(height): + game_map.create_tile(x, y, TileType.PLAIN) + return game_map + +@pytest.fixture +def base_world(base_map): + """创建一个基于 base_map 的世界,时间为 Year 1, Jan""" + return World(map=base_map, month_stamp=create_month_stamp(Year(1), Month.JANUARY)) + +from src.classes.root import Root +from src.classes.alignment import Alignment + +@pytest.fixture +def dummy_avatar(base_world): + """创建一个位于 (0,0) 的标准男性练气期角色""" + # 确保ID生成器重置或不冲突 (get_avatar_id 是随机UUID通常没问题) + av = Avatar( + world=base_world, + name="TestDummy", + id=get_avatar_id(), + birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY), + age=Age(20, Realm.Qi_Refinement), + gender=Gender.MALE, + pos_x=0, + pos_y=0, + root=Root.GOLD, # 固定灵根 + personas=[], # 清空特质,避免随机效果 + alignment=Alignment.RIGHTEOUS # 固定阵营 + ) + + # 赋予一个 Mock 武器,防止 get_avatar_info 报错 + av.weapon = MagicMock() + av.weapon.get_detailed_info.return_value = "测试木剑(凡品)" + av.weapon_proficiency = 0.0 + + return av + diff --git a/tests/test_basic.py b/tests/test_basic.py index a207af0..c3dc28f 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,32 +1,13 @@ -from src.utils.id_generator import get_avatar_id -from src.classes.avatar import Avatar, Gender -from src.classes.calendar import Month, Year, MonthStamp, create_month_stamp -from src.classes.world import World -from src.classes.map import Map -from src.classes.tile import TileType -from src.classes.age import Age -from src.classes.cultivation import Realm -from src.classes.name import get_random_name - -def test_basic(): +from src.classes.avatar import Avatar +# test_basic is now simplified using fixtures +def test_basic(base_world, dummy_avatar): """ 测试整个基础代码能不能run起来 + 使用 conftest.py 中的 fixtures 简化设置 """ - map = Map(width=2, height=2) - for x in range(2): - for y in range(2): - map.create_tile(x, y, TileType.PLAIN) - - world = World(map=map, month_stamp=create_month_stamp(Year(1), Month.JANUARY)) - - avatar = Avatar( - world=world, - name=get_random_name(Gender.MALE), - id=get_avatar_id(), - birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY), - age=Age(20, Realm.Qi_Refinement), - gender=Gender.MALE - ) - - - + # fixtures 已经创建了 map, world, avatar + assert base_world.map.width == 10 + assert base_world.map.height == 10 + + assert dummy_avatar.world == base_world + assert dummy_avatar.age.age == 20 diff --git a/tests/test_save_load.py b/tests/test_save_load.py index aa4e43e..fd33cff 100644 --- a/tests/test_save_load.py +++ b/tests/test_save_load.py @@ -16,11 +16,11 @@ 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): +# Helper to create a simple map (aligned with conftest base_map logic) +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 @@ -37,7 +37,7 @@ def test_save_load_cycle(temp_save_dir): """ # 1. Setup World # Create a deterministic map for testing - game_map = create_simple_map() + game_map = create_test_map() # Set a specific time start_year = Year(100) @@ -45,6 +45,7 @@ def test_save_load_cycle(temp_save_dir): 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() @@ -92,7 +93,7 @@ def test_save_load_cycle(temp_save_dir): # 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()): + with patch('src.run.load_map.load_cultivation_world_map', return_value=create_test_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) @@ -126,7 +127,7 @@ def test_save_load_with_relations(temp_save_dir): """ Test saving and loading avatars with relationships. """ - game_map = create_simple_map() + game_map = create_test_map() world = World(map=game_map, month_stamp=create_month_stamp(Year(1), Month.JANUARY)) # Create two avatars @@ -148,7 +149,7 @@ def test_save_load_with_relations(temp_save_dir): 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()): + with patch('src.run.load_map.load_cultivation_world_map', return_value=create_test_map()): l_world, _, _ = load_game(save_path) l_av1 = l_world.avatar_manager.avatars[av1.id] diff --git a/tests/test_sect_ranks.py b/tests/test_sect_ranks.py index 4f88ea2..49bba11 100644 --- a/tests/test_sect_ranks.py +++ b/tests/test_sect_ranks.py @@ -48,17 +48,13 @@ def test_auto_promote(): assert should_auto_promote(Realm.Qi_Refinement, Realm.Qi_Refinement) == False -def test_avatar_sect_rank_assignment(): + +def test_avatar_sect_rank_assignment(base_world): """测试avatar创建时宗门职位分配""" - from src.run.load_map import load_cultivation_world_map - game_map = load_cultivation_world_map() - world = World( - map=game_map, - month_stamp=MonthStamp(100 * 12), - ) + # 使用 base_world fixture,不需要 load_cultivation_world_map # 创建多个avatar - avatars_dict = make_avatars(world, count=20, current_month_stamp=MonthStamp(100 * 12)) + avatars_dict = make_avatars(base_world, count=20, current_month_stamp=MonthStamp(100 * 12)) avatars = list(avatars_dict.values()) # 检查所有有宗门的avatar都有职位 @@ -75,17 +71,11 @@ def test_avatar_sect_rank_assignment(): assert avatar.sect_rank is None, f"{avatar.name} 散修不应该有职位" -def test_patriarch_uniqueness(): +def test_patriarch_uniqueness(base_world): """测试每个宗门只有一个掌门""" - from src.run.load_map import load_cultivation_world_map - game_map = load_cultivation_world_map() - world = World( - map=game_map, - month_stamp=MonthStamp(100 * 12), - ) # 创建足够多的avatar - avatars_dict = make_avatars(world, count=50, current_month_stamp=MonthStamp(100 * 12)) + avatars_dict = make_avatars(base_world, count=50, current_month_stamp=MonthStamp(100 * 12)) avatars = list(avatars_dict.values()) # 统计每个宗门的掌门数量 @@ -102,16 +92,10 @@ def test_patriarch_uniqueness(): assert len(patriarchs) <= 1, f"宗门 {sect_id} 有多个掌门: {patriarchs}" -def test_sect_str_display(): +def test_sect_str_display(base_world): """测试宗门信息显示""" - from src.run.load_map import load_cultivation_world_map - game_map = load_cultivation_world_map() - world = World( - map=game_map, - month_stamp=MonthStamp(100 * 12), - ) - avatars_dict = make_avatars(world, count=20, current_month_stamp=MonthStamp(100 * 12)) + avatars_dict = make_avatars(base_world, count=20, current_month_stamp=MonthStamp(100 * 12)) avatars = list(avatars_dict.values()) for avatar in avatars: @@ -127,16 +111,10 @@ def test_sect_str_display(): assert rank_name in sect_str -def test_cultivation_breakthrough_promotion(): +def test_cultivation_breakthrough_promotion(base_world): """测试突破境界后自动晋升""" - from src.run.load_map import load_cultivation_world_map - game_map = load_cultivation_world_map() - world = World( - map=game_map, - month_stamp=MonthStamp(100 * 12), - ) - avatars_dict = make_avatars(world, count=10, current_month_stamp=MonthStamp(100 * 12)) + avatars_dict = make_avatars(base_world, count=10, current_month_stamp=MonthStamp(100 * 12)) avatars = list(avatars_dict.values()) # 找一个练气期的宗门弟子 @@ -160,6 +138,7 @@ def test_cultivation_breakthrough_promotion(): assert target_avatar.sect_rank > old_rank + if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/tests/test_simulator.py b/tests/test_simulator.py index 2f4f5be..1395e26 100644 --- a/tests/test_simulator.py +++ b/tests/test_simulator.py @@ -1,54 +1,48 @@ -import random +import asyncio +import pytest +from unittest.mock import patch, MagicMock from src.sim.simulator import Simulator -from src.classes.avatar import Avatar, Gender -from src.classes.calendar import Month, Year, MonthStamp, create_month_stamp -from src.classes.world import World -from src.classes.map import Map +from src.classes.action.move_to_direction import MoveToDirection from src.classes.tile import TileType -from src.classes.action import Move -from src.classes.name import get_random_name -from src.classes.age import Age -from src.classes.cultivation import Realm +from src.classes.action_runtime import ActionInstance +def test_simulator_step_moves_avatar_and_sets_tile(base_world, dummy_avatar): + # Set initial position + dummy_avatar.pos_x = 1 + dummy_avatar.pos_y = 1 + # Ensure tile is updated to initial position (fixture puts it at 0,0) + dummy_avatar.tile = base_world.map.get_tile(1, 1) -def test_simulator_step_moves_avatar_and_sets_tile(): - # 固定随机种子,确保决定的移动是可预测的 - random.seed(0) + sim = Simulator(base_world) + base_world.avatar_manager.avatars[dummy_avatar.id] = dummy_avatar - # 构建 3x3 地图并填充地块 - game_map = Map(width=3, height=3) - for x in range(3): - for y in range(3): - game_map.create_tile(x, y, TileType.PLAIN) + # Manually assign a MoveToDirection action to avoid relying on LLM + action = MoveToDirection(dummy_avatar, base_world) + # "East" means x + 1 + direction = "East" + action.start(direction=direction) # Initialize start_monthstamp etc. + + # Wrap in ActionInstance + dummy_avatar.current_action = ActionInstance(action=action, params={"direction": direction}) - world = World(map=game_map, month_stamp=create_month_stamp(Year(1), Month.JANUARY)) + # Mock LLM to avoid external calls or errors + with patch("src.sim.simulator.llm_ai") as mock_ai: + mock_ai.decide = MagicMock(return_value={}) + + print(f"DEBUG: Before step: pos_x={dummy_avatar.pos_x}") + # Run step synchronously + asyncio.run(sim.step()) + print(f"DEBUG: After step: pos_x={dummy_avatar.pos_x}") + print(f"DEBUG: move_step_length={getattr(dummy_avatar, 'move_step_length', 'Not set')}") + print(f"DEBUG: effects={dummy_avatar.effects}") - # 将角色放在地图中心,避免越界 - avatar = Avatar( - world=world, - name=get_random_name(Gender.MALE), - id="1", - birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY), - age=Age(20, Realm.Qi_Refinement), - gender=Gender.MALE, - pos_x=1, - pos_y=1, - ) - - - sim = Simulator(world) - world.avatar_manager.avatars["1"] = avatar - - # 执行一步模拟 - sim.step() - - # 断言位置在边界内 - assert 0 <= avatar.pos_x < game_map.width - assert 0 <= avatar.pos_y < game_map.height - - # 断言 tile 已正确设置且与位置一致 - assert avatar.tile is not None - assert avatar.tile.x == avatar.pos_x - assert avatar.tile.y == avatar.pos_y + # Assert moved East (x increased by move_step_length) + # Current move step for Qi Refinement is 2 + assert dummy_avatar.pos_x == 3 + assert dummy_avatar.pos_y == 1 + # Assert tile is updated + assert dummy_avatar.tile is not None + assert dummy_avatar.tile.x == 3 + assert dummy_avatar.tile.y == 1