Refactor/event (#31)

重构事件机制和部分拍卖会机制
This commit is contained in:
4thfever
2026-01-14 16:58:50 +08:00
committed by GitHub
parent 63fc2f828e
commit df8b1b9433
16 changed files with 401 additions and 179 deletions

View File

@@ -35,11 +35,6 @@ class Escape(InstantAction):
avatar.clear_plans()
avatar.current_action = None
def _add_event_pair(self, event: Event, initiator: "Avatar", target: "Avatar|None") -> None:
initiator.add_event(event)
if target is not None:
target.add_event(event, to_sidebar=False)
def _execute(self, avatar_name: str) -> None:
target = self._find_avatar_by_name(avatar_name)
if target is None:

View File

@@ -14,12 +14,16 @@ class EventHelper:
"""
@staticmethod
def push_pair(event: Event, initiator: "Avatar", target: Optional["Avatar"], *, to_sidebar_once: bool = True) -> None:
"""
向双方分发事件。
发起者侧会进入侧栏to_sidebar=True从而被 Simulator 收集并存入全局数据库。
由于 Simulator 已经统一处理了相关角色的交互计数,这里无需再手动调用 target.add_event。
"""
initiator.add_event(event, to_sidebar=True)
if target is not None:
target.add_event(event, to_sidebar=(not to_sidebar_once))
@staticmethod
def push_self(event: Event, avatar: "Avatar", *, to_sidebar: bool = True) -> None:
"""仅向自身分发事件。"""
avatar.add_event(event, to_sidebar=to_sidebar)

View File

@@ -168,15 +168,21 @@ class ActionMixin:
if to_sidebar:
self._pending_events.append(event)
# 增加关系交互计数
if event.related_avatars:
for aid in event.related_avatars:
if str(aid) == str(self.id):
continue
# self.id 与 aid 有交互
# Avatar 核心类已定义 relation_interaction_states
self.relation_interaction_states[aid]["count"] += 1
def process_interaction_from_event(self: "Avatar", event: "Event") -> None:
"""
根据事件更新与其他角色的交互计数。
该方法由 Simulator 统一调用。
"""
if not event.related_avatars:
return
for aid in event.related_avatars:
if str(aid) == str(self.id):
continue
# self.id 与 aid 有交互
# relation_interaction_states 是 defaultdict会自动初始化新条目
self.relation_interaction_states[aid]["count"] += 1
def get_planned_actions_str(self: "Avatar") -> str:
"""

View File

@@ -225,65 +225,39 @@ class Auction(Gathering):
return deal_results, unsold_items, all_willing_prices
def _generate_deal_events(
def _generate_auction_events(
self,
world: "World",
deal_results: Dict["Item", tuple["Avatar", int]]
deal_results: Dict["Item", tuple["Avatar", int]],
willing_prices: Dict["Item", Dict["Avatar", int]]
) -> List[Event]:
"""
生成成交事件
生成拍卖事件(合并成交与竞争信息)
"""
events = []
month_stamp = world.month_stamp
for item, (winner, deal_price) in deal_results.items():
deal_event = Event(
month_stamp=month_stamp,
content=f"在拍卖会上,{winner.name}{deal_price} 灵石拍下了{item.name}",
related_avatars=[winner.id],
is_major=False
)
events.append(deal_event)
# to_sidebar=False: 不加入 _pending_events防止重复提交给 Simulator
# 但仍会执行关系计数的更新逻辑
winner.add_event(deal_event, to_sidebar=False)
return events
def _generate_rivalry_events(
self,
world: "World",
willing_prices: Dict["Item", Dict["Avatar", int]],
deal_results: Dict["Item", tuple["Avatar", int]]
) -> List[Event]:
"""
生成竞争(压一头)事件
"""
events = []
month_stamp = world.month_stamp
for item in deal_results.keys():
bids = willing_prices.get(item, {})
if len(bids) < 2:
continue
# 检查是否有竞争者(出价人数 >= 2
if len(bids) >= 2:
# 获取出价第二名
sorted_bids = sorted(bids.items(), key=lambda x: x[1], reverse=True)
runner_up = sorted_bids[1][0]
# 获取出价前两名
sorted_bids = sorted(bids.items(), key=lambda x: x[1], reverse=True)
winner_avatar = sorted_bids[0][0]
runner_up_avatar = sorted_bids[1][0]
# 生成压一头事件
rivalry_content = f"{item.name}的竞拍中,{winner_avatar.name}力压{runner_up_avatar.name}一头,最终将其收入囊中。"
rivalry_event = Event(
content = f"{item.name}的竞拍中,{winner.name}{deal_price} 灵石力压{runner_up.name}一头,将其收入囊中。"
related_avatars = [winner.id, runner_up.id]
else:
content = f"在拍卖会上,{winner.name}{deal_price} 灵石拍下了{item.name}"
related_avatars = [winner.id]
event = Event(
month_stamp=month_stamp,
content=rivalry_content,
related_avatars=[winner_avatar.id, runner_up_avatar.id],
content=content,
related_avatars=related_avatars,
is_major=False
)
events.append(rivalry_event)
# 给双方都添加这个互动事件 (to_sidebar=False 防止重复提交)
winner_avatar.add_event(rivalry_event, to_sidebar=False)
runner_up_avatar.add_event(rivalry_event, to_sidebar=False)
events.append(event)
return events
@@ -390,9 +364,6 @@ class Auction(Gathering):
)
events.append(story_event)
for av in related_avatars:
av.add_event(story_event, to_sidebar=False)
return events
async def execute(self, world: "World") -> List[Event]:
@@ -468,12 +439,9 @@ class Auction(Gathering):
for item in unsold_items:
world.circulation.remove_item(item)
# 5. 生成基础事件
deal_events = self._generate_deal_events(world, deal_results)
rivalry_events = self._generate_rivalry_events(world, willing_prices, deal_results)
events.extend(deal_events)
events.extend(rivalry_events)
# 5. 生成基础事件(合并成交与竞争信息)
auction_events = self._generate_auction_events(world, deal_results, willing_prices)
events.extend(auction_events)
# 6. 生成故事 (StoryTeller)
story_events = await self._generate_story(world, deal_results, willing_prices)

View File

@@ -55,10 +55,6 @@ class DualCultivation(MutualAction):
event = Event(self.world.month_stamp, f"{self.avatar.name} 邀请 {target_name} 进行双修", related_avatars=rel_ids, is_major=True)
# 仅手动添加给 TargetSelf的部分由ActionMixin通过返回值处理
if target is not None:
target.add_event(event, to_sidebar=False)
# 记录开始文本用于故事生成
self._start_event_content = event.content
# 初始化内部标记,避免后续 getattr

View File

@@ -181,11 +181,6 @@ class Gift(MutualAction):
related_avatars=rel_ids
)
# 写入历史
self.avatar.add_event(event, to_sidebar=False)
if target is not None:
target.add_event(event, to_sidebar=False)
self._gift_success = False
return event

View File

@@ -65,10 +65,6 @@ class Impart(MutualAction):
f"{self.avatar.name} 向徒弟 {target_name} 传道授业",
related_avatars=rel_ids
)
# 仅写入历史
self.avatar.add_event(event, to_sidebar=False)
if target is not None:
target.add_event(event, to_sidebar=False)
# 初始化内部标记
self._impart_success = False
self._impart_exp_gain = 0

