diff --git a/src/classes/action.py b/src/classes/action.py index 5b70ebc..d585c05 100644 --- a/src/classes/action.py +++ b/src/classes/action.py @@ -953,19 +953,18 @@ class Talk(DefineAction, ActualActionMixin): return len(self._get_same_region_others()) > 0 def start(self) -> Event: + self.same_region_others = self._get_same_region_others() # 记录开始事件 return Event(self.world.month_stamp, f"{self.avatar.name} 尝试与同区域的他人攀谈") def step(self) -> tuple[StepStatus, list[Event]]: - same_region_others = self._get_same_region_others() - import random - target = random.choice(same_region_others) + target = random.choice(self.same_region_others) # 进入交谈:由概率决定本次是否允许建立关系 from src.classes.mutual_action import Conversation # 由配置决定本次是否有“有机会进入关系”标记 - prob = float(getattr(CONFIG.social, "talk_into_relation_probability", 0.0)) + prob = CONFIG.social.talk_into_relation_probability can_into_relation = (random.random() < prob) conv = Conversation(self.avatar, self.world) diff --git a/src/classes/calendar.py b/src/classes/calendar.py index 1bd11da..f7628a4 100644 --- a/src/classes/calendar.py +++ b/src/classes/calendar.py @@ -39,8 +39,6 @@ class MonthStamp(int): def __add__(self, other: int) -> 'MonthStamp': return MonthStamp(int(self) + other) - - def create_month_stamp(year: Year, month: Month) -> MonthStamp: """从年和月创建MonthStamp""" return MonthStamp(int(year) * 12 + month.value - 1) \ No newline at end of file diff --git a/src/classes/magic_stone.py b/src/classes/magic_stone.py index dc00434..41d9b3e 100644 --- a/src/classes/magic_stone.py +++ b/src/classes/magic_stone.py @@ -11,9 +11,11 @@ class MagicStone(int): self.value = value def exchange(self) -> tuple[int, int, int]: - _value, _upper = divmod(self.value, 100) - _value, _middle = divmod(_value, 100) - return _upper, _middle, _value + # 期望顺序:返回 (上品, 中品, 下品) + # 汇率:100 下品 = 1 中品;100 中品 = 1 上品 + _middle_total, _lower = divmod(self.value, 100) + _upper, _middle = divmod(_middle_total, 100) + return _upper, _middle, _lower def __str__(self) -> str: _upper, _middle, _value = self.exchange() @@ -24,7 +26,21 @@ class MagicStone(int): return MagicStone(self.value + other) return MagicStone(self.value + other.value) + def __iadd__(self, other: Union['MagicStone', int]) -> 'MagicStone': + if isinstance(other, int): + self.value += other + else: + self.value += other.value + return self + def __sub__(self, other: Union['MagicStone', int]) -> 'MagicStone': if isinstance(other, int): return MagicStone(self.value - other) - return MagicStone(self.value - other.value) \ No newline at end of file + return MagicStone(self.value - other.value) + + def __isub__(self, other: Union['MagicStone', int]) -> 'MagicStone': + if isinstance(other, int): + self.value -= other + else: + self.value -= other.value + return self \ No newline at end of file diff --git a/src/classes/mutual_action.py b/src/classes/mutual_action.py index db7021d..ace1299 100644 --- a/src/classes/mutual_action.py +++ b/src/classes/mutual_action.py @@ -303,27 +303,26 @@ class Conversation(MutualAction, ActualActionMixin): target.thinking = thinking - # 拒绝则只记录反馈 - if feedback and feedback != "Talk": + fb = feedback.strip() + # 仅当明确接受时才记录对话与关系;其余一律视为拒绝 + if fb == "Talk": + if talk_content: + content_event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target.name} 的交谈:{talk_content}") + # 进入侧栏一次,并写入双方历史 + self._add_event_pair(content_event, initiator=self.avatar, target=target) + + if can_into_relation and into_relation_str: + rel = Relation.from_chinese(into_relation_str) + if rel is not None: + self.avatar.set_relation(target, rel) + set_event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target.name} 的关系变为:{relation_display_names.get(rel, str(rel))}") + self._add_event_pair(set_event, initiator=self.avatar, target=target) + + return StepStatus.COMPLETED, [] + else: feedback_event = Event(self.world.month_stamp, f"{target.name} 拒绝与 {self.avatar.name} 交谈") self._add_event_pair(feedback_event, initiator=self.avatar, target=target) return StepStatus.COMPLETED, [] - # 接受并记录对话内容 - if talk_content: - content_event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target.name} 的交谈:{talk_content}") - # 进入侧栏一次,并写入双方历史 - self._add_event_pair(content_event, initiator=self.avatar, target=target) - - # 仅当 can_into_relation=True 时,根据返回尝试建立关系 - if can_into_relation and into_relation_str: - rel = Relation.from_chinese(into_relation_str) - if rel is not None: - self.avatar.set_relation(target, rel) - set_event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target.name} 的关系变为:{relation_display_names.get(rel, str(rel))}") - self._add_event_pair(set_event, initiator=self.avatar, target=target) - - return StepStatus.COMPLETED, [] - def finish(self, target_avatar: "Avatar|str", **kwargs) -> list[Event]: return [] \ No newline at end of file diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 1cb68e2..6d75c23 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -16,45 +16,44 @@ class Simulator: self.world = world self.birth_rate = CONFIG.game.npc_birth_rate_per_month # 从配置文件读取NPC每月出生率 - async def step(self): + async def _phase_decide_actions(self): """ - 前进一步(每步模拟是一个月时间) - 结算这个时间内的所有情况。 - 角色行为、世界变化、重大事件、etc。 - 先结算多个角色间互相交互的事件。 - 再去结算单个角色的事件。 + 决策阶段:仅对需要新计划的角色调用 AI(当前无动作且无计划), + 将 AI 的决策结果加载为角色的计划链。 """ - events = [] # list of Event - death_avatar_ids = [] # list of str - - # 决策阶段:仅对需要新计划的角色调用 AI(当前无动作且无计划) avatars_to_decide = [] for avatar in list(self.world.avatar_manager.avatars.values()): if avatar.current_action is None and not avatar.has_plans(): avatars_to_decide.append(avatar) + if not avatars_to_decide: + return if CONFIG.ai.mode == "llm": ai = llm_ai else: ai = rule_ai - if avatars_to_decide: - decide_results = await ai.decide(self.world, avatars_to_decide) - for avatar, result in decide_results.items(): - action_name_params_pairs, avatar_thinking, objective, _event = result - # 仅入队计划,不在此处添加开始事件,避免与提交阶段重复 - avatar.load_decide_result_chain(action_name_params_pairs, avatar_thinking, objective) - - # 提交阶段:为空闲角色提交计划中的下一个可执行动作 + decide_results = await ai.decide(self.world, avatars_to_decide) + for avatar, result in decide_results.items(): + action_name_params_pairs, avatar_thinking, objective, _event = result + # 仅入队计划,不在此处添加开始事件,避免与提交阶段重复 + avatar.load_decide_result_chain(action_name_params_pairs, avatar_thinking, objective) + + def _phase_commit_next_plans(self): + """ + 提交阶段:为空闲角色提交计划中的下一个可执行动作,返回开始事件集合。 + """ + events = [] for avatar in list(self.world.avatar_manager.avatars.values()): if avatar.current_action is None: start_event = avatar.commit_next_plan() if start_event is not None and not is_null_event(start_event): events.append(start_event) + return events - # 执行阶段:推进当前动作,支持同月链式抢占即时结算 - # 采用最多3轮的小循环: - # - 每轮遍历现有角色执行一次 tick_action - # - 若本轮有角色在遍历过程中被抢占并新设了动作(标记 _new_action_set_this_step=True),下一轮继续执行 - # - 最多 3 轮以防极端互相抢占导致长链 + async def _phase_execute_actions(self): + """ + 执行阶段:推进当前动作,支持同月链式抢占即时结算,返回期间产生的事件。 + """ + events = [] MAX_LOCAL_ROUNDS = 3 for _ in range(MAX_LOCAL_ROUNDS): new_action_happened = False @@ -71,8 +70,14 @@ class Simulator: # 若本轮未检测到新动作产生,则结束本地循环 if not new_action_happened: break + return events - # 结算战斗等导致的死亡逻辑 + def _phase_resolve_death(self): + """ + 结算战斗等导致的死亡以及寿终正寝,移除死亡角色,返回死亡事件集合。 + """ + events = [] + death_avatar_ids = [] for avatar_id, avatar in list(self.world.avatar_manager.avatars.items()): if avatar.hp <= 0: death_avatar_ids.append(avatar_id) @@ -82,17 +87,17 @@ class Simulator: death_avatar_ids.append(avatar_id) event = Event(self.world.month_stamp, f"{avatar.name} 老死了,时年{avatar.age.get_age()}岁") events.append(event) - # 删除死亡的角色(由 AvatarManager 清理关系并移除) if death_avatar_ids: self.world.avatar_manager.remove_avatars(death_avatar_ids) - - # 寿命逻辑 + return events + + def _phase_update_age_and_birth(self): + """ + 更新存活角色年龄,并以一定概率生成新修士,返回期间产生的事件集合。 + """ + events = [] for avatar_id, avatar in self.world.avatar_manager.avatars.items(): avatar.update_age(self.world.month_stamp) - - - - # 新角色 if random.random() < self.birth_rate: age = random.randint(16, 60) gender = random.choice(list(Gender)) @@ -101,13 +106,46 @@ class Simulator: self.world.avatar_manager.avatars[new_avatar.id] = new_avatar event = Event(self.world.month_stamp, f"{new_avatar.name}晋升为修士了。") events.append(event) + return events - # 将事件写入日志 + def _phase_log_events(self, events): + """ + 将事件写入日志。 + """ logger = get_logger().logger for event in events: logger.info("EVENT: %s", str(event)) - # 最后结算年月 + + async def step(self): + """ + 前进一步(每步模拟是一个月时间) + 结算这个时间内的所有情况。 + 角色行为、世界变化、重大事件、etc。 + 先结算多个角色间互相交互的事件。 + 再去结算单个角色的事件。 + """ + events = [] # list of Event + + # 1. 决策阶段 + await self._phase_decide_actions() + + # 2. 提交阶段 + events.extend(self._phase_commit_next_plans()) + + # 3. 执行阶段 + events.extend(await self._phase_execute_actions()) + + # 4. 结算死亡 + events.extend(self._phase_resolve_death()) + + # 5. 年龄与新生 + events.extend(self._phase_update_age_and_birth()) + + # 6. 日志 + self._phase_log_events(events) + + # 7. 时间推进 self.world.month_stamp = self.world.month_stamp + 1 return events diff --git a/static/config.yml b/static/config.yml index e5e8d39..55f6b30 100644 --- a/static/config.yml +++ b/static/config.yml @@ -13,7 +13,7 @@ ai: max_decide_num: 3 game: - init_npc_num: 6 + init_npc_num: 3 npc_birth_rate_per_month: 0.001 df: