@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -168,14 +168,20 @@ class ActionMixin:
|
||||
if to_sidebar:
|
||||
self._pending_events.append(event)
|
||||
|
||||
# 增加关系交互计数
|
||||
if event.related_avatars:
|
||||
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 有交互
|
||||
# Avatar 核心类已定义 relation_interaction_states
|
||||
# relation_interaction_states 是 defaultdict,会自动初始化新条目
|
||||
self.relation_interaction_states[aid]["count"] += 1
|
||||
|
||||
def get_planned_actions_str(self: "Avatar") -> str:
|
||||
|
||||
@@ -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)
|
||||
winner_avatar = sorted_bids[0][0]
|
||||
runner_up_avatar = sorted_bids[1][0]
|
||||
runner_up = 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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
# 仅手动添加给 Target,Self的部分由ActionMixin通过返回值处理
|
||||
if target is not None:
|
||||
target.add_event(event, to_sidebar=False)
|
||||
|
||||
# 记录开始文本用于故事生成
|
||||
self._start_event_content = event.content
|
||||
# 初始化内部标记,避免后续 getattr
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
# 仅手动添加给 Target,Self的部分由ActionMixin通过返回值处理
|
||||
# 默认不推Target侧边栏,因为发起事件通常只在发起者侧重要,或者作为"收到发起"的通知
|
||||
if target is not None:
|
||||
target.add_event(event, to_sidebar=False)
|
||||
|
||||
return event
|
||||
|
||||
def step(self, target_avatar: "Avatar|str") -> ActionResult:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -73,6 +73,7 @@ class World():
|
||||
"动作": "你有一系列可以执行的动作。要注意动作的效果、限制条件、区域和时间。",
|
||||
"装备与丹药": "通过兵器、辅助装备、丹药等装备,可以获得额外的属性加成,获得或小或大的增益。拥有好的装备或者服用好的丹药,能获得很大好处。",
|
||||
"购物": "在城市区域可以购买练气级别丹药、兵器。购买丹药后会立刻服用强化自身。购买兵器可以帮自己切换兵器类型为顺手的类型。",
|
||||
"拍卖会": "每隔一段不确定的时间会有神秘人组织的拍卖会,或许有好货出售。"
|
||||
}
|
||||
if self.history:
|
||||
desc["历史"] = self.history
|
||||
|
||||
@@ -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,7 +88,8 @@ 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):
|
||||
"""
|
||||
@@ -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,14 +388,11 @@ 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 的记录
|
||||
# 2. 重置 B 侧
|
||||
t_state = target.relation_interaction_states[str(avatar.id)]
|
||||
t_state["count"] = 0
|
||||
t_state["checked_times"] += 1
|
||||
@@ -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)
|
||||
|
||||
# 14. 时间推进
|
||||
# 3. 记录日志
|
||||
self._phase_log_events(final_events)
|
||||
|
||||
# 4. 时间推进
|
||||
self.world.month_stamp = self.world.month_stamp + 1
|
||||
|
||||
|
||||
return events
|
||||
return final_events
|
||||
|
||||
@@ -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
178
tests/test_event.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user