View File

@@ -179,11 +179,6 @@ class MutualAction(DefineAction, LLMAction, ActualActionMixin, TargetingMixin):
is_major = self.__class__.IS_MAJOR if hasattr(self.__class__, 'IS_MAJOR') else False
event = Event(self._start_month_stamp, f"{self.avatar.name}{target_name} 发起 {action_name}", related_avatars=rel_ids, is_major=is_major)
# 仅手动添加给 TargetSelf的部分由ActionMixin通过返回值处理
# 默认不推Target侧边栏因为发起事件通常只在发起者侧重要或者作为"收到发起"的通知
if target is not None:
target.add_event(event, to_sidebar=False)
return event
def step(self, target_avatar: "Avatar|str") -> ActionResult:

View File

@@ -81,10 +81,6 @@ class Occupy(MutualAction):
related_avatars=rel_ids,
is_major=self.IS_MAJOR
)
# 记录到历史,侧边栏推送由 ActionMixin.commit_next_plan 统一处理
self.avatar.add_event(event, to_sidebar=False)
if host:
host.add_event(event, to_sidebar=False)
return event

View File

@@ -121,10 +121,6 @@ class RelationResolver:
event = Event(month_stamp, event_text, related_avatars=[avatar_a.id, avatar_b.id], is_major=True)
if event:
# 手动调用 add_event(to_sidebar=False) 来更新统计数据,但不加入 pending_events
# 因为事件将由 Simulator 统一处理
avatar_a.add_event(event, to_sidebar=False)
avatar_b.add_event(event, to_sidebar=False)
return event
return None

