* feat: add vue-i18n * feat: add vue-i18n * feat: add vue-i18n * feat: add language class * add: en templates and configs * add: en names * refactor: name gender id and sect id * feat(i18n): add gettext infrastructure for dynamic text translation (#81) * feat(i18n): add gettext infrastructure for dynamic text translation - Add src/i18n/ module with t() translation function - Add .po/.mo files for zh_CN and en_US locales - Update LanguageManager to reload translations on language change - Add comprehensive tests (14 tests, all passing) - Add implementation spec at docs/specs/i18n-dynamic-text.md Phase 1 of i18n dynamic text implementation. * feat(i18n): expand .po files with comprehensive translation entries Add translation messages for: - Battle result messages (fatal/non-fatal outcomes) - Fortune event messages (item discovery, cultivation gains) - Misfortune event messages (losses, damage, regression) - Death reason messages - Item exchange messages (equip, sell, discard) - Single choice context and option labels - Common labels (weapon, auxiliary, technique, elixir) Both zh_CN and en_US locales updated with matching entries. * test: add .po file integrity tests * feat: i18n for actions * feat: i18n for effects * feat: i18n for gathering * feat: i18n for classes * feat: i18n for classes * feat: i18n for classes * feat: i18n for classes * fix bugs * fix bugs * fix bugs * fix bugs * fix bugs * fix bugs * fix bugs * fix bugs * update csv * update world info * update prompt * update prompt * fix bug * fix bug * fix bug * fix bug * fix bug * fix bug * fix bug * fix bug * fix bug * update * update * update * update * update * update * update --------- Co-authored-by: Zihao Xu <xzhseh@gmail.com>
252 lines
8.0 KiB
Python
252 lines
8.0 KiB
Python
import pytest
|
||
import random
|
||
from unittest.mock import MagicMock, AsyncMock, patch
|
||
|
||
from src.classes.map import Map
|
||
|
||
|
||
@pytest.fixture(scope="session", autouse=True)
|
||
def mock_saves_dir(tmp_path_factory):
|
||
"""
|
||
Redirect save path to temp dir for all tests to avoid polluting assets/saves/
|
||
"""
|
||
from src.utils.config import CONFIG
|
||
|
||
# Create temp dir for saves
|
||
temp_saves = tmp_path_factory.mktemp("saves")
|
||
|
||
# Backup original path
|
||
original_path = CONFIG.paths.saves
|
||
|
||
# Redirect
|
||
CONFIG.paths.saves = temp_saves
|
||
print(f"\n[Test Setup] Redirecting saves to: {temp_saves}")
|
||
|
||
yield temp_saves
|
||
|
||
# Restore
|
||
CONFIG.paths.saves = original_path
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def fixed_random_seed():
|
||
"""
|
||
Ensure all tests have deterministic random behavior.
|
||
This fixture is automatically applied to all tests.
|
||
"""
|
||
random.seed(42)
|
||
yield
|
||
|
||
@pytest.fixture(scope="session", autouse=True)
|
||
def force_chinese_language():
|
||
"""
|
||
Force language to Chinese for all tests to match expected string outputs.
|
||
"""
|
||
from src.classes.language import language_manager
|
||
from src.utils.config import update_paths_for_language
|
||
|
||
# Force language to Chinese
|
||
language_manager.set_language("zh-CN")
|
||
|
||
# Ensure game configs are reloaded (in case set_language skipped it due to circular import protection)
|
||
from src.utils.df import reload_game_configs
|
||
reload_game_configs()
|
||
|
||
yield
|
||
from src.classes.tile import TileType, Tile
|
||
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
|
||
from src.classes.root import Root
|
||
from src.classes.alignment import Alignment
|
||
|
||
# Action related imports
|
||
from src.classes.elixir import Elixir, ElixirType
|
||
from src.classes.material import Material
|
||
from src.classes.weapon import Weapon
|
||
from src.classes.weapon_type import WeaponType
|
||
from src.classes.auxiliary import Auxiliary
|
||
from src.classes.region import CityRegion
|
||
|
||
@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))
|
||
|
||
@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
|
||
|
||
# 强制清空特质(因为 __post_init__ 会在 personas 为空时自动随机生成)
|
||
av.personas = []
|
||
# 强制清空功法,防止随机出的功法带有移动步长加成(如逍遥游)
|
||
av.technique = None
|
||
av.recalc_effects()
|
||
|
||
return av
|
||
|
||
@pytest.fixture
|
||
def mock_llm_managers():
|
||
"""
|
||
Mock 所有涉及 LLM 调用的管理器和函数,防止测试中意外调用 LLM。
|
||
"""
|
||
# 创建 mock LLM 配置
|
||
mock_llm_config = MagicMock()
|
||
mock_llm_config.api_key = "test_key"
|
||
mock_llm_config.base_url = "http://test.api/v1"
|
||
mock_llm_config.model_name = "test-model"
|
||
|
||
with patch("src.sim.simulator.llm_ai") as mock_ai, \
|
||
patch("src.sim.simulator.process_avatar_long_term_objective", new_callable=AsyncMock) as mock_lto, \
|
||
patch("src.classes.nickname.process_avatar_nickname", new_callable=AsyncMock) as mock_nick, \
|
||
patch("src.classes.relation_resolver.RelationResolver.run_batch", new_callable=AsyncMock) as mock_rr, \
|
||
patch("src.classes.history.HistoryManager.apply_history_influence", new_callable=AsyncMock) as mock_hist, \
|
||
patch("src.classes.story_teller.StoryTeller.tell_story", new_callable=AsyncMock) as mock_story, \
|
||
patch("src.utils.llm.config.LLMConfig.from_mode", return_value=mock_llm_config) as mock_config:
|
||
|
||
mock_ai.decide = AsyncMock(return_value={})
|
||
mock_lto.return_value = None
|
||
mock_nick.return_value = None
|
||
mock_rr.return_value = []
|
||
mock_hist.return_value = None
|
||
mock_story.return_value = "测试故事"
|
||
|
||
yield {
|
||
"ai": mock_ai,
|
||
"lto": mock_lto,
|
||
"nick": mock_nick,
|
||
"rr": mock_rr,
|
||
"hist": mock_hist,
|
||
"story": mock_story,
|
||
"config": mock_config
|
||
}
|
||
|
||
# --- Shared Helpers for Item Creation ---
|
||
|
||
def create_test_elixir(name, realm, price=100, elixir_id=1, effects=None):
|
||
if effects is None:
|
||
effects = {"max_hp": 10}
|
||
return Elixir(
|
||
id=elixir_id,
|
||
name=name,
|
||
realm=realm,
|
||
type=ElixirType.Breakthrough,
|
||
desc="测试丹药",
|
||
price=price,
|
||
effects=effects
|
||
)
|
||
|
||
def create_test_material(name, realm, material_id=101):
|
||
return Material(
|
||
id=material_id,
|
||
name=name,
|
||
desc="测试物品",
|
||
realm=realm
|
||
)
|
||
|
||
def create_test_weapon(name, realm, weapon_id=201):
|
||
return Weapon(
|
||
id=weapon_id,
|
||
name=name,
|
||
weapon_type=WeaponType.SWORD,
|
||
realm=realm,
|
||
desc="测试兵器",
|
||
effects={},
|
||
effect_desc=""
|
||
)
|
||
|
||
def create_test_auxiliary(name, realm, aux_id=301):
|
||
return Auxiliary(
|
||
id=aux_id,
|
||
name=name,
|
||
realm=realm,
|
||
desc="测试法宝",
|
||
effects={},
|
||
effect_desc=""
|
||
)
|
||
|
||
@pytest.fixture
|
||
def avatar_in_city(dummy_avatar):
|
||
"""
|
||
修改 dummy_avatar,使其位于城市中,并给予初始资金
|
||
"""
|
||
city_region = CityRegion(id=1, name="TestCity", desc="测试城市")
|
||
tile = Tile(0, 0, TileType.CITY)
|
||
tile.region = city_region
|
||
|
||
dummy_avatar.tile = tile
|
||
dummy_avatar.magic_stone = 1000
|
||
dummy_avatar.cultivation_progress.realm = Realm.Qi_Refinement
|
||
dummy_avatar.elixirs = []
|
||
dummy_avatar.materials = {} # 确保背包为空
|
||
dummy_avatar.weapon = None
|
||
dummy_avatar.auxiliary = None
|
||
|
||
return dummy_avatar
|
||
|
||
@pytest.fixture
|
||
def mock_item_data():
|
||
"""
|
||
提供标准的一组测试物品,包括材料、丹药、兵器、法宝。
|
||
返回一个包含这些对象的字典,方便后续 mock 使用。
|
||
"""
|
||
test_elixir = create_test_elixir("聚气丹", Realm.Qi_Refinement, price=100)
|
||
high_level_elixir = create_test_elixir("筑基丹", Realm.Foundation_Establishment, price=1000, elixir_id=2)
|
||
test_material = create_test_material("铁矿石", Realm.Qi_Refinement)
|
||
test_weapon = create_test_weapon("青云剑", Realm.Qi_Refinement)
|
||
test_auxiliary = create_test_auxiliary("聚灵珠", Realm.Qi_Refinement)
|
||
|
||
return {
|
||
"elixirs": {
|
||
"聚气丹": [test_elixir],
|
||
"筑基丹": [high_level_elixir]
|
||
},
|
||
"materials": {
|
||
"铁矿石": test_material
|
||
},
|
||
"weapons": {
|
||
"青云剑": test_weapon
|
||
},
|
||
"auxiliaries": {
|
||
"聚灵珠": test_auxiliary
|
||
},
|
||
# Direct access
|
||
"obj_elixir": test_elixir,
|
||
"obj_high_elixir": high_level_elixir,
|
||
"obj_material": test_material,
|
||
"obj_weapon": test_weapon,
|
||
"obj_auxiliary": test_auxiliary
|
||
}
|