diff --git a/src/classes/action.py b/src/classes/action.py index aabd08a..cffe7d1 100644 --- a/src/classes/action.py +++ b/src/classes/action.py @@ -11,7 +11,7 @@ from src.classes.event import Event, NULL_EVENT from src.classes.item import Item, items_by_name from src.classes.prices import prices from src.classes.hp_and_mp import HP_MAX_BY_REALM, MP_MAX_BY_REALM -from src.classes.battle import decide_battle +from src.classes.battle import decide_battle, get_escape_success_rate from src.utils.config import CONFIG if TYPE_CHECKING: @@ -287,6 +287,153 @@ class MoveToAvatar(DefineAction, ActualActionMixin): def finish(self, avatar_name: str) -> list[Event]: return [] + +@long_action(step_month=6) +class MoveAwayFromAvatar(DefineAction, ActualActionMixin): + """ + 持续远离指定角色,持续6个月。 + - 规则:每月尝试使与目标的曼哈顿距离增大一步 + - 任何时候都可以启动 + """ + COMMENT = "持续远离指定角色" + DOABLES_REQUIREMENTS = "任何时候都可以执行" + PARAMS = {"avatar_name": "AvatarName"} + + def _find_avatar_by_name(self, name: str) -> "Avatar|None": + for v in self.world.avatar_manager.avatars.values(): + if v.name == name: + return v + return None + + def _execute(self, avatar_name: str) -> None: + target = self._find_avatar_by_name(avatar_name) + if target is None: + return + # 计算远离方向:使曼哈顿距离尽量增大 + dx = 1 if self.avatar.pos_x >= target.pos_x else -1 + dy = 1 if self.avatar.pos_y >= target.pos_y else -1 + nx = self.avatar.pos_x + dx + ny = self.avatar.pos_y + dy + if self.world.map.is_in_bounds(nx, ny): + self.avatar.pos_x = nx + self.avatar.pos_y = ny + self.avatar.tile = self.world.map.get_tile(nx, ny) + + def can_start(self, avatar_name: str | None = None) -> bool: + return True + + def start(self, avatar_name: str) -> Event: + target_name = avatar_name + try: + t = self._find_avatar_by_name(avatar_name) + if t is not None: + target_name = t.name + except Exception: + pass + return Event(self.world.month_stamp, f"{self.avatar.name} 开始远离 {target_name}") + + def step(self, avatar_name: str) -> tuple[StepStatus, list[Event]]: + self.execute(avatar_name=avatar_name) + done = getattr(self, "is_finished")() + return (StepStatus.COMPLETED if done else StepStatus.RUNNING), [] + + def finish(self, avatar_name: str) -> list[Event]: + return [] + + +class MoveAwayFromRegion(DefineAction, ActualActionMixin): + COMMENT = "离开指定区域" + DOABLES_REQUIREMENTS = "任何时候都可以执行" + PARAMS = {"region": "RegionName"} + + def _execute(self, region: str) -> None: + # 简化:向地图边缘移动一步 + dx = 1 if self.avatar.pos_x < self.world.map.width - 1 else -1 + dy = 1 if self.avatar.pos_y < self.world.map.height - 1 else -1 + nx = max(0, min(self.world.map.width - 1, self.avatar.pos_x + dx)) + ny = max(0, min(self.world.map.height - 1, self.avatar.pos_y + dy)) + if self.world.map.is_in_bounds(nx, ny): + self.avatar.pos_x = nx + self.avatar.pos_y = ny + self.avatar.tile = self.world.map.get_tile(nx, ny) + + def can_start(self, region: str | None = None) -> bool: + return True + + def start(self, region: str) -> Event: + return Event(self.world.month_stamp, f"{self.avatar.name} 开始离开 {region}") + + def step(self, region: str) -> tuple[StepStatus, list[Event]]: + self.execute(region=region) + return StepStatus.COMPLETED, [] + + def finish(self, region: str) -> list[Event]: + return [] + + +class Escape(DefineAction, ActualActionMixin): + """ + 逃离:尝试从对方身边脱离(有成功率)。 + 成功:抢占并进入 MoveAwayFromAvatar(6个月)。 + 失败:抢占并进入 Battle。 + """ + COMMENT = "逃离对方(基于成功率判定)" + DOABLES_REQUIREMENTS = "任何时候都可以执行" + PARAMS = {"avatar_name": "AvatarName"} + + def _find_avatar_by_name(self, name: str) -> "Avatar|None": + for v in self.world.avatar_manager.avatars.values(): + if v.name == name: + return v + return None + + def _preempt_avatar(self, avatar: "Avatar") -> None: + 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: + return + escape_rate = float(get_escape_success_rate(target, self.avatar)) + import random as _r + success = _r.random() < escape_rate + result_text = "成功" if success else "失败" + result_event = Event(self.world.month_stamp, f"{self.avatar.name} 试图从 {target.name} 逃离:{result_text}") + self._add_event_pair(result_event, initiator=self.avatar, target=target) + if success: + self._preempt_avatar(self.avatar) + self.avatar.load_decide_result_chain([("MoveAwayFromAvatar", {"avatar_name": avatar_name})], self.avatar.thinking, "") + start_event = self.avatar.commit_next_plan() + if start_event is not None: + self._add_event_pair(start_event, initiator=self.avatar, target=target) + else: + self._preempt_avatar(self.avatar) + self.avatar.load_decide_result_chain([("Battle", {"avatar_name": avatar_name})], self.avatar.thinking, "") + start_event = self.avatar.commit_next_plan() + if start_event is not None: + self._add_event_pair(start_event, initiator=self.avatar, target=target) + + def can_start(self, avatar_name: str | None = None) -> bool: + return True + + def start(self, avatar_name: str) -> Event: + target = self._find_avatar_by_name(avatar_name) + target_name = target.name if target is not None else avatar_name + return Event(self.world.month_stamp, f"{self.avatar.name} 尝试从 {target_name} 逃离") + + def step(self, avatar_name: str) -> tuple[StepStatus, list[Event]]: + self.execute(avatar_name=avatar_name) + return StepStatus.COMPLETED, [] + + def finish(self, avatar_name: str) -> list[Event]: + return [] + @long_action(step_month=10) class Cultivate(DefineAction, ActualActionMixin): """ @@ -686,11 +833,11 @@ class Battle(DefineAction, ActualActionMixin): return StepStatus.COMPLETED, [] def finish(self, avatar_name: str) -> list[Event]: - res = getattr(self, "_last_result", None) + res = self._last_result if isinstance(res, tuple) and len(res) == 2: winner, loser = res return [Event(self.world.month_stamp, f"{winner} 战胜了 {loser}")] - return [] + raise ValueError(f"Battle finish error: {res}") @long_action(step_month=3) diff --git a/src/classes/actions.py b/src/classes/actions.py index dfbf373..b140ca3 100644 --- a/src/classes/actions.py +++ b/src/classes/actions.py @@ -20,10 +20,9 @@ from src.classes.action import ( from src.classes.mutual_action import ( DriveAway, Attack, - MoveAwayFromAvatar, - MoveAwayFromRegion, Conversation, ) +from src.classes.action import MoveAwayFromAvatar, MoveAwayFromRegion, Escape ALL_ACTION_CLASSES = [ @@ -46,6 +45,9 @@ ALL_ACTION_CLASSES = [ MoveAwayFromAvatar, MoveAwayFromRegion, Conversation, + Escape, + Conversation, + Escape, ] ALL_ACTUAL_ACTION_CLASSES = [ @@ -63,6 +65,8 @@ ALL_ACTUAL_ACTION_CLASSES = [ DriveAway, Attack, Conversation, + MoveAwayFromAvatar, + MoveAwayFromRegion, ] ALL_ACTION_NAMES = [action.__name__ for action in ALL_ACTION_CLASSES] diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 84a6e86..faf7740 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -179,8 +179,10 @@ class Avatar: """ if self.current_action is None: return [] - action = self.current_action.action - params = self.current_action.params + # 记录当前动作实例引用,用于检测执行过程中是否发生了“抢占/切换” + action_instance_before = self.current_action + action = action_instance_before.action + params = action_instance_before.params try: status, mid_events = action.step(**params) except TypeError: @@ -190,7 +192,10 @@ class Avatar: finish_events = action.finish(**params) except TypeError: finish_events = action.finish() - self.current_action = None + # 仅当当前动作仍然是刚才执行的那个实例时才清空 + # 若在 step() 内部通过“抢占”机制切换了动作(如 Escape 失败立即切到 Battle),不要清空新动作 + if self.current_action is action_instance_before: + self.current_action = None if finish_events: # 允许 finish 直接返回事件(极少用),统一并入 pending for e in finish_events: diff --git a/src/classes/battle.py b/src/classes/battle.py index c4bb83d..00aa244 100644 --- a/src/classes/battle.py +++ b/src/classes/battle.py @@ -54,7 +54,7 @@ def get_escape_success_rate(attacker: "Avatar", defender: "Avatar") -> float: attacker: 追击方(通常为进攻者) defender: 逃跑方(通常为被攻击者) """ - return 0.6 + return 0.1 def get_damage(winner: "Avatar", loser: "Avatar") -> int: """ diff --git a/src/classes/mutual_action.py b/src/classes/mutual_action.py index b01c19a..d08fafb 100644 --- a/src/classes/mutual_action.py +++ b/src/classes/mutual_action.py @@ -131,6 +131,7 @@ class MutualAction(DefineAction, LLMAction): fb_map = { "MoveAwayFromAvatar": "试图远离", "MoveAwayFromRegion": "试图离开区域", + "Escape": "逃离", "Battle": "战斗", } fb_label = fb_map.get(str(feedback).strip(), str(feedback)) @@ -204,11 +205,11 @@ class Attack(MutualAction, ActualActionMixin): COMMENT = "对目标进行攻击。" DOABLES_REQUIREMENTS = "与目标处于同一区域" PARAMS = {"target_avatar": "AvatarName"} - FEEDBACK_ACTIONS = ["MoveAwayFromAvatar", "Battle"] + FEEDBACK_ACTIONS = ["Escape", "Battle"] def _settle_feedback(self, target_avatar: "Avatar", feedback_name: str) -> None: fb = str(feedback_name).strip() - if fb == "MoveAwayFromAvatar": + if fb == "Escape": params = {"avatar_name": self.avatar.name} self._set_target_immediate_action(target_avatar, fb, params) elif fb == "Battle": @@ -216,50 +217,7 @@ class Attack(MutualAction, ActualActionMixin): self._set_target_immediate_action(target_avatar, fb, params) -# 轻量实现三个动作类,供互动动作反馈直接使用 -class MoveAwayFromAvatar(DefineAction, ActualActionMixin): - COMMENT = "远离指定角色" - DOABLES_REQUIREMENTS = "任何时候都可以执行" - PARAMS = {"avatar_name": "AvatarName"} - def _execute(self, avatar_name: str) -> None: - target = self._find_avatar_by_name(avatar_name) - if target is None: - return - # 被攻击时逃跑的成功率:从 battle 模块获取(暂时固定值) - escape_rate = float(get_escape_success_rate(target, self.avatar)) - if random.random() < escape_rate: - dx = 1 if self.avatar.pos_x >= target.pos_x else -1 - dy = 1 if self.avatar.pos_y >= target.pos_y else -1 - nx = self.avatar.pos_x + dx - ny = self.avatar.pos_y + dy - if self.world.map.is_in_bounds(nx, ny): - self.avatar.pos_x = nx - self.avatar.pos_y = ny - self.avatar.tile = self.world.map.get_tile(nx, ny) - else: - # 抢占:中断自身动作并清空队列后入队并提交 - self._preempt_avatar(self.avatar) - self.avatar.load_decide_result_chain([("Battle", {"avatar_name": avatar_name})], self.avatar.thinking, "") - start_event = self.avatar.commit_next_plan() - if start_event is not None: - # 仅在本方推送到侧边栏;对方仅写历史 - self._add_event_pair(start_event, initiator=self.avatar, target=target) - - -class MoveAwayFromRegion(DefineAction, ActualActionMixin): - COMMENT = "离开指定区域" - DOABLES_REQUIREMENTS = "任何时候都可以执行" - PARAMS = {"region": "RegionName"} - def _execute(self, region: str) -> None: - # 驱赶离开:若选择离开,必定成功。简化为向地图边缘移动一步 - dx = 1 if self.avatar.pos_x < self.world.map.width - 1 else -1 - dy = 1 if self.avatar.pos_y < self.world.map.height - 1 else -1 - nx = max(0, min(self.world.map.width - 1, self.avatar.pos_x + dx)) - ny = max(0, min(self.world.map.height - 1, self.avatar.pos_y + dy)) - if self.world.map.is_in_bounds(nx, ny): - self.avatar.pos_x = nx - self.avatar.pos_y = ny - self.avatar.tile = self.world.map.get_tile(nx, ny) + class Conversation(MutualAction, ActualActionMixin): diff --git a/src/run/run.py b/src/run/run.py index 3c7554c..d57ed77 100644 --- a/src/run/run.py +++ b/src/run/run.py @@ -90,17 +90,17 @@ def make_avatars(world: World, count: int = 12, current_month_stamp: MonthStamp avatar.alignment = random.choice(list(Alignment)) avatars[avatar.id] = avatar # # —— 为演示添加少量示例关系 —— - # avatar_list = list(avatars.values()) - # if len(avatar_list) >= 2: - # avatar_list[0].set_relation(avatar_list[1], Relation.ENEMY) - # if len(avatar_list) >= 4: - # avatar_list[2].set_relation(avatar_list[3], Relation.FRIEND) - # if len(avatar_list) >= 6: - # # 师徒(有向):第5位是师傅,第6位是徒弟 - # avatar_list[4].set_relation(avatar_list[5], Relation.MASTER) - # if len(avatar_list) >= 8: - # # 情侣 - # avatar_list[6].set_relation(avatar_list[7], Relation.LOVERS) + avatar_list = list(avatars.values()) + if len(avatar_list) >= 2: + avatar_list[0].set_relation(avatar_list[1], Relation.ENEMY) + if len(avatar_list) >= 4: + avatar_list[2].set_relation(avatar_list[3], Relation.FRIEND) + if len(avatar_list) >= 6: + # 师徒(有向):第5位是师傅,第6位是徒弟 + avatar_list[4].set_relation(avatar_list[5], Relation.MASTER) + if len(avatar_list) >= 8: + # 情侣 + avatar_list[6].set_relation(avatar_list[7], Relation.LOVERS) return avatars diff --git a/static/config.yml b/static/config.yml index 7479061..9f82578 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: 2 npc_birth_rate_per_month: 0.001 df: @@ -23,4 +23,4 @@ avatar: persona_num: 1 social: - talk_into_relation_probability: 0.9 \ No newline at end of file + talk_into_relation_probability: 0.1 \ No newline at end of file diff --git a/static/game_configs/persona.csv b/static/game_configs/persona.csv index 5ea5a3a..8c432ce 100644 --- a/static/game_configs/persona.csv +++ b/static/game_configs/persona.csv @@ -1,3 +1,3 @@ id,name,exclusion_ids,prompt ,,和本persona互斥的persona的id,输入给LLM的prompt -22,外向,13;14;21,你是一个外向的人,你乐于与人交流,主动结识伙伴,倾向接受对话和合作。 +12,复仇,11;14,你是一个复仇心强的人,你绝不轻易放下仇怨,为了复仇愿意付出代价与时间。 \ No newline at end of file