View File

@@ -73,6 +73,7 @@ class World():
"动作": "你有一系列可以执行的动作。要注意动作的效果、限制条件、区域和时间。",
"装备与丹药": "通过兵器、辅助装备、丹药等装备,可以获得额外的属性加成,获得或小或大的增益。拥有好的装备或者服用好的丹药,能获得很大好处。",
"购物": "在城市区域可以购买练气级别丹药、兵器。购买丹药后会立刻服用强化自身。购买兵器可以帮自己切换兵器类型为顺手的类型。",
"拍卖会": "每隔一段不确定的时间会有神秘人组织的拍卖会,或许有好货出售。"
}
if self.history:
desc["历史"] = self.history

View File

@@ -33,11 +33,10 @@ class Simulator:
from src.classes.observe import get_avatar_observation_radius
from src.classes.region import CultivateRegion
events = []
# 1. 缓存当前有洞府的角色ID
avatars_with_home = set()
# 注意:这里我们只关心 CultivateRegion 的 host
# map.cultivate_regions 可能需要确保被正确初始化,如果没有,可以回退到遍历所有 regions
# 为了稳妥,遍历所有 Region 筛选
# ...
cultivate_regions = [
r for r in self.world.map.regions.values()
if isinstance(r, CultivateRegion)
@@ -49,13 +48,11 @@ class Simulator:
# 2. 遍历所有存活角色
for avatar in self.world.avatar_manager.get_living_avatars():
# ...
# 计算感知半径(曼哈顿距离)
radius = get_avatar_observation_radius(avatar)
# 扫描范围内的坐标
# 优化只扫描半径内的坐标可能比遍历所有region快也可能慢取决于地图大小和半径
# 地图可能很大,半径通常很小(<10所以基于坐标扫描更优
# ...
# 获取范围内的有效坐标
start_x = max(0, avatar.pos_x - radius)
end_x = min(self.world.map.width - 1, avatar.pos_x + radius)
@@ -91,8 +88,9 @@ class Simulator:
f"{avatar.name} 路过 {region.name},发现无主,将其占据。",
related_avatars=[avatar.id]
)
avatar.add_event(event)
events.append(event)
return events
async def _phase_decide_actions(self):
"""
决策阶段:仅对需要新计划的角色调用 AI当前无动作且无计划
@@ -328,6 +326,34 @@ class Simulator:
for event in events:
logger.info("EVENT: %s", str(event))
def _phase_process_interactions(self, events: list[Event]):
"""
处理事件中的交互逻辑:
遍历所有事件,如果事件涉及多个角色,自动更新这些角色之间的交互计数。
"""
for event in events:
if not event.related_avatars or len(event.related_avatars) < 2:
continue
# 只有当事件涉及 >=2 个角色时才视为交互
for aid in event.related_avatars:
avatar = self.world.avatar_manager.get_avatar(aid)
if avatar:
avatar.process_interaction_from_event(event)
def _phase_handle_interactions(self, events: list[Event], processed_ids: set[str]):
"""
从事件列表中提取尚未处理过的交互事件,并更新交互计数。
"""
new_interactions = []
for e in events:
if e.id not in processed_ids:
if e.related_avatars and len(e.related_avatars) >= 2:
new_interactions.append(e)
processed_ids.add(e.id)
if new_interactions:
self._phase_process_interactions(new_interactions)
async def _phase_evolve_relations(self):
"""
@@ -351,15 +377,8 @@ class Simulator:
continue
# 判定是否触发
count = state["count"]
should_trigger = False
threshold = CONFIG.social.relation_check_threshold
if count >= threshold:
should_trigger = True
if should_trigger:
if state["count"] >= threshold:
# 确保唯一性
id1, id2 = sorted([str(avatar.id), str(target.id)])
pair_key = (id1, id2)
@@ -369,17 +388,14 @@ class Simulator:
pairs_to_resolve.append((avatar, target))
# 重置双方的计数器,防止重复触发
# 1. 重置 A 侧
state["count"] = 0
state["checked_times"] += 1
# 2. 重置 B 侧 (如果 B 也有状态记录)
if hasattr(target, "relation_interaction_states"):
# target 对 avatar 的记录
t_state = target.relation_interaction_states[str(avatar.id)]
t_state["count"] = 0
t_state["checked_times"] += 1
# 2. 重置 B 侧
t_state = target.relation_interaction_states[str(avatar.id)]
t_state["count"] = 0
t_state["checked_times"] += 1
events = []
if pairs_to_resolve:
@@ -392,22 +408,33 @@ class Simulator:
async def step(self):
"""
前进一步(每步模拟是一个月时间)
结算这个时间内的所有情况。
角色行为、世界变化、重大事件、etc。
先结算多个角色间互相交互的事件。
再去结算单个角色的事件。
前进一个时间步(一个月):
1. 感知与认知更新(及自动占据洞府)
2. 长期目标思考
3. Gathering 多人聚集结算
4. 决策阶段 (AI 选择动作)
5. 提交阶段 (开始执行动作)
6. 执行阶段 (动作 Tick)
7. 处理初步交互计数 (用于后续关系演化)
8. 关系演化阶段
9. 结算死亡
10. 年龄与新生
11. 被动结算 (丹药、时间效果、奇遇)
12. 绰号生成
13. 天地灵机更新
14. 处理剩余交互计数 (如奇遇产生的交互)
15. 归档与时间推进
"""
events = [] # list of Event
events: list[Event] = []
processed_event_ids: set[str] = set()
# 1. 感知与认知更新阶段(包括自动占据洞府)
# 在思考和决策之前,先让角色感知世界
self._phase_update_perception_and_knowledge()
# 1. 感知与认知更新
events.extend(self._phase_update_perception_and_knowledge())
# 2. 长期目标思考阶段(在决策之前)
# 2. 长期目标思考
events.extend(await self._phase_long_term_objective_thinking())
# 3. Gathering 结算阶段
# 3. Gathering 结算
events.extend(await self._phase_process_gatherings())
# 4. 决策阶段
@@ -419,42 +446,53 @@ class Simulator:
# 6. 执行阶段
events.extend(await self._phase_execute_actions())
# 7. 关系演化阶段
# 7. 处理初步交互计数
self._phase_handle_interactions(events, processed_event_ids)
# 8. 关系演化
events.extend(await self._phase_evolve_relations())
# 8. 结算死亡
# 9. 结算死亡
events.extend(self._phase_resolve_death())
# 9. 年龄与新生
# 10. 年龄与新生
events.extend(self._phase_update_age_and_birth())
# 10. 被动结算(时间效果+奇遇)
# 11. 被动结算
events.extend(await self._phase_passive_effects())
# 11. 绰号生成
# 12. 绰号生成
events.extend(await self._phase_nickname_generation())
# 12. 更新天地灵机
# 13. 更新天地灵机
events.extend(self._phase_update_celestial_phenomenon())
# 13. 日志
# 去重:基于 ID 去重防止同一个事件对象或相同ID的事件被多次添加
# 常见情况Gathering 既返回了事件,又将其加入了 Avatar 的 pending_events
unique_events = {}
# 14. 处理剩余阶段的交互计数
self._phase_handle_interactions(events, processed_event_ids)
# 15. 归档与时间推进
return self._finalize_step(events)
def _finalize_step(self, events: list[Event]) -> list[Event]:
"""
本轮步进的最终归档:去重、入库、打日志、推进时间。
"""
# 1. 基于 ID 去重(防止同一个事件对象被多次添加)
unique_events: dict[str, Event] = {}
for e in events:
if e.id not in unique_events:
unique_events[e.id] = e
# 保持原有顺序Python 3.7+ dict 保持插入序)
events = list(unique_events.values())
final_events = list(unique_events.values())
# 统一写入事件管理器
if hasattr(self.world, "event_manager") and self.world.event_manager is not None:
for e in events:
# 2. 统一写入事件管理器
if self.world.event_manager:
for e in final_events:
self.world.event_manager.add_event(e)
self._phase_log_events(events)
# 3. 记录日志
self._phase_log_events(final_events)
# 14. 时间推进
# 4. 时间推进
self.world.month_stamp = self.world.month_stamp + 1
return events
return final_events

View File

@@ -1,5 +1,5 @@
meta:
version: "1.1.0"
version: "1.2.0"
llm:
default_modes:
@@ -28,7 +28,7 @@ game:
fortune_probability: 0.005
misfortune_probability: 0.005
gathering:
auction_trigger_count: 10
auction_trigger_count: 5
df:
ids_separator: ";"

178
tests/test_event.py Normal file
View File

@@ -0,0 +1,178 @@
import pytest
from unittest.mock import MagicMock, patch
from src.classes.event import Event
from src.sim.simulator import Simulator
from src.classes.calendar import create_month_stamp, Year, Month
class TestEventLogic:
@pytest.fixture
def avatar_a(self, dummy_avatar):
dummy_avatar.id = "avatar_a"
dummy_avatar.name = "角色A"
# 重置交互状态
dummy_avatar.relation_interaction_states.clear()
return dummy_avatar
@pytest.fixture
def avatar_b(self, base_world):
from src.classes.avatar.core import Avatar, Gender
from src.classes.age import Age
from src.classes.cultivation import Realm
from src.classes.root import Root
from src.classes.alignment import Alignment
av = Avatar(
world=base_world,
name="角色B",
id="avatar_b",
birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY),
age=Age(20, Realm.Qi_Refinement),
gender=Gender.FEMALE,
pos_x=0,
pos_y=0,
root=Root.WATER,
personas=[],
alignment=Alignment.RIGHTEOUS
)
av.relation_interaction_states.clear()
return av
def test_process_interaction_from_event(self, avatar_a, avatar_b):
"""测试 Avatar.process_interaction_from_event 是否正确增加计数"""
month_stamp = avatar_a.world.month_stamp
event = Event(
month_stamp=month_stamp,
content="A与B发生了互动",
related_avatars=[avatar_a.id, avatar_b.id]
)
# 初始计数应为 0
assert avatar_a.relation_interaction_states[avatar_b.id]["count"] == 0
assert avatar_b.relation_interaction_states[avatar_a.id]["count"] == 0
# 处理事件
avatar_a.process_interaction_from_event(event)
avatar_b.process_interaction_from_event(event)
# 计数应增加
assert avatar_a.relation_interaction_states[avatar_b.id]["count"] == 1
assert avatar_b.relation_interaction_states[avatar_a.id]["count"] == 1
def test_process_interaction_self_exclusion(self, avatar_a):
"""测试交互计数是否排除了自身"""
month_stamp = avatar_a.world.month_stamp
event = Event(
month_stamp=month_stamp,
content="A自己做了某事",
related_avatars=[avatar_a.id]
)
avatar_a.process_interaction_from_event(event)
# 不应该有自己的交互记录
assert avatar_a.id not in avatar_a.relation_interaction_states
def test_simulator_phase_handle_interactions(self, base_world, avatar_a, avatar_b):
"""测试 Simulator._phase_handle_interactions 的增量处理逻辑"""
# 注册角色
base_world.avatar_manager.register_avatar(avatar_a)
base_world.avatar_manager.register_avatar(avatar_b)
sim = Simulator(base_world)
processed_ids = set()
event1 = Event(base_world.month_stamp, "事件1", related_avatars=[avatar_a.id, avatar_b.id])
event2 = Event(base_world.month_stamp, "事件2", related_avatars=[avatar_a.id, avatar_b.id])
# 1. 处理第一批事件
sim._phase_handle_interactions([event1], processed_ids)
assert avatar_a.relation_interaction_states[avatar_b.id]["count"] == 1
assert event1.id in processed_ids
# 2. 再次处理相同的事件(模拟 Phase 14 补漏但去重)
sim._phase_handle_interactions([event1, event2], processed_ids)
# event1 应该被跳过event2 应该被处理
assert avatar_a.relation_interaction_states[avatar_b.id]["count"] == 2
assert event2.id in processed_ids
@pytest.mark.asyncio
async def test_simulator_full_step_interaction_counting(self, base_world, avatar_a, avatar_b, mock_llm_managers):
"""测试 Simulator.step 完整流程中的交互计数统计"""
# 将角色注册到世界
base_world.avatar_manager.register_avatar(avatar_a)
base_world.avatar_manager.register_avatar(avatar_b)
# Mock 一个返回事件的阶段,例如 Action 执行
# 我们直接手动模拟 events 列表的变化
sim = Simulator(base_world)
# 构造一个交互事件
interaction_event = Event(
base_world.month_stamp,
"发生了某种跨阶段的交互",
related_avatars=[avatar_a.id, avatar_b.id]
)
# 我们通过 patch 让某些阶段返回这个事件
with patch.object(Simulator, "_phase_execute_actions", return_value=[interaction_event]):
# 执行一步
await sim.step()
# 验证交互计数是否正确增加
# 注意:由于我们在 step 中有两个 Phase (7 和 14) 调用了 _phase_handle_interactions
# 且通过 processed_event_ids 去重,所以最终计数应该是 1 而不是 2。
assert avatar_a.relation_interaction_states[avatar_b.id]["count"] == 1
assert avatar_b.relation_interaction_states[avatar_a.id]["count"] == 1
@pytest.mark.asyncio
async def test_simulator_relation_evolution_trigger(self, base_world, avatar_a, avatar_b, mock_llm_managers):
"""测试交互计数达到阈值时触发关系演化并重置计数"""
from src.utils.config import CONFIG
threshold = CONFIG.social.relation_check_threshold
# 1. 注册角色并人工设置高计数
base_world.avatar_manager.register_avatar(avatar_a)
base_world.avatar_manager.register_avatar(avatar_b)
avatar_a.relation_interaction_states[avatar_b.id]["count"] = threshold
avatar_b.relation_interaction_states[avatar_a.id]["count"] = threshold
sim = Simulator(base_world)
# 2. 模拟 LLM 返回关系变化
mock_llm_managers["rr"].return_value = [Event(base_world.month_stamp, "关系进化了", related_avatars=[avatar_a.id, avatar_b.id])]
# 3. 执行关系演化阶段
events = await sim._phase_evolve_relations()
# 4. 验证
assert len(events) == 1
assert "关系进化了" in events[0].content
# 计数器应该被重置
assert avatar_a.relation_interaction_states[avatar_b.id]["count"] == 0
assert avatar_b.relation_interaction_states[avatar_a.id]["count"] == 0
# 检查次数应该增加
assert avatar_a.relation_interaction_states[avatar_b.id]["checked_times"] == 1
assert avatar_b.relation_interaction_states[avatar_a.id]["checked_times"] == 1
def test_event_helper_push_pair_no_longer_calls_target_add_event(self, avatar_a, avatar_b):
"""测试 EventHelper.push_pair 是否已移除冗余的 target.add_event 调用"""
from src.classes.action.event_helper import EventHelper
event = Event(avatar_a.world.month_stamp, "测试事件", related_avatars=[avatar_a.id, avatar_b.id])
# 重置 mock 或记录
avatar_a._pending_events = []
avatar_b._pending_events = []
EventHelper.push_pair(event, initiator=avatar_a, target=avatar_b)
# 应该只有发起者收到了事件(进入待处理队列)
assert len(avatar_a._pending_events) == 1
assert len(avatar_b._pending_events) == 0
# 验证交互计数没有在这里被增加(因为现在由 Simulator 统一处理)
assert avatar_a.relation_interaction_states[avatar_b.id]["count"] == 0

View File

@@ -7,7 +7,8 @@ from src.classes.action.move_to_direction import MoveToDirection
from src.classes.tile import TileType
from src.classes.action_runtime import ActionInstance
def test_simulator_step_moves_avatar_and_sets_tile(base_world, dummy_avatar, mock_llm_managers):
@pytest.mark.asyncio
async def test_simulator_step_moves_avatar_and_sets_tile(base_world, dummy_avatar, mock_llm_managers):
# Set initial position
dummy_avatar.pos_x = 1
dummy_avatar.pos_y = 1
@@ -29,8 +30,8 @@ def test_simulator_step_moves_avatar_and_sets_tile(base_world, dummy_avatar, moc
# Mock LLM to avoid external calls or errors (Handled by mock_llm_managers fixture)
print(f"DEBUG: Before step: pos_x={dummy_avatar.pos_x}")
# Run step synchronously
asyncio.run(sim.step())
# Run step
await 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}")
@@ -44,3 +45,81 @@ def test_simulator_step_moves_avatar_and_sets_tile(base_world, dummy_avatar, moc
assert dummy_avatar.tile is not None
assert dummy_avatar.tile.x == 3
assert dummy_avatar.tile.y == 1
@pytest.mark.asyncio
async def test_simulator_interaction_counting(base_world, dummy_avatar, mock_llm_managers):
"""测试交互计数在 step 中被正确处理且去重"""
from src.classes.event import Event
from src.classes.avatar import Avatar, Gender
from src.classes.age import Age
from src.classes.cultivation import Realm
from src.classes.calendar import create_month_stamp, Year, Month
from src.classes.root import Root
from src.classes.alignment import Alignment
sim = Simulator(base_world)
# 创建另一个角色用于交互
other_avatar = Avatar(
world=base_world,
name="OtherAvatar",
id="other_id",
birth_month_stamp=create_month_stamp(Year(2000), Month.JANUARY),
age=Age(20, Realm.Qi_Refinement),
gender=Gender.FEMALE,
pos_x=0,
pos_y=0,
root=Root.WOOD,
alignment=Alignment.NEUTRAL
)
base_world.avatar_manager.register_avatar(dummy_avatar)
base_world.avatar_manager.register_avatar(other_avatar)
# 1. 模拟第一阶段(执行动作)产生的事件
ev1 = Event(base_world.month_stamp, "阶段1交互", related_avatars=[dummy_avatar.id, other_avatar.id])
# 2. 模拟第二阶段(被动效果/奇遇)产生的事件
ev2 = Event(base_world.month_stamp, "阶段2交互", related_avatars=[dummy_avatar.id, other_avatar.id])
with patch.object(sim, '_phase_execute_actions', new_callable=AsyncMock) as mock_exec, \
patch.object(sim, '_phase_passive_effects', new_callable=AsyncMock) as mock_passive:
mock_exec.return_value = [ev1]
mock_passive.return_value = [ev2]
await sim.step()
# 验证交互计数是否增加了 2 (两个独立事件)
count = dummy_avatar.relation_interaction_states[other_avatar.id]["count"]
assert count == 2
@pytest.mark.asyncio
async def test_simulator_event_deduplication(base_world, dummy_avatar, mock_llm_managers):
"""测试事件去重逻辑:同一个事件对象在多个阶段被返回时,入库应只有一次"""
from src.classes.event import Event
sim = Simulator(base_world)
base_world.avatar_manager.register_avatar(dummy_avatar)
ev = Event(base_world.month_stamp, "重复事件")
ev_id = ev.id
# 模拟两个不同的阶段返回了同一个 ID 的事件对象
with patch.object(sim, '_phase_execute_actions', new_callable=AsyncMock) as mock_exec, \
patch.object(sim, '_phase_passive_effects', new_callable=AsyncMock) as mock_passive:
mock_exec.return_value = [ev]
mock_passive.return_value = [ev]
# 拦截 event_manager.add_event
base_world.event_manager.add_event = MagicMock()
await sim.step()
# 验证该 ID 的事件只被添加了一次
# 我们过滤出 ID 匹配的调用
target_calls = [
call for call in base_world.event_manager.add_event.call_args_list
if call.args[0].id == ev_id
]
assert len(target_calls) == 1

View File

@@ -14,16 +14,6 @@ const phaseTexts: Record<string, string | string[]> = {
'initializing_sects': '宗门入世',
'generating_avatars': '众修士降临',
'checking_llm': '连通天道意志',
'generating_initial_events': [
'天道轮转,命运初显',
'因果交织,机缘暗涌',
'气运流转,风云将起',
'众生沉浮,天机莫测',
'劫数将至,各凭造化',
'红尘万丈,道心初定',
'缘起缘灭,皆是天意',
'大道无形,万法归一',
],
'loading_save': '读取前世因果',
'parsing_data': '解析天地法则',
'restoring_state': '恢复时空位面',
@@ -32,9 +22,6 @@ const phaseTexts: Record<string, string | string[]> = {
'': '混沌初始',
}
// 用于 generating_initial_events 阶段的轮换文案。
const eventPhaseTextIndex = ref(0)
// Tips 列表
const tips = [
'修改角色目标,可以改变该角色的行事风格',
@@ -56,6 +43,7 @@ const tips = [
'由于大模型需要思考,游戏启动可能耗时较久',
'模拟世界对大模型token消耗较大请注意',
'开局时设定历史,整个修仙世界也会随之而改变',
'拍卖会中拍到的珍宝可能大大提升你的实力,但是要留好灵石',
]
const currentTip = ref(tips[Math.floor(Math.random() * tips.length)])
@@ -67,11 +55,7 @@ let elapsedInterval: ReturnType<typeof setInterval> | null = null
const progress = computed(() => displayProgress.value)
const phaseText = computed(() => {
const phaseName = props.status?.phase_name || ''
const text = phaseTexts[phaseName] || phaseTexts['']
if (Array.isArray(text)) {
return text[eventPhaseTextIndex.value % text.length]
}
return text
return phaseTexts[phaseName] || phaseTexts['']
})
const isError = computed(() => props.status?.status === 'error')
const errorMessage = computed(() => props.status?.error || '未知错误')
@@ -141,11 +125,6 @@ function startTimers() {
}
}
}
// 每 5 秒切换一次 generating_initial_events 的文案。
if (localElapsed.value % 5 === 0) {
eventPhaseTextIndex.value++
}
}, 1000)
}